@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,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
|
+
}
|