expo-hiu-print 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +5 -0
- package/README.md +35 -0
- package/android/build.gradle +47 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/hiuprint/BitmapRenderer.kt +175 -0
- package/android/src/main/java/expo/modules/hiuprint/BitmapToESCPOS.kt +27 -0
- package/android/src/main/java/expo/modules/hiuprint/ExpoHiuPrintModule.kt +59 -0
- package/android/src/main/java/expo/modules/hiuprint/NetworkPrinter.kt +43 -0
- package/expo-module.config.json +9 -0
- package/index.d.ts +4 -0
- package/index.d.ts.map +1 -0
- package/index.ts +4 -0
- package/ios/BitmapRenderer.swift +233 -0
- package/ios/BitmapToESCPOS.swift +42 -0
- package/ios/ExpoHiuPrint.podspec +29 -0
- package/ios/ExpoHiuprintModule.swift +73 -0
- package/ios/NetworkPrinter.swift +40 -0
- package/package.json +43 -0
- package/src/ExpoHiuPrintModule.d.ts +4 -0
- package/src/ExpoHiuPrintModule.d.ts.map +1 -0
- package/src/ExpoHiuPrintModule.js +4 -0
- package/src/ExpoHiuPrintModule.js.map +1 -0
- package/src/ExpoHiuPrintModule.ts +7 -0
- package/src/api.d.ts +7 -0
- package/src/api.d.ts.map +1 -0
- package/src/api.js +12 -0
- package/src/api.js.map +1 -0
- package/src/api.ts +17 -0
- package/src/types.d.ts +39 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +2 -0
- package/src/types.js.map +1 -0
- package/src/types.ts +41 -0
- package/tsconfig.json +9 -0
package/.eslintrc.js
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# expo-hiu-print
|
|
2
|
+
|
|
3
|
+
Connect printer
|
|
4
|
+
|
|
5
|
+
# API documentation
|
|
6
|
+
|
|
7
|
+
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/hiu-print/)
|
|
8
|
+
- [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/hiu-print/)
|
|
9
|
+
|
|
10
|
+
# Installation in managed Expo projects
|
|
11
|
+
|
|
12
|
+
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
|
|
13
|
+
|
|
14
|
+
# Installation in bare React Native projects
|
|
15
|
+
|
|
16
|
+
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
|
|
17
|
+
|
|
18
|
+
### Add the package to your npm dependencies
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
npm install expo-hiu-print
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Configure for Android
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Configure for iOS
|
|
30
|
+
|
|
31
|
+
Run `npx pod-install` after installing the npm package.
|
|
32
|
+
|
|
33
|
+
# Contributing
|
|
34
|
+
|
|
35
|
+
Contributions are very welcome! Please refer to guidelines described in the [contributing guide]( https://github.com/expo/expo#contributing).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.hiuprint'
|
|
4
|
+
version = '0.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
+
def useManagedAndroidSdkVersions = false
|
|
16
|
+
if (useManagedAndroidSdkVersions) {
|
|
17
|
+
useDefaultAndroidSdkVersions()
|
|
18
|
+
} else {
|
|
19
|
+
buildscript {
|
|
20
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
+
ext.safeExtGet = { prop, fallback ->
|
|
22
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
project.android {
|
|
26
|
+
compileSdkVersion = safeExtGet("compileSdkVersion", 36)
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion = safeExtGet("minSdkVersion", 24)
|
|
29
|
+
targetSdkVersion = safeExtGet("targetSdkVersion", 36)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace = "expo.modules.hiuprint"
|
|
36
|
+
defaultConfig {
|
|
37
|
+
versionCode = 1
|
|
38
|
+
versionName = "0.1.0"
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError = false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dependencies {
|
|
46
|
+
implementation 'com.google.zxing:core:3.5.1'
|
|
47
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
package expo.modules.hiuprint
|
|
2
|
+
|
|
3
|
+
import android.graphics.*
|
|
4
|
+
import android.text.StaticLayout
|
|
5
|
+
import android.text.Layout
|
|
6
|
+
import android.text.TextPaint
|
|
7
|
+
import com.google.zxing.BarcodeFormat
|
|
8
|
+
import com.google.zxing.qrcode.QRCodeWriter
|
|
9
|
+
|
|
10
|
+
class BitmapRenderer(private val paperWidthPx: Int) {
|
|
11
|
+
|
|
12
|
+
fun render(elements: List<Map<String, Any>>): Bitmap {
|
|
13
|
+
val notoSans = Typeface.create("NotoSans", Typeface.NORMAL)
|
|
14
|
+
val notoSansBold = Typeface.create("NotoSans", Typeface.BOLD)
|
|
15
|
+
val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
16
|
+
color = Color.BLACK
|
|
17
|
+
textSize = 24f
|
|
18
|
+
typeface = notoSans
|
|
19
|
+
style = Paint.Style.FILL
|
|
20
|
+
isFakeBoldText = true // Make text bolder for thermal printers
|
|
21
|
+
strokeWidth = 2f // Increase stroke width for more solid text
|
|
22
|
+
}
|
|
23
|
+
val bitmap = Bitmap.createBitmap(paperWidthPx, 2000, Bitmap.Config.ARGB_8888)
|
|
24
|
+
val canvas = Canvas(bitmap)
|
|
25
|
+
canvas.drawColor(Color.WHITE)
|
|
26
|
+
|
|
27
|
+
var yOffset = 0f
|
|
28
|
+
|
|
29
|
+
elements.forEach { element ->
|
|
30
|
+
when (element["type"]) {
|
|
31
|
+
"text" -> {
|
|
32
|
+
val text = element["value"] as String
|
|
33
|
+
val align = element["align"] as String? ?: "left"
|
|
34
|
+
val bold = element["bold"] as Boolean? ?: false
|
|
35
|
+
val size = element["size"] as String? ?: "normal"
|
|
36
|
+
|
|
37
|
+
textPaint.typeface = if (bold) Typeface.create("NotoSans", Typeface.BOLD) else notoSans
|
|
38
|
+
textPaint.textSize = when (size) {
|
|
39
|
+
"small" -> 20f
|
|
40
|
+
"large" -> 32f
|
|
41
|
+
else -> 24f
|
|
42
|
+
}
|
|
43
|
+
textPaint.textAlign = when (align) {
|
|
44
|
+
"center" -> Paint.Align.CENTER
|
|
45
|
+
"right" -> Paint.Align.RIGHT
|
|
46
|
+
else -> Paint.Align.LEFT
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
val x = when (align) {
|
|
50
|
+
"center" -> paperWidthPx / 2f
|
|
51
|
+
"right" -> paperWidthPx.toFloat()
|
|
52
|
+
else -> 0f
|
|
53
|
+
}
|
|
54
|
+
canvas.drawText(text, x, yOffset + textPaint.textSize, textPaint)
|
|
55
|
+
yOffset += textPaint.textSize + 10
|
|
56
|
+
}
|
|
57
|
+
"line" -> {
|
|
58
|
+
canvas.drawLine(0f, yOffset, paperWidthPx.toFloat(), yOffset, textPaint)
|
|
59
|
+
yOffset += 10
|
|
60
|
+
}
|
|
61
|
+
"table" -> {
|
|
62
|
+
val headersRaw = element["headers"] as List<*>
|
|
63
|
+
val headers = headersRaw.map {
|
|
64
|
+
if (it is Map<*, *>) (it["text"] as? String) ?: "" else it.toString()
|
|
65
|
+
}
|
|
66
|
+
val headerAligns = headersRaw.map {
|
|
67
|
+
if (it is Map<*, *>) (it["align"] as? String) ?: "left" else "left"
|
|
68
|
+
}
|
|
69
|
+
val headerBold = headersRaw.map {
|
|
70
|
+
if (it is Map<*, *>) (it["bold"] as? Boolean) ?: false else false
|
|
71
|
+
}
|
|
72
|
+
val rowsRaw = element["rows"] as List<*>
|
|
73
|
+
val rows = rowsRaw.map { row ->
|
|
74
|
+
(row as List<*>).map {
|
|
75
|
+
if (it is Map<*, *>) (it["text"] as? String) ?: "" else it.toString()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
val rowAligns = rowsRaw.map { row ->
|
|
79
|
+
(row as List<*>).map {
|
|
80
|
+
if (it is Map<*, *>) (it["align"] as? String) ?: "left" else "left"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
val widthPercent = (element["widthPercent"] as? Number)?.toFloat() ?: 1f
|
|
84
|
+
val tableWidthPx = (paperWidthPx * widthPercent).toInt()
|
|
85
|
+
val alignTable = element["align"] as? String ?: "left"
|
|
86
|
+
val xOffset = when (alignTable) {
|
|
87
|
+
"center" -> (paperWidthPx - tableWidthPx) / 2f
|
|
88
|
+
"right" -> (paperWidthPx - tableWidthPx).toFloat()
|
|
89
|
+
else -> 0f
|
|
90
|
+
}
|
|
91
|
+
val columnWidth = tableWidthPx / headers.size
|
|
92
|
+
|
|
93
|
+
fun drawWrappedText(text: String, x: Float, y: Float, maxWidth: Float, paint: TextPaint): Float {
|
|
94
|
+
val staticLayout = StaticLayout(
|
|
95
|
+
text,
|
|
96
|
+
paint,
|
|
97
|
+
maxWidth.toInt(),
|
|
98
|
+
Layout.Alignment.ALIGN_NORMAL,
|
|
99
|
+
1.0f,
|
|
100
|
+
0.0f,
|
|
101
|
+
false
|
|
102
|
+
)
|
|
103
|
+
canvas.save()
|
|
104
|
+
canvas.translate(x, y)
|
|
105
|
+
staticLayout.draw(canvas)
|
|
106
|
+
canvas.restore()
|
|
107
|
+
return staticLayout.height.toFloat()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
headers.forEachIndexed { index, header ->
|
|
111
|
+
textPaint.typeface = if (headerBold[index]) notoSansBold else notoSans
|
|
112
|
+
textPaint.textAlign = when (headerAligns[index]) {
|
|
113
|
+
"center" -> Paint.Align.CENTER
|
|
114
|
+
"right" -> Paint.Align.RIGHT
|
|
115
|
+
else -> Paint.Align.LEFT
|
|
116
|
+
}
|
|
117
|
+
val x = xOffset + when (headerAligns[index]) {
|
|
118
|
+
"center" -> index * columnWidth + columnWidth / 2f
|
|
119
|
+
"right" -> (index + 1) * columnWidth.toFloat()
|
|
120
|
+
else -> index * columnWidth.toFloat()
|
|
121
|
+
}
|
|
122
|
+
drawWrappedText(header, x, yOffset + textPaint.textSize, columnWidth.toFloat(), textPaint)
|
|
123
|
+
}
|
|
124
|
+
yOffset += textPaint.textSize + 10
|
|
125
|
+
|
|
126
|
+
rows.forEachIndexed { rowIdx, row ->
|
|
127
|
+
var maxRowHeight = textPaint.textSize
|
|
128
|
+
val rowRaw = rowsRaw[rowIdx] as List<*>
|
|
129
|
+
row.forEachIndexed { colIdx, cell ->
|
|
130
|
+
val cellRaw = rowRaw[colIdx]
|
|
131
|
+
val isBold = if (cellRaw is Map<*, *>) (cellRaw["bold"] as? Boolean) ?: false else false
|
|
132
|
+
textPaint.typeface = if (isBold) notoSansBold else notoSans
|
|
133
|
+
textPaint.textAlign = when (rowAligns[rowIdx][colIdx]) {
|
|
134
|
+
"center" -> Paint.Align.CENTER
|
|
135
|
+
"right" -> Paint.Align.RIGHT
|
|
136
|
+
else -> Paint.Align.LEFT
|
|
137
|
+
}
|
|
138
|
+
val x = xOffset + when (rowAligns[rowIdx][colIdx]) {
|
|
139
|
+
"center" -> colIdx * columnWidth + columnWidth / 2f
|
|
140
|
+
"right" -> (colIdx + 1) * columnWidth.toFloat()
|
|
141
|
+
else -> colIdx * columnWidth.toFloat()
|
|
142
|
+
}
|
|
143
|
+
val cellHeight = drawWrappedText(cell, x, yOffset + textPaint.textSize, columnWidth.toFloat(), textPaint)
|
|
144
|
+
if (cellHeight > maxRowHeight) maxRowHeight = cellHeight
|
|
145
|
+
}
|
|
146
|
+
yOffset += maxRowHeight + 10
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
"qr" -> {
|
|
150
|
+
val data = element["data"] as String
|
|
151
|
+
val qrCode = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, 200, 200)
|
|
152
|
+
val qrBitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888)
|
|
153
|
+
for (x in 0 until 200) {
|
|
154
|
+
for (y in 0 until 200) {
|
|
155
|
+
qrBitmap.setPixel(x, y, if (qrCode.get(x, y)) Color.BLACK else Color.WHITE)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
canvas.drawBitmap(qrBitmap, (paperWidthPx - 200) / 2f, yOffset, textPaint)
|
|
159
|
+
yOffset += 210
|
|
160
|
+
}
|
|
161
|
+
"feed" -> {
|
|
162
|
+
val lines = (element["lines"] as? Number)?.toInt() ?: 0
|
|
163
|
+
yOffset += lines * 24f
|
|
164
|
+
}
|
|
165
|
+
"cut" -> {
|
|
166
|
+
// ESC/POS cut command: GS V 0
|
|
167
|
+
// Do not crop or affect bitmap rendering here. Handle cut in ESC/POS or printer logic only.
|
|
168
|
+
// No change to yOffset or bitmap cropping.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Bitmap.createBitmap(bitmap, 0, 0, paperWidthPx, yOffset.toInt())
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package expo.modules.hiuprint
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
|
|
5
|
+
object BitmapToESCPOS {
|
|
6
|
+
fun convert(bitmap: Bitmap): ByteArray {
|
|
7
|
+
val width = bitmap.width
|
|
8
|
+
val height = bitmap.height
|
|
9
|
+
val bytes = mutableListOf<Byte>()
|
|
10
|
+
|
|
11
|
+
bytes.addAll(byteArrayOf(0x1D, 0x76, 0x30, 0x00, (width / 8).toByte(), 0x00, (height and 0xFF).toByte(), (height shr 8).toByte()).toList())
|
|
12
|
+
|
|
13
|
+
for (y in 0 until height) {
|
|
14
|
+
for (x in 0 until width step 8) {
|
|
15
|
+
var byte = 0
|
|
16
|
+
for (bit in 0..7) {
|
|
17
|
+
if (x + bit < width && bitmap.getPixel(x + bit, y) == -0x1000000) {
|
|
18
|
+
byte = byte or (1 shl (7 - bit))
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
bytes.add(byte.toByte())
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return bytes.toByteArray()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package expo.modules.hiuprint
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
import expo.modules.hiuprint.BitmapRenderer
|
|
6
|
+
import expo.modules.hiuprint.BitmapToESCPOS
|
|
7
|
+
import expo.modules.hiuprint.NetworkPrinter
|
|
8
|
+
|
|
9
|
+
class ExpoHiuPrintModule : Module() {
|
|
10
|
+
override fun definition() = ModuleDefinition {
|
|
11
|
+
Name("ExpoHiuPrint")
|
|
12
|
+
|
|
13
|
+
AsyncFunction("print") { job: Map<String, Any> ->
|
|
14
|
+
val printers = job["printers"] as List<Map<String, Any>>
|
|
15
|
+
val elements = job["elements"] as List<Map<String, Any>>
|
|
16
|
+
try {
|
|
17
|
+
printers.forEach { printer ->
|
|
18
|
+
val host = printer["host"] as String
|
|
19
|
+
val port = (printer["port"] as Double).toInt()
|
|
20
|
+
val paperWidth = (printer["paperWidth"] as Double).toInt()
|
|
21
|
+
|
|
22
|
+
val renderer = BitmapRenderer(if (paperWidth == 58) 384 else 576)
|
|
23
|
+
val bitmap = renderer.render(elements)
|
|
24
|
+
val escposData = BitmapToESCPOS.convert(bitmap)
|
|
25
|
+
|
|
26
|
+
// Check if there is a cut element
|
|
27
|
+
val hasCut = elements.any { it["type"] == "cut" }
|
|
28
|
+
|
|
29
|
+
val networkPrinter = NetworkPrinter(host, port)
|
|
30
|
+
networkPrinter.send(escposData, cut = hasCut)
|
|
31
|
+
}
|
|
32
|
+
} catch (e: Exception) {
|
|
33
|
+
throw e
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
AsyncFunction("openCashBox") { job: Map<String, Any> ->
|
|
38
|
+
val printers = job["printers"] as List<Map<String, Any>>
|
|
39
|
+
try {
|
|
40
|
+
printers.forEach { printer ->
|
|
41
|
+
val host = printer["host"] as String
|
|
42
|
+
val port = (printer["port"] as Double).toInt()
|
|
43
|
+
val networkPrinter = NetworkPrinter(host, port)
|
|
44
|
+
networkPrinter.openCashBox()
|
|
45
|
+
}
|
|
46
|
+
} catch (e: Exception) {
|
|
47
|
+
throw e
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
AsyncFunction("checkPrinterStatus") { printer: Map<String, Any> ->
|
|
52
|
+
val host = printer["host"] as? String ?: return@AsyncFunction "disconnected"
|
|
53
|
+
val port = (printer["port"] as? Double)?.toInt() ?: 9100
|
|
54
|
+
val networkPrinter = NetworkPrinter(host, port)
|
|
55
|
+
val isConnected = networkPrinter.checkPrinterStatus()
|
|
56
|
+
return@AsyncFunction if (isConnected) "connected" else "disconnected"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package expo.modules.hiuprint
|
|
2
|
+
|
|
3
|
+
import java.io.OutputStream
|
|
4
|
+
import java.net.Socket
|
|
5
|
+
|
|
6
|
+
class NetworkPrinter(private val host: String, private val port: Int) {
|
|
7
|
+
fun send(data: ByteArray, cut: Boolean = false) {
|
|
8
|
+
Socket(host, port).use { socket ->
|
|
9
|
+
val outputStream: OutputStream = socket.getOutputStream()
|
|
10
|
+
outputStream.write(data)
|
|
11
|
+
outputStream.flush()
|
|
12
|
+
if (cut) {
|
|
13
|
+
// Gửi thêm 3 dòng feed (ESC d 3) để đẩy giấy ra trước khi cắt
|
|
14
|
+
outputStream.write(byteArrayOf(0x1B, 0x64, 0x03))
|
|
15
|
+
outputStream.flush()
|
|
16
|
+
// ESC/POS cut command: GS V 0
|
|
17
|
+
outputStream.write(byteArrayOf(0x1D, 0x56, 0x00))
|
|
18
|
+
outputStream.flush()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fun openCashBox() {
|
|
24
|
+
Socket(host, port).use { socket ->
|
|
25
|
+
val outputStream: OutputStream = socket.getOutputStream()
|
|
26
|
+
// ESC/POS open cash drawer command: DLE DC4 n m t
|
|
27
|
+
// Most common: 0x10 0x14 0x01 0x00 0x00
|
|
28
|
+
outputStream.write(byteArrayOf(0x10, 0x14, 0x01, 0x00, 0x00))
|
|
29
|
+
outputStream.flush()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fun checkPrinterStatus(): Boolean {
|
|
34
|
+
return try {
|
|
35
|
+
Socket(host, port).use { socket ->
|
|
36
|
+
// If we can open a socket, the printer is reachable
|
|
37
|
+
true
|
|
38
|
+
}
|
|
39
|
+
} catch (e: Exception) {
|
|
40
|
+
false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/index.d.ts
ADDED
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC"}
|
package/index.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
class BitmapRenderer {
|
|
4
|
+
let paperWidthPx: Int
|
|
5
|
+
|
|
6
|
+
init(paperWidthPx: Int) {
|
|
7
|
+
self.paperWidthPx = paperWidthPx
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Hàm helper để tạo font
|
|
11
|
+
private func getFont(size: String = "normal", bold: Bool = false) -> UIFont {
|
|
12
|
+
let fontSize: CGFloat = (size == "small") ? 20 : (size == "large") ? 32 : 24
|
|
13
|
+
return bold ? UIFont.boldSystemFont(ofSize: fontSize) : UIFont.systemFont(ofSize: fontSize)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private func getAttributes(font: UIFont, align: String) -> [NSAttributedString.Key: Any] {
|
|
17
|
+
let paragraph = NSMutableParagraphStyle()
|
|
18
|
+
paragraph.alignment = (align == "center") ? .center : (align == "right") ? .right : .left
|
|
19
|
+
paragraph.lineBreakMode = .byWordWrapping
|
|
20
|
+
return [
|
|
21
|
+
.font: font,
|
|
22
|
+
.foregroundColor: UIColor.black,
|
|
23
|
+
.paragraphStyle: paragraph
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Tính chiều cao cần thiết cho một đoạn text
|
|
28
|
+
private func measureTextHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
|
|
29
|
+
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
|
|
30
|
+
let boundingBox = text.boundingRect(with: constraintRect,
|
|
31
|
+
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
|
32
|
+
attributes: [.font: font],
|
|
33
|
+
context: nil)
|
|
34
|
+
return ceil(boundingBox.height)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Tính tổng chiều cao cần thiết cho toàn bộ bill
|
|
38
|
+
private func calculateTotalHeight(elements: [[String: Any]]) -> CGFloat {
|
|
39
|
+
var totalHeight: CGFloat = 0
|
|
40
|
+
|
|
41
|
+
for element in elements {
|
|
42
|
+
guard let type = element["type"] as? String else { continue }
|
|
43
|
+
|
|
44
|
+
if type == "text" {
|
|
45
|
+
let text = element["value"] as? String ?? ""
|
|
46
|
+
let bold = element["bold"] as? Bool ?? false
|
|
47
|
+
let size = element["size"] as? String ?? "normal"
|
|
48
|
+
let font = getFont(size: size, bold: bold)
|
|
49
|
+
let h = measureTextHeight(text: text, width: CGFloat(paperWidthPx), font: font)
|
|
50
|
+
totalHeight += h + 10 // padding bottom
|
|
51
|
+
} else if type == "line" {
|
|
52
|
+
totalHeight += 10 + 10 // height + padding
|
|
53
|
+
} else if type == "feed" {
|
|
54
|
+
let lines = (element["lines"] as? Int) ?? 0
|
|
55
|
+
totalHeight += CGFloat(lines) * 24
|
|
56
|
+
} else if type == "qr" {
|
|
57
|
+
totalHeight += 210 // 200 size + 10 padding
|
|
58
|
+
} else if type == "table" {
|
|
59
|
+
let headersRaw = element["headers"] as? [[String: Any]] ?? []
|
|
60
|
+
let rowsRaw = element["rows"] as? [[[String: Any]]] ?? []
|
|
61
|
+
let widthPercent = (element["widthPercent"] as? CGFloat) ?? 1.0
|
|
62
|
+
let tableWidth = CGFloat(paperWidthPx) * widthPercent
|
|
63
|
+
let colCount = CGFloat(max(1, headersRaw.count))
|
|
64
|
+
let colWidth = tableWidth / colCount
|
|
65
|
+
|
|
66
|
+
// Header height
|
|
67
|
+
if !headersRaw.isEmpty {
|
|
68
|
+
var maxHeaderH: CGFloat = 0
|
|
69
|
+
for header in headersRaw {
|
|
70
|
+
let text = header["text"] as? String ?? ""
|
|
71
|
+
let bold = header["bold"] as? Bool ?? false
|
|
72
|
+
let font = getFont(size: "normal", bold: bold)
|
|
73
|
+
let h = measureTextHeight(text: text, width: colWidth, font: font)
|
|
74
|
+
if h > maxHeaderH { maxHeaderH = h }
|
|
75
|
+
}
|
|
76
|
+
totalHeight += maxHeaderH + 10
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Rows height
|
|
80
|
+
for row in rowsRaw {
|
|
81
|
+
var maxRowH: CGFloat = 0
|
|
82
|
+
for (idx, col) in row.enumerated() {
|
|
83
|
+
if idx >= Int(colCount) { break }
|
|
84
|
+
let text = col["text"] as? String ?? ""
|
|
85
|
+
let bold = col["bold"] as? Bool ?? false
|
|
86
|
+
let font = getFont(size: "normal", bold: bold)
|
|
87
|
+
let h = measureTextHeight(text: text, width: colWidth, font: font)
|
|
88
|
+
if h > maxRowH { maxRowH = h }
|
|
89
|
+
}
|
|
90
|
+
totalHeight += maxRowH + 10
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return totalHeight + 50 // Buffer cuối cùng
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func render(elements: [[String: Any]]) -> UIImage? {
|
|
98
|
+
// Bước 1: Tính chiều cao dynamic để không bị cắt giấy
|
|
99
|
+
let totalHeight = calculateTotalHeight(elements: elements)
|
|
100
|
+
|
|
101
|
+
UIGraphicsBeginImageContextWithOptions(CGSize(width: CGFloat(paperWidthPx), height: totalHeight), false, 1.0)
|
|
102
|
+
guard let context = UIGraphicsGetCurrentContext() else { return nil }
|
|
103
|
+
|
|
104
|
+
// Fill nền trắng
|
|
105
|
+
context.setFillColor(UIColor.white.cgColor)
|
|
106
|
+
context.fill(CGRect(x: 0, y: 0, width: CGFloat(paperWidthPx), height: totalHeight))
|
|
107
|
+
|
|
108
|
+
var yOffset: CGFloat = 0
|
|
109
|
+
|
|
110
|
+
for element in elements {
|
|
111
|
+
guard let type = element["type"] as? String else { continue }
|
|
112
|
+
|
|
113
|
+
if type == "text" {
|
|
114
|
+
let text = element["value"] as? String ?? ""
|
|
115
|
+
let align = element["align"] as? String ?? "left"
|
|
116
|
+
let bold = element["bold"] as? Bool ?? false
|
|
117
|
+
let size = element["size"] as? String ?? "normal"
|
|
118
|
+
|
|
119
|
+
let font = getFont(size: size, bold: bold)
|
|
120
|
+
let attrs = getAttributes(font: font, align: align)
|
|
121
|
+
let height = measureTextHeight(text: text, width: CGFloat(paperWidthPx), font: font)
|
|
122
|
+
|
|
123
|
+
let rect = CGRect(x: 0, y: yOffset, width: CGFloat(paperWidthPx), height: height)
|
|
124
|
+
(text as NSString).draw(in: rect, withAttributes: attrs)
|
|
125
|
+
|
|
126
|
+
yOffset += height + 10
|
|
127
|
+
|
|
128
|
+
} else if type == "line" {
|
|
129
|
+
context.setStrokeColor(UIColor.black.cgColor)
|
|
130
|
+
context.setLineWidth(2)
|
|
131
|
+
context.move(to: CGPoint(x: 0, y: yOffset + 5))
|
|
132
|
+
context.addLine(to: CGPoint(x: CGFloat(paperWidthPx), y: yOffset + 5))
|
|
133
|
+
context.strokePath()
|
|
134
|
+
yOffset += 20 // 10 line + 10 space
|
|
135
|
+
|
|
136
|
+
} else if type == "feed" {
|
|
137
|
+
let lines = (element["lines"] as? Int) ?? 0
|
|
138
|
+
yOffset += CGFloat(lines) * 24
|
|
139
|
+
|
|
140
|
+
} else if type == "qr" {
|
|
141
|
+
if let data = element["data"] as? String {
|
|
142
|
+
let filter = CIFilter(name: "CIQRCodeGenerator")
|
|
143
|
+
filter?.setValue(data.data(using: .utf8), forKey: "inputMessage")
|
|
144
|
+
filter?.setValue("M", forKey: "inputCorrectionLevel")
|
|
145
|
+
|
|
146
|
+
if let ciImage = filter?.outputImage {
|
|
147
|
+
let qrSize: CGFloat = 200
|
|
148
|
+
let scaleX = qrSize / ciImage.extent.size.width
|
|
149
|
+
let scaleY = qrSize / ciImage.extent.size.height
|
|
150
|
+
let transformed = ciImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
|
|
151
|
+
let qrImage = UIImage(ciImage: transformed)
|
|
152
|
+
|
|
153
|
+
let xPos = (CGFloat(paperWidthPx) - qrSize) / 2
|
|
154
|
+
qrImage.draw(in: CGRect(x: xPos, y: yOffset, width: qrSize, height: qrSize))
|
|
155
|
+
yOffset += qrSize + 10
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
} else if type == "table" {
|
|
160
|
+
let headersRaw = element["headers"] as? [[String: Any]] ?? []
|
|
161
|
+
let rowsRaw = element["rows"] as? [[[String: Any]]] ?? []
|
|
162
|
+
let widthPercent = (element["widthPercent"] as? CGFloat) ?? 1.0
|
|
163
|
+
let tableWidth = CGFloat(paperWidthPx) * widthPercent
|
|
164
|
+
let alignTable = element["align"] as? String ?? "left"
|
|
165
|
+
let xStart: CGFloat = (alignTable == "center") ? (CGFloat(paperWidthPx) - tableWidth) / 2 : (alignTable == "right") ? (CGFloat(paperWidthPx) - tableWidth) : 0
|
|
166
|
+
|
|
167
|
+
let colCount = CGFloat(max(1, headersRaw.count))
|
|
168
|
+
let colWidth = tableWidth / colCount
|
|
169
|
+
|
|
170
|
+
// Draw Headers
|
|
171
|
+
if !headersRaw.isEmpty {
|
|
172
|
+
var maxH: CGFloat = 0
|
|
173
|
+
// Tính max height của row header trước
|
|
174
|
+
for header in headersRaw {
|
|
175
|
+
let text = header["text"] as? String ?? ""
|
|
176
|
+
let bold = header["bold"] as? Bool ?? false
|
|
177
|
+
let font = getFont(size: "normal", bold: bold)
|
|
178
|
+
let h = measureTextHeight(text: text, width: colWidth, font: font)
|
|
179
|
+
if h > maxH { maxH = h }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Vẽ
|
|
183
|
+
for (i, header) in headersRaw.enumerated() {
|
|
184
|
+
let text = header["text"] as? String ?? ""
|
|
185
|
+
let align = header["align"] as? String ?? "left"
|
|
186
|
+
let bold = header["bold"] as? Bool ?? false
|
|
187
|
+
let font = getFont(size: "normal", bold: bold)
|
|
188
|
+
let attrs = getAttributes(font: font, align: align)
|
|
189
|
+
|
|
190
|
+
let cellX = xStart + CGFloat(i) * colWidth
|
|
191
|
+
let rect = CGRect(x: cellX, y: yOffset, width: colWidth, height: maxH)
|
|
192
|
+
(text as NSString).draw(in: rect, withAttributes: attrs)
|
|
193
|
+
}
|
|
194
|
+
yOffset += maxH + 10
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Draw Rows
|
|
198
|
+
for row in rowsRaw {
|
|
199
|
+
var maxRowH: CGFloat = 0
|
|
200
|
+
// Pass 1: Tính chiều cao row
|
|
201
|
+
for (i, col) in row.enumerated() {
|
|
202
|
+
if i >= Int(colCount) { break }
|
|
203
|
+
let text = col["text"] as? String ?? ""
|
|
204
|
+
let bold = col["bold"] as? Bool ?? false
|
|
205
|
+
let font = getFont(size: "normal", bold: bold)
|
|
206
|
+
let h = measureTextHeight(text: text, width: colWidth, font: font)
|
|
207
|
+
if h > maxRowH { maxRowH = h }
|
|
208
|
+
}
|
|
209
|
+
if maxRowH < 24 { maxRowH = 24 } // Min height
|
|
210
|
+
|
|
211
|
+
// Pass 2: Vẽ
|
|
212
|
+
for (i, col) in row.enumerated() {
|
|
213
|
+
if i >= Int(colCount) { break }
|
|
214
|
+
let text = col["text"] as? String ?? ""
|
|
215
|
+
let align = col["align"] as? String ?? "left"
|
|
216
|
+
let bold = col["bold"] as? Bool ?? false
|
|
217
|
+
let font = getFont(size: "normal", bold: bold)
|
|
218
|
+
let attrs = getAttributes(font: font, align: align)
|
|
219
|
+
|
|
220
|
+
let cellX = xStart + CGFloat(i) * colWidth
|
|
221
|
+
let rect = CGRect(x: cellX, y: yOffset, width: colWidth, height: maxRowH)
|
|
222
|
+
(text as NSString).draw(in: rect, withAttributes: attrs)
|
|
223
|
+
}
|
|
224
|
+
yOffset += maxRowH + 10
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let image = UIGraphicsGetImageFromCurrentImageContext()
|
|
230
|
+
UIGraphicsEndImageContext()
|
|
231
|
+
return image
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
class BitmapToESCPOS {
|
|
4
|
+
// Chuyển UIImage sang ESC/POS raster bytes
|
|
5
|
+
static func convert(_ image: UIImage) -> Data? {
|
|
6
|
+
guard let cgImage = image.cgImage else { return nil }
|
|
7
|
+
let width = cgImage.width
|
|
8
|
+
let height = cgImage.height
|
|
9
|
+
let bytesPerRow = (width + 7) / 8
|
|
10
|
+
var escpos = Data()
|
|
11
|
+
// ESC * m nL nH raster bit image
|
|
12
|
+
escpos.append(0x1D)
|
|
13
|
+
escpos.append(0x76)
|
|
14
|
+
escpos.append(0x30)
|
|
15
|
+
escpos.append(0x00) // Normal mode
|
|
16
|
+
escpos.append(UInt8(bytesPerRow & 0xFF))
|
|
17
|
+
escpos.append(UInt8((bytesPerRow >> 8) & 0xFF))
|
|
18
|
+
escpos.append(UInt8(height & 0xFF))
|
|
19
|
+
escpos.append(UInt8((height >> 8) & 0xFF))
|
|
20
|
+
// Lấy pixel data
|
|
21
|
+
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue) else { return nil }
|
|
22
|
+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
23
|
+
guard let pixelData = context.data else { return nil }
|
|
24
|
+
let buffer = pixelData.bindMemory(to: UInt8.self, capacity: width * height)
|
|
25
|
+
for y in 0..<height {
|
|
26
|
+
for xByte in 0..<bytesPerRow {
|
|
27
|
+
var byte: UInt8 = 0
|
|
28
|
+
for bit in 0..<8 {
|
|
29
|
+
let x = xByte * 8 + bit
|
|
30
|
+
if x < width {
|
|
31
|
+
let pixel = buffer[y * width + x]
|
|
32
|
+
if pixel < 128 { // ngưỡng đen trắng
|
|
33
|
+
byte |= (0x80 >> bit)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
escpos.append(byte)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return escpos
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ExpoHiuPrint'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '15.1',
|
|
15
|
+
:tvos => '15.1'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.9'
|
|
18
|
+
s.source = { git: 'https://github.com/hieufix1710/expo-hiu-print' }
|
|
19
|
+
s.static_framework = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'ExpoModulesCore'
|
|
22
|
+
|
|
23
|
+
# Swift/Objective-C compatibility
|
|
24
|
+
s.pod_target_xcconfig = {
|
|
25
|
+
'DEFINES_MODULE' => 'YES',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
29
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class ExpoHiuPrintModule: Module {
|
|
4
|
+
// Each module class must implement the definition function. The definition consists of components
|
|
5
|
+
// that describes the module's functionality and behavior.
|
|
6
|
+
// See https://docs.expo.dev/modules/module-api for more details about available components.
|
|
7
|
+
public func definition() -> ModuleDefinition {
|
|
8
|
+
Name("ExpoHiuPrint")
|
|
9
|
+
|
|
10
|
+
AsyncFunction("print") { (job: [String: Any]) in
|
|
11
|
+
guard let printers = job["printers"] as? [[String: Any]], let elements = job["elements"] as? [[String: Any]] else {
|
|
12
|
+
throw NSError(domain: "ExpoHiuPrint", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid arguments"])
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for printer in printers {
|
|
16
|
+
let host = printer["host"] as? String ?? ""
|
|
17
|
+
let port = printer["port"] as? Int ?? 9100
|
|
18
|
+
let printerWidth = printer["paperWidth"] as? Int ?? 576
|
|
19
|
+
|
|
20
|
+
// Logic convert giống Android: nếu input là 58(mm) thì dùng 384px, mặc định 576px
|
|
21
|
+
let paperWidthPx = (printerWidth == 58) ? 384 : 576
|
|
22
|
+
|
|
23
|
+
let renderer = BitmapRenderer(paperWidthPx: paperWidthPx)
|
|
24
|
+
|
|
25
|
+
guard let image = renderer.render(elements: elements) else {
|
|
26
|
+
throw NSError(domain: "ExpoHiuPrint", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to render image"])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
guard let escposData = BitmapToESCPOS.convert(image) else {
|
|
30
|
+
throw NSError(domain: "ExpoHiuPrint", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to convert to ESC/POS"])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let hasCut = elements.contains { ($0["type"] as? String) == "cut" }
|
|
34
|
+
let networkPrinter = NetworkPrinter(host: host, port: port)
|
|
35
|
+
|
|
36
|
+
do {
|
|
37
|
+
try networkPrinter.send(data: escposData, cut: hasCut)
|
|
38
|
+
} catch {
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
AsyncFunction("openCashBox") { (job: [String: Any]) in
|
|
45
|
+
guard let printers = job["printers"] as? [[String: Any]] else { return }
|
|
46
|
+
for printer in printers {
|
|
47
|
+
let host = printer["host"] as? String ?? ""
|
|
48
|
+
let port = printer["port"] as? Int ?? 9100
|
|
49
|
+
let networkPrinter = NetworkPrinter(host: host, port: port)
|
|
50
|
+
do {
|
|
51
|
+
try networkPrinter.openCashBox()
|
|
52
|
+
} catch {
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
AsyncFunction("checkPrinterStatus") { (printer: [String: Any]) -> String in
|
|
59
|
+
let host = printer["host"] as? String ?? ""
|
|
60
|
+
let port = printer["port"] as? Int ?? 9100
|
|
61
|
+
var inputStream: InputStream?
|
|
62
|
+
var outputStream: OutputStream?
|
|
63
|
+
Stream.getStreamsToHost(withName: host, port: port, inputStream: &inputStream, outputStream: &outputStream)
|
|
64
|
+
|
|
65
|
+
guard let out = outputStream else { return "disconnected" }
|
|
66
|
+
out.open()
|
|
67
|
+
// Basic connection check via stream status
|
|
68
|
+
let isConnected = out.streamStatus == .open || out.streamStatus == .writing
|
|
69
|
+
out.close()
|
|
70
|
+
return isConnected ? "connected" : "disconnected"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
class NetworkPrinter {
|
|
4
|
+
let host: String
|
|
5
|
+
let port: Int
|
|
6
|
+
|
|
7
|
+
init(host: String, port: Int) {
|
|
8
|
+
self.host = host
|
|
9
|
+
self.port = port
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func send(data: Data, cut: Bool = false) throws {
|
|
13
|
+
var inputStream: InputStream?
|
|
14
|
+
var outputStream: OutputStream?
|
|
15
|
+
Stream.getStreamsToHost(withName: host, port: port, inputStream: &inputStream, outputStream: &outputStream)
|
|
16
|
+
guard let out = outputStream else { throw NSError(domain: "NetworkPrinter", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot open output stream"]) }
|
|
17
|
+
out.open()
|
|
18
|
+
let bytes = [UInt8](data)
|
|
19
|
+
out.write(bytes, maxLength: bytes.count)
|
|
20
|
+
if cut {
|
|
21
|
+
// Gửi thêm 3 dòng feed (ESC d 3) để đẩy giấy ra trước khi cắt
|
|
22
|
+
out.write([0x1B, 0x64, 0x03], maxLength: 3)
|
|
23
|
+
// ESC/POS cut command: GS V 0
|
|
24
|
+
out.write([0x1D, 0x56, 0x00], maxLength: 3)
|
|
25
|
+
}
|
|
26
|
+
out.close()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func openCashBox() throws {
|
|
30
|
+
var inputStream: InputStream?
|
|
31
|
+
var outputStream: OutputStream?
|
|
32
|
+
Stream.getStreamsToHost(withName: host, port: port, inputStream: &inputStream, outputStream: &outputStream)
|
|
33
|
+
guard let out = outputStream else { throw NSError(domain: "NetworkPrinter", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot open output stream"]) }
|
|
34
|
+
out.open()
|
|
35
|
+
// ESC/POS open cash drawer command: DLE DC4 n m t
|
|
36
|
+
let cmd: [UInt8] = [0x10, 0x14, 0x01, 0x00, 0x00]
|
|
37
|
+
out.write(cmd, maxLength: cmd.count)
|
|
38
|
+
out.close()
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "expo-hiu-print",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Connect printer ",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "expo-module build",
|
|
9
|
+
"clean": "expo-module clean",
|
|
10
|
+
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"prepare": "expo-module prepare",
|
|
13
|
+
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
|
+
"expo-module": "expo-module",
|
|
15
|
+
"open:ios": "xed example/ios",
|
|
16
|
+
"open:android": "open -a \"Android Studio\" example/android"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react-native",
|
|
20
|
+
"expo",
|
|
21
|
+
"expo-hiu-print",
|
|
22
|
+
"ExpoHiuPrint"
|
|
23
|
+
],
|
|
24
|
+
"repository": "https://github.com/hieufix1710/expo-hiu-print",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/hieufix1710/expo-hiu-print/issues"
|
|
27
|
+
},
|
|
28
|
+
"author": "hiun <hieufix1710@gmail.com> (https://github.com/hieufix1710)",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"homepage": "https://github.com/hieufix1710/expo-hiu-print#readme",
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "~19.1.0",
|
|
34
|
+
"expo-module-scripts": "^5.0.8",
|
|
35
|
+
"expo": "^54.0.27",
|
|
36
|
+
"react-native": "0.81.5"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"expo": "*",
|
|
40
|
+
"react": "*",
|
|
41
|
+
"react-native": "*"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoHiuPrintModule.d.ts","sourceRoot":"","sources":["ExpoHiuPrintModule.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEtD,QAAA,MAAM,kBAAkB,wBACqC,CAAC;AAE9D,eAAe,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoHiuPrintModule.js","sourceRoot":"","sources":["ExpoHiuPrintModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAG3C,MAAM,kBAAkB,GACtB,mBAAmB,CAAyB,cAAc,CAAC,CAAC;AAE9D,eAAe,kBAAkB,CAAC","sourcesContent":["import { requireNativeModule } from \"expo\";\nimport type { ExpoHiuPrintModuleType } from \"./types\";\n\nconst ExpoHiuPrintModule =\n requireNativeModule<ExpoHiuPrintModuleType>(\"ExpoHiuPrint\");\n\nexport default ExpoHiuPrintModule;\n"]}
|
package/src/api.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PrintJob, Printer } from "./types";
|
|
2
|
+
export declare function print(job: PrintJob): Promise<void>;
|
|
3
|
+
export declare function openCashBox(job: {
|
|
4
|
+
printers: Printer[];
|
|
5
|
+
}): Promise<void>;
|
|
6
|
+
export declare function checkPrinterStatus(printer: Printer): Promise<"connected" | "disconnected">;
|
|
7
|
+
//# sourceMappingURL=api.d.ts.map
|
package/src/api.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["api.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEjD,wBAAgB,KAAK,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAElD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,WAAW,GAAG,cAAc,CAAC,CAEvC"}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// API wrapper for expo-hiu-print
|
|
2
|
+
import ExpoHiuPrintModule from "./ExpoHiuPrintModule";
|
|
3
|
+
export function print(job) {
|
|
4
|
+
return ExpoHiuPrintModule.print(job);
|
|
5
|
+
}
|
|
6
|
+
export function openCashBox(job) {
|
|
7
|
+
return ExpoHiuPrintModule.openCashBox(job);
|
|
8
|
+
}
|
|
9
|
+
export function checkPrinterStatus(printer) {
|
|
10
|
+
return ExpoHiuPrintModule.checkPrinterStatus(printer);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=api.js.map
|
package/src/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["api.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AAGtD,MAAM,UAAU,KAAK,CAAC,GAAa;IACjC,OAAO,kBAAkB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAA4B;IACtD,OAAO,kBAAkB,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,OAAgB;IAEhB,OAAO,kBAAkB,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACxD,CAAC","sourcesContent":["// API wrapper for expo-hiu-print\nimport ExpoHiuPrintModule from \"./ExpoHiuPrintModule\";\nimport type { PrintJob, Printer } from \"./types\";\n\nexport function print(job: PrintJob): Promise<void> {\n return ExpoHiuPrintModule.print(job);\n}\n\nexport function openCashBox(job: { printers: Printer[] }): Promise<void> {\n return ExpoHiuPrintModule.openCashBox(job);\n}\n\nexport function checkPrinterStatus(\n printer: Printer,\n): Promise<\"connected\" | \"disconnected\"> {\n return ExpoHiuPrintModule.checkPrinterStatus(printer);\n}\n"]}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// API wrapper for expo-hiu-print
|
|
2
|
+
import ExpoHiuPrintModule from "./ExpoHiuPrintModule";
|
|
3
|
+
import type { PrintJob, Printer } from "./types";
|
|
4
|
+
|
|
5
|
+
export function print(job: PrintJob): Promise<void> {
|
|
6
|
+
return ExpoHiuPrintModule.print(job);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function openCashBox(job: { printers: Printer[] }): Promise<void> {
|
|
10
|
+
return ExpoHiuPrintModule.openCashBox(job);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function checkPrinterStatus(
|
|
14
|
+
printer: Printer,
|
|
15
|
+
): Promise<"connected" | "disconnected"> {
|
|
16
|
+
return ExpoHiuPrintModule.checkPrinterStatus(printer);
|
|
17
|
+
}
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NativeModule } from "react-native";
|
|
2
|
+
export interface Printer {
|
|
3
|
+
name: string;
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
paperWidth: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PrintElement {
|
|
9
|
+
type: "text" | "line" | "table" | "qr" | "feed" | "cut";
|
|
10
|
+
value?: string;
|
|
11
|
+
align?: "left" | "center" | "right";
|
|
12
|
+
bold?: boolean;
|
|
13
|
+
size?: "small" | "normal" | "large";
|
|
14
|
+
headers?: {
|
|
15
|
+
text: string;
|
|
16
|
+
align?: "left" | "center" | "right";
|
|
17
|
+
bold?: boolean;
|
|
18
|
+
}[];
|
|
19
|
+
rows?: {
|
|
20
|
+
text: string;
|
|
21
|
+
align?: "left" | "center" | "right";
|
|
22
|
+
bold?: boolean;
|
|
23
|
+
}[][];
|
|
24
|
+
data?: string;
|
|
25
|
+
lines?: number;
|
|
26
|
+
widthPercent?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface PrintJob {
|
|
29
|
+
printers: Printer[];
|
|
30
|
+
elements: PrintElement[];
|
|
31
|
+
}
|
|
32
|
+
export interface ExpoHiuPrintModuleType extends NativeModule {
|
|
33
|
+
print(job: PrintJob): Promise<void>;
|
|
34
|
+
openCashBox(job: {
|
|
35
|
+
printers: Printer[];
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
checkPrinterStatus(printer: Printer): Promise<"connected" | "disconnected">;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,KAAK,CAAC;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,OAAO,CAAC,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;QACpC,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,EAAE,CAAC;IACJ,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;QACpC,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,EAAE,EAAE,CAAC;IACN,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAC1D,KAAK,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,WAAW,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,GAAG,cAAc,CAAC,CAAC;CAC7E"}
|
package/src/types.js
ADDED
package/src/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"","sourcesContent":["// Types for expo-hiu-print\nimport { NativeModule } from \"react-native\";\n\nexport interface Printer {\n name: string;\n host: string;\n port: number;\n paperWidth: number;\n}\n\nexport interface PrintElement {\n type: \"text\" | \"line\" | \"table\" | \"qr\" | \"feed\" | \"cut\";\n value?: string;\n align?: \"left\" | \"center\" | \"right\";\n bold?: boolean;\n size?: \"small\" | \"normal\" | \"large\";\n headers?: {\n text: string;\n align?: \"left\" | \"center\" | \"right\";\n bold?: boolean;\n }[];\n rows?: {\n text: string;\n align?: \"left\" | \"center\" | \"right\";\n bold?: boolean;\n }[][];\n data?: string;\n lines?: number;\n widthPercent?: number;\n}\n\nexport interface PrintJob {\n printers: Printer[];\n elements: PrintElement[];\n}\n\nexport interface ExpoHiuPrintModuleType extends NativeModule {\n print(job: PrintJob): Promise<void>;\n openCashBox(job: { printers: Printer[] }): Promise<void>;\n checkPrinterStatus(printer: Printer): Promise<\"connected\" | \"disconnected\">;\n}\n"]}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Types for expo-hiu-print
|
|
2
|
+
import { NativeModule } from "react-native";
|
|
3
|
+
|
|
4
|
+
export interface Printer {
|
|
5
|
+
name: string;
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
paperWidth: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PrintElement {
|
|
12
|
+
type: "text" | "line" | "table" | "qr" | "feed" | "cut";
|
|
13
|
+
value?: string;
|
|
14
|
+
align?: "left" | "center" | "right";
|
|
15
|
+
bold?: boolean;
|
|
16
|
+
size?: "small" | "normal" | "large";
|
|
17
|
+
headers?: {
|
|
18
|
+
text: string;
|
|
19
|
+
align?: "left" | "center" | "right";
|
|
20
|
+
bold?: boolean;
|
|
21
|
+
}[];
|
|
22
|
+
rows?: {
|
|
23
|
+
text: string;
|
|
24
|
+
align?: "left" | "center" | "right";
|
|
25
|
+
bold?: boolean;
|
|
26
|
+
}[][];
|
|
27
|
+
data?: string;
|
|
28
|
+
lines?: number;
|
|
29
|
+
widthPercent?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PrintJob {
|
|
33
|
+
printers: Printer[];
|
|
34
|
+
elements: PrintElement[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExpoHiuPrintModuleType extends NativeModule {
|
|
38
|
+
print(job: PrintJob): Promise<void>;
|
|
39
|
+
openCashBox(job: { printers: Printer[] }): Promise<void>;
|
|
40
|
+
checkPrinterStatus(printer: Printer): Promise<"connected" | "disconnected">;
|
|
41
|
+
}
|