@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,131 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ // Le module ePOS2 iOS est livré en xcframework manuel (licence Epson, non
5
+ // redistribuable). Le nom de module peut varier selon la version du SDK
6
+ // (`libepos2`). Si l'import échoue -> stub inerte (aucune casse de build).
7
+ // Voir docs/SDK_INTEGRATION.md (§ Epson iOS).
8
+ #if canImport(libepos2)
9
+ import libepos2
10
+ #endif
11
+
12
+ /// Adapter Epson iOS basé sur le SDK ePOS2.
13
+ ///
14
+ /// ⚠️ API ObjC pilotée en Swift : à VÉRIFIER sur device avec le xcframework réel
15
+ /// (la version du SDK peut ajuster les signatures / le nom de module).
16
+ /// Chemin principal : impression IMAGE (réception rendue en bitmap).
17
+ final class EpsonAdapter: PrinterAdapter {
18
+
19
+ let id: AdapterId = .epson
20
+ private var printers: [String: AnyObject] = [:]
21
+ private let lock = NSLock()
22
+
23
+ func isAvailable() -> Bool {
24
+ #if canImport(libepos2)
25
+ return true
26
+ #else
27
+ // Détection runtime de secours si le framework est lié sans module Swift.
28
+ return NSClassFromString("Epos2Printer") != nil
29
+ #endif
30
+ }
31
+
32
+ func canHandle(_ profile: PrinterProfile) -> Bool { isAvailable() && profile.adapter == .epson }
33
+
34
+ func discover(timeoutMs: Int, onFound: @escaping (DiscoveredPrinter) -> Void) async {
35
+ // La découverte Epson (Epos2Discovery) nécessite un delegate ObjC ; les
36
+ // imprimantes Epson réseau restent trouvées par le scan TCP générique.
37
+ // Découverte SDK dédiée à activer si besoin (voir docs).
38
+ }
39
+
40
+ func connect(_ profile: PrinterProfile, timeoutMs: Int) async throws {
41
+ #if canImport(libepos2)
42
+ if isConnected(profile.id) { return }
43
+ guard let printer = Epos2Printer(printerSeries: Self.series(for: profile.model), lang: EPOS2_MODEL_ANK.rawValue) else {
44
+ throw PrinterError(.SDK_NOT_AVAILABLE, "Init Epos2Printer échouée")
45
+ }
46
+ let result = printer.connect(Self.target(for: profile), timeout: Int(timeoutMs))
47
+ guard result == EPOS2_SUCCESS.rawValue else {
48
+ throw PrinterError(.CONNECTION_FAILED, "Connexion Epson échouée: \(profile.address)", detail: "\(result)", retryable: true)
49
+ }
50
+ lock.lock(); printers[profile.id] = printer; lock.unlock()
51
+ #else
52
+ throw PrinterError(.SDK_NOT_AVAILABLE, "SDK Epson ePOS2 absent")
53
+ #endif
54
+ }
55
+
56
+ func isConnected(_ printerId: String) -> Bool {
57
+ lock.lock(); defer { lock.unlock() }
58
+ return printers[printerId] != nil
59
+ }
60
+
61
+ func disconnect(_ printerId: String) async {
62
+ #if canImport(libepos2)
63
+ lock.lock(); let p = printers.removeValue(forKey: printerId) as? Epos2Printer; lock.unlock()
64
+ p?.disconnect()
65
+ p?.clearCommandBuffer()
66
+ #endif
67
+ }
68
+
69
+ func printImage(_ profile: PrinterProfile, image: UIImage, options: RenderOptions) async throws -> Int {
70
+ #if canImport(libepos2)
71
+ lock.lock(); let p = printers[profile.id] as? Epos2Printer; lock.unlock()
72
+ guard let printer = p else { throw PrinterError(.CONNECTION_FAILED, "Epson non connecté: \(profile.id)") }
73
+ for _ in 0..<max(1, options.copies) {
74
+ printer.beginTransaction()
75
+ printer.add(image, x: 0, y: 0,
76
+ width: Int(image.size.width), height: Int(image.size.height),
77
+ color: Int(EPOS2_COLOR_1.rawValue), mode: Int(EPOS2_MODE_MONO.rawValue),
78
+ halftone: Self.halftone(options.dithering), brightness: 1.0,
79
+ compress: Int(EPOS2_COMPRESS_AUTO.rawValue))
80
+ if options.cut && profile.capabilities.supportsCut { printer.addCut(Int(EPOS2_CUT_FEED.rawValue)) }
81
+ if options.openCashDrawer && profile.capabilities.supportsCashDrawer {
82
+ printer.addPulse(Int(EPOS2_DRAWER_2PIN.rawValue), time: Int(EPOS2_PULSE_100.rawValue))
83
+ }
84
+ let result = printer.sendData(Int(EPOS2_PARAM_DEFAULT))
85
+ printer.endTransaction()
86
+ printer.clearCommandBuffer()
87
+ if result != EPOS2_SUCCESS.rawValue {
88
+ throw PrinterError(.PRINT_FAILED, "Impression Epson échouée", detail: "\(result)", retryable: true)
89
+ }
90
+ }
91
+ return Int(image.size.width * image.size.height) / 8
92
+ #else
93
+ throw PrinterError(.SDK_NOT_AVAILABLE, "SDK Epson ePOS2 absent")
94
+ #endif
95
+ }
96
+
97
+ func getStatus(_ profile: PrinterProfile) async throws -> PrinterStatus {
98
+ let connected = isConnected(profile.id)
99
+ return PrinterStatus(id: profile.id, connection: connected ? "connected" : "disconnected",
100
+ online: connected, paper: "unknown",
101
+ rawStatus: "Epson: statut détaillé via getStatus() à activer si besoin")
102
+ }
103
+
104
+ // MARK: Helpers typés
105
+
106
+ #if canImport(libepos2)
107
+ private static func target(for profile: PrinterProfile) -> String {
108
+ if profile.address.hasPrefix("TCP:") || profile.address.hasPrefix("BT:") || profile.address.hasPrefix("USB:") {
109
+ return profile.address
110
+ }
111
+ switch profile.transport {
112
+ case .wifi, .ethernet: return "TCP:\(profile.address.split(separator: ":").first.map(String.init) ?? profile.address)"
113
+ case .bluetooth, .ble: return "BT:\(profile.address)"
114
+ case .usb: return "USB:\(profile.address)"
115
+ }
116
+ }
117
+
118
+ private static func series(for model: String?) -> Int32 {
119
+ // Best effort : série générique. Affiner via EPOS2_TM_* si le modèle est connu.
120
+ return EPOS2_TM_M30.rawValue
121
+ }
122
+
123
+ private static func halftone(_ dithering: String) -> Int {
124
+ switch dithering {
125
+ case "none": return Int(EPOS2_HALFTONE_THRESHOLD.rawValue)
126
+ case "atkinson", "floyd_steinberg": return Int(EPOS2_HALFTONE_ERROR_DIFFUSION.rawValue)
127
+ default: return Int(EPOS2_HALFTONE_DITHER.rawValue)
128
+ }
129
+ }
130
+ #endif
131
+ }
@@ -0,0 +1,106 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /// Adapter ESC/POS générique iOS.
5
+ ///
6
+ /// ⚠️ LIMITE iOS : pas de Bluetooth Classic / SPP générique (voir README).
7
+ /// Cet adapter ne gère donc QUE le TCP (Wi-Fi / Ethernet). Pour le Bluetooth
8
+ /// d'une imprimante ESC/POS sur iOS, deux seules options réelles :
9
+ /// - le fabricant fournit un SDK MFi (Epson/Star/...) -> utiliser leur adapter,
10
+ /// - l'imprimante expose un service BLE exploitable -> BleAdapter (allowlist).
11
+ final class EscPosAdapter: PrinterAdapter {
12
+
13
+ let id: AdapterId = .escpos
14
+ private var connections: [String: TcpTransport] = [:]
15
+ private let lock = NSLock()
16
+
17
+ func isAvailable() -> Bool { true }
18
+
19
+ func supportsTextItems() -> Bool { true }
20
+
21
+ func discover(timeoutMs: Int, onFound: @escaping (DiscoveredPrinter) -> Void) async {
22
+ // Délégué au TcpScanner / Bonjour (DiscoveryManager).
23
+ }
24
+
25
+ func canHandle(_ profile: PrinterProfile) -> Bool {
26
+ profile.adapter == .escpos && (profile.transport == .wifi || profile.transport == .ethernet)
27
+ }
28
+
29
+ func connect(_ profile: PrinterProfile, timeoutMs: Int) async throws {
30
+ if isConnected(profile.id) { return }
31
+ // iOS : pas de Bluetooth Classic/SPP ni de GATT BLE générique exposé par le
32
+ // plugin. Le BLE/Bluetooth passe par les SDK MFi (Star/Epson/Brother).
33
+ guard profile.transport == .wifi || profile.transport == .ethernet else {
34
+ throw PrinterError(
35
+ .UNSUPPORTED_TRANSPORT,
36
+ "iOS : le transport \(profile.transport.rawValue) générique n'est pas exposé. " +
37
+ "Utilisez une imprimante via son SDK MFi (Star/Epson/Brother). Voir docs/SDK_INTEGRATION.md."
38
+ )
39
+ }
40
+ let (host, port) = splitHostPort(profile.address, defaultPort: 9100)
41
+ let t = TcpTransport(host: host, port: port)
42
+ try await t.open(timeoutMs: timeoutMs)
43
+ lock.lock(); connections[profile.id] = t; lock.unlock()
44
+ }
45
+
46
+ func isConnected(_ printerId: String) -> Bool {
47
+ lock.lock(); defer { lock.unlock() }
48
+ return connections[printerId]?.isOpen == true
49
+ }
50
+
51
+ func disconnect(_ printerId: String) async {
52
+ lock.lock(); let t = connections.removeValue(forKey: printerId); lock.unlock()
53
+ t?.close()
54
+ }
55
+
56
+ func printImage(_ profile: PrinterProfile, image: UIImage, options: RenderOptions) async throws -> Int {
57
+ lock.lock(); let t = connections[profile.id]; lock.unlock()
58
+ guard let transport = t else {
59
+ throw PrinterError(.CONNECTION_FAILED, "ESC/POS non connecté: \(profile.id)")
60
+ }
61
+ let mono = try ImageProcessor.toMono(image, options: options)
62
+ let raster = ImageProcessor.encodeEscPosRaster(mono)
63
+ let job = EscPosCommands.buildJob(
64
+ raster: raster, align: options.align, feedLines: options.feedLines,
65
+ cut: options.cut && profile.capabilities.supportsCut,
66
+ openDrawer: options.openCashDrawer && profile.capabilities.supportsCashDrawer
67
+ )
68
+ var sent = 0
69
+ for _ in 0..<max(1, options.copies) {
70
+ try await transport.write(job)
71
+ sent += job.count
72
+ }
73
+ return sent
74
+ }
75
+
76
+ func printItems(_ profile: PrinterProfile, items: [PrintItem], defaultCodePage: String, cut: Bool, feedLines: Int) async throws -> Int {
77
+ lock.lock(); let t = connections[profile.id]; lock.unlock()
78
+ guard let transport = t else { throw PrinterError(.CONNECTION_FAILED, "ESC/POS non connecté: \(profile.id)") }
79
+ let columns = profile.capabilities.printableDots <= 420 ? 32 : 48
80
+ let encoded = EscPosTextEncoder.encode(items, defaultCodePage: defaultCodePage, columns: columns)
81
+ var job = encoded.bytes
82
+ if feedLines > 0 { job += EscPosCommands.feed(feedLines) }
83
+ if cut && profile.capabilities.supportsCut { job += EscPosCommands.CUT_PARTIAL }
84
+ try await transport.write(job)
85
+ return job.count
86
+ }
87
+
88
+ func getStatus(_ profile: PrinterProfile) async throws -> PrinterStatus {
89
+ let connected = isConnected(profile.id)
90
+ return PrinterStatus(
91
+ id: profile.id,
92
+ connection: connected ? "connected" : "disconnected",
93
+ online: connected, paper: "unknown",
94
+ rawStatus: "ESC/POS TCP: statut temps réel non lu (unidirectionnel)"
95
+ )
96
+ }
97
+
98
+ private func splitHostPort(_ addr: String, defaultPort: UInt16) -> (String, UInt16) {
99
+ if let idx = addr.lastIndex(of: ":"), addr.firstIndex(of: ":") == idx {
100
+ let host = String(addr[..<idx])
101
+ let portStr = String(addr[addr.index(after: idx)...])
102
+ return (host, UInt16(portStr) ?? defaultPort)
103
+ }
104
+ return (addr, defaultPort)
105
+ }
106
+ }
@@ -0,0 +1,32 @@
1
+ import Foundation
2
+
3
+ /// Constantes et helpers de commandes ESC/POS (miroir iOS de EscPosCommands.kt).
4
+ enum EscPosCommands {
5
+ static let INIT: [UInt8] = [0x1B, 0x40]
6
+ static let ALIGN_LEFT: [UInt8] = [0x1B, 0x61, 0x00]
7
+ static let ALIGN_CENTER: [UInt8] = [0x1B, 0x61, 0x01]
8
+ static let ALIGN_RIGHT: [UInt8] = [0x1B, 0x61, 0x02]
9
+ static let CUT_PARTIAL: [UInt8] = [0x1D, 0x56, 0x01]
10
+ static let DRAWER_PIN2: [UInt8] = [0x1B, 0x70, 0x00, 0x19, 0xFA]
11
+
12
+ static func feed(_ lines: Int) -> [UInt8] { [0x1B, 0x64, UInt8(clamping: lines)] }
13
+
14
+ static func alignOf(_ align: String) -> [UInt8] {
15
+ switch align {
16
+ case "center": return ALIGN_CENTER
17
+ case "right": return ALIGN_RIGHT
18
+ default: return ALIGN_LEFT
19
+ }
20
+ }
21
+
22
+ static func buildJob(raster: [UInt8], align: String, feedLines: Int, cut: Bool, openDrawer: Bool) -> [UInt8] {
23
+ var out: [UInt8] = []
24
+ out.append(contentsOf: INIT)
25
+ out.append(contentsOf: alignOf(align))
26
+ out.append(contentsOf: raster)
27
+ out.append(contentsOf: feed(feedLines))
28
+ if cut { out.append(contentsOf: CUT_PARTIAL) }
29
+ if openDrawer { out.append(contentsOf: DRAWER_PIN2) }
30
+ return out
31
+ }
32
+ }
@@ -0,0 +1,115 @@
1
+ import Foundation
2
+
3
+ /// Encodeur ESC/POS texte iOS (miroir de escpos-text.ts / EscPosTextEncoder.kt).
4
+ enum EscPosTextEncoder {
5
+
6
+ private static let ESC: UInt8 = 0x1B
7
+ private static let GS: UInt8 = 0x1D
8
+ private static let LF: UInt8 = 0x0A
9
+
10
+ private static let codePageToEscT: [String: UInt8] = [
11
+ "CP437": 0, "CP850": 2, "CP858": 19, "WPC1252": 16, "CP852": 18, "CP866": 17,
12
+ ]
13
+ private static let barcodeM: [String: UInt8] = [
14
+ "UPC_A": 65, "UPC_E": 66, "EAN13": 67, "EAN8": 68,
15
+ "CODE39": 69, "ITF": 70, "CODABAR": 71, "CODE93": 72, "CODE128": 73,
16
+ ]
17
+
18
+ struct Encoded { let bytes: [UInt8]; let imageIndexes: [Int] }
19
+
20
+ static func encodeString(_ value: String) -> [UInt8] {
21
+ value.unicodeScalars.map { $0.value <= 0xFF ? UInt8($0.value) : 0x3F }
22
+ }
23
+
24
+ static func sizeByte(_ w: Int, _ h: Int) -> UInt8 {
25
+ let ww = UInt8(min(8, max(1, w)) - 1)
26
+ let hh = UInt8(min(8, max(1, h)) - 1)
27
+ return (ww << 4) | hh
28
+ }
29
+
30
+ private static func openStyle(_ out: inout [UInt8], _ s: TextStyle, _ defaultCodePage: String) {
31
+ let cp = s.codePageId.map { UInt8(truncatingIfNeeded: $0) } ?? (codePageToEscT[s.codePage ?? defaultCodePage] ?? 16)
32
+ out += [ESC, 0x74, cp]
33
+ let align: UInt8 = s.align == "center" ? 1 : (s.align == "right" ? 2 : 0)
34
+ out += [ESC, 0x61, align]
35
+ out += [ESC, 0x4D, s.font == "B" ? 1 : 0]
36
+ out += [ESC, 0x45, s.bold ? 1 : 0]
37
+ out += [ESC, 0x47, s.doubleStrike ? 1 : 0]
38
+ let ul: UInt8 = s.underline == "single" ? 1 : (s.underline == "double" ? 2 : 0)
39
+ out += [ESC, 0x2D, ul]
40
+ out += [GS, 0x42, s.invert ? 1 : 0]
41
+ out += [ESC, 0x7B, s.upsideDown ? 1 : 0]
42
+ out += [ESC, 0x56, s.rotate90 ? 1 : 0]
43
+ out += [GS, 0x21, sizeByte(s.widthMultiplier, s.heightMultiplier)]
44
+ if let ls = s.letterSpacing { out += [ESC, 0x20, UInt8(truncatingIfNeeded: ls)] }
45
+ if let lsp = s.lineSpacing { out += [ESC, 0x33, UInt8(truncatingIfNeeded: lsp)] } else { out += [ESC, 0x32] }
46
+ }
47
+
48
+ private static func qrCode(_ out: inout [UInt8], value: String, size: Int, ec: String, align: String) {
49
+ let a: UInt8 = align == "left" ? 0 : (align == "right" ? 2 : 1)
50
+ out += [ESC, 0x61, a]
51
+ out += [GS, 0x28, 0x6B, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00]
52
+ out += [GS, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x43, UInt8(min(16, max(1, size)))]
53
+ let ecv: UInt8 = ec == "L" ? 48 : (ec == "Q" ? 50 : (ec == "H" ? 51 : 49))
54
+ out += [GS, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x45, ecv]
55
+ let data = encodeString(value)
56
+ let len = data.count + 3
57
+ out += [GS, 0x28, 0x6B, UInt8(len & 0xFF), UInt8((len >> 8) & 0xFF), 0x31, 0x50, 0x30]
58
+ out += data
59
+ out += [GS, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30]
60
+ }
61
+
62
+ private static func barcode(_ out: inout [UInt8], value: String, symbology: String, height: Int, width: Int, hri: String, align: String) {
63
+ let a: UInt8 = align == "left" ? 0 : (align == "right" ? 2 : 1)
64
+ out += [ESC, 0x61, a]
65
+ let h: UInt8 = hri == "above" ? 1 : (hri == "both" ? 3 : (hri == "none" ? 0 : 2))
66
+ out += [GS, 0x48, h]
67
+ out += [GS, 0x68, UInt8(min(255, max(1, height)))]
68
+ out += [GS, 0x77, UInt8(min(6, max(2, width)))]
69
+ let m = barcodeM[symbology] ?? 73
70
+ var data = encodeString(value)
71
+ if symbology == "CODE128" && !(data.first == 0x7B) { data = [0x7B, 0x42] + data }
72
+ out += [GS, 0x6B, m, UInt8(data.count)]
73
+ out += data
74
+ }
75
+
76
+ static func encode(_ items: [PrintItem], defaultCodePage: String = "WPC1252", columns: Int = 48) -> Encoded {
77
+ var out: [UInt8] = [ESC, 0x40] // reset
78
+ var imageIndexes: [Int] = []
79
+ for (index, item) in items.enumerated() {
80
+ switch item {
81
+ case let .text(value, style):
82
+ openStyle(&out, style, defaultCodePage)
83
+ out += encodeString(value)
84
+ if style.newline { out.append(LF) }
85
+ out += [ESC, 0x40]
86
+ case let .feed(lines):
87
+ out += [ESC, 0x64, UInt8(min(255, max(0, lines)))]
88
+ case let .divider(char, cols, align, bold):
89
+ let ch = char.unicodeScalars.first.map { UInt8(truncatingIfNeeded: $0.value) } ?? 0x2D
90
+ let n = cols ?? columns
91
+ let a: UInt8 = align == "center" ? 1 : (align == "right" ? 2 : 0)
92
+ out += [ESC, 0x61, a]
93
+ if bold { out += [ESC, 0x45, 1] }
94
+ out += [UInt8](repeating: ch, count: n)
95
+ out.append(LF)
96
+ out += [ESC, 0x40]
97
+ case let .qrcode(value, size, ec, align):
98
+ qrCode(&out, value: value, size: size, ec: ec, align: align)
99
+ case let .barcode(value, symbology, height, width, hri, align):
100
+ barcode(&out, value: value, symbology: symbology, height: height, width: width, hri: hri, align: align)
101
+ case let .cashDrawer(pin):
102
+ out += pin == 5 ? [ESC, 0x70, 0x01, 0x19, 0xFA] : [ESC, 0x70, 0x00, 0x19, 0xFA]
103
+ case let .cut(mode, feedBefore):
104
+ if feedBefore > 0 { out += [ESC, 0x64, UInt8(truncatingIfNeeded: feedBefore)] }
105
+ out += mode == "full" ? [GS, 0x56, 0x00] : [GS, 0x56, 0x01]
106
+ case let .raw(bytesBase64):
107
+ let clean = bytesBase64.contains("base64,") ? String(bytesBase64.split(separator: ",").last ?? "") : bytesBase64
108
+ if let data = Data(base64Encoded: clean, options: .ignoreUnknownCharacters) { out += [UInt8](data) }
109
+ case .image:
110
+ imageIndexes.append(index)
111
+ }
112
+ }
113
+ return Encoded(bytes: out, imageIndexes: imageIndexes)
114
+ }
115
+ }
@@ -0,0 +1,44 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /// Contrat commun à tous les adapters iOS (miroir de l'interface Android).
5
+ ///
6
+ /// Toutes les méthodes longues sont `async` et lèvent `PrinterError` en cas d'échec.
7
+ protocol PrinterAdapter {
8
+ var id: AdapterId { get }
9
+
10
+ /// True si le SDK requis est lié à l'app (sinon adapter ignoré).
11
+ func isAvailable() -> Bool
12
+
13
+ /// Découverte propre à l'adapter, résultats poussés via `onFound`.
14
+ func discover(timeoutMs: Int, onFound: @escaping (DiscoveredPrinter) -> Void) async
15
+
16
+ func canHandle(_ profile: PrinterProfile) -> Bool
17
+
18
+ /// True si l'adapter imprime des items texte NATIVEMENT. Sinon le moteur rend
19
+ /// les items en image (TextRasterizer) puis appelle `printImage`.
20
+ func supportsTextItems() -> Bool
21
+
22
+ func connect(_ profile: PrinterProfile, timeoutMs: Int) async throws
23
+ func isConnected(_ printerId: String) -> Bool
24
+ func disconnect(_ printerId: String) async
25
+
26
+ /// Imprime un UIImage DÉJÀ redimensionné à la largeur cible.
27
+ /// Retourne le nombre d'octets envoyés (best effort).
28
+ func printImage(_ profile: PrinterProfile, image: UIImage, options: RenderOptions) async throws -> Int
29
+
30
+ /// Imprime une liste d'items texte stylés (+ QR/code-barres/feed/cut...).
31
+ /// Pour ESC/POS : encodage via EscPosTextEncoder. SDK : builder du SDK.
32
+ func printItems(_ profile: PrinterProfile, items: [PrintItem], defaultCodePage: String, cut: Bool, feedLines: Int) async throws -> Int
33
+
34
+ func getStatus(_ profile: PrinterProfile) async throws -> PrinterStatus
35
+ }
36
+
37
+ /// Implémentation par défaut : printText non supporté (les SDK la surchargent).
38
+ extension PrinterAdapter {
39
+ func supportsTextItems() -> Bool { false }
40
+
41
+ func printItems(_ profile: PrinterProfile, items: [PrintItem], defaultCodePage: String, cut: Bool, feedLines: Int) async throws -> Int {
42
+ throw PrinterError(.SDK_NOT_AVAILABLE, "printText non implémenté pour cet adapter (\(id.rawValue)) — voir docs/SDK_INTEGRATION.md")
43
+ }
44
+ }
@@ -0,0 +1,70 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /// Adapter filet de sécurité réseau iOS : raster ESC/POS sur TCP brut, sans statut.
5
+ final class RawTcpAdapter: PrinterAdapter {
6
+
7
+ let id: AdapterId = .rawTcp
8
+ private var connections: [String: TcpTransport] = [:]
9
+ private let lock = NSLock()
10
+
11
+ func isAvailable() -> Bool { true }
12
+
13
+ func supportsTextItems() -> Bool { true }
14
+
15
+ func discover(timeoutMs: Int, onFound: @escaping (DiscoveredPrinter) -> Void) async {}
16
+
17
+ func canHandle(_ profile: PrinterProfile) -> Bool {
18
+ profile.adapter == .rawTcp && (profile.transport == .wifi || profile.transport == .ethernet)
19
+ }
20
+
21
+ func connect(_ profile: PrinterProfile, timeoutMs: Int) async throws {
22
+ if isConnected(profile.id) { return }
23
+ var host = profile.address
24
+ var port: UInt16 = 9100
25
+ if let idx = profile.address.lastIndex(of: ":") {
26
+ host = String(profile.address[..<idx])
27
+ port = UInt16(profile.address[profile.address.index(after: idx)...]) ?? 9100
28
+ }
29
+ let t = TcpTransport(host: host, port: port)
30
+ try await t.open(timeoutMs: timeoutMs)
31
+ lock.lock(); connections[profile.id] = t; lock.unlock()
32
+ }
33
+
34
+ func isConnected(_ printerId: String) -> Bool {
35
+ lock.lock(); defer { lock.unlock() }
36
+ return connections[printerId]?.isOpen == true
37
+ }
38
+
39
+ func disconnect(_ printerId: String) async {
40
+ lock.lock(); let t = connections.removeValue(forKey: printerId); lock.unlock()
41
+ t?.close()
42
+ }
43
+
44
+ func printImage(_ profile: PrinterProfile, image: UIImage, options: RenderOptions) async throws -> Int {
45
+ lock.lock(); let t = connections[profile.id]; lock.unlock()
46
+ guard let transport = t else { throw PrinterError(.CONNECTION_FAILED, "rawTcp non connecté") }
47
+ let mono = try ImageProcessor.toMono(image, options: options)
48
+ let raster = ImageProcessor.encodeEscPosRaster(mono)
49
+ let job = EscPosCommands.buildJob(raster: raster, align: options.align, feedLines: options.feedLines, cut: options.cut, openDrawer: options.openCashDrawer)
50
+ try await transport.write(job)
51
+ return job.count
52
+ }
53
+
54
+ func printItems(_ profile: PrinterProfile, items: [PrintItem], defaultCodePage: String, cut: Bool, feedLines: Int) async throws -> Int {
55
+ lock.lock(); let t = connections[profile.id]; lock.unlock()
56
+ guard let transport = t else { throw PrinterError(.CONNECTION_FAILED, "rawTcp non connecté") }
57
+ let columns = profile.capabilities.printableDots <= 420 ? 32 : 48
58
+ let encoded = EscPosTextEncoder.encode(items, defaultCodePage: defaultCodePage, columns: columns)
59
+ var job = encoded.bytes
60
+ if feedLines > 0 { job += EscPosCommands.feed(feedLines) }
61
+ if cut { job += EscPosCommands.CUT_PARTIAL }
62
+ try await transport.write(job)
63
+ return job.count
64
+ }
65
+
66
+ func getStatus(_ profile: PrinterProfile) async throws -> PrinterStatus {
67
+ let c = isConnected(profile.id)
68
+ return PrinterStatus(id: profile.id, connection: c ? "connected" : "disconnected", online: c, paper: "unknown", rawStatus: "rawTcp: statut non supporté")
69
+ }
70
+ }