retail-print-kit 0.0.0-stage → 0.2.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -2
  3. package/bin/retail-print-kit.js +318 -0
  4. package/dist/browser/retail-print-kit.js +26 -0
  5. package/dist/browser/retail-print-kit.js.map +7 -0
  6. package/dist/browser/retail-print-kit.mjs +25 -0
  7. package/dist/browser.d.ts +15 -0
  8. package/dist/browser.js +35 -0
  9. package/dist/core/barcode1d.d.ts +12 -0
  10. package/dist/core/barcode1d.js +192 -0
  11. package/dist/core/bitmap.d.ts +31 -0
  12. package/dist/core/bitmap.js +126 -0
  13. package/dist/core/builder.d.ts +58 -0
  14. package/dist/core/builder.js +177 -0
  15. package/dist/core/encode.d.ts +8 -0
  16. package/dist/core/encode.js +17 -0
  17. package/dist/core/escpos-encoder.d.ts +4 -0
  18. package/dist/core/escpos-encoder.js +150 -0
  19. package/dist/core/identify.d.ts +22 -0
  20. package/dist/core/identify.js +71 -0
  21. package/dist/core/image.d.ts +13 -0
  22. package/dist/core/image.js +32 -0
  23. package/dist/core/ops.d.ts +78 -0
  24. package/dist/core/ops.js +2 -0
  25. package/dist/core/printer.d.ts +35 -0
  26. package/dist/core/printer.js +96 -0
  27. package/dist/core/rasterizer.d.ts +29 -0
  28. package/dist/core/rasterizer.js +199 -0
  29. package/dist/core/star-graphic-encoder.d.ts +12 -0
  30. package/dist/core/star-graphic-encoder.js +48 -0
  31. package/dist/core/star-line-encoder.d.ts +8 -0
  32. package/dist/core/star-line-encoder.js +119 -0
  33. package/dist/core/status.d.ts +22 -0
  34. package/dist/core/status.js +52 -0
  35. package/dist/index.d.ts +26 -0
  36. package/dist/index.js +49 -0
  37. package/dist/label/builder.d.ts +39 -0
  38. package/dist/label/builder.js +37 -0
  39. package/dist/label/ezpl.d.ts +6 -0
  40. package/dist/label/ezpl.js +78 -0
  41. package/dist/label/index.d.ts +12 -0
  42. package/dist/label/index.js +42 -0
  43. package/dist/label/svg.d.ts +3 -0
  44. package/dist/label/svg.js +46 -0
  45. package/dist/label/template.d.ts +61 -0
  46. package/dist/label/template.js +33 -0
  47. package/dist/label/tspl.d.ts +3 -0
  48. package/dist/label/tspl.js +83 -0
  49. package/dist/label/types.d.ts +69 -0
  50. package/dist/label/types.js +10 -0
  51. package/dist/label/zpl.d.ts +3 -0
  52. package/dist/label/zpl.js +57 -0
  53. package/dist/preview/html.d.ts +14 -0
  54. package/dist/preview/html.js +132 -0
  55. package/dist/preview/index.d.ts +8 -0
  56. package/dist/preview/index.js +14 -0
  57. package/dist/preview/png.d.ts +3 -0
  58. package/dist/preview/png.js +89 -0
  59. package/dist/profiles/epson.d.ts +10 -0
  60. package/dist/profiles/epson.js +54 -0
  61. package/dist/profiles/index.d.ts +7 -0
  62. package/dist/profiles/index.js +36 -0
  63. package/dist/profiles/star.d.ts +7 -0
  64. package/dist/profiles/star.js +39 -0
  65. package/dist/profiles/types.d.ts +41 -0
  66. package/dist/profiles/types.js +2 -0
  67. package/dist/template/expr.d.ts +16 -0
  68. package/dist/template/expr.js +85 -0
  69. package/dist/template/index.d.ts +4 -0
  70. package/dist/template/index.js +27 -0
  71. package/dist/template/render.d.ts +8 -0
  72. package/dist/template/render.js +119 -0
  73. package/dist/template/types.d.ts +81 -0
  74. package/dist/template/types.js +2 -0
  75. package/dist/transports/discover.d.ts +21 -0
  76. package/dist/transports/discover.js +73 -0
  77. package/dist/transports/net.d.ts +20 -0
  78. package/dist/transports/net.js +72 -0
  79. package/dist/transports/serial.d.ts +20 -0
  80. package/dist/transports/serial.js +71 -0
  81. package/dist/transports/types.d.ts +16 -0
  82. package/dist/transports/types.js +41 -0
  83. package/dist/transports/usb.d.ts +30 -0
  84. package/dist/transports/usb.js +128 -0
  85. package/dist/util/base64.d.ts +2 -0
  86. package/dist/util/base64.js +38 -0
  87. package/package.json +133 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arkaprova Majumder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,210 @@
1
- # Temporary Holding Version
1
+ # retail-print-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/retail-print-kit.svg)](https://www.npmjs.com/package/retail-print-kit)
4
+ [![ci](https://github.com/arka162/thermal-print/actions/workflows/ci.yml/badge.svg)](https://github.com/arka162/thermal-print/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/retail-print-kit.svg)](LICENSE)
6
+
7
+ **Receipt printers, cash drawers and label printers for Node.js and Electron, in one package.**
8
+
9
+ A modern replacement for `escpos` / `node-thermal-printer`: ESC/POS **and** Star printers, USB /
10
+ network / serial, printer profiles instead of model `if`s, receipt and label **templates**, live
11
+ **previews**, a visual **designer**, and a CLI for testing hardware in the field.
12
+
13
+ **[Open the designer](https://arka162.github.io/thermal-print/)** · [Printer matrix](docs/printer-matrix.md) · [Hardware checklist](docs/hardware-checklist.md)
14
+
15
+ ![Receipt designer](https://raw.githubusercontent.com/arka162/thermal-print/main/docs/images/designer-receipt.png)
16
+
17
+ ## Why
18
+
19
+ | | retail-print-kit | escpos | node-thermal-printer |
20
+ |---|---|---|---|
21
+ | Epson ESC/POS and compatibles | yes | yes | yes |
22
+ | Star Line Mode (TSP650, mC-Print) | yes | no | yes |
23
+ | Star raster-only (TSP100 / TSP143) | yes | no | no |
24
+ | Label printers (ZPL, TSPL, EZPL) | yes | no | no |
25
+ | Printer profiles (cut, drawer pulse, code page, columns) | yes | no | no |
26
+ | Templates with data binding | yes | no | no |
27
+ | HTML / PNG / SVG preview without a printer | yes | no | no |
28
+ | Status (paper, cover, drawer) and model detection | yes | no | partly |
29
+ | Native dependencies in the core | none | none | none |
30
+ | TypeScript types | built in | community | built in |
31
+
32
+ ## Install
33
+
34
+ ```
35
+ npm install retail-print-kit
36
+ ```
37
+
38
+ The core is pure JavaScript. Add a transport only if you need it:
39
+
40
+ ```
41
+ npm install usb # USB printers
42
+ npm install serialport # RS-232 / USB-serial printers
43
+ npm install @napi-rs/canvas # PNG preview, image files, Star TSP100
44
+ ```
45
+
46
+ Node 18 or newer, including Electron's main process. CI runs on Linux, Windows and macOS.
47
+
48
+ ## Quick start
49
+
50
+ ```js
51
+ const { Printer, profiles } = require('retail-print-kit');
52
+ const { NetTransport } = require('retail-print-kit/net');
53
+
54
+ const printer = new Printer(new NetTransport('192.168.1.50'), profiles['epson-tm-t88']);
55
+ await printer.open();
56
+ printer
57
+ .align('ct').style('b').size(2, 2).text('MY STORE')
58
+ .size(1, 1).style('normal').align('lt')
59
+ .tableCustom([
60
+ { text: 'Coffee', align: 'LEFT', width: 0.7 },
61
+ { text: '$2.50', align: 'RIGHT', width: 0.3 },
62
+ ])
63
+ .drawLine()
64
+ .qr('https://example.com/receipt/1042')
65
+ .feed(2).cut().cashdraw();
66
+ await printer.close();
67
+ ```
68
+
69
+ The method names match the `escpos` package (`text`, `align`, `style`, `size`, `feed`, `cut`,
70
+ `cashdraw`, `tableCustom`, `drawLine`, `barcode`, `qrimage`), so existing receipt code moves over
71
+ with a device swap. Other transports:
72
+
73
+ ```js
74
+ const { UsbTransport } = require('retail-print-kit/usb'); // new UsbTransport() or { vendorId, productId }
75
+ const { SerialTransport } = require('retail-print-kit/serial'); // new SerialTransport('COM3', { baudRate: 9600 })
76
+ ```
77
+
78
+ ## Supported printers
79
+
80
+ | Family | Profile ids | Command set |
81
+ |---|---|---|
82
+ | Epson TM-T88 V/VI/VII, TM-T20III, TM-m30 | `epson-tm-t88`, `epson-tm-t20`, `epson-tm-m30` | ESC/POS |
83
+ | Bixolon, Citizen, SNBC, Xprinter, Rongta, HPRT, POS-X | `generic-escpos`, `generic-escpos-58` | ESC/POS |
84
+ | Star TSP650II, TSP700II, mC-Print3 | `star-tsp650`, `star-mc-print3` | Star Line Mode |
85
+ | Star TSP100 / TSP143 | `star-tsp100` | Star raster |
86
+ | Zebra, TSC and clones, Godex (labels) | `encodeZpl`, `encodeTspl`, `encodeEzpl` | ZPL, TSPL, EZPL |
87
+
88
+ A profile carries paper width, columns, code page, cut command, drawer pulse and features, so the
89
+ same receipt code prints correctly on each. `identify()` and `profileForUsb()` pick one for you.
90
+ See the [printer matrix](docs/printer-matrix.md) for what has been run on real hardware.
91
+
92
+ ## Cash drawer and status
93
+
94
+ ```js
95
+ printer.cashdraw(); // pin 2; cashdraw(5) for the second drawer
96
+ await printer.flush();
97
+
98
+ const s = await printer.status();
99
+ // { online, coverOpen, paperOut, paperNearEnd, cutterError, drawerPinHigh, ... }
100
+ ```
101
+
102
+ ## Templates
103
+
104
+ A receipt can be a JSON layout rendered with data. This is what the designer produces.
105
+
106
+ ```js
107
+ const template = {
108
+ name: 'receipt', version: 1,
109
+ blocks: [
110
+ { type: 'text', value: '{{store.name | upper}}', align: 'center', style: 'b', size: [2, 2] },
111
+ { type: 'table', rows: 'items', columns: [
112
+ { value: '{{name}}', header: 'Item', width: 0.6 },
113
+ { value: '{{qty}}', header: 'Qty', width: 0.1, align: 'CENTER' },
114
+ { value: '{{total | money:$}}', header: 'Amount', width: 0.3, align: 'RIGHT' },
115
+ ] },
116
+ { type: 'row', cells: [{ value: 'TOTAL', width: 0.7 }, { value: '{{total | money:$}}', width: 0.3, align: 'RIGHT' }] },
117
+ { type: 'if', when: 'customer', blocks: [{ type: 'text', value: 'Thanks {{customer.name}}' }] },
118
+ { type: 'qr', value: 'https://example.com/r/{{id}}', align: 'center' },
119
+ { type: 'cut' }, { type: 'drawer' },
120
+ ],
121
+ };
122
+ printer.render(template, data);
123
+ ```
124
+
125
+ Blocks: `text`, `line`, `row`, `table`, `image`, `feed`, `newline`, `cut`, `drawer`, `qr`,
126
+ `barcode`, `each`, `if`. Values use `{{path | filter}}` with `upper`, `lower`, `trim`,
127
+ `money[:prefix]`, `fixed[:n]`, `pad:n`, `padEnd:n`, `default:x`, `date[:time|datetime]`, `count`.
128
+ Inside `each` and table rows, `this`, `@index` and `@number` refer to the current item.
129
+
130
+ ## Images and logos
131
+
132
+ ```js
133
+ const { loadImage } = require('retail-print-kit');
134
+ const logo = await loadImage('logo.png', { width: 384, dither: true }); // needs @napi-rs/canvas
135
+ printer.align('ct').image(logo);
136
+ ```
137
+
138
+ `fromRgba` and `ditherRgba` convert raw pixels; `serializeBitmap` stores a bitmap as JSON for the
139
+ template `image` block.
140
+
141
+ ## Labels
142
+
143
+ Labels are positioned elements on a fixed page, encoded for Zebra (ZPL), TSC and clones (TSPL) or
144
+ Godex (EZPL). Positions are in dots at the label's dpi; `mm()` converts.
145
+
146
+ ```js
147
+ const { LabelBuilder, encodeZpl, encodeTspl, encodeEzpl, labelToSvg, renderLabel } = require('retail-print-kit');
148
+ const b = new LabelBuilder({ widthMm: 50.8, heightMm: 25.4, dpi: 203 });
149
+ b.text(18, 14, 'Coffee Beans 1lb', 26).text(92, 65, '$12.99', 85, { bold: true })
150
+ .barcode(35, 140, '036000291452', 'UPC_A', 35).qr(330, 20, 'https://x.io/i/1', { module: 3 });
151
+ const zpl = encodeZpl(b.label); // send to the printer on port 9100
152
+ const svg = labelToSvg(b.label); // preview
153
+ const label = renderLabel(labelTemplate, data); // from a designer JSON
154
+ ```
155
+
156
+ ![Label designer](https://raw.githubusercontent.com/arka162/thermal-print/main/docs/images/designer-label.png)
157
+
158
+ ## Designer
159
+
160
+ **https://arka162.github.io/thermal-print/** edits receipt and label templates in the browser:
161
+ block palette, nested `each` / `if`, property editor, sample data, live preview, drag-to-position
162
+ on labels, logo upload with dithering, JSON import and export. It runs entirely in the page from
163
+ the browser bundle (`retail-print-kit/browser`); nothing is uploaded.
164
+
165
+ ## Preview without a printer
166
+
167
+ ```js
168
+ printer.toHtml(); // paper-like HTML of the recorded ops
169
+ opsToHtml(ops, profile); opsToPng(ops, profile); // same without a Printer
170
+ ```
171
+
172
+ <img src="https://raw.githubusercontent.com/arka162/thermal-print/main/docs/images/test-page.png" width="300" alt="PNG preview of the test page">
173
+
174
+ ## Find and identify printers
175
+
176
+ ```js
177
+ const { discoverNetworkPrinters, identify, profileForUsb } = require('retail-print-kit');
178
+ await discoverNetworkPrinters({ identify: true }); // [{ host, port, identity: { maker, model, profile } }]
179
+ await identify(transport); // GS I for ESC/POS, status probe for Star
180
+ profileForUsb(0x04b8); // profile from a USB vendor id
181
+ ```
182
+
183
+ ## CLI
184
+
185
+ ```
186
+ npx retail-print-kit discover --identify # printers on this network
187
+ npx retail-print-kit test --net 192.168.1.50 --profile epson-tm-t20 # test page + status
188
+ npx retail-print-kit verify --net 192.168.1.50 --drawer # numbered hardware checks
189
+ npx retail-print-kit drawer --usb # kick the drawer
190
+ npx retail-print-kit status --serial /dev/ttyUSB0
191
+ npx retail-print-kit identify --net 192.168.1.50
192
+ npx retail-print-kit image --file logo.png --width 384 --dither --net 192.168.1.50
193
+ npx retail-print-kit print --template receipt.json --data sale.json --net 192.168.1.50
194
+ npx retail-print-kit print --template shelf.json --data item.json --language zpl --net 192.168.1.60
195
+ npx retail-print-kit verify-label --language tspl --net 192.168.1.60
196
+ npx retail-print-kit preview --profile star-tsp100 --out page.png # no printer needed
197
+ npx retail-print-kit profiles
198
+ ```
199
+
200
+ Add `--dry` to any printing command to dump the bytes as hex instead of sending them.
201
+
202
+ ## Status of hardware verification
203
+
204
+ Encoders are covered by byte-exact tests. Items still to be confirmed on real units are listed in
205
+ the [hardware checklist](docs/hardware-checklist.md): Star raster cut modes, Star status bits,
206
+ Godex font sizes, and QR syntax on EZPL and TSPL. Reports from the field are welcome as issues.
207
+
208
+ ## License
209
+
210
+ MIT, Arkaprova Majumder.
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs = require('fs');
4
+ const { Printer, profiles, profileById, NetTransport, identify, opsToHtml, opsToPng, OpBuilder, render, renderLabel, encodeLabel, labelToSvg, discoverNetworkPrinters, emptyBitmap, setPixel, qrBitmap, LabelBuilder, loadImage } = require('../dist');
5
+
6
+ process.stdout.on('error', (err) => {
7
+ if (err.code === 'EPIPE') process.exit(0);
8
+ throw err;
9
+ });
10
+
11
+ const USAGE = `retail-print-kit <command> [options]
12
+
13
+ Commands
14
+ test print a test page (text styles, table, barcode, QR) and show status
15
+ drawer kick the cash drawer
16
+ status print the printer status as JSON
17
+ verify print the numbered hardware verification page (see docs/hardware-checklist.md)
18
+ add --drawer to kick pin 2 then pin 5
19
+ verify-label print a verification label: --language zpl|tspl|ezpl [--width-mm 50.8 --height-mm 25.4]
20
+ discover find printers on the local network (port 9100); --identify asks each one its model
21
+ image print an image file: --file logo.png [--width 384] [--dither]
22
+ identify ask the printer its maker, model and firmware; suggest a profile
23
+ preview write the test page as HTML (--out page.html) or PNG (--out page.png)
24
+ print print a template: --template t.json --data d.json (receipt or label)
25
+ a label needs --language zpl|tspl|ezpl; --out file.svg|.html|.txt previews instead
26
+ profiles list printer profiles
27
+ list list USB printers and serial ports
28
+
29
+ Connection (one of)
30
+ --net <host[:port]> raw TCP, port 9100 by default
31
+ --usb [<vid:pid>] first USB printer-class device, or the given ids (hex)
32
+ --serial <path> [--baud n] serial port, 9600 baud by default
33
+
34
+ Options
35
+ --profile <id> printer profile (default epson-tm-t88)
36
+ --pin 2|5 drawer pin for "drawer" (default 2)
37
+ --dry print the bytes as hex instead of sending
38
+
39
+ Examples
40
+ retail-print-kit test --net 192.168.1.50 --profile epson-tm-t20
41
+ retail-print-kit drawer --usb
42
+ retail-print-kit status --serial /dev/ttyUSB0`;
43
+
44
+ function parse(argv) {
45
+ const args = { _: [] };
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const a = argv[i];
48
+ if (!a.startsWith('--')) {
49
+ args._.push(a);
50
+ continue;
51
+ }
52
+ const key = a.slice(2);
53
+ const next = argv[i + 1];
54
+ if (next !== undefined && !next.startsWith('--')) {
55
+ args[key] = next;
56
+ i++;
57
+ } else {
58
+ args[key] = true;
59
+ }
60
+ }
61
+ return args;
62
+ }
63
+
64
+ function transportFor(args) {
65
+ if (args.dry) {
66
+ const chunks = [];
67
+ return {
68
+ name: 'dry',
69
+ chunks,
70
+ async open() {},
71
+ async write(b) { chunks.push(Buffer.from(b)); },
72
+ async close() { process.stdout.write(Buffer.concat(chunks).toString('hex') + '\n'); },
73
+ };
74
+ }
75
+ if (args.net) {
76
+ const [host, port] = String(args.net).split(':');
77
+ return new NetTransport(host, { port: port ? Number(port) : undefined });
78
+ }
79
+ if (args.usb !== undefined) {
80
+ const { UsbTransport } = require('../dist/transports/usb');
81
+ if (args.usb === true) return new UsbTransport();
82
+ const [vid, pid] = String(args.usb).split(':');
83
+ return new UsbTransport({ vendorId: parseInt(vid, 16), productId: pid ? parseInt(pid, 16) : undefined });
84
+ }
85
+ if (args.serial) {
86
+ const { SerialTransport } = require('../dist/transports/serial');
87
+ return new SerialTransport(String(args.serial), { baudRate: args.baud ? Number(args.baud) : undefined });
88
+ }
89
+ throw new Error('give a connection: --net, --usb or --serial (or --dry)');
90
+ }
91
+
92
+ async function withPrinter(args, fn) {
93
+ const profile = profileById(String(args.profile || 'epson-tm-t88'));
94
+ const printer = new Printer(transportFor(args), profile);
95
+ await printer.open();
96
+ try {
97
+ await fn(printer, profile);
98
+ } finally {
99
+ await printer.close();
100
+ }
101
+ }
102
+
103
+ async function testPage(printer, profile) {
104
+ const now = new Date();
105
+ printer
106
+ .align('ct').style('b').size(2, 2).text('retail-print-kit').size(1, 1).style('normal')
107
+ .text(`${profile.vendor} ${profile.model}`)
108
+ .text(`profile ${profile.id} / ${profile.set} / ${profile.columns.a} cols`)
109
+ .text(now.toLocaleString())
110
+ .align('lt').drawLine()
111
+ .text('normal text')
112
+ .style('b').text('bold text').style('normal')
113
+ .style('u').text('underlined text').style('normal')
114
+ .size(2, 1).text('double width').size(1, 1)
115
+ .size(1, 2).text('double height').size(1, 1)
116
+ .setFont('b').text('font B: ' + '0123456789'.repeat(5)).setFont('a')
117
+ .drawLine()
118
+ .tableCustom([{ text: 'Item', width: 0.6 }, { text: 'Qty', width: 0.1, align: 'CENTER' }, { text: 'Amount', width: 0.3, align: 'RIGHT' }])
119
+ .tableCustom([{ text: 'Coffee, large, oat milk', width: 0.6 }, { text: '2', width: 0.1, align: 'CENTER' }, { text: '$5.00', width: 0.3, align: 'RIGHT' }])
120
+ .tableCustom([{ text: 'Bagel', width: 0.6 }, { text: '1', width: 0.1, align: 'CENTER' }, { text: '$7.50', width: 0.3, align: 'RIGHT' }])
121
+ .drawLine()
122
+ .tableCustom([{ text: 'TOTAL', width: 0.7, style: 'b' }, { text: '$12.50', width: 0.3, align: 'RIGHT' }])
123
+ .newLine()
124
+ .align('ct').barcode('036000291452', 'UPC_A', { height: 60, width: 2 }).newLine()
125
+ .qr('https://github.com/arka162/thermal-print', { size: 5 }).newLine()
126
+ .align('lt').text(`${'='.repeat(10)} end of test ${'='.repeat(10)}`)
127
+ .cut();
128
+ await printer.flush();
129
+ await showStatus(printer);
130
+ }
131
+
132
+ function section(p, id, title) {
133
+ p.align('lt').style('b').text(`[${id}] ${title}`).style('normal');
134
+ }
135
+
136
+ async function verifyPage(printer, profile, args) {
137
+ const cols = profile.columns.a;
138
+ const ruler = (n) => '1234567890'.repeat(Math.ceil(n / 10)).slice(0, n);
139
+ printer.align('ct').style('b').size(2, 2).text('VERIFY').size(1, 1).style('normal')
140
+ .text(`${profile.id} / ${profile.set}`).text(new Date().toLocaleString()).align('lt').drawLine();
141
+ section(printer, 'V1', `Font A ruler: last digit must be ${ruler(cols).slice(-1)} at the right edge (${cols} cols)`);
142
+ printer.text(ruler(cols));
143
+ section(printer, 'V2', `Font B ruler (${profile.columns.b} cols)`);
144
+ printer.setFont('b').text(ruler(profile.columns.b)).setFont('a');
145
+ section(printer, 'V3', 'Styles: bold, underline, double width, double height, inverse');
146
+ printer.style('b').text('bold').style('u').text('underline').style('normal').size(2, 1).text('wide').size(1, 2).text('tall').size(1, 1)
147
+ .style({ invert: true }).text(' inverse ').style('normal');
148
+ section(printer, 'V4', 'Alignment: left, centre, right');
149
+ printer.align('lt').text('left').align('ct').text('centre').align('rt').text('right').align('lt');
150
+ section(printer, 'V5', 'Code page: bytes 80..FF (expect CP437 box drawing in B0..DF)');
151
+ for (let row = 0x80; row < 0x100; row += 16) {
152
+ printer.print(row.toString(16).toUpperCase() + ' ').raw(Array.from({ length: 16 }, (_, i) => row + i)).newLine();
153
+ }
154
+ section(printer, 'V6', 'Barcodes: UPC-A, EAN-13, CODE128, CODE39');
155
+ printer.align('ct')
156
+ .barcode('036000291452', 'UPC_A', { height: 50 }).newLine()
157
+ .barcode('5901234123457', 'EAN13', { height: 50 }).newLine()
158
+ .barcode('RPK-12345', 'CODE128', { height: 50 }).newLine()
159
+ .barcode('RPK 123', 'CODE39', { height: 50 }).newLine().align('lt');
160
+ section(printer, 'V7', 'QR A = printer native, QR B = bitmap; both must scan to the same URL');
161
+ printer.align('ct').text('A');
162
+ if (profile.features.nativeQr || profile.set === 'star-graphic') printer.qr('https://github.com/arka162/thermal-print', { size: 5 });
163
+ else printer.text('(no native QR on this profile)');
164
+ printer.newLine().text('B').image(qrBitmap('https://github.com/arka162/thermal-print', { size: 5 })).newLine().align('lt');
165
+ section(printer, 'V8', 'Raster: checkerboard, then a diagonal line, edges sharp');
166
+ const bm = emptyBitmap(256, 64);
167
+ for (let y = 0; y < 64; y++) for (let x = 0; x < 256; x++) if (x < 128 ? ((x >> 3) + (y >> 3)) % 2 === 0 : Math.abs(x - 128 - y * 2) < 2) setPixel(bm, x, y, true);
168
+ printer.align('ct').image(bm).newLine().align('lt');
169
+ section(printer, 'V9', 'Partial cut follows this line');
170
+ printer.cut(true);
171
+ section(printer, 'V10', 'Full cut follows this line');
172
+ printer.text('End of verification page. Fill in docs/hardware-checklist.md.').cut(false);
173
+ await printer.flush();
174
+ if (args.drawer) {
175
+ printer.cashdraw(2);
176
+ await printer.flush();
177
+ await new Promise((r) => setTimeout(r, 1500));
178
+ printer.cashdraw(5);
179
+ await printer.flush();
180
+ console.log('V11 drawer: kicked pin 2, then pin 5 after 1.5 s');
181
+ }
182
+ await showStatus(printer);
183
+ }
184
+
185
+ function verifyLabel(args) {
186
+ const b = new LabelBuilder({ widthMm: Number(args['width-mm'] || 50.8), heightMm: Number(args['height-mm'] || 25.4), dpi: Number(args.dpi || 203) });
187
+ const w = b.label.width;
188
+ const h = b.label.height;
189
+ b.box(2, 2, w - 4, h - 4, 2)
190
+ .text(10, 8, 'L1 text 20', 20).text(10, 32, 'L2 text 32', 32)
191
+ .barcode(10, 72, '036000291452', 'UPC_A', 40)
192
+ .qr(w - 110, 8, 'https://github.com/arka162/thermal-print', { module: 3 })
193
+ .rect(10, h - 14, w - 20, 4)
194
+ .text(w - 150, h - 60, String(args.language || 'zpl').toUpperCase(), 40, { bold: true });
195
+ return b.label;
196
+ }
197
+
198
+ async function showStatus(printer) {
199
+ if (!printer.transport.read) return;
200
+ try {
201
+ const s = await printer.status();
202
+ console.log(JSON.stringify(s));
203
+ } catch (err) {
204
+ console.log(`status: ${err.message}`);
205
+ }
206
+ }
207
+
208
+ async function main() {
209
+ const args = parse(process.argv.slice(2));
210
+ const cmd = args._[0];
211
+ switch (cmd) {
212
+ case 'test':
213
+ return withPrinter(args, testPage);
214
+ case 'drawer':
215
+ return withPrinter(args, async (p) => { p.cashdraw(args.pin === '5' ? 5 : 2); });
216
+ case 'status':
217
+ return withPrinter(args, showStatus);
218
+ case 'verify':
219
+ return withPrinter(args, (p, profile) => verifyPage(p, profile, args));
220
+ case 'verify-label': {
221
+ const label = verifyLabel(args);
222
+ const language = String(args.language || 'zpl');
223
+ if (args.out) {
224
+ const out = String(args.out);
225
+ fs.writeFileSync(out, out.endsWith('.svg') ? labelToSvg(label) : Buffer.from(encodeLabel(label, language)));
226
+ console.log(`wrote ${out}`);
227
+ return;
228
+ }
229
+ const t = transportFor(args);
230
+ await t.open();
231
+ try { await t.write(Buffer.from(encodeLabel(label, language))); } finally { await t.close(); }
232
+ return;
233
+ }
234
+ case 'discover': {
235
+ const found = await discoverNetworkPrinters({ port: args.port ? Number(args.port) : undefined, identify: !!args.identify });
236
+ if (!found.length) console.log('no hosts answered on the printing port');
237
+ for (const p of found) {
238
+ const id = p.identity;
239
+ console.log(`${p.host}:${p.port}` + (id ? ` ${id.maker || '?'} ${id.model || ''} -> ${id.profile ? id.profile.id : 'unknown'}` : ''));
240
+ }
241
+ return;
242
+ }
243
+ case 'image': {
244
+ const bm = await loadImage(String(args.file), { width: args.width ? Number(args.width) : undefined, dither: !!args.dither });
245
+ return withPrinter(args, async (p) => { p.align('ct').image(bm).newLine().cut(); });
246
+ }
247
+ case 'identify': {
248
+ const t = transportFor(args);
249
+ await t.open();
250
+ try {
251
+ const id = await identify(t);
252
+ console.log(JSON.stringify({ ...id, profile: id.profile ? id.profile.id : null }));
253
+ } finally {
254
+ await t.close();
255
+ }
256
+ return;
257
+ }
258
+ case 'print': {
259
+ const tpl = JSON.parse(fs.readFileSync(String(args.template), 'utf8'));
260
+ const data = args.data ? JSON.parse(fs.readFileSync(String(args.data), 'utf8')) : {};
261
+ if (tpl.kind === 'label') {
262
+ const label = renderLabel(tpl, data);
263
+ const language = String(args.language || 'zpl');
264
+ if (args.out) {
265
+ const out = String(args.out);
266
+ fs.writeFileSync(out, out.endsWith('.svg') ? labelToSvg(label) : Buffer.from(encodeLabel(label, language)));
267
+ console.log(`wrote ${out}`);
268
+ return;
269
+ }
270
+ const t = transportFor(args);
271
+ await t.open();
272
+ try { await t.write(Buffer.from(encodeLabel(label, language))); } finally { await t.close(); }
273
+ return;
274
+ }
275
+ const profile = profileById(String(args.profile || 'epson-tm-t88'));
276
+ if (args.out) {
277
+ const out = String(args.out);
278
+ const ops = render(tpl, data, profile);
279
+ fs.writeFileSync(out, out.endsWith('.png') ? opsToPng(ops, profile) : opsToHtml(ops, profile));
280
+ console.log(`wrote ${out}`);
281
+ return;
282
+ }
283
+ return withPrinter(args, async (p) => { p.render(tpl, data); });
284
+ }
285
+ case 'preview': {
286
+ const profile = profileById(String(args.profile || 'epson-tm-t88'));
287
+ const out = String(args.out || 'preview.html');
288
+ const b = new OpBuilder(profile);
289
+ await testPage(Object.assign(b, { flush: async () => {}, transport: {} }), profile);
290
+ const ops = b.operations;
291
+ fs.writeFileSync(out, out.endsWith('.png') ? opsToPng(ops, profile) : opsToHtml(ops, profile));
292
+ console.log(`wrote ${out}`);
293
+ return;
294
+ }
295
+ case 'profiles':
296
+ for (const p of Object.values(profiles)) console.log(`${p.id.padEnd(20)} ${p.set.padEnd(13)} ${p.paper}mm ${String(p.columns.a).padStart(3)} cols ${p.vendor} ${p.model}`);
297
+ return;
298
+ case 'list': {
299
+ try {
300
+ const { UsbTransport } = require('../dist/transports/usb');
301
+ for (const d of UsbTransport.list()) console.log(`usb ${d.vendorId.toString(16).padStart(4, '0')}:${d.productId.toString(16).padStart(4, '0')}`);
302
+ } catch (err) { console.log(`usb ${err.message}`); }
303
+ try {
304
+ const { SerialTransport } = require('../dist/transports/serial');
305
+ for (const p of await SerialTransport.list()) console.log(`serial ${p}`);
306
+ } catch (err) { console.log(`serial ${err.message}`); }
307
+ return;
308
+ }
309
+ default:
310
+ console.log(USAGE);
311
+ process.exitCode = cmd ? 1 : 0;
312
+ }
313
+ }
314
+
315
+ main().catch((err) => {
316
+ console.error(err.message || err);
317
+ process.exitCode = 1;
318
+ });