@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
package/README.md ADDED
@@ -0,0 +1,649 @@
1
+ # @delicity/capacitor-thermal-printer
2
+
3
+ > Capacitor **7** plugin for **thermal / receipt / label printing** — **multi-brand** (ESC/POS, Epson, Star, Brother, Zebra), **image-based**, with **aggregated discovery**, **deduplication**, **automatic reconnection** and a **single JavaScript API**.
4
+
5
+ For **any Capacitor app that needs to print** — point of sale, receipts, order tickets, shipping/label printing, kitchen slips, etc. The user taps "Add a printer", picks one from a list, runs a test print, and never has to touch the phone's Bluetooth/Wi-Fi settings again.
6
+
7
+ **Requires Capacitor 7** · Android (`compileSdk 35`, JDK 21) · iOS 14+ / Xcode 16+.
8
+
9
+ ---
10
+
11
+ ## Contents
12
+
13
+ 1. [Philosophy](#philosophy)
14
+ 2. [Architecture](#architecture)
15
+ 3. [Installation](#installation)
16
+ 4. [Manufacturer SDKs](#manufacturer-sdks)
17
+ 5. [Permissions](#permissions)
18
+ 6. [Public API](#public-api)
19
+ 7. [Types](#types)
20
+ 8. [Image printing flow](#image-printing-flow)
21
+ 9. [Image processing](#image-processing)
22
+ 10. [Aggregated discovery & adapter priority](#aggregated-discovery--adapter-priority)
23
+ 11. [Default printer & reconnection](#default-printer--reconnection)
24
+ 12. [Normalized errors](#normalized-errors)
25
+ 13. [Android / iOS differences](#android--ios-differences)
26
+ 14. [Image cache & logs/diagnostics](#image-cache--logsdiagnostics)
27
+ 15. [Full example](#full-example)
28
+
29
+ ---
30
+
31
+ ## Philosophy
32
+
33
+ - **The app generates an image of what to print** (PNG/bitmap — receipt, ticket, label). It never sends structured text to the SDKs.
34
+ - The plugin **receives an image**, **normalizes** it (resize → grayscale → 1-bit + dithering), **converts** it to the adapter's format, and **sends** it.
35
+ - **One JS API.** Internally, an **adapter-based architecture** routes to the right implementation.
36
+ - **There is no universal protocol**: each family has its adapter. Adapter priority guarantees the best choice.
37
+
38
+ ## Architecture
39
+
40
+ ```
41
+ ┌─────────────────────────────────────────────────────────────┐
42
+ │ App (Ionic/JS/TS) │
43
+ │ discoverPrinters / connect / setDefault / printImage ... │
44
+ └───────────────────────────────┬───────────────────────────────┘
45
+ │ Single API (definitions.ts)
46
+ ┌───────────────┴───────────────┐
47
+ │ Capacitor Bridge │
48
+ ┌────────┴─────────┐ ┌──────────┴─────────┐
49
+ │ Android (Kotlin) │ │ iOS (Swift) │
50
+ │ ThermalPrinter… │ │ ThermalPrinter… │
51
+ └────────┬─────────┘ └──────────┬─────────┘
52
+ │ ThermalPrinterEngine │ ThermalPrinterEngine
53
+ ┌─────────────┼──────────────┐ ┌───────────┼──────────────┐
54
+ │ Discovery │ Adapters │ │ Discovery │ Adapters │
55
+ │ Manager │ (registry) │ │ Manager │ (registry) │
56
+ └─────────────┴──────────────┘ └───────────┴──────────────┘
57
+ │ │
58
+ ┌───────┴────────────────────────────────────┴─────────┐
59
+ │ EscPos · Epson · Star · Brother · Zebra · RawTcp · BLE │
60
+ │ Transport: TCP9100 / SPP(Android) / NWConnection(iOS) │
61
+ │ Image: decode → resize → grayscale → dither → raster │
62
+ │ Store: profiles + default printer (persisted) │
63
+ └────────────────────────────────────────────────────────┘
64
+ ```
65
+
66
+ > 📁 Repo layout, internal architecture, tests and the contribution guide live in
67
+ > [`CONTRIBUTING.md`](CONTRIBUTING.md).
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ npm install @delicity/capacitor-thermal-printer
73
+ npx cap sync
74
+ ```
75
+
76
+ **Capacitor 7 requirements**: Android `compileSdk 35` / JDK 21 ; iOS 14+ / Xcode 16+.
77
+
78
+ ## Manufacturer SDKs
79
+
80
+ The plugin supports **Star, Epson, Brother, Zebra** via their native SDK, **optionally**:
81
+ it compiles and works **without any SDK** (generic ESC/POS over TCP/Bluetooth/USB/BLE),
82
+ and each brand **activates automatically** as soon as its binary is present.
83
+
84
+ > **Why isn't it 100% automatic on `npm install`?**
85
+ > Only SDKs published to a standard package repository download by themselves.
86
+ > The others are distributed only through the manufacturer's portal and their
87
+ > **license forbids redistribution** — so they cannot be put on Maven Central /
88
+ > CocoaPods, nor committed here. The consuming app downloads the binary itself
89
+ > (accepting the license); the plugin provides all the code to use it.
90
+
91
+ | Brand | Android | iOS | What to do in the app |
92
+ |---|---|---|---|
93
+ | **Star** | ✅ auto (Maven Central) | ✅ auto (SPM) | Add the `StarXpand-SDK-iOS` SPM package (iOS). Android: nothing. |
94
+ | **Brother** | ⛔ manual `.aar` | ✅ auto (CocoaPods) | `pod 'BRLMPrinterKit'` (iOS); drop `BrotherPrintLibrary.aar` (Android). |
95
+ | **Epson** | ⛔ manual `.jar`+`.so` | ⛔ manual xcframework | Drop `ePOS2.jar` (Android) / `libepos2.xcframework` (iOS). |
96
+ | **Zebra** | ⚠️ private Maven (token) or `.jar` | ⛔ manual xcframework | Zebra token or `ZSDK_ANDROID_API.jar`; `ZSDK_API.xcframework` (iOS). |
97
+
98
+ **Official download links:**
99
+ - **Star**: [StarXpand-SDK-Android](https://github.com/star-micronics/StarXpand-SDK-Android) · [StarXpand-SDK-iOS](https://github.com/star-micronics/StarXpand-SDK-iOS) (nothing to download: Maven Central / SPM)
100
+ - **Epson**: [Epson Developers](https://epson.com/developers-products) · [MFi / ePOS SDK](https://global.epson.com/products_and_drivers/tm/en/mfi.html)
101
+ - **Brother**: [Mobile SDK (download)](https://support.brother.com/g/s/es/dev/en/mobilesdk/download/index.html) · US: [Brother Developer Program](https://developerprogram.brother-usa.com/sdk-download) · iOS pod: [BRLMPrinterKit](https://cocoapods.org/pods/BRLMPrinterKit)
102
+ - **Zebra**: [Link-OS Multiplatform SDK](https://developer.zebra.com/products/printers/link-os-multiplatform-sdk) · [Downloads & support](https://www.zebra.com/us/en/support-downloads/software/printer-software/link-os-multiplatform-sdk.html)
103
+
104
+ > Epson / Brother / Zebra: a free developer account + license acceptance are required.
105
+
106
+ Step-by-step setup (where to drop each binary, Zebra private Maven repo, iOS module
107
+ names, git-ignored test folder): **[`docs/SDK_INTEGRATION.md`](docs/SDK_INTEGRATION.md)**.
108
+
109
+ ### Know which SDKs are active (runtime)
110
+
111
+ `getActiveSdks()` reports, at the current moment, which adapters/SDKs are available:
112
+
113
+ ```ts
114
+ import { ThermalPrinter } from '@delicity/capacitor-thermal-printer';
115
+
116
+ const { sdks } = await ThermalPrinter.getActiveSdks();
117
+ // [
118
+ // { adapter: 'escpos', label: 'Generic ESC/POS', available: true, requiresSdk: false, transports: ['wifi','ethernet','bluetooth','usb'] },
119
+ // { adapter: 'star', label: 'Star StarXpand', available: true, requiresSdk: true, transports: ['wifi','bluetooth','ble','usb'] },
120
+ // { adapter: 'epson', label: 'Epson ePOS2', available: false, requiresSdk: true, transports: ['wifi','bluetooth','usb'] },
121
+ // ...
122
+ // ]
123
+ const active = sdks.filter(s => s.available).map(s => s.label);
124
+ ```
125
+
126
+ Useful for a "Printer diagnostics" screen, or to only show the brands actually
127
+ available on the device.
128
+
129
+ ## Permissions
130
+
131
+ ### Android (plugin `AndroidManifest.xml`, already provided)
132
+
133
+ | Permission | Use | API |
134
+ |---|---|---|
135
+ | `BLUETOOTH_SCAN` (`neverForLocation`) | BT/BLE scan | 31+ |
136
+ | `BLUETOOTH_CONNECT` | SPP/GATT connection | 31+ |
137
+ | `BLUETOOTH`, `BLUETOOTH_ADMIN` | scan/connection | ≤30 |
138
+ | `ACCESS_FINE_LOCATION` | BLE scan | ≤30 |
139
+ | `INTERNET`, `ACCESS_NETWORK_STATE`, `ACCESS_WIFI_STATE` | TCP 9100 + network detection | all |
140
+ | `CHANGE_WIFI_MULTICAST_STATE` | mDNS | all |
141
+ | `android.hardware.usb.host` (feature) | USB | optional |
142
+
143
+ Call `requestPermissions()` before the first scan.
144
+
145
+ ### iOS (host app `Info.plist`)
146
+
147
+ ```xml
148
+ <key>NSLocalNetworkUsageDescription</key>
149
+ <string>Discover and print to printers on the local network.</string>
150
+ <key>NSBonjourServices</key>
151
+ <array>
152
+ <string>_pdl-datastream._tcp</string>
153
+ <string>_printer._tcp</string>
154
+ <string>_ipp._tcp</string>
155
+ </array>
156
+ <!-- If BLE is enabled: -->
157
+ <key>NSBluetoothAlwaysUsageDescription</key>
158
+ <string>Connect to compatible Bluetooth printers.</string>
159
+ <!-- If using an MFi SDK (Epson/Star/Zebra Bluetooth): declare the protocols -->
160
+ <key>UISupportedExternalAccessoryProtocols</key>
161
+ <array>
162
+ <string>com.epson.escpos</string>
163
+ <string>jp.star-m.starpro</string>
164
+ </array>
165
+ ```
166
+
167
+ ## Public API
168
+
169
+ ```ts
170
+ import { ThermalPrinter } from '@delicity/capacitor-thermal-printer';
171
+
172
+ ThermalPrinter.discoverPrinters(options?) // → { printers: DiscoveredPrinter[] }
173
+ ThermalPrinter.connectPrinter({ printerId, timeoutMs?, forceAdapter?, setAsDefault? }) // → { connected }
174
+ ThermalPrinter.disconnectPrinter({ printerId }) // → void
175
+ ThermalPrinter.setDefaultPrinter({ printerId }) // → { profile }
176
+ ThermalPrinter.getDefaultPrinter() // → { profile | null }
177
+ ThermalPrinter.getSavedPrinters() // → { profiles }
178
+ ThermalPrinter.removePrinter({ printerId }) // → void
179
+ ThermalPrinter.printImage(options) // → PrintResult (await = printed)
180
+ ThermalPrinter.printText({ items, ... }) // → PrintResult (await = printed)
181
+ ThermalPrinter.getPrinterStatus({ printerId? }) // → PrinterStatus
182
+ ThermalPrinter.requestPermissions() / checkPermissions() // → PermissionStatus
183
+ ThermalPrinter.startStatusMonitor({ printerId, intervalMs? }) // background status polling
184
+ ThermalPrinter.stopStatusMonitor({ printerId })
185
+ ThermalPrinter.getActiveSdks() // → { sdks: SdkStatus[] }
186
+ ThermalPrinter.getDebugLog() // → { log: DebugLogEntry[] }
187
+
188
+ // Events
189
+ ThermalPrinter.addListener('printerFound', e => ...) // incremental scan results
190
+ ThermalPrinter.addListener('discoveryComplete', e => ...)
191
+ ThermalPrinter.addListener('statusChange', e => ...) // PrinterStatus (paper/cover/connection)
192
+ ThermalPrinter.addListener('printJobStatus', e => ...) // JobState: pending/printing/hold/completed/failed
193
+ ```
194
+
195
+ > **`connectPrinter({ setAsDefault: true })`** sets the default printer **only if
196
+ > the connection succeeds** (`connect` + `setDefaultPrinter` in one step, without
197
+ > persisting an unreachable printer).
198
+
199
+ ### Print completion / `await`
200
+
201
+ `printImage` and `printText` are **async and resolve when physical printing is done**
202
+ (best-effort) — so you can `await` them. Details:
203
+
204
+ - **Manufacturer SDKs**: the promise waits for the SDK's **completion callback** (max reliability).
205
+ - **ESC/POS TCP/SPP**: a **one-way** channel → the promise resolves once all bytes are
206
+ **written and flushed**. A **status pre-check** runs before sending: paper empty /
207
+ cover open → job set to `hold` + rejection `PAPER_EMPTY` / `COVER_OPEN`
208
+ (`retryable: true`).
209
+
210
+ ## Types
211
+
212
+ ```ts
213
+ type PrinterTransport = 'wifi' | 'ethernet' | 'bluetooth' | 'ble' | 'usb';
214
+ type PrinterAdapterId = 'escpos' | 'epson' | 'star' | 'brother' | 'zebra' | 'rawTcp';
215
+
216
+ interface PrinterCapabilities {
217
+ paperWidthMm: number; // 58 | 80 | 112…
218
+ printableDots: number; // 384 (58mm) | 576 (80mm) | 832 (112mm)
219
+ dpi: number; // 203 most of the time
220
+ supportsCut: boolean;
221
+ supportsCashDrawer: boolean;
222
+ supportsStatus: boolean;
223
+ supportsRasterImage: boolean;
224
+ supportsQrCode?: boolean;
225
+ supportsBarcode?: boolean;
226
+ }
227
+
228
+ interface DiscoveredPrinter {
229
+ id: string; // stable id: "wifi:192.168.1.50", "bluetooth:AA:BB:.."
230
+ name: string;
231
+ brand?: string; model?: string;
232
+ transport: PrinterTransport;
233
+ adapter: PrinterAdapterId; // resolved by priority
234
+ address: string; // "ip:port" | MAC | UUID
235
+ capabilities?: Partial<PrinterCapabilities>;
236
+ discoveredBy?: PrinterAdapterId[];
237
+ lastSeenAt: number;
238
+ isDefault: boolean;
239
+ isConnected: boolean;
240
+ }
241
+
242
+ interface PrinterProfile {
243
+ id: string;
244
+ adapter: PrinterAdapterId;
245
+ transport: PrinterTransport;
246
+ address: string;
247
+ brand?: string; model?: string;
248
+ name: string;
249
+ capabilities: PrinterCapabilities;
250
+ defaultPrintOptions?: PrintRenderOptions;
251
+ adapterMeta?: Record<string, string | number | boolean>;
252
+ isDefault: boolean;
253
+ createdAt: number; updatedAt: number;
254
+ }
255
+
256
+ // ---- States / statuses ----
257
+ type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
258
+ type PaperStatus = 'ok' | 'near_end' | 'empty' | 'unknown';
259
+ type JobState = 'pending' | 'printing' | 'hold' | 'completed' | 'failed' | 'canceled';
260
+ type HoldReason = 'paper_empty' | 'paper_near_end' | 'cover_open' | 'buffer_full' | 'offline' | 'unknown';
261
+
262
+ interface PrinterStatus {
263
+ id: string;
264
+ connection: ConnectionState;
265
+ online: boolean;
266
+ paper: PaperStatus;
267
+ coverOpen?: boolean;
268
+ errorCode?: PrintErrorCode;
269
+ rawStatus?: string;
270
+ checkedAt: number;
271
+ }
272
+
273
+ interface PrintJobStatus {
274
+ jobId: string;
275
+ printerId: string;
276
+ state: JobState;
277
+ holdReason?: HoldReason;
278
+ progress?: number; // 0..1 (best-effort)
279
+ errorCode?: PrintErrorCode;
280
+ message?: string;
281
+ updatedAt: number;
282
+ }
283
+
284
+ interface PrintResult {
285
+ success: boolean;
286
+ printerId: string;
287
+ adapter: PrinterAdapterId;
288
+ jobId: string; // correlated with printJobStatus events
289
+ state: JobState; // 'completed' on success
290
+ bytesSent?: number;
291
+ durationMs?: number;
292
+ status?: PrinterStatus;
293
+ }
294
+
295
+ // ---- Image print options ----
296
+ type DitheringAlgorithm = 'none' | 'floyd_steinberg' | 'atkinson';
297
+ type ImageAlign = 'left' | 'center' | 'right';
298
+
299
+ interface ImageSource { filePath?: string; url?: string; base64?: string; } // exactly one key
300
+
301
+ interface PrintRenderOptions {
302
+ widthDots?: number; // otherwise derived from the profile (384/576/832)
303
+ resize?: boolean; // default true; false = image already at the right width
304
+ grayscale?: boolean; // default true; false = image already 1-bit (simple threshold)
305
+ threshold?: number; // default 128 (when dithering 'none' or grayscale false)
306
+ dithering?: DitheringAlgorithm; // default 'floyd_steinberg'
307
+ align?: ImageAlign; // default 'center'
308
+ invert?: boolean;
309
+ cut?: boolean; // default true
310
+ feedLines?: number; // default 3
311
+ openCashDrawer?: boolean;
312
+ copies?: number; // default 1
313
+ }
314
+
315
+ interface PrintImageOptions {
316
+ printerId?: string; // otherwise the default printer
317
+ image: ImageSource;
318
+ render?: PrintRenderOptions;
319
+ timeoutMs?: number; // default 15000
320
+ autoReconnect?: boolean; // default true
321
+ }
322
+
323
+ // ---- SDK status ----
324
+ interface SdkStatus {
325
+ adapter: PrinterAdapterId;
326
+ label: string;
327
+ available: boolean; // detected & usable right now
328
+ requiresSdk: boolean; // true for brand SDKs, false for built-in adapters
329
+ transports: PrinterTransport[];
330
+ }
331
+
332
+ // ---- Events ----
333
+ interface PrinterFoundEvent { printer: DiscoveredPrinter; }
334
+ interface DiscoveryCompleteEvent { printers: DiscoveredPrinter[]; failedSources?: string[]; }
335
+ interface StatusChangeEvent { status: PrinterStatus; }
336
+ interface PrintJobStatusEvent { job: PrintJobStatus; }
337
+ ```
338
+
339
+ ### `printText` types
340
+
341
+ ```ts
342
+ type TextAlign = 'left' | 'center' | 'right';
343
+ type Underline = 'none' | 'single' | 'double';
344
+ type EscPosFont = 'A' | 'B';
345
+ type CodePage = 'CP437' | 'CP850' | 'CP858' | 'WPC1252' | 'CP852' | 'CP866'; // Latin-1/Western: WPC1252
346
+ type BarcodeSymbology = 'UPC_A'|'UPC_E'|'EAN13'|'EAN8'|'CODE39'|'ITF'|'CODABAR'|'CODE93'|'CODE128';
347
+ type HriPosition = 'none' | 'above' | 'below' | 'both';
348
+ type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';
349
+
350
+ interface TextStyle {
351
+ align?: TextAlign;
352
+ bold?: boolean;
353
+ underline?: Underline;
354
+ font?: EscPosFont;
355
+ widthMultiplier?: number; // 1..8
356
+ heightMultiplier?: number; // 1..8
357
+ doubleStrike?: boolean;
358
+ invert?: boolean; // white on black
359
+ upsideDown?: boolean;
360
+ rotate90?: boolean;
361
+ letterSpacing?: number; // dots
362
+ lineSpacing?: number; // dots (otherwise default)
363
+ codePage?: CodePage;
364
+ codePageId?: number; // raw ESC t n override
365
+ newline?: boolean; // default true
366
+ }
367
+
368
+ type PrintItem =
369
+ | { type: 'text'; value: string; style?: TextStyle }
370
+ | { type: 'feed'; lines?: number }
371
+ | { type: 'cut'; mode?: 'full' | 'partial'; feedBefore?: number }
372
+ | { type: 'divider'; char?: string; columns?: number; style?: Pick<TextStyle,'align'|'bold'|'font'> }
373
+ | { type: 'qrcode'; value: string; size?: number; errorCorrection?: QrErrorCorrection; align?: TextAlign }
374
+ | { type: 'barcode'; value: string; symbology: BarcodeSymbology; height?: number; width?: number; hri?: HriPosition; align?: TextAlign }
375
+ | { type: 'cashDrawer'; pin?: 2 | 5 }
376
+ | { type: 'image'; image: ImageSource; render?: PrintRenderOptions }
377
+ | { type: 'raw'; bytesBase64: string };
378
+
379
+ interface PrintTextOptions {
380
+ printerId?: string;
381
+ items: PrintItem[];
382
+ defaultCodePage?: CodePage; // Western/Latin-1: 'WPC1252'
383
+ cut?: boolean; // default false
384
+ feedLines?: number; // default 3
385
+ timeoutMs?: number;
386
+ autoReconnect?: boolean;
387
+ }
388
+ ```
389
+
390
+ ## Image printing flow
391
+
392
+ `printImage` performs exactly:
393
+
394
+ 1. Resolve the target printer (otherwise the **default printer**).
395
+ 2. Check whether it is connected.
396
+ 3. If not, **automatic reconnection** (when `autoReconnect`, default `true`).
397
+ 4. **Open the image** (`filePath` > `url` (cached) > `base64`).
398
+ 5. **Resize** to the exact width (`widthDots` or the profile's capabilities).
399
+ 6. **Grayscale** (BT.601 luminance), flatten onto a white background (transparent PNG).
400
+ 7. **Dithering** (Floyd-Steinberg by default, Atkinson, or threshold).
401
+ 8. **Convert to the adapter** (`GS v 0` raster for ESC/POS, `addImage` for SDKs, ZPL for Zebra).
402
+ 9. **Send** (in transport-sized chunks).
403
+ 10. **Feed + cut** (if supported) + optional cash drawer.
404
+ 11. **Normalized result** + best-effort status read.
405
+
406
+ ```ts
407
+ await ThermalPrinter.printImage({
408
+ // printerId omitted → default printer
409
+ image: { filePath: '/data/.../receipt.png' }, // recommended in production
410
+ render: { dithering: 'floyd_steinberg', cut: true, feedLines: 3, align: 'center' },
411
+ timeoutMs: 15000,
412
+ autoReconnect: true,
413
+ });
414
+ ```
415
+
416
+ ### Concrete image-printing examples
417
+
418
+ ```ts
419
+ // 1) Local file (RECOMMENDED in production) — most reliable/performant
420
+ await ThermalPrinter.printImage({ image: { filePath: '/data/user/0/app/files/receipt.png' } });
421
+
422
+ // 2) Remote URL — downloaded and cached by the plugin
423
+ await ThermalPrinter.printImage({
424
+ image: { url: 'https://api.example.com/receipts/123/render.png' },
425
+ render: { dithering: 'atkinson', cut: true },
426
+ });
427
+
428
+ // 3) base64 (handy for tests, less performant)
429
+ await ThermalPrinter.printImage({ image: { base64: 'iVBORw0KGgoAAAANS...' } });
430
+
431
+ // 4) Image ALREADY rendered server-side at the right width and as 1-bit black/white:
432
+ // disable resize + grayscale → pixel-perfect, faster send.
433
+ await ThermalPrinter.printImage({
434
+ image: { filePath: '/data/.../receipt_576px_1bit.png' },
435
+ render: { resize: false, grayscale: false, cut: true },
436
+ });
437
+
438
+ // 5) Target a specific printer + 2 copies + cash drawer
439
+ await ThermalPrinter.printImage({
440
+ printerId: 'wifi:192.168.1.50',
441
+ image: { filePath: '/data/.../receipt.png' },
442
+ render: { widthDots: 576, copies: 2, openCashDrawer: true },
443
+ });
444
+
445
+ // 6) await = printed (best-effort); typed error handling
446
+ try {
447
+ const res = await ThermalPrinter.printImage({ image: { filePath } });
448
+ console.log('Printed', res.jobId, res.bytesSent, 'bytes in', res.durationMs, 'ms');
449
+ } catch (e) {
450
+ if ((e as PrinterError).code === PrintErrorCode.PAPER_EMPTY) alert('Out of paper');
451
+ }
452
+ ```
453
+
454
+ > **`resize`/`grayscale` are optional**: if your server already produces a PNG at the
455
+ > exact width (`576px`/`384px`) and 1-bit black/white, pass
456
+ > `render: { resize: false, grayscale: false }`. The plugin then applies a simple
457
+ > threshold (no dithering) and does not alter the geometry.
458
+
459
+ ## Text printing (`printText`)
460
+
461
+ `printText` accepts an **ordered array of typed items**. Ideal for purely textual
462
+ output, with no server-side pre-rendering.
463
+
464
+ ```ts
465
+ await ThermalPrinter.printText({
466
+ defaultCodePage: 'WPC1252', // Western/Latin-1 accents
467
+ items: [
468
+ { type: 'text', value: 'MY STORE', style: { align: 'center', bold: true, widthMultiplier: 2, heightMultiplier: 2 } },
469
+ { type: 'text', value: '12 Main Street', style: { align: 'center' } },
470
+ { type: 'divider', char: '-' },
471
+ { type: 'text', value: 'Order #1042', style: { bold: true } },
472
+ { type: 'text', value: 'Item A...........12.00' },
473
+ { type: 'text', value: 'Item B........... 2.00' },
474
+ { type: 'divider' },
475
+ { type: 'text', value: 'TOTAL 14.00', style: { align: 'right', bold: true, widthMultiplier: 2 } },
476
+ { type: 'feed', lines: 1 },
477
+ { type: 'qrcode', value: 'https://example.com/order/1042', size: 6, align: 'center' },
478
+ { type: 'barcode', value: '4006381333931', symbology: 'EAN13', hri: 'below' },
479
+ { type: 'cut', mode: 'partial', feedBefore: 3 },
480
+ ],
481
+ });
482
+ ```
483
+
484
+ ### Supported styles (ESC/POS) and SDK mapping
485
+
486
+ | Style / item | ESC/POS (escpos, rawTcp) | Epson ePOS2 | Star StarXpand | Brother | Zebra (ZPL) |
487
+ |---|:--:|:--:|:--:|:--:|:--:|
488
+ | `align` (left/center/right) | ✅ `ESC a` | ✅ | ✅ | ✅ | ✅ (field) |
489
+ | `bold` | ✅ `ESC E` | ✅ | ✅ | ✅ | ⚠️ via font |
490
+ | `underline` (single/double) | ✅ `ESC -` | ✅ | ✅ | ⚠️ | ❌ |
491
+ | `font` A/B | ✅ `ESC M` | ✅ | ✅ | ⚠️ | ⚠️ |
492
+ | `widthMultiplier`/`heightMultiplier` (1..8) | ✅ `GS !` | ✅ | ✅ | ✅ | ✅ (size) |
493
+ | `doubleStrike` | ✅ `ESC G` | ✅ | ⚠️ | ❌ | ❌ |
494
+ | `invert` (white/black) | ✅ `GS B` | ✅ | ✅ | ⚠️ | ✅ (reverse) |
495
+ | `upsideDown` | ✅ `ESC {` | ✅ | ⚠️ | ❌ | ✅ |
496
+ | `rotate90` | ✅ `ESC V` | ✅ | ⚠️ | ⚠️ | ✅ |
497
+ | `letterSpacing` | ✅ `ESC SP` | ✅ | ⚠️ | ❌ | ⚠️ |
498
+ | `lineSpacing` | ✅ `ESC 3` | ✅ | ✅ | ⚠️ | ✅ |
499
+ | `codePage` (accents) | ✅ `ESC t` | ✅ | ✅ | ✅ | ✅ |
500
+ | `qrcode` | ✅ `GS ( k` | ✅ native | ✅ native | ✅ native | ✅ `^BQ` |
501
+ | `barcode` (EAN/CODE128…) | ✅ `GS k` | ✅ native | ✅ native | ✅ native | ✅ `^BC`… |
502
+ | `divider` / `feed` / `cut` | ✅ | ✅ | ✅ | ✅ | ✅ |
503
+ | `cashDrawer` | ✅ `ESC p` | ✅ | ✅ | ⚠️ | ⚠️ |
504
+ | `image` (inline) | ✅ raster | ✅ addImage | ✅ actionPrintImage | ✅ printImage | ✅ ^GF |
505
+ | `raw` (raw bytes) | ✅ | ⚠️ | ⚠️ | ❌ | ⚠️ (raw ZPL) |
506
+
507
+ > ✅ supported · ⚠️ partial/model-dependent equivalent · ❌ not available.
508
+ > Styles not supported by an SDK are **ignored gracefully** (never a hard failure).
509
+ > The reference ESC/POS encoder lives in `src/core/escpos-text.ts` (tested), mirrored
510
+ > in Kotlin (`EscPosTextEncoder.kt`) and Swift (`EscPosTextEncoder.swift`).
511
+
512
+ > **`printText` per brand.** It works on all brands: ESC/POS and **Star** (both
513
+ > platforms) and **Epson Android** map text to a native builder; **Epson iOS,
514
+ > Brother, Zebra** fall back automatically to **rendering the items to an image**
515
+ > (`TextRasterizer`) printed via the SDK's image path. See `docs/SDK_INTEGRATION.md`.
516
+
517
+ ## Client-side events & status
518
+
519
+ ```ts
520
+ // Job tracking: pending → printing → completed | hold | failed
521
+ const jobSub = await ThermalPrinter.addListener('printJobStatus', ({ job }) => {
522
+ switch (job.state) {
523
+ case 'printing': showSpinner(job.progress); break;
524
+ case 'hold': toast(job.holdReason === 'paper_empty' ? 'Add paper' : 'Cover open'); break;
525
+ case 'completed': hideSpinner(); break;
526
+ case 'failed': alert(`Failed: ${job.errorCode}`); break;
527
+ }
528
+ });
529
+
530
+ // Printer status (connection, paper, cover)
531
+ const statusSub = await ThermalPrinter.addListener('statusChange', ({ status }) => {
532
+ updateBadge(status.online, status.paper); // 'ok' | 'near_end' | 'empty' | 'unknown'
533
+ });
534
+
535
+ // ... later
536
+ await jobSub.remove();
537
+ await statusSub.remove();
538
+ ```
539
+
540
+ ## Image processing
541
+
542
+ - **Reference widths @203 dpi**: `58mm → 384 px`, `80mm → 576 px`, `112mm → 832 px`. Some 80mm models print `640 px`: **always prefer the profile/SDK `printableDots`** when known.
543
+ - **Pipeline**: proportional resize to the target width → grayscale → binarization.
544
+ - **Dithering**:
545
+ - `none` (threshold): crisp for text/lines.
546
+ - `floyd_steinberg` (**default**): logos/photos.
547
+ - `atkinson`: more contrast, pleasant on receipts.
548
+ - **ESC/POS raster**: `GS v 0` command (`0x1D 0x76 0x30 m xL xH yL yH data`), width padded to a multiple of 8, MSB = leftmost pixel. Testable reference implementation in `src/core/imaging.ts`, mirrored in Kotlin (`ImageProcessor.kt`) and Swift (`ImageProcessor.swift`).
549
+
550
+ ## Aggregated discovery & adapter priority
551
+
552
+ Several sources run **in parallel**: Epson/Star/Brother/Zebra SDKs, TCP 9100 scan, Bluetooth Classic (Android), BLE (allowlisted services), USB (Android). Results are **merged** by stable `id` and **deduplicated**.
553
+
554
+ **Priority rules** (`priority.ts` / `AdapterPriority.kt` / `.swift`):
555
+
556
+ | Case | Selected adapter | Score |
557
+ |---|---|---|
558
+ | Printer recognized by an official SDK | `epson` / `star` / `brother` | 880–900 |
559
+ | **Zebra** | **`zebra` only** (ESC/POS banned) | 1000 / −1000 |
560
+ | ESC/POS confirmed over Bluetooth (Android) | `escpos` | 620 |
561
+ | ESC/POS confirmed over TCP | `escpos` | 600 |
562
+ | BLE with a usable service | (BLE) | 500 |
563
+ | Unidentified network device | `rawTcp` | 300 |
564
+
565
+ ## Default printer & reconnection
566
+
567
+ - After a **successful test print**, the app calls `setDefaultPrinter({ printerId })`: the plugin **persists a `PrinterProfile`** (id, adapter, transport, address, brand, model, paper width, `printableDots`, dpi, cut options, reconnection metadata).
568
+ - On **startup** or **before printing**, the plugin reloads this profile.
569
+ - **Reconnection is not a permanent connection**: it is attempted **just before `printImage`** (step 3). This avoids keeping a socket/Bluetooth link open needlessly and improves reliability for occasional printing. It uses **exponential backoff** (up to 3 attempts) and detects recovery after a `hold` (paper reloaded / cover closed / back online).
570
+
571
+ ## Normalized errors
572
+
573
+ Every rejected promise carries a **stable code** (`error.code`):
574
+
575
+ `PRINTER_NOT_FOUND`, `PRINTER_OFFLINE`, `CONNECTION_FAILED`, `PERMISSION_DENIED`, `BLUETOOTH_DISABLED`, `WIFI_NOT_CONNECTED`, `PAIRING_REQUIRED`, `UNSUPPORTED_TRANSPORT`, `UNSUPPORTED_PRINTER`, `IMAGE_INVALID`, `IMAGE_TOO_LARGE`, `PRINT_FAILED`, `PAPER_EMPTY`, `COVER_OPEN`, `SDK_NOT_AVAILABLE`, `TIMEOUT`, `UNKNOWN`.
576
+
577
+ ```ts
578
+ import { PrinterError, PrintErrorCode } from '@delicity/capacitor-thermal-printer';
579
+ try { await ThermalPrinter.printImage({ image: { filePath } }); }
580
+ catch (e) {
581
+ const err = e as PrinterError; // { code, message, detail, retryable }
582
+ if (err.code === PrintErrorCode.PAPER_EMPTY) showPaperAlert();
583
+ }
584
+ ```
585
+
586
+ ## Android / iOS differences
587
+
588
+ ### Android — broad hardware coverage
589
+ - Modern Bluetooth permissions (12+) handled.
590
+ - **Bluetooth Classic / SPP**: supported → covers the very common generic ESC/POS printers. ✅
591
+ - BLE supported (UUID allowlist recommended).
592
+ - Retrieval of **already-paired devices** (instant, no scan).
593
+ - TCP 9100 (Wi-Fi/Ethernet). ✅
594
+ - USB host (optional).
595
+
596
+ ### iOS — Apple constraints
597
+ - ❌ **No generic Bluetooth Classic / SPP.** A generic "no-name" BT printer **is not addressable** unless it exposes a usable BLE service.
598
+ - ✅ **MFi manufacturer SDKs** (Epson/Star/Brother/Zebra): this is **the** path for Bluetooth on iOS.
599
+ - ✅ **Wi-Fi TCP** (port 9100) via `Network.framework` → triggers the **Local Network** prompt.
600
+ - ❌ **No generic BLE GATT exposed by the plugin on iOS.** BLE goes through the MFi
601
+ SDKs (Star/Epson/Brother). Attempting a `ble`/`bluetooth`/`usb` transport on the
602
+ generic ESC/POS adapter returns an explicit `UNSUPPORTED_TRANSPORT` error.
603
+ - ❌ No USB host for this use case.
604
+
605
+ > **Never promise** universal Bluetooth compatibility on iOS. In practice: **Wi-Fi for everyone, Bluetooth via the manufacturer SDK**.
606
+
607
+ ## Image cache & logs/diagnostics
608
+
609
+ - **Cache**: `url` images are downloaded into `cache/thermal-images/` (key = URL hash, 32 MB quota, LRU eviction). The `filePath` mode remains the most reliable.
610
+ - **Logs**: in-memory ring buffer (500 lines) + Logcat/os_log. Retrievable via `getDebugLog()` for a "Diagnostics" screen attachable to support tickets. Never raw image data (only dimensions/byte counts).
611
+
612
+ > Implementation status, tests and development setup live in
613
+ > [`CONTRIBUTING.md`](CONTRIBUTING.md) · roadmap in [`ROADMAP.md`](ROADMAP.md).
614
+
615
+ ## Full example
616
+
617
+ ```ts
618
+ import { ThermalPrinter, PrinterError } from '@delicity/capacitor-thermal-printer';
619
+
620
+ // 1) Discovery (with incremental results)
621
+ const sub = await ThermalPrinter.addListener('printerFound', e => {
622
+ console.log('Found:', e.printer.name, e.printer.adapter);
623
+ });
624
+ await ThermalPrinter.requestPermissions();
625
+ const { printers } = await ThermalPrinter.discoverPrinters({ timeoutMs: 8000 });
626
+ await sub.remove();
627
+
628
+ // 2) Connect, set as default IF it succeeds, then test print
629
+ const target = printers[0];
630
+ await ThermalPrinter.connectPrinter({ printerId: target.id, setAsDefault: true });
631
+ await ThermalPrinter.printImage({ printerId: target.id, image: { base64: testReceiptBase64 } });
632
+
633
+ // 3) Later: simple print (default printer + auto reconnection)
634
+ await ThermalPrinter.printImage({ image: { filePath: '/data/.../receipt.png' } });
635
+
636
+ // 4) Or styled text printing
637
+ await ThermalPrinter.printText({
638
+ items: [
639
+ { type: 'text', value: 'Thank you!', style: { align: 'center', bold: true } },
640
+ { type: 'cut' },
641
+ ],
642
+ });
643
+ ```
644
+
645
+ ---
646
+
647
+ ## License
648
+
649
+ MIT © Delicity