retail-print-kit 0.1.0 → 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.
package/README.md CHANGED
@@ -1,39 +1,109 @@
1
1
  # retail-print-kit
2
2
 
3
- Receipt printer and cash drawer driver for Node and Electron. ESC/POS and Star command sets,
4
- printer profiles, USB / network / serial transports, no native dependency in the core.
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)
5
6
 
6
- Status: early development. Not published yet.
7
+ **Receipt printers, cash drawers and label printers for Node.js and Electron, in one package.**
7
8
 
8
- * Receipt command sets: ESC/POS (Epson and compatibles), Star Line Mode (TSP650 / mC-Print), Star raster (TSP100 / TSP143).
9
- * Label languages: ZPL (Zebra), TSPL (TSC, Xprinter, Rongta), EZPL (Godex).
10
- * Browser bundle (`retail-print-kit/browser`) with templates, previews and label encoders; a designer app in `apps/designer`.
11
- * Transports: `retail-print-kit/net` (raw TCP :9100, no deps), `retail-print-kit/usb` (needs `usb`), `retail-print-kit/serial` (needs `serialport`).
12
- * QR: native where the printer has it, bitmap fallback otherwise. Images: 1-bit `Bitmap` from RGBA.
13
- * Status: `printer.status()` reads ESC/POS DLE EOT (online, cover, paper, cutter, drawer pin).
14
- * Bitmap-only printers render through a canvas (`@napi-rs/canvas`, or pass your own factory in Electron).
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
15
49
 
16
50
  ```js
17
51
  const { Printer, profiles } = require('retail-print-kit');
18
52
  const { NetTransport } = require('retail-print-kit/net');
19
53
 
20
- const printer = new Printer(new NetTransport('192.168.1.50'), profiles.epsonTmT88);
54
+ const printer = new Printer(new NetTransport('192.168.1.50'), profiles['epson-tm-t88']);
21
55
  await printer.open();
22
56
  printer
23
57
  .align('ct').style('b').size(2, 2).text('MY STORE')
24
58
  .size(1, 1).style('normal').align('lt')
25
- .tableCustom([{ text: 'Coffee', align: 'LEFT', width: 0.7 }, { text: '$2.50', align: 'RIGHT', width: 0.3 }])
59
+ .tableCustom([
60
+ { text: 'Coffee', align: 'LEFT', width: 0.7 },
61
+ { text: '$2.50', align: 'RIGHT', width: 0.3 },
62
+ ])
26
63
  .drawLine()
64
+ .qr('https://example.com/receipt/1042')
27
65
  .feed(2).cut().cashdraw();
28
66
  await printer.close();
29
67
  ```
30
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
+
31
102
  ## Templates
32
103
 
33
- A receipt can be a JSON layout rendered with data, which is what a visual designer will produce:
104
+ A receipt can be a JSON layout rendered with data. This is what the designer produces.
34
105
 
35
106
  ```js
36
- const { render } = require('retail-print-kit');
37
107
  const template = {
38
108
  name: 'receipt', version: 1,
39
109
  blocks: [
@@ -49,14 +119,24 @@ const template = {
49
119
  { type: 'cut' }, { type: 'drawer' },
50
120
  ],
51
121
  };
52
- printer.render(template, data); // appends to a Printer
53
- const ops = render(template, data, profiles.epsonTmT88); // or just the ops
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);
54
136
  ```
55
137
 
56
- Blocks: `text`, `line`, `row`, `table`, `feed`, `newline`, `cut`, `drawer`, `qr`, `barcode`, `each`, `if`.
57
- Values use `{{path | filter}}`; filters: `upper`, `lower`, `trim`, `money[:prefix]`, `fixed[:n]`,
58
- `pad:n`, `padEnd:n`, `default:x`, `date[:time|datetime]`, `count`. Inside `each`/`table` rows, `this`,
59
- `@index` and `@number` refer to the current item.
138
+ `fromRgba` and `ditherRgba` convert raw pixels; `serializeBitmap` stores a bitmap as JSON for the
139
+ template `image` block.
60
140
 
61
141
  ## Labels
62
142
 
@@ -68,40 +148,63 @@ const { LabelBuilder, encodeZpl, encodeTspl, encodeEzpl, labelToSvg, renderLabel
68
148
  const b = new LabelBuilder({ widthMm: 50.8, heightMm: 25.4, dpi: 203 });
69
149
  b.text(18, 14, 'Coffee Beans 1lb', 26).text(92, 65, '$12.99', 85, { bold: true })
70
150
  .barcode(35, 140, '036000291452', 'UPC_A', 35).qr(330, 20, 'https://x.io/i/1', { module: 3 });
71
- const zpl = encodeZpl(b.label); // string
72
- const svg = labelToSvg(b.label); // preview
151
+ const zpl = encodeZpl(b.label); // send to the printer on port 9100
152
+ const svg = labelToSvg(b.label); // preview
73
153
  const label = renderLabel(labelTemplate, data); // from a designer JSON
74
154
  ```
75
155
 
156
+ ![Label designer](https://raw.githubusercontent.com/arka162/thermal-print/main/docs/images/designer-label.png)
157
+
76
158
  ## Designer
77
159
 
78
- `apps/designer/index.html` is a browser app (no build, no server) that edits receipt and label
79
- templates with a live preview, drag-to-position for labels, sample data, and exports the JSON that
80
- `render()` / `renderLabel()` and the CLI's `print --template` consume. Open it after `npm run build`.
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">
81
173
 
82
- ## Preview and identify
174
+ ## Find and identify printers
83
175
 
84
176
  ```js
85
- printer.toHtml(); // paper-like HTML of the recorded ops
86
- opsToHtml(ops, profile); opsToPng(ops, profile); // same without a Printer (PNG needs a canvas)
87
- const id = await identify(transport); // { maker, model, firmware, set, profile }
88
- profileForUsb(0x04b8); // profile from a USB vendor/product id
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
89
181
  ```
90
182
 
91
183
  ## CLI
92
184
 
93
185
  ```
186
+ npx retail-print-kit discover --identify # printers on this network
94
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
95
189
  npx retail-print-kit drawer --usb # kick the drawer
96
190
  npx retail-print-kit status --serial /dev/ttyUSB0
97
- npx retail-print-kit identify --net 192.168.1.50 # maker/model/firmware + suggested profile
98
- npx retail-print-kit preview --profile star-tsp100 --out page.png # or .html, no printer needed
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
99
193
  npx retail-print-kit print --template receipt.json --data sale.json --net 192.168.1.50
100
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
101
197
  npx retail-print-kit profiles
102
- npx retail-print-kit test --dry --profile star-tsp100 | head -c 200 # bytes only, no printer
103
198
  ```
104
199
 
105
- Docs: [ADR 0001](docs/adr/0001-replace-escpos.md), [printer matrix](docs/printer-matrix.md).
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
106
209
 
107
- License: MIT, Arkaprova Majumder.
210
+ MIT, Arkaprova Majumder.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  const fs = require('fs');
4
- const { Printer, profiles, profileById, NetTransport, identify, opsToHtml, opsToPng, OpBuilder, render, renderLabel, encodeLabel, labelToSvg } = require('../dist');
4
+ const { Printer, profiles, profileById, NetTransport, identify, opsToHtml, opsToPng, OpBuilder, render, renderLabel, encodeLabel, labelToSvg, discoverNetworkPrinters, emptyBitmap, setPixel, qrBitmap, LabelBuilder, loadImage } = require('../dist');
5
5
 
6
6
  process.stdout.on('error', (err) => {
7
7
  if (err.code === 'EPIPE') process.exit(0);
@@ -14,6 +14,11 @@ Commands
14
14
  test print a test page (text styles, table, barcode, QR) and show status
15
15
  drawer kick the cash drawer
16
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]
17
22
  identify ask the printer its maker, model and firmware; suggest a profile
18
23
  preview write the test page as HTML (--out page.html) or PNG (--out page.png)
19
24
  print print a template: --template t.json --data d.json (receipt or label)
@@ -124,6 +129,72 @@ async function testPage(printer, profile) {
124
129
  await showStatus(printer);
125
130
  }
126
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
+
127
198
  async function showStatus(printer) {
128
199
  if (!printer.transport.read) return;
129
200
  try {
@@ -144,6 +215,35 @@ async function main() {
144
215
  return withPrinter(args, async (p) => { p.cashdraw(args.pin === '5' ? 5 : 2); });
145
216
  case 'status':
146
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
+ }
147
247
  case 'identify': {
148
248
  const t = transportFor(args);
149
249
  await t.open();