@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.
- package/BricksSoftCapacitorThermalPrinter.podspec +17 -0
- package/LICENSE +201 -0
- package/Package.swift +13 -0
- package/README.md +80 -2
- package/android/build.gradle +40 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/brickssoft/thermalprinter/ThermalPrinterPlugin.java +463 -0
- package/dist/esm/definitions.d.ts +73 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +23 -0
- package/dist/esm/web.d.ts +13 -0
- package/dist/esm/web.js +33 -0
- package/dist/plugin.cjs.js +65 -0
- package/dist/plugin.js +68 -0
- package/ios/Sources/ThermalPrinter/EscPosBarcode.swift +121 -0
- package/ios/Sources/ThermalPrinter/EscPosBitmap.swift +178 -0
- package/ios/Sources/ThermalPrinter/EscPosBluetooth.swift +341 -0
- package/ios/Sources/ThermalPrinter/EscPosBluetoothConnection.swift +298 -0
- package/ios/Sources/ThermalPrinter/EscPosBluetoothRules.swift +78 -0
- package/ios/Sources/ThermalPrinter/EscPosCharsetEncoding.swift +115 -0
- package/ios/Sources/ThermalPrinter/EscPosCoreBluetoothLink.swift +287 -0
- package/ios/Sources/ThermalPrinter/EscPosDeviceConnection.swift +47 -0
- package/ios/Sources/ThermalPrinter/EscPosJavaCompat.swift +213 -0
- package/ios/Sources/ThermalPrinter/EscPosKnownPrinterStore.swift +44 -0
- package/ios/Sources/ThermalPrinter/EscPosPrinter.swift +55 -0
- package/ios/Sources/ThermalPrinter/EscPosPrinterCommands.swift +367 -0
- package/ios/Sources/ThermalPrinter/EscPosPrinterSize.swift +53 -0
- package/ios/Sources/ThermalPrinter/EscPosQRCode.swift +70 -0
- package/ios/Sources/ThermalPrinter/EscPosQueue.swift +30 -0
- package/ios/Sources/ThermalPrinter/EscPosTcpConnection.swift +265 -0
- package/ios/Sources/ThermalPrinter/PrinterTextParser.swift +622 -0
- package/ios/Sources/ThermalPrinter/ThermalPrinterPlugin.swift +85 -0
- package/ios/Sources/ThermalPrinter/ThermalPrinterService.swift +380 -0
- 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
|
+
}
|