@bricks-soft/capacitor-thermal-printer 0.0.0-stage → 8.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 (35) hide show
  1. package/BricksSoftCapacitorThermalPrinter.podspec +17 -0
  2. package/LICENSE +201 -0
  3. package/Package.swift +13 -0
  4. package/README.md +80 -2
  5. package/android/build.gradle +40 -0
  6. package/android/src/main/AndroidManifest.xml +8 -0
  7. package/android/src/main/java/com/brickssoft/thermalprinter/ThermalPrinterPlugin.java +463 -0
  8. package/dist/esm/definitions.d.ts +73 -0
  9. package/dist/esm/definitions.js +1 -0
  10. package/dist/esm/index.d.ts +3 -0
  11. package/dist/esm/index.js +23 -0
  12. package/dist/esm/web.d.ts +13 -0
  13. package/dist/esm/web.js +33 -0
  14. package/dist/plugin.cjs.js +65 -0
  15. package/dist/plugin.js +68 -0
  16. package/ios/Sources/ThermalPrinter/EscPosBarcode.swift +121 -0
  17. package/ios/Sources/ThermalPrinter/EscPosBitmap.swift +178 -0
  18. package/ios/Sources/ThermalPrinter/EscPosBluetooth.swift +341 -0
  19. package/ios/Sources/ThermalPrinter/EscPosBluetoothConnection.swift +298 -0
  20. package/ios/Sources/ThermalPrinter/EscPosBluetoothRules.swift +78 -0
  21. package/ios/Sources/ThermalPrinter/EscPosCharsetEncoding.swift +115 -0
  22. package/ios/Sources/ThermalPrinter/EscPosCoreBluetoothLink.swift +287 -0
  23. package/ios/Sources/ThermalPrinter/EscPosDeviceConnection.swift +47 -0
  24. package/ios/Sources/ThermalPrinter/EscPosJavaCompat.swift +213 -0
  25. package/ios/Sources/ThermalPrinter/EscPosKnownPrinterStore.swift +44 -0
  26. package/ios/Sources/ThermalPrinter/EscPosPrinter.swift +55 -0
  27. package/ios/Sources/ThermalPrinter/EscPosPrinterCommands.swift +367 -0
  28. package/ios/Sources/ThermalPrinter/EscPosPrinterSize.swift +53 -0
  29. package/ios/Sources/ThermalPrinter/EscPosQRCode.swift +70 -0
  30. package/ios/Sources/ThermalPrinter/EscPosQueue.swift +30 -0
  31. package/ios/Sources/ThermalPrinter/EscPosTcpConnection.swift +265 -0
  32. package/ios/Sources/ThermalPrinter/PrinterTextParser.swift +622 -0
  33. package/ios/Sources/ThermalPrinter/ThermalPrinterPlugin.swift +85 -0
  34. package/ios/Sources/ThermalPrinter/ThermalPrinterService.swift +380 -0
  35. package/package.json +61 -4
@@ -0,0 +1,53 @@
1
+ import Foundation
2
+
3
+ class EscPosPrinterSize {
4
+ static let inchToMM: Float = 25.4
5
+
6
+ let printerDpi: Int
7
+ let printerWidthMM: Float
8
+ let printerNbrCharactersPerLine: Int
9
+ let printerWidthPx: Int
10
+ let printerCharSizeWidthPx: Int
11
+
12
+ init(printerDpi: Int, printerWidthMM: Float, printerNbrCharactersPerLine: Int) throws {
13
+ self.printerDpi = printerDpi
14
+ self.printerWidthMM = printerWidthMM
15
+ self.printerNbrCharactersPerLine = printerNbrCharactersPerLine
16
+ let printingWidthPx = EscPosPrinterSize.mmToPx(printerWidthMM, dpi: printerDpi)
17
+ self.printerWidthPx = printingWidthPx + (printingWidthPx % 8)
18
+ guard printerNbrCharactersPerLine != 0 else {
19
+ throw EscPosError.runtime("/ by zero")
20
+ }
21
+ self.printerCharSizeWidthPx = printingWidthPx / printerNbrCharactersPerLine
22
+ }
23
+
24
+ func mmToPx(_ mmSize: Float) -> Int {
25
+ EscPosPrinterSize.mmToPx(mmSize, dpi: printerDpi)
26
+ }
27
+
28
+ static func mmToPx(_ mmSize: Float, dpi: Int) -> Int {
29
+ JavaMath.round(mmSize * Float(dpi) / inchToMM)
30
+ }
31
+
32
+ func bitmapToBytes(_ bitmap: EscPosBitmap) -> [UInt8] {
33
+ var isSizeEdit = false
34
+ var bitmapWidth = bitmap.width
35
+ var bitmapHeight = bitmap.height
36
+ let maxWidth = printerWidthPx
37
+ let maxHeight = 256
38
+
39
+ if bitmapWidth > maxWidth {
40
+ bitmapHeight = JavaMath.round(Float(bitmapHeight) * Float(maxWidth) / Float(bitmapWidth))
41
+ bitmapWidth = maxWidth
42
+ isSizeEdit = true
43
+ }
44
+ if bitmapHeight > maxHeight {
45
+ bitmapWidth = JavaMath.round(Float(bitmapWidth) * Float(maxHeight) / Float(bitmapHeight))
46
+ bitmapHeight = maxHeight
47
+ isSizeEdit = true
48
+ }
49
+
50
+ let source = isSizeEdit ? bitmap.scaled(width: bitmapWidth, height: bitmapHeight) : bitmap
51
+ return EscPosPrinterCommands.bitmapToBytes(source)
52
+ }
53
+ }
@@ -0,0 +1,70 @@
1
+ import CoreGraphics
2
+ import CoreImage
3
+ import Foundation
4
+
5
+ enum EscPosQRCode {
6
+ private static let context = CIContext(options: [.useSoftwareRenderer: true])
7
+
8
+ static func matrix(for data: String) throws -> [[Bool]] {
9
+ guard let filter = CIFilter(name: "CIQRCodeGenerator") else {
10
+ throw EscPosError.barcode("Unable to encode QR code")
11
+ }
12
+ filter.setValue(Data(data.utf8), forKey: "inputMessage")
13
+ filter.setValue("L", forKey: "inputCorrectionLevel")
14
+ guard let output = filter.outputImage,
15
+ let image = context.createCGImage(output, from: output.extent)
16
+ else {
17
+ throw EscPosError.barcode("Unable to encode QR code")
18
+ }
19
+ let width = image.width
20
+ let height = image.height
21
+ var gray = [UInt8](repeating: 255, count: width * height)
22
+ let drawn: Bool = gray.withUnsafeMutableBytes { raw in
23
+ guard let bitmap = CGContext(
24
+ data: raw.baseAddress,
25
+ width: width,
26
+ height: height,
27
+ bitsPerComponent: 8,
28
+ bytesPerRow: width,
29
+ space: CGColorSpaceCreateDeviceGray(),
30
+ bitmapInfo: CGImageAlphaInfo.none.rawValue
31
+ ) else { return false }
32
+ bitmap.interpolationQuality = .none
33
+ bitmap.setFillColor(gray: 1, alpha: 1)
34
+ bitmap.fill(CGRect(x: 0, y: 0, width: width, height: height))
35
+ bitmap.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
36
+ return true
37
+ }
38
+ guard drawn else {
39
+ throw EscPosError.barcode("Unable to encode QR code")
40
+ }
41
+
42
+ var minX = width
43
+ var minY = height
44
+ var maxX = -1
45
+ var maxY = -1
46
+ for y in 0..<height {
47
+ for x in 0..<width where gray[y * width + x] < 128 {
48
+ minX = min(minX, x)
49
+ minY = min(minY, y)
50
+ maxX = max(maxX, x)
51
+ maxY = max(maxY, y)
52
+ }
53
+ }
54
+ guard maxX >= minX, maxY >= minY else {
55
+ throw EscPosError.barcode("Unable to encode QR code")
56
+ }
57
+ let size = max(maxX - minX, maxY - minY) + 1
58
+ var modules = [[Bool]](repeating: [Bool](repeating: false, count: size), count: size)
59
+ for row in 0..<size {
60
+ for column in 0..<size {
61
+ let x = minX + column
62
+ let y = minY + row
63
+ if x < width && y < height {
64
+ modules[row][column] = gray[y * width + x] < 128
65
+ }
66
+ }
67
+ }
68
+ return modules
69
+ }
70
+ }
@@ -0,0 +1,30 @@
1
+ import Foundation
2
+
3
+ final class EscPosQueue {
4
+ let queue: DispatchQueue
5
+ private let key = DispatchSpecificKey<Bool>()
6
+
7
+ init(label: String) {
8
+ queue = DispatchQueue(label: label)
9
+ queue.setSpecific(key: key, value: true)
10
+ }
11
+
12
+ var isCurrent: Bool {
13
+ DispatchQueue.getSpecific(key: key) == true
14
+ }
15
+
16
+ func sync<T>(_ block: () throws -> T) rethrows -> T {
17
+ if isCurrent {
18
+ return try block()
19
+ }
20
+ return try queue.sync(execute: block)
21
+ }
22
+
23
+ func async(after delay: TimeInterval = 0, _ block: @escaping () -> Void) {
24
+ if delay <= 0 {
25
+ queue.async(execute: block)
26
+ } else {
27
+ queue.asyncAfter(deadline: .now() + delay, execute: block)
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,265 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ protocol EscPosNetworkConnection: AnyObject {
5
+ var stateUpdateHandler: ((NWConnection.State) -> Void)? { get set }
6
+ var unsatisfiedReason: NWPath.UnsatisfiedReason? { get }
7
+ func start(queue: DispatchQueue)
8
+ func send(_ data: Data, completion: @escaping (NWError?) -> Void)
9
+ func receive(completion: @escaping (_ isComplete: Bool, _ error: NWError?) -> Void)
10
+ func cancel()
11
+ }
12
+
13
+ final class EscPosNWConnection: EscPosNetworkConnection {
14
+ private let connection: NWConnection
15
+
16
+ init(host: String, port: NWEndpoint.Port) {
17
+ let tcp = NWProtocolTCP.Options()
18
+ tcp.noDelay = true
19
+ connection = NWConnection(host: NWEndpoint.Host(host), port: port, using: NWParameters(tls: nil, tcp: tcp))
20
+ }
21
+
22
+ var stateUpdateHandler: ((NWConnection.State) -> Void)? {
23
+ get { connection.stateUpdateHandler }
24
+ set { connection.stateUpdateHandler = newValue }
25
+ }
26
+
27
+ var unsatisfiedReason: NWPath.UnsatisfiedReason? {
28
+ connection.currentPath?.unsatisfiedReason
29
+ }
30
+
31
+ func start(queue: DispatchQueue) {
32
+ connection.start(queue: queue)
33
+ }
34
+
35
+ func send(_ data: Data, completion: @escaping (NWError?) -> Void) {
36
+ connection.send(content: data, completion: .contentProcessed(completion))
37
+ }
38
+
39
+ func receive(completion: @escaping (Bool, NWError?) -> Void) {
40
+ connection.receive(minimumIncompleteLength: 1, maximumLength: 4096) { _, _, isComplete, error in
41
+ completion(isComplete, error)
42
+ }
43
+ }
44
+
45
+ func cancel() {
46
+ connection.cancel()
47
+ }
48
+ }
49
+
50
+ final class EscPosTcpConnection: EscPosDeviceConnection {
51
+ static var connectTimeout: TimeInterval = 5
52
+ static var localNetworkPermissionTimeout: TimeInterval = 60
53
+ static var sendTimeout: TimeInterval = 30
54
+ static let localNetworkPolicyDenied: Int32 = -65570
55
+
56
+ enum WaitingDisposition: Equatable {
57
+ case awaitLocalNetworkPermission
58
+ case fail
59
+ }
60
+
61
+ static func waitingDisposition(for error: NWError, unsatisfiedReason: NWPath.UnsatisfiedReason?) -> WaitingDisposition {
62
+ if unsatisfiedReason == .localNetworkDenied {
63
+ return .awaitLocalNetworkPermission
64
+ }
65
+ if case .dns(let code) = error, code == localNetworkPolicyDenied {
66
+ return .awaitLocalNetworkPermission
67
+ }
68
+ return .fail
69
+ }
70
+
71
+ private final class ConnectProgress {
72
+ private let lock = NSLock()
73
+ private let semaphore = DispatchSemaphore(value: 0)
74
+ private var settled = false
75
+ private var permissionPending = false
76
+
77
+ var isSettled: Bool {
78
+ lock.lock()
79
+ defer { lock.unlock() }
80
+ return settled
81
+ }
82
+
83
+ var isPermissionPending: Bool {
84
+ lock.lock()
85
+ defer { lock.unlock() }
86
+ return permissionPending
87
+ }
88
+
89
+ func settle() {
90
+ lock.lock()
91
+ let first = !settled
92
+ settled = true
93
+ lock.unlock()
94
+ if first {
95
+ semaphore.signal()
96
+ }
97
+ }
98
+
99
+ func markPermissionPending() {
100
+ lock.lock()
101
+ permissionPending = true
102
+ lock.unlock()
103
+ }
104
+
105
+ func wait(_ seconds: TimeInterval) {
106
+ _ = semaphore.wait(timeout: .now() + seconds)
107
+ }
108
+ }
109
+
110
+ private final class LockedError {
111
+ private let lock = NSLock()
112
+ private var stored: NWError?
113
+
114
+ var value: NWError? {
115
+ lock.lock()
116
+ defer { lock.unlock() }
117
+ return stored
118
+ }
119
+
120
+ func set(_ error: NWError?) {
121
+ lock.lock()
122
+ stored = error
123
+ lock.unlock()
124
+ }
125
+ }
126
+
127
+ let address: String
128
+ let port: Int
129
+
130
+ private let queue = DispatchQueue(label: "de.paystory.thermal-printer.tcp")
131
+ private let lock = NSLock()
132
+ private let makeConnection: (String, NWEndpoint.Port) -> EscPosNetworkConnection
133
+ private var connection: EscPosNetworkConnection?
134
+ private var ready = false
135
+
136
+ init(address: String, port: Int, makeConnection: @escaping (String, NWEndpoint.Port) -> EscPosNetworkConnection = { EscPosNWConnection(host: $0, port: $1) }) {
137
+ self.address = address
138
+ self.port = port
139
+ self.makeConnection = makeConnection
140
+ }
141
+
142
+ override var isConnected: Bool {
143
+ lock.lock()
144
+ defer { lock.unlock() }
145
+ return connection != nil && ready
146
+ }
147
+
148
+ @discardableResult
149
+ override func connect() throws -> EscPosDeviceConnection {
150
+ if isConnected {
151
+ return self
152
+ }
153
+ guard (0...65535).contains(port), let endpointPort = NWEndpoint.Port(rawValue: UInt16(port)) else {
154
+ throw EscPosError.runtime("port out of range:\(port)")
155
+ }
156
+ disconnect()
157
+
158
+ let candidate = makeConnection(address, endpointPort)
159
+ let progress = ConnectProgress()
160
+
161
+ candidate.stateUpdateHandler = { [weak self, weak candidate] state in
162
+ guard let self = self, let candidate = candidate else { return }
163
+ switch state {
164
+ case .ready:
165
+ self.lock.lock()
166
+ if self.connection === candidate {
167
+ self.ready = true
168
+ }
169
+ self.lock.unlock()
170
+ progress.settle()
171
+ case .waiting(let error):
172
+ if EscPosTcpConnection.waitingDisposition(for: error, unsatisfiedReason: candidate.unsatisfiedReason) == .awaitLocalNetworkPermission {
173
+ progress.markPermissionPending()
174
+ } else {
175
+ self.markClosed(candidate)
176
+ progress.settle()
177
+ }
178
+ case .failed, .cancelled:
179
+ self.markClosed(candidate)
180
+ progress.settle()
181
+ default:
182
+ break
183
+ }
184
+ }
185
+
186
+ lock.lock()
187
+ connection = candidate
188
+ ready = false
189
+ lock.unlock()
190
+
191
+ candidate.start(queue: queue)
192
+
193
+ let started = Date()
194
+ while !progress.isSettled {
195
+ let limit = progress.isPermissionPending ? EscPosTcpConnection.localNetworkPermissionTimeout : EscPosTcpConnection.connectTimeout
196
+ let remaining = started.addingTimeInterval(limit).timeIntervalSinceNow
197
+ if remaining <= 0 {
198
+ break
199
+ }
200
+ progress.wait(min(remaining, 0.05))
201
+ }
202
+ if !isConnected {
203
+ disconnect()
204
+ throw EscPosError.connection("Unable to connect to TCP device.")
205
+ }
206
+ receiveLoop(candidate)
207
+ clearBuffer()
208
+ return self
209
+ }
210
+
211
+ @discardableResult
212
+ override func disconnect() -> EscPosDeviceConnection {
213
+ clearBuffer()
214
+ lock.lock()
215
+ let current = connection
216
+ connection = nil
217
+ ready = false
218
+ lock.unlock()
219
+ current?.stateUpdateHandler = nil
220
+ current?.cancel()
221
+ return self
222
+ }
223
+
224
+ override func transmit(_ bytes: [UInt8]) throws {
225
+ lock.lock()
226
+ let current = connection
227
+ lock.unlock()
228
+ guard let current = current else {
229
+ throw EscPosError.connection("Unable to send data to device.")
230
+ }
231
+ let done = DispatchSemaphore(value: 0)
232
+ let outcome = LockedError()
233
+ current.send(Data(bytes)) { error in
234
+ outcome.set(error)
235
+ done.signal()
236
+ }
237
+ if done.wait(timeout: .now() + EscPosTcpConnection.sendTimeout) == .timedOut {
238
+ disconnect()
239
+ throw EscPosError.connection("Timed out while sending data to device.")
240
+ }
241
+ if let error = outcome.value {
242
+ disconnect()
243
+ throw EscPosError.connection(error.localizedDescription)
244
+ }
245
+ }
246
+
247
+ private func receiveLoop(_ current: EscPosNetworkConnection) {
248
+ current.receive { [weak self, weak current] isComplete, error in
249
+ guard let self = self, let current = current else { return }
250
+ if isComplete || error != nil {
251
+ self.markClosed(current)
252
+ return
253
+ }
254
+ self.receiveLoop(current)
255
+ }
256
+ }
257
+
258
+ private func markClosed(_ closed: EscPosNetworkConnection) {
259
+ lock.lock()
260
+ if connection === closed {
261
+ ready = false
262
+ }
263
+ lock.unlock()
264
+ }
265
+ }