@delicity/capacitor-thermal-printer 7.0.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/DelicityThermalPrinter.podspec +28 -0
- package/LICENSE +21 -0
- package/README.md +649 -0
- package/android/build.gradle +122 -0
- package/android/src/main/AndroidManifest.xml +38 -0
- package/android/src/main/java/com/delicity/thermalprinter/Logger.kt +50 -0
- package/android/src/main/java/com/delicity/thermalprinter/ThermalPrinterEngine.kt +528 -0
- package/android/src/main/java/com/delicity/thermalprinter/ThermalPrinterPlugin.kt +334 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/BleAdapter.kt +125 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/BrotherAdapter.kt +206 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/EpsonAdapter.kt +384 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosAdapter.kt +160 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosCommands.kt +42 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosTextEncoder.kt +138 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/PrinterAdapter.kt +95 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/RawTcpAdapter.kt +96 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/SdkContract.kt +158 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/SdkReflect.kt +104 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/StarAdapter.kt +322 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/UsbAdapter.kt +248 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/ZebraAdapter.kt +207 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/AdapterPriority.kt +39 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/BleScanner.kt +70 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/BluetoothClassicScanner.kt +112 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/DiscoveryManager.kt +136 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/TcpScanner.kt +96 -0
- package/android/src/main/java/com/delicity/thermalprinter/image/ImageCache.kt +88 -0
- package/android/src/main/java/com/delicity/thermalprinter/image/ImageProcessor.kt +220 -0
- package/android/src/main/java/com/delicity/thermalprinter/image/TextRasterizer.kt +99 -0
- package/android/src/main/java/com/delicity/thermalprinter/model/Models.kt +206 -0
- package/android/src/main/java/com/delicity/thermalprinter/model/PrintItem.kt +100 -0
- package/android/src/main/java/com/delicity/thermalprinter/store/PrinterStore.kt +71 -0
- package/android/src/main/java/com/delicity/thermalprinter/transport/BleGattClient.kt +201 -0
- package/android/src/main/java/com/delicity/thermalprinter/transport/BluetoothSppTransport.kt +110 -0
- package/android/src/main/java/com/delicity/thermalprinter/transport/ByteTransport.kt +18 -0
- package/android/src/main/java/com/delicity/thermalprinter/transport/TcpTransport.kt +83 -0
- package/dist/esm/adapters/dedup.d.ts +26 -0
- package/dist/esm/adapters/dedup.js +66 -0
- package/dist/esm/adapters/dedup.js.map +1 -0
- package/dist/esm/adapters/priority.d.ts +29 -0
- package/dist/esm/adapters/priority.js +55 -0
- package/dist/esm/adapters/priority.js.map +1 -0
- package/dist/esm/core/enums.d.ts +61 -0
- package/dist/esm/core/enums.js +25 -0
- package/dist/esm/core/enums.js.map +1 -0
- package/dist/esm/core/errors.d.ts +16 -0
- package/dist/esm/core/errors.js +53 -0
- package/dist/esm/core/errors.js.map +1 -0
- package/dist/esm/core/escpos-text.d.ts +33 -0
- package/dist/esm/core/escpos-text.js +239 -0
- package/dist/esm/core/escpos-text.js.map +1 -0
- package/dist/esm/core/imaging.d.ts +91 -0
- package/dist/esm/core/imaging.js +184 -0
- package/dist/esm/core/imaging.js.map +1 -0
- package/dist/esm/core/models.d.ts +131 -0
- package/dist/esm/core/models.js +2 -0
- package/dist/esm/core/models.js.map +1 -0
- package/dist/esm/core/options.d.ts +154 -0
- package/dist/esm/core/options.js +2 -0
- package/dist/esm/core/options.js.map +1 -0
- package/dist/esm/core/text.d.ts +138 -0
- package/dist/esm/core/text.js +14 -0
- package/dist/esm/core/text.js.map +1 -0
- package/dist/esm/definitions.d.ts +155 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +63 -0
- package/dist/esm/web.js +112 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +224 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +227 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/Adapters/BrotherAdapter.swift +139 -0
- package/ios/Plugin/Adapters/EpsonAdapter.swift +131 -0
- package/ios/Plugin/Adapters/EscPosAdapter.swift +106 -0
- package/ios/Plugin/Adapters/EscPosCommands.swift +32 -0
- package/ios/Plugin/Adapters/EscPosTextEncoder.swift +115 -0
- package/ios/Plugin/Adapters/PrinterAdapter.swift +44 -0
- package/ios/Plugin/Adapters/RawTcpAdapter.swift +70 -0
- package/ios/Plugin/Adapters/StarAdapter.swift +305 -0
- package/ios/Plugin/Adapters/ZebraAdapter.swift +119 -0
- package/ios/Plugin/Discovery/AdapterPriority.swift +21 -0
- package/ios/Plugin/Discovery/BonjourScanner.swift +51 -0
- package/ios/Plugin/Discovery/DiscoveryManager.swift +86 -0
- package/ios/Plugin/Image/ImageCache.swift +73 -0
- package/ios/Plugin/Image/ImageProcessor.swift +168 -0
- package/ios/Plugin/Image/TextRasterizer.swift +81 -0
- package/ios/Plugin/Logger.swift +33 -0
- package/ios/Plugin/Model/Models.swift +174 -0
- package/ios/Plugin/Model/PrintItem.swift +111 -0
- package/ios/Plugin/Store/PrinterStore.swift +51 -0
- package/ios/Plugin/ThermalPrinterEngine.swift +395 -0
- package/ios/Plugin/ThermalPrinterPlugin.m +22 -0
- package/ios/Plugin/ThermalPrinterPlugin.swift +258 -0
- package/ios/Plugin/Transport/TcpTransport.swift +89 -0
- package/package.json +96 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
|
|
5
|
+
/// Pipeline image -> raster thermique (miroir iOS de ImageProcessor.kt).
|
|
6
|
+
///
|
|
7
|
+
/// Étapes : decode -> resize largeur -> aplatir sur blanc -> niveaux de gris ->
|
|
8
|
+
/// binarisation (threshold / Floyd-Steinberg / Atkinson) -> raster ESC/POS GS v 0.
|
|
9
|
+
enum ImageProcessor {
|
|
10
|
+
|
|
11
|
+
private static let MAX_HEIGHT = 20_000
|
|
12
|
+
|
|
13
|
+
// MARK: - Décodage
|
|
14
|
+
|
|
15
|
+
static func decodeFile(_ path: String) throws -> UIImage {
|
|
16
|
+
let clean = path.replacingOccurrences(of: "file://", with: "")
|
|
17
|
+
guard FileManager.default.fileExists(atPath: clean),
|
|
18
|
+
let img = UIImage(contentsOfFile: clean) else {
|
|
19
|
+
throw PrinterError(.IMAGE_INVALID, "Fichier image introuvable: \(clean)")
|
|
20
|
+
}
|
|
21
|
+
return img
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static func decodeBase64(_ b64: String) throws -> UIImage {
|
|
25
|
+
let payload = b64.contains("base64,") ? String(b64.split(separator: ",").last ?? "") : b64
|
|
26
|
+
guard let data = Data(base64Encoded: payload, options: .ignoreUnknownCharacters),
|
|
27
|
+
let img = UIImage(data: data) else {
|
|
28
|
+
throw PrinterError(.IMAGE_INVALID, "Base64 image invalide")
|
|
29
|
+
}
|
|
30
|
+
return img
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// MARK: - Resize sur fond blanc
|
|
34
|
+
|
|
35
|
+
static func resizeToWidth(_ image: UIImage, targetWidth: Int) throws -> UIImage {
|
|
36
|
+
let w = max(8, targetWidth)
|
|
37
|
+
let ratio = CGFloat(w) / image.size.width
|
|
38
|
+
let h = Int((image.size.height * ratio).rounded())
|
|
39
|
+
guard h <= MAX_HEIGHT else {
|
|
40
|
+
throw PrinterError(.IMAGE_TOO_LARGE, "Image trop haute: \(h)px (max \(MAX_HEIGHT))")
|
|
41
|
+
}
|
|
42
|
+
let size = CGSize(width: w, height: h)
|
|
43
|
+
let format = UIGraphicsImageRendererFormat()
|
|
44
|
+
format.scale = 1
|
|
45
|
+
format.opaque = true
|
|
46
|
+
let renderer = UIGraphicsImageRenderer(size: size, format: format)
|
|
47
|
+
return renderer.image { ctx in
|
|
48
|
+
UIColor.white.setFill()
|
|
49
|
+
ctx.fill(CGRect(origin: .zero, size: size))
|
|
50
|
+
image.draw(in: CGRect(origin: .zero, size: size))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - Niveaux de gris
|
|
55
|
+
|
|
56
|
+
/// Extrait un buffer 8-bit gris (0=noir..255=blanc) en redessinant en contexte gris.
|
|
57
|
+
private static func grayscaleBuffer(_ image: UIImage) throws -> (px: [UInt8], w: Int, h: Int) {
|
|
58
|
+
guard let cg = image.cgImage else { throw PrinterError(.IMAGE_INVALID, "CGImage indisponible") }
|
|
59
|
+
let w = cg.width
|
|
60
|
+
let h = cg.height
|
|
61
|
+
var buffer = [UInt8](repeating: 0, count: w * h)
|
|
62
|
+
let colorSpace = CGColorSpaceCreateDeviceGray()
|
|
63
|
+
guard let ctx = CGContext(
|
|
64
|
+
data: &buffer, width: w, height: h, bitsPerComponent: 8, bytesPerRow: w,
|
|
65
|
+
space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue
|
|
66
|
+
) else { throw PrinterError(.IMAGE_INVALID, "Contexte gris impossible") }
|
|
67
|
+
ctx.draw(cg, in: CGRect(x: 0, y: 0, width: w, height: h))
|
|
68
|
+
return (buffer, w, h)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// MARK: - Binarisation
|
|
72
|
+
|
|
73
|
+
/// Produit un MonoBitmap (1 = encre/noir).
|
|
74
|
+
/// Si `options.grayscale == false`, l'image est considérée déjà préparée : seuil simple.
|
|
75
|
+
static func toMono(_ image: UIImage, options: RenderOptions) throws -> MonoBitmap {
|
|
76
|
+
var (gray, w, h) = try grayscaleBuffer(image)
|
|
77
|
+
if options.invert {
|
|
78
|
+
for i in 0..<gray.count { gray[i] = 255 - gray[i] }
|
|
79
|
+
}
|
|
80
|
+
let data: [UInt8]
|
|
81
|
+
switch (options.grayscale, options.dithering) {
|
|
82
|
+
case (false, _): data = threshold(gray, options.threshold)
|
|
83
|
+
case (_, "none"): data = threshold(gray, options.threshold)
|
|
84
|
+
case (_, "atkinson"): data = atkinson(gray, w, h)
|
|
85
|
+
default: data = floydSteinberg(gray, w, h)
|
|
86
|
+
}
|
|
87
|
+
return MonoBitmap(width: w, height: h, data: data)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private static func threshold(_ gray: [UInt8], _ t: Int) -> [UInt8] {
|
|
91
|
+
let tt = UInt8(clamping: t)
|
|
92
|
+
return gray.map { $0 < tt ? UInt8(1) : UInt8(0) }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static func floydSteinberg(_ grayInput: [UInt8], _ w: Int, _ h: Int) -> [UInt8] {
|
|
96
|
+
var gray = grayInput.map { Float($0) }
|
|
97
|
+
var out = [UInt8](repeating: 0, count: grayInput.count)
|
|
98
|
+
func at(_ x: Int, _ y: Int) -> Int { y * w + x }
|
|
99
|
+
for y in 0..<h {
|
|
100
|
+
for x in 0..<w {
|
|
101
|
+
let idx = at(x, y)
|
|
102
|
+
let old = gray[idx]
|
|
103
|
+
let new: Float = old < 128 ? 0 : 255
|
|
104
|
+
out[idx] = new == 0 ? 1 : 0
|
|
105
|
+
let err = old - new
|
|
106
|
+
if x + 1 < w { gray[at(x + 1, y)] += err * 7 / 16 }
|
|
107
|
+
if x - 1 >= 0 && y + 1 < h { gray[at(x - 1, y + 1)] += err * 3 / 16 }
|
|
108
|
+
if y + 1 < h { gray[at(x, y + 1)] += err * 5 / 16 }
|
|
109
|
+
if x + 1 < w && y + 1 < h { gray[at(x + 1, y + 1)] += err / 16 }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private static func atkinson(_ grayInput: [UInt8], _ w: Int, _ h: Int) -> [UInt8] {
|
|
116
|
+
var gray = grayInput.map { Float($0) }
|
|
117
|
+
var out = [UInt8](repeating: 0, count: grayInput.count)
|
|
118
|
+
func at(_ x: Int, _ y: Int) -> Int { y * w + x }
|
|
119
|
+
func spread(_ x: Int, _ y: Int, _ e: Float) {
|
|
120
|
+
if x >= 0 && x < w && y >= 0 && y < h { gray[at(x, y)] += e }
|
|
121
|
+
}
|
|
122
|
+
for y in 0..<h {
|
|
123
|
+
for x in 0..<w {
|
|
124
|
+
let idx = at(x, y)
|
|
125
|
+
let old = gray[idx]
|
|
126
|
+
let new: Float = old < 128 ? 0 : 255
|
|
127
|
+
out[idx] = new == 0 ? 1 : 0
|
|
128
|
+
let err = (old - new) / 8
|
|
129
|
+
spread(x + 1, y, err); spread(x + 2, y, err)
|
|
130
|
+
spread(x - 1, y + 1, err); spread(x, y + 1, err); spread(x + 1, y + 1, err)
|
|
131
|
+
spread(x, y + 2, err)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// MARK: - Raster ESC/POS GS v 0
|
|
138
|
+
|
|
139
|
+
static func encodeEscPosRaster(_ mono: MonoBitmap) -> [UInt8] {
|
|
140
|
+
let w = mono.width
|
|
141
|
+
let h = mono.height
|
|
142
|
+
let bytesPerRow = (w + 7) / 8
|
|
143
|
+
let xL = UInt8(bytesPerRow & 0xff)
|
|
144
|
+
let xH = UInt8((bytesPerRow >> 8) & 0xff)
|
|
145
|
+
let yL = UInt8(h & 0xff)
|
|
146
|
+
let yH = UInt8((h >> 8) & 0xff)
|
|
147
|
+
var out: [UInt8] = [0x1D, 0x76, 0x30, 0x00, xL, xH, yL, yH]
|
|
148
|
+
var body = [UInt8](repeating: 0, count: bytesPerRow * h)
|
|
149
|
+
for y in 0..<h {
|
|
150
|
+
let rowOff = y * bytesPerRow
|
|
151
|
+
let srcOff = y * w
|
|
152
|
+
for x in 0..<w where mono.data[srcOff + x] == 1 {
|
|
153
|
+
let byteIndex = rowOff + (x >> 3)
|
|
154
|
+
let bit = 7 - (x & 7)
|
|
155
|
+
body[byteIndex] |= UInt8(1 << bit)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
out.append(contentsOf: body)
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Image 1-bit : data[i]=1 => point noir/encre.
|
|
164
|
+
struct MonoBitmap {
|
|
165
|
+
let width: Int
|
|
166
|
+
let height: Int
|
|
167
|
+
let data: [UInt8]
|
|
168
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Rend une liste d'items texte (`printText`) en **UIImage**, pour les adapters dont
|
|
5
|
+
/// le SDK n'a pas de builder texte natif (Brother, Zebra). Le moteur appelle ensuite
|
|
6
|
+
/// `printImage` → le SDK imprime l'image.
|
|
7
|
+
///
|
|
8
|
+
/// Police monospace (alignement colonne fiable). Supporte texte (align, gras,
|
|
9
|
+
/// souligné, multiplicateur de taille), séparateur, saut de ligne. Les items
|
|
10
|
+
/// QR/code-barres sont rendus en texte de repli ; image/raw/cut/tiroir ignorés.
|
|
11
|
+
enum TextRasterizer {
|
|
12
|
+
|
|
13
|
+
private struct Line {
|
|
14
|
+
let text: String
|
|
15
|
+
let sizeMul: Int
|
|
16
|
+
let bold: Bool
|
|
17
|
+
let underline: Bool
|
|
18
|
+
let align: String
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static func render(_ items: [PrintItem], widthDots: Int) -> UIImage {
|
|
22
|
+
let width = CGFloat(max(128, widthDots))
|
|
23
|
+
let columns = widthDots <= 420 ? 32 : 48
|
|
24
|
+
|
|
25
|
+
// Taille de base : caler N caractères monospace sur la largeur cible.
|
|
26
|
+
var fontSize: CGFloat = 24
|
|
27
|
+
let probe = UIFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
|
|
28
|
+
let charW = ("M" as NSString).size(withAttributes: [.font: probe]).width
|
|
29
|
+
fontSize = fontSize * (width / CGFloat(columns)) / max(1, charW)
|
|
30
|
+
let baseFont = UIFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
|
|
31
|
+
let baseLineH = baseFont.lineHeight
|
|
32
|
+
|
|
33
|
+
var lines: [Line?] = []
|
|
34
|
+
for item in items {
|
|
35
|
+
switch item {
|
|
36
|
+
case let .text(value, style):
|
|
37
|
+
for raw in value.components(separatedBy: "\n") {
|
|
38
|
+
lines.append(Line(text: raw, sizeMul: min(max(style.heightMultiplier, 1), 6),
|
|
39
|
+
bold: style.bold, underline: style.underline != "none", align: style.align ?? "left"))
|
|
40
|
+
}
|
|
41
|
+
case let .divider(char, cols, align, bold):
|
|
42
|
+
let n = cols ?? columns
|
|
43
|
+
let c = char.isEmpty ? "-" : String(char.prefix(1))
|
|
44
|
+
lines.append(Line(text: String(repeating: c, count: min(max(n, 1), 96)), sizeMul: 1, bold: bold, underline: false, align: align ?? "left"))
|
|
45
|
+
case let .feed(n):
|
|
46
|
+
for _ in 0..<min(max(n, 1), 20) { lines.append(nil) }
|
|
47
|
+
case let .qrcode(value, _, _, align):
|
|
48
|
+
lines.append(Line(text: "[QR] \(value)", sizeMul: 1, bold: false, underline: false, align: align))
|
|
49
|
+
case let .barcode(value, symbology, _, _, _, align):
|
|
50
|
+
lines.append(Line(text: "[\(symbology)] \(value)", sizeMul: 1, bold: false, underline: false, align: align))
|
|
51
|
+
case .cut, .cashDrawer, .image, .raw:
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if lines.isEmpty { lines.append(nil) }
|
|
56
|
+
|
|
57
|
+
let totalH = lines.reduce(CGFloat(0)) { $0 + CGFloat($1?.sizeMul ?? 1) * baseLineH } + 8
|
|
58
|
+
let height = max(baseLineH, totalH)
|
|
59
|
+
|
|
60
|
+
let format = UIGraphicsImageRendererFormat.default()
|
|
61
|
+
format.scale = 1
|
|
62
|
+
format.opaque = true
|
|
63
|
+
let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height), format: format)
|
|
64
|
+
return renderer.image { ctx in
|
|
65
|
+
UIColor.white.setFill()
|
|
66
|
+
ctx.fill(CGRect(x: 0, y: 0, width: width, height: height))
|
|
67
|
+
var y: CGFloat = 0
|
|
68
|
+
for line in lines {
|
|
69
|
+
guard let l = line else { y += baseLineH; continue }
|
|
70
|
+
let font = UIFont.monospacedSystemFont(ofSize: fontSize * CGFloat(l.sizeMul), weight: l.bold ? .bold : .regular)
|
|
71
|
+
var attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: UIColor.black]
|
|
72
|
+
if l.underline { attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue }
|
|
73
|
+
let ns = l.text as NSString
|
|
74
|
+
let tw = ns.size(withAttributes: attrs).width
|
|
75
|
+
let x: CGFloat = l.align == "center" ? max(0, (width - tw) / 2) : (l.align == "right" ? max(0, width - tw) : 0)
|
|
76
|
+
ns.draw(at: CGPoint(x: x, y: y), withAttributes: attrs)
|
|
77
|
+
y += font.lineHeight
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os.log
|
|
3
|
+
|
|
4
|
+
/// Logger circulaire en mémoire + os_log, pour le diagnostic support (miroir Logger.kt).
|
|
5
|
+
final class Logger {
|
|
6
|
+
static let shared = Logger()
|
|
7
|
+
private let maxLines = 500
|
|
8
|
+
private var ring: [[String: Any]] = []
|
|
9
|
+
private let queue = DispatchQueue(label: "thermalprinter.logger")
|
|
10
|
+
var verbose = false
|
|
11
|
+
private let osLog = OSLog(subsystem: "com.delicity.thermalprinter", category: "plugin")
|
|
12
|
+
|
|
13
|
+
func log(_ category: String, _ message: String, _ data: [String: Any]? = nil) {
|
|
14
|
+
queue.sync {
|
|
15
|
+
var entry: [String: Any] = [
|
|
16
|
+
"ts": Date().timeIntervalSince1970 * 1000,
|
|
17
|
+
"category": category, "message": message,
|
|
18
|
+
]
|
|
19
|
+
data?.forEach { entry[$0.key] = $0.value }
|
|
20
|
+
if ring.count >= maxLines { ring.removeFirst() }
|
|
21
|
+
ring.append(entry)
|
|
22
|
+
if verbose { os_log("[%{public}@] %{public}@", log: osLog, type: .debug, category, message) }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func error(_ category: String, _ message: String) {
|
|
27
|
+
log(category, message, ["level": "error"])
|
|
28
|
+
os_log("[%{public}@] %{public}@", log: osLog, type: .error, category, message)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func snapshot() -> [[String: Any]] { queue.sync { ring } }
|
|
32
|
+
func clear() { queue.sync { ring.removeAll() } }
|
|
33
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Transports physiques. Aligné avec PrinterTransport (TypeScript).
|
|
4
|
+
enum Transport: String, Codable {
|
|
5
|
+
case wifi, ethernet, bluetooth, ble, usb
|
|
6
|
+
static func from(_ v: String?) -> Transport { Transport(rawValue: v ?? "") ?? .wifi }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/// Identifiants d'adapter. Aligné avec PrinterAdapterId (TypeScript).
|
|
10
|
+
enum AdapterId: String, Codable {
|
|
11
|
+
case escpos, epson, star, brother, zebra
|
|
12
|
+
case rawTcp = "rawTcp"
|
|
13
|
+
static func from(_ v: String?) -> AdapterId { AdapterId(rawValue: v ?? "") ?? .escpos }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Codes d'erreur normalisés. Aligné avec PrintErrorCode (TypeScript).
|
|
17
|
+
enum ErrorCode: String {
|
|
18
|
+
case PRINTER_NOT_FOUND, PRINTER_OFFLINE, CONNECTION_FAILED, PERMISSION_DENIED
|
|
19
|
+
case BLUETOOTH_DISABLED, WIFI_NOT_CONNECTED, PAIRING_REQUIRED, UNSUPPORTED_TRANSPORT
|
|
20
|
+
case UNSUPPORTED_PRINTER, IMAGE_INVALID, IMAGE_TOO_LARGE, PRINT_FAILED, PAPER_EMPTY
|
|
21
|
+
case COVER_OPEN, SDK_NOT_AVAILABLE, TIMEOUT, UNKNOWN
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Erreur interne portant un code normalisé.
|
|
25
|
+
struct PrinterError: Error {
|
|
26
|
+
let code: ErrorCode
|
|
27
|
+
let message: String
|
|
28
|
+
let detail: String?
|
|
29
|
+
let retryable: Bool
|
|
30
|
+
init(_ code: ErrorCode, _ message: String, detail: String? = nil, retryable: Bool = false) {
|
|
31
|
+
self.code = code; self.message = message; self.detail = detail; self.retryable = retryable
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
struct Capabilities: Codable {
|
|
36
|
+
var paperWidthMm: Int = 80
|
|
37
|
+
var printableDots: Int = 576
|
|
38
|
+
var dpi: Int = 203
|
|
39
|
+
var supportsCut: Bool = true
|
|
40
|
+
var supportsCashDrawer: Bool = false
|
|
41
|
+
var supportsStatus: Bool = false
|
|
42
|
+
var supportsRasterImage: Bool = true
|
|
43
|
+
var supportsQrCode: Bool = false
|
|
44
|
+
var supportsBarcode: Bool = false
|
|
45
|
+
|
|
46
|
+
func toDict() -> [String: Any] {
|
|
47
|
+
[
|
|
48
|
+
"paperWidthMm": paperWidthMm, "printableDots": printableDots, "dpi": dpi,
|
|
49
|
+
"supportsCut": supportsCut, "supportsCashDrawer": supportsCashDrawer,
|
|
50
|
+
"supportsStatus": supportsStatus, "supportsRasterImage": supportsRasterImage,
|
|
51
|
+
"supportsQrCode": supportsQrCode, "supportsBarcode": supportsBarcode,
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static func fromDict(_ d: [String: Any]?) -> Capabilities {
|
|
56
|
+
guard let d = d else { return Capabilities() }
|
|
57
|
+
var c = Capabilities()
|
|
58
|
+
c.paperWidthMm = d["paperWidthMm"] as? Int ?? 80
|
|
59
|
+
c.printableDots = d["printableDots"] as? Int ?? 576
|
|
60
|
+
c.dpi = d["dpi"] as? Int ?? 203
|
|
61
|
+
c.supportsCut = d["supportsCut"] as? Bool ?? true
|
|
62
|
+
c.supportsCashDrawer = d["supportsCashDrawer"] as? Bool ?? false
|
|
63
|
+
c.supportsStatus = d["supportsStatus"] as? Bool ?? false
|
|
64
|
+
c.supportsRasterImage = d["supportsRasterImage"] as? Bool ?? true
|
|
65
|
+
c.supportsQrCode = d["supportsQrCode"] as? Bool ?? false
|
|
66
|
+
c.supportsBarcode = d["supportsBarcode"] as? Bool ?? false
|
|
67
|
+
return c
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
struct DiscoveredPrinter {
|
|
72
|
+
let id: String
|
|
73
|
+
let name: String
|
|
74
|
+
var brand: String?
|
|
75
|
+
var model: String?
|
|
76
|
+
let transport: Transport
|
|
77
|
+
var adapter: AdapterId
|
|
78
|
+
let address: String
|
|
79
|
+
var capabilities: Capabilities?
|
|
80
|
+
var discoveredBy: Set<String> = []
|
|
81
|
+
var lastSeenAt: Double = Date().timeIntervalSince1970 * 1000
|
|
82
|
+
var isDefault: Bool = false
|
|
83
|
+
var isConnected: Bool = false
|
|
84
|
+
|
|
85
|
+
func toDict() -> [String: Any] {
|
|
86
|
+
var d: [String: Any] = [
|
|
87
|
+
"id": id, "name": name, "transport": transport.rawValue, "adapter": adapter.rawValue,
|
|
88
|
+
"address": address, "lastSeenAt": lastSeenAt, "isDefault": isDefault, "isConnected": isConnected,
|
|
89
|
+
"discoveredBy": Array(discoveredBy),
|
|
90
|
+
]
|
|
91
|
+
if let brand = brand { d["brand"] = brand }
|
|
92
|
+
if let model = model { d["model"] = model }
|
|
93
|
+
if let caps = capabilities { d["capabilities"] = caps.toDict() }
|
|
94
|
+
return d
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
struct PrinterProfile {
|
|
99
|
+
let id: String
|
|
100
|
+
var adapter: AdapterId
|
|
101
|
+
let transport: Transport
|
|
102
|
+
let address: String
|
|
103
|
+
var brand: String?
|
|
104
|
+
var model: String?
|
|
105
|
+
let name: String
|
|
106
|
+
var capabilities: Capabilities
|
|
107
|
+
var adapterMeta: [String: Any] = [:]
|
|
108
|
+
var isDefault: Bool = false
|
|
109
|
+
var createdAt: Double = Date().timeIntervalSince1970 * 1000
|
|
110
|
+
var updatedAt: Double = Date().timeIntervalSince1970 * 1000
|
|
111
|
+
|
|
112
|
+
func toDict() -> [String: Any] {
|
|
113
|
+
var d: [String: Any] = [
|
|
114
|
+
"id": id, "adapter": adapter.rawValue, "transport": transport.rawValue, "address": address,
|
|
115
|
+
"name": name, "capabilities": capabilities.toDict(), "adapterMeta": adapterMeta,
|
|
116
|
+
"isDefault": isDefault, "createdAt": createdAt, "updatedAt": updatedAt,
|
|
117
|
+
]
|
|
118
|
+
if let brand = brand { d["brand"] = brand }
|
|
119
|
+
if let model = model { d["model"] = model }
|
|
120
|
+
return d
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static func fromDict(_ d: [String: Any]) -> PrinterProfile {
|
|
124
|
+
PrinterProfile(
|
|
125
|
+
id: d["id"] as? String ?? "",
|
|
126
|
+
adapter: AdapterId.from(d["adapter"] as? String),
|
|
127
|
+
transport: Transport.from(d["transport"] as? String),
|
|
128
|
+
address: d["address"] as? String ?? "",
|
|
129
|
+
brand: d["brand"] as? String,
|
|
130
|
+
model: d["model"] as? String,
|
|
131
|
+
name: d["name"] as? String ?? "",
|
|
132
|
+
capabilities: Capabilities.fromDict(d["capabilities"] as? [String: Any]),
|
|
133
|
+
adapterMeta: d["adapterMeta"] as? [String: Any] ?? [:],
|
|
134
|
+
isDefault: d["isDefault"] as? Bool ?? false,
|
|
135
|
+
createdAt: d["createdAt"] as? Double ?? Date().timeIntervalSince1970 * 1000,
|
|
136
|
+
updatedAt: d["updatedAt"] as? Double ?? Date().timeIntervalSince1970 * 1000
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
struct PrinterStatus {
|
|
142
|
+
let id: String
|
|
143
|
+
var connection: String
|
|
144
|
+
var online: Bool
|
|
145
|
+
var paper: String
|
|
146
|
+
var coverOpen: Bool?
|
|
147
|
+
var errorCode: ErrorCode?
|
|
148
|
+
var rawStatus: String?
|
|
149
|
+
var checkedAt: Double = Date().timeIntervalSince1970 * 1000
|
|
150
|
+
|
|
151
|
+
func toDict() -> [String: Any] {
|
|
152
|
+
var d: [String: Any] = [
|
|
153
|
+
"id": id, "connection": connection, "online": online, "paper": paper, "checkedAt": checkedAt,
|
|
154
|
+
]
|
|
155
|
+
if let coverOpen = coverOpen { d["coverOpen"] = coverOpen }
|
|
156
|
+
if let errorCode = errorCode { d["errorCode"] = errorCode.rawValue }
|
|
157
|
+
if let rawStatus = rawStatus { d["rawStatus"] = rawStatus }
|
|
158
|
+
return d
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
struct RenderOptions {
|
|
163
|
+
var widthDots: Int
|
|
164
|
+
var resize: Bool = true
|
|
165
|
+
var grayscale: Bool = true
|
|
166
|
+
var threshold: Int = 128
|
|
167
|
+
var dithering: String = "floyd_steinberg"
|
|
168
|
+
var align: String = "center"
|
|
169
|
+
var invert: Bool = false
|
|
170
|
+
var cut: Bool = true
|
|
171
|
+
var feedLines: Int = 3
|
|
172
|
+
var openCashDrawer: Bool = false
|
|
173
|
+
var copies: Int = 1
|
|
174
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Modèle d'items texte iOS (miroir de src/core/text.ts et de PrintItem.kt).
|
|
4
|
+
|
|
5
|
+
struct TextStyle {
|
|
6
|
+
var align: String?
|
|
7
|
+
var bold = false
|
|
8
|
+
var underline = "none"
|
|
9
|
+
var font = "A"
|
|
10
|
+
var widthMultiplier = 1
|
|
11
|
+
var heightMultiplier = 1
|
|
12
|
+
var doubleStrike = false
|
|
13
|
+
var invert = false
|
|
14
|
+
var upsideDown = false
|
|
15
|
+
var rotate90 = false
|
|
16
|
+
var letterSpacing: Int?
|
|
17
|
+
var lineSpacing: Int?
|
|
18
|
+
var codePage: String?
|
|
19
|
+
var codePageId: Int?
|
|
20
|
+
var newline = true
|
|
21
|
+
|
|
22
|
+
static func fromDict(_ d: [String: Any]?) -> TextStyle {
|
|
23
|
+
guard let d = d else { return TextStyle() }
|
|
24
|
+
var s = TextStyle()
|
|
25
|
+
s.align = d["align"] as? String
|
|
26
|
+
s.bold = d["bold"] as? Bool ?? false
|
|
27
|
+
s.underline = d["underline"] as? String ?? "none"
|
|
28
|
+
s.font = d["font"] as? String ?? "A"
|
|
29
|
+
s.widthMultiplier = d["widthMultiplier"] as? Int ?? 1
|
|
30
|
+
s.heightMultiplier = d["heightMultiplier"] as? Int ?? 1
|
|
31
|
+
s.doubleStrike = d["doubleStrike"] as? Bool ?? false
|
|
32
|
+
s.invert = d["invert"] as? Bool ?? false
|
|
33
|
+
s.upsideDown = d["upsideDown"] as? Bool ?? false
|
|
34
|
+
s.rotate90 = d["rotate90"] as? Bool ?? false
|
|
35
|
+
s.letterSpacing = d["letterSpacing"] as? Int
|
|
36
|
+
s.lineSpacing = d["lineSpacing"] as? Int
|
|
37
|
+
s.codePage = d["codePage"] as? String
|
|
38
|
+
s.codePageId = d["codePageId"] as? Int
|
|
39
|
+
s.newline = d["newline"] as? Bool ?? true
|
|
40
|
+
return s
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
enum PrintItem {
|
|
45
|
+
case text(value: String, style: TextStyle)
|
|
46
|
+
case feed(lines: Int)
|
|
47
|
+
case cut(mode: String, feedBefore: Int)
|
|
48
|
+
case divider(char: String, columns: Int?, align: String?, bold: Bool)
|
|
49
|
+
case qrcode(value: String, size: Int, ec: String, align: String)
|
|
50
|
+
case barcode(value: String, symbology: String, height: Int, width: Int, hri: String, align: String)
|
|
51
|
+
case cashDrawer(pin: Int)
|
|
52
|
+
case image(filePath: String?, url: String?, base64: String?, render: [String: Any]?)
|
|
53
|
+
case raw(bytesBase64: String)
|
|
54
|
+
|
|
55
|
+
static func parseList(_ arr: [[String: Any]]) -> [PrintItem] {
|
|
56
|
+
arr.compactMap { parse($0) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static func parse(_ d: [String: Any]) -> PrintItem? {
|
|
60
|
+
switch d["type"] as? String {
|
|
61
|
+
case "text":
|
|
62
|
+
return .text(value: d["value"] as? String ?? "", style: TextStyle.fromDict(d["style"] as? [String: Any]))
|
|
63
|
+
case "feed":
|
|
64
|
+
return .feed(lines: d["lines"] as? Int ?? 1)
|
|
65
|
+
case "cut":
|
|
66
|
+
return .cut(mode: d["mode"] as? String ?? "partial", feedBefore: d["feedBefore"] as? Int ?? 0)
|
|
67
|
+
case "divider":
|
|
68
|
+
let style = d["style"] as? [String: Any]
|
|
69
|
+
return .divider(char: d["char"] as? String ?? "-", columns: d["columns"] as? Int,
|
|
70
|
+
align: style?["align"] as? String, bold: style?["bold"] as? Bool ?? false)
|
|
71
|
+
case "qrcode":
|
|
72
|
+
return .qrcode(value: d["value"] as? String ?? "", size: d["size"] as? Int ?? 6,
|
|
73
|
+
ec: d["errorCorrection"] as? String ?? "M", align: d["align"] as? String ?? "center")
|
|
74
|
+
case "barcode":
|
|
75
|
+
return .barcode(value: d["value"] as? String ?? "", symbology: d["symbology"] as? String ?? "CODE128",
|
|
76
|
+
height: d["height"] as? Int ?? 80, width: d["width"] as? Int ?? 3,
|
|
77
|
+
hri: d["hri"] as? String ?? "below", align: d["align"] as? String ?? "center")
|
|
78
|
+
case "cashDrawer":
|
|
79
|
+
return .cashDrawer(pin: d["pin"] as? Int ?? 2)
|
|
80
|
+
case "image":
|
|
81
|
+
let img = d["image"] as? [String: Any]
|
|
82
|
+
return .image(filePath: img?["filePath"] as? String, url: img?["url"] as? String,
|
|
83
|
+
base64: img?["base64"] as? String, render: d["render"] as? [String: Any])
|
|
84
|
+
case "raw":
|
|
85
|
+
return .raw(bytesBase64: d["bytesBase64"] as? String ?? "")
|
|
86
|
+
default:
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Mise à jour d'état d'un job d'impression (émis via printJobStatus).
|
|
93
|
+
struct JobUpdate {
|
|
94
|
+
let jobId: String
|
|
95
|
+
let printerId: String
|
|
96
|
+
let state: String
|
|
97
|
+
var holdReason: String?
|
|
98
|
+
var progress: Double?
|
|
99
|
+
var errorCode: ErrorCode?
|
|
100
|
+
var message: String?
|
|
101
|
+
|
|
102
|
+
func toDict() -> [String: Any] {
|
|
103
|
+
var d: [String: Any] = ["jobId": jobId, "printerId": printerId, "state": state,
|
|
104
|
+
"updatedAt": Date().timeIntervalSince1970 * 1000]
|
|
105
|
+
if let holdReason = holdReason { d["holdReason"] = holdReason }
|
|
106
|
+
if let progress = progress { d["progress"] = progress }
|
|
107
|
+
if let errorCode = errorCode { d["errorCode"] = errorCode.rawValue }
|
|
108
|
+
if let message = message { d["message"] = message }
|
|
109
|
+
return d
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Persistance des profils via UserDefaults (JSON). Miroir de PrinterStore.kt.
|
|
4
|
+
final class PrinterStore {
|
|
5
|
+
|
|
6
|
+
private let key = "delicity.thermalprinter.profiles_v1"
|
|
7
|
+
private let defaults = UserDefaults.standard
|
|
8
|
+
private let queue = DispatchQueue(label: "thermalprinter.store")
|
|
9
|
+
|
|
10
|
+
func all() -> [PrinterProfile] {
|
|
11
|
+
queue.sync {
|
|
12
|
+
guard let data = defaults.data(forKey: key),
|
|
13
|
+
let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return [] }
|
|
14
|
+
return arr.map { PrinterProfile.fromDict($0) }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func get(_ id: String) -> PrinterProfile? { all().first { $0.id == id } }
|
|
19
|
+
func getDefault() -> PrinterProfile? { all().first { $0.isDefault } }
|
|
20
|
+
|
|
21
|
+
func upsert(_ profile: PrinterProfile) {
|
|
22
|
+
var list = all()
|
|
23
|
+
var p = profile
|
|
24
|
+
p.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
25
|
+
if let idx = list.firstIndex(where: { $0.id == p.id }) { list[idx] = p } else { list.append(p) }
|
|
26
|
+
persist(list)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@discardableResult
|
|
30
|
+
func setDefault(_ id: String) -> PrinterProfile? {
|
|
31
|
+
var list = all()
|
|
32
|
+
var target: PrinterProfile?
|
|
33
|
+
for i in list.indices {
|
|
34
|
+
list[i].isDefault = list[i].id == id
|
|
35
|
+
if list[i].isDefault { target = list[i] }
|
|
36
|
+
}
|
|
37
|
+
persist(list)
|
|
38
|
+
return target
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func remove(_ id: String) { persist(all().filter { $0.id != id }) }
|
|
42
|
+
|
|
43
|
+
private func persist(_ list: [PrinterProfile]) {
|
|
44
|
+
queue.sync {
|
|
45
|
+
let arr = list.map { $0.toDict() }
|
|
46
|
+
if let data = try? JSONSerialization.data(withJSONObject: arr) {
|
|
47
|
+
defaults.set(data, forKey: key)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|