@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,213 @@
1
+ import Foundation
2
+
3
+ struct EscPosError: Error, Equatable {
4
+ enum Kind: Equatable {
5
+ case connection
6
+ case parser
7
+ case encoding
8
+ case barcode
9
+ case runtime
10
+ }
11
+
12
+ let kind: Kind
13
+ let message: String
14
+
15
+ static func connection(_ message: String) -> EscPosError { EscPosError(kind: .connection, message: message) }
16
+ static func parser(_ message: String) -> EscPosError { EscPosError(kind: .parser, message: message) }
17
+ static func encoding(_ message: String) -> EscPosError { EscPosError(kind: .encoding, message: message) }
18
+ static func barcode(_ message: String) -> EscPosError { EscPosError(kind: .barcode, message: message) }
19
+ static func runtime(_ message: String) -> EscPosError { EscPosError(kind: .runtime, message: message) }
20
+ }
21
+
22
+ struct JavaString: Equatable {
23
+ var units: [UInt16]
24
+
25
+ init(_ string: String) {
26
+ units = Array(string.utf16)
27
+ }
28
+
29
+ init<S: Sequence>(units: S) where S.Element == UInt16 {
30
+ self.units = Array(units)
31
+ }
32
+
33
+ var length: Int { units.count }
34
+
35
+ var isEmpty: Bool { units.isEmpty }
36
+
37
+ var string: String { String(decoding: units, as: UTF16.self) }
38
+
39
+ func indexOf(_ needle: String, from: Int = 0) -> Int {
40
+ let pattern = Array(needle.utf16)
41
+ let start = max(0, from)
42
+ if pattern.isEmpty {
43
+ return start <= length ? start : length
44
+ }
45
+ guard pattern.count <= length else { return -1 }
46
+ var index = start
47
+ let first = pattern[0]
48
+ while index + pattern.count <= length {
49
+ if units[index] == first && units[index..<(index + pattern.count)].elementsEqual(pattern) {
50
+ return index
51
+ }
52
+ index += 1
53
+ }
54
+ return -1
55
+ }
56
+
57
+ func contains(_ needle: String) -> Bool {
58
+ indexOf(needle) != -1
59
+ }
60
+
61
+ func substring(_ begin: Int, _ end: Int? = nil) throws -> JavaString {
62
+ let finish = end ?? length
63
+ guard begin >= 0, finish <= length, begin <= finish else {
64
+ throw EscPosError.runtime("begin \(begin), end \(finish), length \(length)")
65
+ }
66
+ return JavaString(units: units[begin..<finish])
67
+ }
68
+
69
+ func trim() -> JavaString {
70
+ var start = 0
71
+ var finish = length
72
+ while start < finish && units[start] <= 0x20 {
73
+ start += 1
74
+ }
75
+ while finish > start && units[finish - 1] <= 0x20 {
76
+ finish -= 1
77
+ }
78
+ return JavaString(units: units[start..<finish])
79
+ }
80
+
81
+ func lowercased() -> JavaString { JavaString(string.lowercased()) }
82
+
83
+ func uppercased() -> JavaString { JavaString(string.uppercased()) }
84
+
85
+ static func == (lhs: JavaString, rhs: String) -> Bool {
86
+ lhs.units.elementsEqual(rhs.utf16)
87
+ }
88
+
89
+ static func splitLines(_ text: JavaString) -> [JavaString] {
90
+ var parts: [JavaString] = []
91
+ var start = 0
92
+ var index = 0
93
+ var matched = false
94
+ let units = text.units
95
+ while index < units.count {
96
+ if units[index] == 0x0A {
97
+ parts.append(JavaString(units: units[start..<index]))
98
+ index += 1
99
+ start = index
100
+ matched = true
101
+ } else if units[index] == 0x0D && index + 1 < units.count && units[index + 1] == 0x0A {
102
+ parts.append(JavaString(units: units[start..<index]))
103
+ index += 2
104
+ start = index
105
+ matched = true
106
+ } else {
107
+ index += 1
108
+ }
109
+ }
110
+ if !matched {
111
+ return [text]
112
+ }
113
+ parts.append(JavaString(units: units[start..<units.count]))
114
+ while let last = parts.last, last.isEmpty {
115
+ parts.removeLast()
116
+ }
117
+ return parts
118
+ }
119
+ }
120
+
121
+ enum JavaMath {
122
+ static func round(_ value: Float) -> Int {
123
+ if value.isNaN { return 0 }
124
+ return saturatingInt32((Double(value) + 0.5).rounded(.down))
125
+ }
126
+
127
+ static func round(_ value: Double) -> Int {
128
+ if value.isNaN { return 0 }
129
+ let rounded = (value + 0.5).rounded(.down)
130
+ if rounded >= Double(Int64.max) { return Int(Int64.max) }
131
+ if rounded <= Double(Int64.min) { return Int(Int64.min) }
132
+ return Int(rounded)
133
+ }
134
+
135
+ static func toInt(_ value: Float) -> Int {
136
+ if value.isNaN { return 0 }
137
+ return saturatingInt32(Double(value).rounded(.towardZero))
138
+ }
139
+
140
+ static func toInt(_ value: Double) -> Int {
141
+ if value.isNaN { return 0 }
142
+ return saturatingInt32(value.rounded(.towardZero))
143
+ }
144
+
145
+ static func floorDiv(_ numerator: Float, _ denominator: Float) -> Int {
146
+ toInt((numerator / denominator).rounded(.down))
147
+ }
148
+
149
+ static func parseFloat(_ string: String) -> Float? {
150
+ var text = JavaString(string).trim().string
151
+ if let last = text.last, "fFdD".contains(last) {
152
+ text.removeLast()
153
+ }
154
+ if text.isEmpty || text.lowercased().hasPrefix("inf") || text.lowercased().hasPrefix("+inf") || text.lowercased().hasPrefix("-inf") {
155
+ return nil
156
+ }
157
+ return Float(text)
158
+ }
159
+
160
+ static func parseDouble(_ string: String) -> Double? {
161
+ var text = JavaString(string).trim().string
162
+ if let last = text.last, "fFdD".contains(last) {
163
+ text.removeLast()
164
+ }
165
+ if text.isEmpty { return nil }
166
+ return Double(text)
167
+ }
168
+
169
+ private static func saturatingInt32(_ value: Double) -> Int {
170
+ if value >= Double(Int32.max) { return Int(Int32.max) }
171
+ if value <= Double(Int32.min) { return Int(Int32.min) }
172
+ return Int(value)
173
+ }
174
+ }
175
+
176
+ enum EscPosHex {
177
+ private static let digits = Array("0123456789abcdef".utf8)
178
+
179
+ static func string(_ bytes: [UInt8]) -> String {
180
+ var output = [UInt8]()
181
+ output.reserveCapacity(bytes.count * 2)
182
+ for byte in bytes {
183
+ output.append(digits[Int(byte >> 4)])
184
+ output.append(digits[Int(byte & 0x0F)])
185
+ }
186
+ return String(decoding: output, as: UTF8.self)
187
+ }
188
+
189
+ static func bytes(_ hex: JavaString) throws -> [UInt8] {
190
+ let count = hex.length / 2
191
+ var bytes = [UInt8](repeating: 0, count: count)
192
+ let units = hex.units
193
+ for index in 0..<count {
194
+ let high = units[index * 2]
195
+ let low = units[index * 2 + 1]
196
+ guard let highValue = value(high), let lowValue = value(low) else {
197
+ let pair = JavaString(units: [high, low]).string
198
+ throw EscPosError.runtime("For input string: \"\(pair)\" under radix 16")
199
+ }
200
+ bytes[index] = UInt8(highValue << 4 | lowValue)
201
+ }
202
+ return bytes
203
+ }
204
+
205
+ private static func value(_ unit: UInt16) -> UInt8? {
206
+ switch unit {
207
+ case 0x30...0x39: return UInt8(unit - 0x30)
208
+ case 0x41...0x46: return UInt8(unit - 0x41 + 10)
209
+ case 0x61...0x66: return UInt8(unit - 0x61 + 10)
210
+ default: return nil
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,44 @@
1
+ import Foundation
2
+
3
+ final class EscPosKnownPrinterStore {
4
+ static func applicationSupportFile() -> URL {
5
+ let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
6
+ ?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
7
+ return base
8
+ .appendingPathComponent("ThermalPrinter", isDirectory: true)
9
+ .appendingPathComponent("known-bluetooth-printers.json")
10
+ }
11
+
12
+ let fileURL: URL
13
+ private let lock = NSLock()
14
+
15
+ init(fileURL: URL = EscPosKnownPrinterStore.applicationSupportFile()) {
16
+ self.fileURL = fileURL
17
+ }
18
+
19
+ func load() -> [String: String] {
20
+ lock.lock()
21
+ defer { lock.unlock() }
22
+ return read()
23
+ }
24
+
25
+ func remember(identifier: UUID, name: String) {
26
+ lock.lock()
27
+ defer { lock.unlock() }
28
+ var printers = read()
29
+ printers[identifier.uuidString] = name
30
+ guard let data = try? JSONSerialization.data(withJSONObject: printers, options: [.sortedKeys]) else { return }
31
+ try? FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
32
+ try? data.write(to: fileURL, options: [.atomic])
33
+ }
34
+
35
+ private func read() -> [String: String] {
36
+ guard let data = try? Data(contentsOf: fileURL),
37
+ let object = try? JSONSerialization.jsonObject(with: data),
38
+ let printers = object as? [String: String]
39
+ else {
40
+ return [:]
41
+ }
42
+ return printers
43
+ }
44
+ }
@@ -0,0 +1,55 @@
1
+ import Foundation
2
+
3
+ final class EscPosPrinter: EscPosPrinterSize {
4
+ private(set) var printer: EscPosPrinterCommands?
5
+
6
+ init(connection: EscPosDeviceConnection?, printerDpi: Int, printerWidthMM: Float, printerNbrCharactersPerLine: Int, charsetEncoding: EscPosCharsetEncoding? = nil) throws {
7
+ let commands = connection.map { EscPosPrinterCommands(printerConnection: $0, charsetEncoding: charsetEncoding) }
8
+ try super.init(printerDpi: printerDpi, printerWidthMM: printerWidthMM, printerNbrCharactersPerLine: printerNbrCharactersPerLine)
9
+ if let commands = commands {
10
+ printer = try commands.connect()
11
+ }
12
+ }
13
+
14
+ var encoding: EscPosCharsetEncoding? { printer?.charsetEncoding }
15
+
16
+ @discardableResult
17
+ func disconnectPrinter() -> EscPosPrinter {
18
+ printer?.disconnect()
19
+ printer = nil
20
+ return self
21
+ }
22
+
23
+ @discardableResult
24
+ func printFormattedText(_ text: String, dotsFeedPaper: Int) throws -> EscPosPrinter {
25
+ guard let printer = printer, printerNbrCharactersPerLine != 0 else { return self }
26
+
27
+ let linesParsed = try PrinterTextParser(printer: self).setFormattedText(JavaString(text)).parse()
28
+
29
+ try printer.reset()
30
+
31
+ for line in linesParsed {
32
+ var lastElement: PrinterTextParserElement?
33
+ for column in line.columns {
34
+ for element in column.elements {
35
+ try element.print(printer)
36
+ lastElement = element
37
+ }
38
+ }
39
+ if lastElement is PrinterTextParserString {
40
+ try printer.newLine()
41
+ }
42
+ }
43
+
44
+ try printer.feedPaper(dotsFeedPaper)
45
+ return self
46
+ }
47
+
48
+ @discardableResult
49
+ func printFormattedTextAndCut(_ text: String, dotsFeedPaper: Int) throws -> EscPosPrinter {
50
+ guard let printer = printer, printerNbrCharactersPerLine != 0 else { return self }
51
+ try printFormattedText(text, dotsFeedPaper: dotsFeedPaper)
52
+ try printer.cutPaper()
53
+ return self
54
+ }
55
+ }
@@ -0,0 +1,367 @@
1
+ import Foundation
2
+
3
+ final class EscPosPrinterCommands {
4
+ static let lineFeed: UInt8 = 0x0A
5
+
6
+ static let resetPrinter: [UInt8] = [0x1B, 0x40]
7
+
8
+ static let textAlignLeft: [UInt8] = [0x1B, 0x61, 0x00]
9
+ static let textAlignCenter: [UInt8] = [0x1B, 0x61, 0x01]
10
+ static let textAlignRight: [UInt8] = [0x1B, 0x61, 0x02]
11
+
12
+ static let textWeightNormal: [UInt8] = [0x1B, 0x45, 0x00]
13
+ static let textWeightBold: [UInt8] = [0x1B, 0x45, 0x01]
14
+
15
+ static let lineSpacing24: [UInt8] = [0x1B, 0x33, 0x18]
16
+ static let lineSpacing30: [UInt8] = [0x1B, 0x33, 0x1E]
17
+
18
+ static let textSizeNormal: [UInt8] = [0x1D, 0x21, 0x00]
19
+ static let textSizeDoubleHeight: [UInt8] = [0x1D, 0x21, 0x01]
20
+ static let textSizeDoubleWidth: [UInt8] = [0x1D, 0x21, 0x10]
21
+ static let textSizeBig: [UInt8] = [0x1D, 0x21, 0x11]
22
+ static let textSizeBig2: [UInt8] = [0x1D, 0x21, 0x22]
23
+ static let textSizeBig3: [UInt8] = [0x1D, 0x21, 0x33]
24
+ static let textSizeBig4: [UInt8] = [0x1D, 0x21, 0x44]
25
+ static let textSizeBig5: [UInt8] = [0x1D, 0x21, 0x55]
26
+ static let textSizeBig6: [UInt8] = [0x1D, 0x21, 0x66]
27
+
28
+ static let textUnderlineOff: [UInt8] = [0x1B, 0x2D, 0x00]
29
+ static let textUnderlineOn: [UInt8] = [0x1B, 0x2D, 0x01]
30
+ static let textUnderlineLarge: [UInt8] = [0x1B, 0x2D, 0x02]
31
+
32
+ static let textDoubleStrikeOff: [UInt8] = [0x1B, 0x47, 0x00]
33
+ static let textDoubleStrikeOn: [UInt8] = [0x1B, 0x47, 0x01]
34
+
35
+ static let textColorBlack: [UInt8] = [0x1B, 0x72, 0x00]
36
+ static let textColorRed: [UInt8] = [0x1B, 0x72, 0x01]
37
+
38
+ static let textColorReverseOff: [UInt8] = [0x1D, 0x42, 0x00]
39
+ static let textColorReverseOn: [UInt8] = [0x1D, 0x42, 0x01]
40
+
41
+ static let barcodeTypeUPCA = 65
42
+ static let barcodeTypeUPCE = 66
43
+ static let barcodeTypeEAN13 = 67
44
+ static let barcodeTypeEAN8 = 68
45
+ static let barcodeTypeITF = 70
46
+ static let barcodeType128 = 73
47
+
48
+ static let barcodeTextPositionNone = 0
49
+ static let barcodeTextPositionAbove = 1
50
+ static let barcodeTextPositionBelow = 2
51
+
52
+ let printerConnection: EscPosDeviceConnection
53
+ let charsetEncoding: EscPosCharsetEncoding
54
+ var useEscAsteriskCommand = false
55
+
56
+ private var currentTextSize: [UInt8] = []
57
+ private var currentTextColor: [UInt8] = []
58
+ private var currentTextReverseColor: [UInt8] = []
59
+ private var currentTextBold: [UInt8] = []
60
+ private var currentTextUnderline: [UInt8] = []
61
+ private var currentTextDoubleStrike: [UInt8] = []
62
+
63
+ init(printerConnection: EscPosDeviceConnection, charsetEncoding: EscPosCharsetEncoding? = nil) {
64
+ self.printerConnection = printerConnection
65
+ self.charsetEncoding = charsetEncoding ?? EscPosCharsetEncoding(name: "windows-1252", escPosCharsetId: 6)
66
+ }
67
+
68
+ static func initGSv0Command(bytesByLine: Int, bitmapHeight: Int) -> [UInt8] {
69
+ let xH = bytesByLine / 256
70
+ let xL = bytesByLine - xH * 256
71
+ let yH = bitmapHeight / 256
72
+ let yL = bitmapHeight - yH * 256
73
+ var imageBytes = [UInt8](repeating: 0, count: 8 + bytesByLine * bitmapHeight)
74
+ imageBytes[0] = 0x1D
75
+ imageBytes[1] = 0x76
76
+ imageBytes[2] = 0x30
77
+ imageBytes[3] = 0x00
78
+ imageBytes[4] = UInt8(truncatingIfNeeded: xL)
79
+ imageBytes[5] = UInt8(truncatingIfNeeded: xH)
80
+ imageBytes[6] = UInt8(truncatingIfNeeded: yL)
81
+ imageBytes[7] = UInt8(truncatingIfNeeded: yH)
82
+ return imageBytes
83
+ }
84
+
85
+ static func bitmapToBytes(_ bitmap: EscPosBitmap) -> [UInt8] {
86
+ let bitmapWidth = bitmap.width
87
+ let bitmapHeight = bitmap.height
88
+ let bytesByLine = Int((Float(bitmapWidth) / 8).rounded(.up))
89
+ var imageBytes = initGSv0Command(bytesByLine: bytesByLine, bitmapHeight: bitmapHeight)
90
+
91
+ var index = 8
92
+ var greyscaleCoefficientInit = 0
93
+ let gradientStep = 6
94
+ let colorLevelStep = 765.0 / Double(15 * gradientStep + gradientStep - 1)
95
+
96
+ for posY in 0..<bitmapHeight {
97
+ var greyscaleCoefficient = greyscaleCoefficientInit
98
+ let greyscaleLine = posY % gradientStep
99
+ var column = 0
100
+ while column < bitmapWidth {
101
+ var byte = 0
102
+ for bit in 0..<8 {
103
+ let posX = column + bit
104
+ if posX < bitmapWidth {
105
+ let color = bitmap.pixel(posX, posY)
106
+ let red = Int((color >> 16) & 255)
107
+ let green = Int((color >> 8) & 255)
108
+ let blue = Int(color & 255)
109
+ if Double(red + green + blue) < Double(greyscaleCoefficient * gradientStep + greyscaleLine) * colorLevelStep {
110
+ byte |= 1 << (7 - bit)
111
+ }
112
+ greyscaleCoefficient += 5
113
+ if greyscaleCoefficient > 15 {
114
+ greyscaleCoefficient -= 16
115
+ }
116
+ }
117
+ }
118
+ imageBytes[index] = UInt8(truncatingIfNeeded: byte)
119
+ index += 1
120
+ column += 8
121
+ }
122
+ greyscaleCoefficientInit += 2
123
+ if greyscaleCoefficientInit > 15 {
124
+ greyscaleCoefficientInit = 0
125
+ }
126
+ }
127
+ return imageBytes
128
+ }
129
+
130
+ static func convertGSv0ToEscAsterisk(_ bytes: [UInt8]) -> [[UInt8]] {
131
+ let xL = Int(bytes[4])
132
+ let xH = Int(bytes[5])
133
+ let yL = Int(bytes[6])
134
+ let yH = Int(bytes[7])
135
+ let bytesByLine = xH * 256 + xL
136
+ let dotsByLine = bytesByLine * 8
137
+ let nH = dotsByLine / 256
138
+ let nL = dotsByLine % 256
139
+ let imageHeight = yH * 256 + yL
140
+ let imageLineHeightCount = Int((Double(imageHeight) / 24.0).rounded(.up))
141
+ let imageBytesSize = 6 + bytesByLine * 24
142
+
143
+ var returnedBytes: [[UInt8]] = [lineSpacing24]
144
+ for lineIndex in 0..<imageLineHeightCount {
145
+ let pxBaseRow = lineIndex * 24
146
+ var imageBytes = [UInt8](repeating: 0, count: imageBytesSize)
147
+ imageBytes[0] = 0x1B
148
+ imageBytes[1] = 0x2A
149
+ imageBytes[2] = 0x21
150
+ imageBytes[3] = UInt8(truncatingIfNeeded: nL)
151
+ imageBytes[4] = UInt8(truncatingIfNeeded: nH)
152
+ for byteIndex in 5..<imageBytes.count {
153
+ let imgByte = byteIndex - 5
154
+ let byteRow = imgByte % 3
155
+ let pxColumn = imgByte / 3
156
+ let bitColumn = 1 << (7 - pxColumn % 8)
157
+ let pxRow = pxBaseRow + byteRow * 8
158
+ for bit in 0..<8 {
159
+ let indexBytes = bytesByLine * (pxRow + bit) + pxColumn / 8 + 8
160
+ if indexBytes >= bytes.count {
161
+ break
162
+ }
163
+ let isBlack = (Int(bytes[indexBytes]) & bitColumn) == bitColumn
164
+ if isBlack {
165
+ imageBytes[byteIndex] |= UInt8(truncatingIfNeeded: 1 << (7 - bit))
166
+ }
167
+ }
168
+ }
169
+ imageBytes[imageBytes.count - 1] = lineFeed
170
+ returnedBytes.append(imageBytes)
171
+ }
172
+ returnedBytes.append(lineSpacing30)
173
+ return returnedBytes
174
+ }
175
+
176
+ static func qrCodeDataToBytes(_ data: String, size: Int) throws -> [UInt8] {
177
+ let matrix = try EscPosQRCode.matrix(for: data)
178
+ let width = matrix.count
179
+ let height = matrix.count
180
+ guard width > 0 else {
181
+ return initGSv0Command(bytesByLine: 0, bitmapHeight: 0)
182
+ }
183
+ let coefficient = JavaMath.round(Float(size) / Float(width))
184
+ let imageWidth = width * coefficient
185
+ let imageHeight = height * coefficient
186
+ let bytesByLine = Int((Float(imageWidth) / 8).rounded(.up))
187
+
188
+ if coefficient < 1 {
189
+ return initGSv0Command(bytesByLine: 0, bitmapHeight: 0)
190
+ }
191
+
192
+ var imageBytes = initGSv0Command(bytesByLine: bytesByLine, bitmapHeight: imageHeight)
193
+ var index = 8
194
+ for y in 0..<height {
195
+ var lineBytes = [UInt8](repeating: 0, count: bytesByLine)
196
+ var x = -1
197
+ var multipleX = coefficient
198
+ var isBlack = false
199
+ for byteIndex in 0..<bytesByLine {
200
+ var byte = 0
201
+ for bit in 0..<8 {
202
+ if multipleX == coefficient {
203
+ x += 1
204
+ isBlack = x < width && matrix[y][x]
205
+ multipleX = 0
206
+ }
207
+ if isBlack {
208
+ byte |= 1 << (7 - bit)
209
+ }
210
+ multipleX += 1
211
+ }
212
+ lineBytes[byteIndex] = UInt8(truncatingIfNeeded: byte)
213
+ }
214
+ for _ in 0..<coefficient {
215
+ imageBytes.replaceSubrange(index..<(index + lineBytes.count), with: lineBytes)
216
+ index += lineBytes.count
217
+ }
218
+ }
219
+ return imageBytes
220
+ }
221
+
222
+ @discardableResult
223
+ func connect() throws -> EscPosPrinterCommands {
224
+ try printerConnection.connect()
225
+ return self
226
+ }
227
+
228
+ func disconnect() {
229
+ printerConnection.disconnect()
230
+ }
231
+
232
+ private func requireConnection() throws {
233
+ guard printerConnection.isConnected else {
234
+ printerConnection.clearBuffer()
235
+ throw EscPosError.connection("Unable to send data to device.")
236
+ }
237
+ }
238
+
239
+ @discardableResult
240
+ func reset() throws -> EscPosPrinterCommands {
241
+ try requireConnection()
242
+ printerConnection.write(EscPosPrinterCommands.resetPrinter)
243
+ return self
244
+ }
245
+
246
+ @discardableResult
247
+ func setAlign(_ align: [UInt8]) throws -> EscPosPrinterCommands {
248
+ try requireConnection()
249
+ printerConnection.write(align)
250
+ return self
251
+ }
252
+
253
+ @discardableResult
254
+ func printText(
255
+ _ text: JavaString,
256
+ textSize: [UInt8]? = nil,
257
+ textColor: [UInt8]? = nil,
258
+ textReverseColor: [UInt8]? = nil,
259
+ textBold: [UInt8]? = nil,
260
+ textUnderline: [UInt8]? = nil,
261
+ textDoubleStrike: [UInt8]? = nil
262
+ ) throws -> EscPosPrinterCommands {
263
+ try requireConnection()
264
+
265
+ let size = textSize ?? EscPosPrinterCommands.textSizeNormal
266
+ let color = textColor ?? EscPosPrinterCommands.textColorBlack
267
+ let reverseColor = textReverseColor ?? EscPosPrinterCommands.textColorReverseOff
268
+ let bold = textBold ?? EscPosPrinterCommands.textWeightNormal
269
+ let underline = textUnderline ?? EscPosPrinterCommands.textUnderlineOff
270
+ let doubleStrike = textDoubleStrike ?? EscPosPrinterCommands.textDoubleStrikeOff
271
+
272
+ let textBytes = try charsetEncoding.encode(text)
273
+ printerConnection.write(charsetEncoding.command)
274
+
275
+ if currentTextSize != size {
276
+ printerConnection.write(size)
277
+ currentTextSize = size
278
+ }
279
+ if currentTextDoubleStrike != doubleStrike {
280
+ printerConnection.write(doubleStrike)
281
+ currentTextDoubleStrike = doubleStrike
282
+ }
283
+ if currentTextUnderline != underline {
284
+ printerConnection.write(underline)
285
+ currentTextUnderline = underline
286
+ }
287
+ if currentTextBold != bold {
288
+ printerConnection.write(bold)
289
+ currentTextBold = bold
290
+ }
291
+ if currentTextColor != color {
292
+ printerConnection.write(color)
293
+ currentTextColor = color
294
+ }
295
+ if currentTextReverseColor != reverseColor {
296
+ printerConnection.write(reverseColor)
297
+ currentTextReverseColor = reverseColor
298
+ }
299
+
300
+ printerConnection.write(textBytes)
301
+ return self
302
+ }
303
+
304
+ @discardableResult
305
+ func printImage(_ image: [UInt8]) throws -> EscPosPrinterCommands {
306
+ try requireConnection()
307
+ let bytesToPrint = useEscAsteriskCommand ? EscPosPrinterCommands.convertGSv0ToEscAsterisk(image) : [image]
308
+ for bytes in bytesToPrint {
309
+ printerConnection.write(bytes)
310
+ try printerConnection.send()
311
+ }
312
+ return self
313
+ }
314
+
315
+ @discardableResult
316
+ func printBarcode(_ barcode: EscPosBarcode) throws -> EscPosPrinterCommands {
317
+ try requireConnection()
318
+ let code = barcode.code
319
+ let barcodeLength = barcode.codeLength
320
+ var barcodeCommand: [UInt8] = [0x1D, 0x6B, UInt8(truncatingIfNeeded: barcode.barcodeType), UInt8(truncatingIfNeeded: barcodeLength)]
321
+ for index in 0..<barcodeLength {
322
+ barcodeCommand.append(UInt8(truncatingIfNeeded: code.units[index]))
323
+ }
324
+ printerConnection.write([0x1D, 0x48, UInt8(truncatingIfNeeded: barcode.textPosition)])
325
+ printerConnection.write([0x1D, 0x77, UInt8(truncatingIfNeeded: barcode.colWidth)])
326
+ printerConnection.write([0x1D, 0x68, UInt8(truncatingIfNeeded: barcode.height)])
327
+ printerConnection.write(barcodeCommand)
328
+ return self
329
+ }
330
+
331
+ @discardableResult
332
+ func newLine(_ align: [UInt8]? = nil) throws -> EscPosPrinterCommands {
333
+ try requireConnection()
334
+ printerConnection.write([EscPosPrinterCommands.lineFeed])
335
+ try printerConnection.send()
336
+ if let align = align {
337
+ printerConnection.write(align)
338
+ }
339
+ return self
340
+ }
341
+
342
+ @discardableResult
343
+ func feedPaper(_ dots: Int) throws -> EscPosPrinterCommands {
344
+ try requireConnection()
345
+ if dots > 0 {
346
+ printerConnection.write([0x1B, 0x4A, UInt8(truncatingIfNeeded: dots)])
347
+ try printerConnection.send(dots)
348
+ }
349
+ return self
350
+ }
351
+
352
+ @discardableResult
353
+ func cutPaper() throws -> EscPosPrinterCommands {
354
+ try requireConnection()
355
+ printerConnection.write([0x1D, 0x56, 0x01])
356
+ try printerConnection.send(100)
357
+ return self
358
+ }
359
+
360
+ @discardableResult
361
+ func openCashBox() throws -> EscPosPrinterCommands {
362
+ try requireConnection()
363
+ printerConnection.write([0x1B, 0x70, 0x00, 0x3C, 0xFF])
364
+ try printerConnection.send(100)
365
+ return self
366
+ }
367
+ }