@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.
Files changed (100) hide show
  1. package/DelicityThermalPrinter.podspec +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +649 -0
  4. package/android/build.gradle +122 -0
  5. package/android/src/main/AndroidManifest.xml +38 -0
  6. package/android/src/main/java/com/delicity/thermalprinter/Logger.kt +50 -0
  7. package/android/src/main/java/com/delicity/thermalprinter/ThermalPrinterEngine.kt +528 -0
  8. package/android/src/main/java/com/delicity/thermalprinter/ThermalPrinterPlugin.kt +334 -0
  9. package/android/src/main/java/com/delicity/thermalprinter/adapters/BleAdapter.kt +125 -0
  10. package/android/src/main/java/com/delicity/thermalprinter/adapters/BrotherAdapter.kt +206 -0
  11. package/android/src/main/java/com/delicity/thermalprinter/adapters/EpsonAdapter.kt +384 -0
  12. package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosAdapter.kt +160 -0
  13. package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosCommands.kt +42 -0
  14. package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosTextEncoder.kt +138 -0
  15. package/android/src/main/java/com/delicity/thermalprinter/adapters/PrinterAdapter.kt +95 -0
  16. package/android/src/main/java/com/delicity/thermalprinter/adapters/RawTcpAdapter.kt +96 -0
  17. package/android/src/main/java/com/delicity/thermalprinter/adapters/SdkContract.kt +158 -0
  18. package/android/src/main/java/com/delicity/thermalprinter/adapters/SdkReflect.kt +104 -0
  19. package/android/src/main/java/com/delicity/thermalprinter/adapters/StarAdapter.kt +322 -0
  20. package/android/src/main/java/com/delicity/thermalprinter/adapters/UsbAdapter.kt +248 -0
  21. package/android/src/main/java/com/delicity/thermalprinter/adapters/ZebraAdapter.kt +207 -0
  22. package/android/src/main/java/com/delicity/thermalprinter/discovery/AdapterPriority.kt +39 -0
  23. package/android/src/main/java/com/delicity/thermalprinter/discovery/BleScanner.kt +70 -0
  24. package/android/src/main/java/com/delicity/thermalprinter/discovery/BluetoothClassicScanner.kt +112 -0
  25. package/android/src/main/java/com/delicity/thermalprinter/discovery/DiscoveryManager.kt +136 -0
  26. package/android/src/main/java/com/delicity/thermalprinter/discovery/TcpScanner.kt +96 -0
  27. package/android/src/main/java/com/delicity/thermalprinter/image/ImageCache.kt +88 -0
  28. package/android/src/main/java/com/delicity/thermalprinter/image/ImageProcessor.kt +220 -0
  29. package/android/src/main/java/com/delicity/thermalprinter/image/TextRasterizer.kt +99 -0
  30. package/android/src/main/java/com/delicity/thermalprinter/model/Models.kt +206 -0
  31. package/android/src/main/java/com/delicity/thermalprinter/model/PrintItem.kt +100 -0
  32. package/android/src/main/java/com/delicity/thermalprinter/store/PrinterStore.kt +71 -0
  33. package/android/src/main/java/com/delicity/thermalprinter/transport/BleGattClient.kt +201 -0
  34. package/android/src/main/java/com/delicity/thermalprinter/transport/BluetoothSppTransport.kt +110 -0
  35. package/android/src/main/java/com/delicity/thermalprinter/transport/ByteTransport.kt +18 -0
  36. package/android/src/main/java/com/delicity/thermalprinter/transport/TcpTransport.kt +83 -0
  37. package/dist/esm/adapters/dedup.d.ts +26 -0
  38. package/dist/esm/adapters/dedup.js +66 -0
  39. package/dist/esm/adapters/dedup.js.map +1 -0
  40. package/dist/esm/adapters/priority.d.ts +29 -0
  41. package/dist/esm/adapters/priority.js +55 -0
  42. package/dist/esm/adapters/priority.js.map +1 -0
  43. package/dist/esm/core/enums.d.ts +61 -0
  44. package/dist/esm/core/enums.js +25 -0
  45. package/dist/esm/core/enums.js.map +1 -0
  46. package/dist/esm/core/errors.d.ts +16 -0
  47. package/dist/esm/core/errors.js +53 -0
  48. package/dist/esm/core/errors.js.map +1 -0
  49. package/dist/esm/core/escpos-text.d.ts +33 -0
  50. package/dist/esm/core/escpos-text.js +239 -0
  51. package/dist/esm/core/escpos-text.js.map +1 -0
  52. package/dist/esm/core/imaging.d.ts +91 -0
  53. package/dist/esm/core/imaging.js +184 -0
  54. package/dist/esm/core/imaging.js.map +1 -0
  55. package/dist/esm/core/models.d.ts +131 -0
  56. package/dist/esm/core/models.js +2 -0
  57. package/dist/esm/core/models.js.map +1 -0
  58. package/dist/esm/core/options.d.ts +154 -0
  59. package/dist/esm/core/options.js +2 -0
  60. package/dist/esm/core/options.js.map +1 -0
  61. package/dist/esm/core/text.d.ts +138 -0
  62. package/dist/esm/core/text.js +14 -0
  63. package/dist/esm/core/text.js.map +1 -0
  64. package/dist/esm/definitions.d.ts +155 -0
  65. package/dist/esm/definitions.js +2 -0
  66. package/dist/esm/definitions.js.map +1 -0
  67. package/dist/esm/index.d.ts +15 -0
  68. package/dist/esm/index.js +18 -0
  69. package/dist/esm/index.js.map +1 -0
  70. package/dist/esm/web.d.ts +63 -0
  71. package/dist/esm/web.js +112 -0
  72. package/dist/esm/web.js.map +1 -0
  73. package/dist/plugin.cjs.js +224 -0
  74. package/dist/plugin.cjs.js.map +1 -0
  75. package/dist/plugin.js +227 -0
  76. package/dist/plugin.js.map +1 -0
  77. package/ios/Plugin/Adapters/BrotherAdapter.swift +139 -0
  78. package/ios/Plugin/Adapters/EpsonAdapter.swift +131 -0
  79. package/ios/Plugin/Adapters/EscPosAdapter.swift +106 -0
  80. package/ios/Plugin/Adapters/EscPosCommands.swift +32 -0
  81. package/ios/Plugin/Adapters/EscPosTextEncoder.swift +115 -0
  82. package/ios/Plugin/Adapters/PrinterAdapter.swift +44 -0
  83. package/ios/Plugin/Adapters/RawTcpAdapter.swift +70 -0
  84. package/ios/Plugin/Adapters/StarAdapter.swift +305 -0
  85. package/ios/Plugin/Adapters/ZebraAdapter.swift +119 -0
  86. package/ios/Plugin/Discovery/AdapterPriority.swift +21 -0
  87. package/ios/Plugin/Discovery/BonjourScanner.swift +51 -0
  88. package/ios/Plugin/Discovery/DiscoveryManager.swift +86 -0
  89. package/ios/Plugin/Image/ImageCache.swift +73 -0
  90. package/ios/Plugin/Image/ImageProcessor.swift +168 -0
  91. package/ios/Plugin/Image/TextRasterizer.swift +81 -0
  92. package/ios/Plugin/Logger.swift +33 -0
  93. package/ios/Plugin/Model/Models.swift +174 -0
  94. package/ios/Plugin/Model/PrintItem.swift +111 -0
  95. package/ios/Plugin/Store/PrinterStore.swift +51 -0
  96. package/ios/Plugin/ThermalPrinterEngine.swift +395 -0
  97. package/ios/Plugin/ThermalPrinterPlugin.m +22 -0
  98. package/ios/Plugin/ThermalPrinterPlugin.swift +258 -0
  99. package/ios/Plugin/Transport/TcpTransport.swift +89 -0
  100. 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
+ }