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.
- package/LICENSE +21 -0
- package/README.md +209 -2
- package/bin/retail-print-kit.js +318 -0
- package/dist/browser/retail-print-kit.js +26 -0
- package/dist/browser/retail-print-kit.js.map +7 -0
- package/dist/browser/retail-print-kit.mjs +25 -0
- package/dist/browser.d.ts +15 -0
- package/dist/browser.js +35 -0
- package/dist/core/barcode1d.d.ts +12 -0
- package/dist/core/barcode1d.js +192 -0
- package/dist/core/bitmap.d.ts +31 -0
- package/dist/core/bitmap.js +126 -0
- package/dist/core/builder.d.ts +58 -0
- package/dist/core/builder.js +177 -0
- package/dist/core/encode.d.ts +8 -0
- package/dist/core/encode.js +17 -0
- package/dist/core/escpos-encoder.d.ts +4 -0
- package/dist/core/escpos-encoder.js +150 -0
- package/dist/core/identify.d.ts +22 -0
- package/dist/core/identify.js +71 -0
- package/dist/core/image.d.ts +13 -0
- package/dist/core/image.js +32 -0
- package/dist/core/ops.d.ts +78 -0
- package/dist/core/ops.js +2 -0
- package/dist/core/printer.d.ts +35 -0
- package/dist/core/printer.js +96 -0
- package/dist/core/rasterizer.d.ts +29 -0
- package/dist/core/rasterizer.js +199 -0
- package/dist/core/star-graphic-encoder.d.ts +12 -0
- package/dist/core/star-graphic-encoder.js +48 -0
- package/dist/core/star-line-encoder.d.ts +8 -0
- package/dist/core/star-line-encoder.js +119 -0
- package/dist/core/status.d.ts +22 -0
- package/dist/core/status.js +52 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +49 -0
- package/dist/label/builder.d.ts +39 -0
- package/dist/label/builder.js +37 -0
- package/dist/label/ezpl.d.ts +6 -0
- package/dist/label/ezpl.js +78 -0
- package/dist/label/index.d.ts +12 -0
- package/dist/label/index.js +42 -0
- package/dist/label/svg.d.ts +3 -0
- package/dist/label/svg.js +46 -0
- package/dist/label/template.d.ts +61 -0
- package/dist/label/template.js +33 -0
- package/dist/label/tspl.d.ts +3 -0
- package/dist/label/tspl.js +83 -0
- package/dist/label/types.d.ts +69 -0
- package/dist/label/types.js +10 -0
- package/dist/label/zpl.d.ts +3 -0
- package/dist/label/zpl.js +57 -0
- package/dist/preview/html.d.ts +14 -0
- package/dist/preview/html.js +132 -0
- package/dist/preview/index.d.ts +8 -0
- package/dist/preview/index.js +14 -0
- package/dist/preview/png.d.ts +3 -0
- package/dist/preview/png.js +89 -0
- package/dist/profiles/epson.d.ts +10 -0
- package/dist/profiles/epson.js +54 -0
- package/dist/profiles/index.d.ts +7 -0
- package/dist/profiles/index.js +36 -0
- package/dist/profiles/star.d.ts +7 -0
- package/dist/profiles/star.js +39 -0
- package/dist/profiles/types.d.ts +41 -0
- package/dist/profiles/types.js +2 -0
- package/dist/template/expr.d.ts +16 -0
- package/dist/template/expr.js +85 -0
- package/dist/template/index.d.ts +4 -0
- package/dist/template/index.js +27 -0
- package/dist/template/render.d.ts +8 -0
- package/dist/template/render.js +119 -0
- package/dist/template/types.d.ts +81 -0
- package/dist/template/types.js +2 -0
- package/dist/transports/discover.d.ts +21 -0
- package/dist/transports/discover.js +73 -0
- package/dist/transports/net.d.ts +20 -0
- package/dist/transports/net.js +72 -0
- package/dist/transports/serial.d.ts +20 -0
- package/dist/transports/serial.js +71 -0
- package/dist/transports/types.d.ts +16 -0
- package/dist/transports/types.js +41 -0
- package/dist/transports/usb.d.ts +30 -0
- package/dist/transports/usb.js +128 -0
- package/dist/util/base64.d.ts +2 -0
- package/dist/util/base64.js +38 -0
- package/package.json +133 -4
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpBuilder = void 0;
|
|
4
|
+
exports.parseStyle = parseStyle;
|
|
5
|
+
const bitmap_1 = require("./bitmap");
|
|
6
|
+
const ALIGN = { lt: 'left', ct: 'center', rt: 'right', left: 'left', center: 'center', right: 'right' };
|
|
7
|
+
/**
|
|
8
|
+
* Records print operations. Method names match the `escpos` package so a host can swap the
|
|
9
|
+
* device without rewriting its receipt code. Nothing is sent from here; see `Printer`.
|
|
10
|
+
*/
|
|
11
|
+
class OpBuilder {
|
|
12
|
+
constructor(profile) {
|
|
13
|
+
this.profile = profile;
|
|
14
|
+
this.ops = [];
|
|
15
|
+
this.font = 'a';
|
|
16
|
+
}
|
|
17
|
+
/** A copy of the recorded ops. */
|
|
18
|
+
get operations() {
|
|
19
|
+
return this.ops.slice();
|
|
20
|
+
}
|
|
21
|
+
append(ops) {
|
|
22
|
+
for (const op of ops)
|
|
23
|
+
this.ops.push(op);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
get columns() {
|
|
27
|
+
return this.profile.columns[this.font];
|
|
28
|
+
}
|
|
29
|
+
push(op) {
|
|
30
|
+
this.ops.push(op);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
raw(bytes) {
|
|
34
|
+
return this.push({ kind: 'raw', bytes: Uint8Array.from(bytes) });
|
|
35
|
+
}
|
|
36
|
+
text(content) {
|
|
37
|
+
return this.push({ kind: 'text', text: `${content}\n` });
|
|
38
|
+
}
|
|
39
|
+
/** Text without a trailing line break. */
|
|
40
|
+
print(content) {
|
|
41
|
+
return this.push({ kind: 'text', text: content });
|
|
42
|
+
}
|
|
43
|
+
newLine(count = 1) {
|
|
44
|
+
return this.push({ kind: 'newline', count });
|
|
45
|
+
}
|
|
46
|
+
feed(lines = 1) {
|
|
47
|
+
return this.push({ kind: 'feed', lines });
|
|
48
|
+
}
|
|
49
|
+
align(align) {
|
|
50
|
+
const a = ALIGN[align];
|
|
51
|
+
if (!a)
|
|
52
|
+
throw new Error(`retail-print-kit: unknown align "${align}"`);
|
|
53
|
+
return this.push({ kind: 'align', align: a });
|
|
54
|
+
}
|
|
55
|
+
setFont(font) {
|
|
56
|
+
this.font = font;
|
|
57
|
+
return this.push({ kind: 'font', font });
|
|
58
|
+
}
|
|
59
|
+
size(width, height) {
|
|
60
|
+
return this.push({ kind: 'size', width, height });
|
|
61
|
+
}
|
|
62
|
+
style(style) {
|
|
63
|
+
const flags = typeof style === 'string' ? parseStyle(style) : style;
|
|
64
|
+
return this.push({ kind: 'style', style: flags });
|
|
65
|
+
}
|
|
66
|
+
lineSpace(dots = null) {
|
|
67
|
+
return this.push({ kind: 'lineSpacing', dots });
|
|
68
|
+
}
|
|
69
|
+
cut(partial = false, feed = 3) {
|
|
70
|
+
return this.push({ kind: 'cut', partial, feed });
|
|
71
|
+
}
|
|
72
|
+
cashdraw(pin = 2) {
|
|
73
|
+
return this.push({ kind: 'drawer', pin });
|
|
74
|
+
}
|
|
75
|
+
beep(times = 1, duration = 1) {
|
|
76
|
+
return this.push({ kind: 'beep', times, duration });
|
|
77
|
+
}
|
|
78
|
+
barcode(data, type = 'CODE128', options = {}) {
|
|
79
|
+
return this.push({ kind: 'barcode', data, type, options });
|
|
80
|
+
}
|
|
81
|
+
/** Native QR where the printer has one, otherwise a bitmap QR. */
|
|
82
|
+
qr(data, options = {}) {
|
|
83
|
+
if (this.profile.features.nativeQr || this.profile.set === 'star-graphic')
|
|
84
|
+
return this.push({ kind: 'qr', data, options });
|
|
85
|
+
return this.image((0, bitmap_1.qrBitmap)(data, options));
|
|
86
|
+
}
|
|
87
|
+
image(bitmap) {
|
|
88
|
+
return this.push({ kind: 'raster', width: bitmap.width, height: bitmap.height, bits: bitmap.bits });
|
|
89
|
+
}
|
|
90
|
+
/** `escpos` name for a QR code; prints a native QR when the profile has one. */
|
|
91
|
+
qrimage(data, options = {}) {
|
|
92
|
+
return this.qr(data, options);
|
|
93
|
+
}
|
|
94
|
+
drawLine(char = '-') {
|
|
95
|
+
return this.text(char.repeat(this.columns));
|
|
96
|
+
}
|
|
97
|
+
tableCustom(cells, options = {}) {
|
|
98
|
+
const total = options.width ?? this.columns;
|
|
99
|
+
const widths = cellWidths(cells, total);
|
|
100
|
+
const wrap = options.wrap !== false;
|
|
101
|
+
const lines = cells.map((c, i) => (wrap ? wrapText(c.text ?? '', widths[i]) : [String(c.text ?? '').slice(0, widths[i])]));
|
|
102
|
+
const rows = Math.max(1, ...lines.map((l) => l.length));
|
|
103
|
+
for (let r = 0; r < rows; r++) {
|
|
104
|
+
let line = '';
|
|
105
|
+
cells.forEach((c, i) => {
|
|
106
|
+
line += pad(lines[i][r] ?? '', widths[i], c.align ?? 'LEFT');
|
|
107
|
+
});
|
|
108
|
+
this.text(line.replace(/\s+$/, ''));
|
|
109
|
+
}
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.OpBuilder = OpBuilder;
|
|
114
|
+
/** `escpos` style letters: b bold, i italic, u underline, u2 double underline. */
|
|
115
|
+
function parseStyle(style) {
|
|
116
|
+
if (style === 'normal')
|
|
117
|
+
return { bold: false, underline: 0, italic: false, invert: false };
|
|
118
|
+
const s = style.toLowerCase();
|
|
119
|
+
return {
|
|
120
|
+
bold: s.includes('b'),
|
|
121
|
+
italic: s.includes('i'),
|
|
122
|
+
underline: s.includes('u2') ? 2 : s.includes('u') ? 1 : 0,
|
|
123
|
+
invert: false,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function cellWidths(cells, total) {
|
|
127
|
+
const fixed = cells.map((c) => (c.width === undefined ? null : c.width > 1 ? Math.floor(c.width) : Math.floor(c.width * total)));
|
|
128
|
+
const used = fixed.reduce((n, w) => n + (w ?? 0), 0);
|
|
129
|
+
const free = fixed.filter((w) => w === null).length;
|
|
130
|
+
const each = free ? Math.floor(Math.max(0, total - used) / free) : 0;
|
|
131
|
+
const widths = fixed.map((w) => (w === null ? each : w));
|
|
132
|
+
const sum = widths.reduce((a, b) => a + b, 0);
|
|
133
|
+
if (sum < total)
|
|
134
|
+
widths[widths.length - 1] += total - sum;
|
|
135
|
+
return widths;
|
|
136
|
+
}
|
|
137
|
+
function wrapText(text, width) {
|
|
138
|
+
if (width <= 0)
|
|
139
|
+
return [''];
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const para of String(text).split('\n')) {
|
|
142
|
+
let line = '';
|
|
143
|
+
for (const word of para.split(/\s+/)) {
|
|
144
|
+
if (!word)
|
|
145
|
+
continue;
|
|
146
|
+
if (word.length > width) {
|
|
147
|
+
if (line)
|
|
148
|
+
out.push(line);
|
|
149
|
+
line = '';
|
|
150
|
+
for (let i = 0; i < word.length; i += width)
|
|
151
|
+
out.push(word.slice(i, i + width));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const next = line ? `${line} ${word}` : word;
|
|
155
|
+
if (next.length > width) {
|
|
156
|
+
out.push(line);
|
|
157
|
+
line = word;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
line = next;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
out.push(line);
|
|
164
|
+
}
|
|
165
|
+
return out.length ? out : [''];
|
|
166
|
+
}
|
|
167
|
+
function pad(text, width, align) {
|
|
168
|
+
const t = text.slice(0, width);
|
|
169
|
+
const gap = width - t.length;
|
|
170
|
+
if (align === 'RIGHT')
|
|
171
|
+
return ' '.repeat(gap) + t;
|
|
172
|
+
if (align === 'CENTER') {
|
|
173
|
+
const left = Math.floor(gap / 2);
|
|
174
|
+
return ' '.repeat(left) + t + ' '.repeat(gap - left);
|
|
175
|
+
}
|
|
176
|
+
return t + ' '.repeat(gap);
|
|
177
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Op } from './ops';
|
|
2
|
+
import { Profile } from '../profiles/types';
|
|
3
|
+
import { Rasterizer } from './rasterizer';
|
|
4
|
+
export interface EncodeContext {
|
|
5
|
+
/** Used by bitmap-only command sets. Defaults to a canvas-backed rasterizer. */
|
|
6
|
+
rasterizer?: Rasterizer;
|
|
7
|
+
}
|
|
8
|
+
export declare function encode(ops: Op[], profile: Profile, ctx?: EncodeContext): Buffer;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encode = encode;
|
|
4
|
+
const escpos_encoder_1 = require("./escpos-encoder");
|
|
5
|
+
const star_line_encoder_1 = require("./star-line-encoder");
|
|
6
|
+
const star_graphic_encoder_1 = require("./star-graphic-encoder");
|
|
7
|
+
const rasterizer_1 = require("./rasterizer");
|
|
8
|
+
function encode(ops, profile, ctx = {}) {
|
|
9
|
+
switch (profile.set) {
|
|
10
|
+
case 'escpos':
|
|
11
|
+
return (0, escpos_encoder_1.encodeEscPos)(ops, profile);
|
|
12
|
+
case 'star-line':
|
|
13
|
+
return (0, star_line_encoder_1.encodeStarLine)(ops, profile);
|
|
14
|
+
case 'star-graphic':
|
|
15
|
+
return (0, star_graphic_encoder_1.encodeStarGraphic)(ops, profile, ctx.rasterizer ?? new rasterizer_1.CanvasRasterizer());
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.encodeEscPos = encodeEscPos;
|
|
7
|
+
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
|
8
|
+
const ESC = 0x1b;
|
|
9
|
+
const GS = 0x1d;
|
|
10
|
+
const LF = 0x0a;
|
|
11
|
+
const BARCODE_CODE = {
|
|
12
|
+
UPC_A: 65, UPC_E: 66, EAN13: 67, EAN8: 68, CODE39: 69, ITF: 70, CODABAR: 71, CODE128: 73,
|
|
13
|
+
};
|
|
14
|
+
const QR_CORRECTION = { L: 48, M: 49, Q: 50, H: 51 };
|
|
15
|
+
class Bytes {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.chunks = [];
|
|
18
|
+
}
|
|
19
|
+
push(...parts) {
|
|
20
|
+
for (const p of parts) {
|
|
21
|
+
if (typeof p === 'number')
|
|
22
|
+
this.chunks.push(Buffer.from([p]));
|
|
23
|
+
else if (Array.isArray(p))
|
|
24
|
+
this.chunks.push(Buffer.from(p));
|
|
25
|
+
else
|
|
26
|
+
this.chunks.push(Buffer.from(p));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
toBuffer() {
|
|
30
|
+
return Buffer.concat(this.chunks);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function clamp(n, lo, hi) {
|
|
34
|
+
return Math.max(lo, Math.min(hi, Math.floor(n)));
|
|
35
|
+
}
|
|
36
|
+
/** Turns ops into ESC/POS bytes for the given profile. Pure: no I/O, no state across calls. */
|
|
37
|
+
function encodeEscPos(ops, profile) {
|
|
38
|
+
const out = new Bytes();
|
|
39
|
+
for (const op of ops) {
|
|
40
|
+
switch (op.kind) {
|
|
41
|
+
case 'init':
|
|
42
|
+
out.push([ESC, 0x40]);
|
|
43
|
+
if (profile.codepage !== null)
|
|
44
|
+
out.push([ESC, 0x74, profile.codepage]);
|
|
45
|
+
break;
|
|
46
|
+
case 'text':
|
|
47
|
+
out.push(iconv_lite_1.default.encode(op.text, profile.encoding));
|
|
48
|
+
break;
|
|
49
|
+
case 'newline':
|
|
50
|
+
for (let i = 0; i < op.count; i++)
|
|
51
|
+
out.push(LF);
|
|
52
|
+
break;
|
|
53
|
+
case 'feed':
|
|
54
|
+
out.push([ESC, 0x64, clamp(op.lines, 0, 255)]);
|
|
55
|
+
break;
|
|
56
|
+
case 'align':
|
|
57
|
+
out.push([ESC, 0x61, op.align === 'left' ? 0 : op.align === 'center' ? 1 : 2]);
|
|
58
|
+
break;
|
|
59
|
+
case 'font':
|
|
60
|
+
out.push([ESC, 0x4d, op.font === 'a' ? 0 : 1]);
|
|
61
|
+
break;
|
|
62
|
+
case 'size': {
|
|
63
|
+
const w = clamp(op.width, 1, 8) - 1;
|
|
64
|
+
const h = clamp(op.height, 1, 8) - 1;
|
|
65
|
+
out.push([GS, 0x21, (w << 4) | h]);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'style':
|
|
69
|
+
out.push([ESC, 0x45, op.style.bold ? 1 : 0]);
|
|
70
|
+
out.push([ESC, 0x2d, op.style.underline ?? 0]);
|
|
71
|
+
if (profile.features.italic)
|
|
72
|
+
out.push([ESC, 0x34, op.style.italic ? 1 : 0]);
|
|
73
|
+
if (profile.features.invert)
|
|
74
|
+
out.push([GS, 0x42, op.style.invert ? 1 : 0]);
|
|
75
|
+
break;
|
|
76
|
+
case 'lineSpacing':
|
|
77
|
+
if (op.dots === null)
|
|
78
|
+
out.push([ESC, 0x32]);
|
|
79
|
+
else
|
|
80
|
+
out.push([ESC, 0x33, clamp(op.dots, 0, 255)]);
|
|
81
|
+
break;
|
|
82
|
+
case 'cut': {
|
|
83
|
+
const feed = clamp(op.feed, 0, 255);
|
|
84
|
+
if (profile.cut === 'feed-and-cut') {
|
|
85
|
+
out.push([GS, 0x56, op.partial ? 66 : 65, feed]);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (feed > 0)
|
|
89
|
+
out.push([ESC, 0x64, feed]);
|
|
90
|
+
out.push([GS, 0x56, op.partial ? 1 : 0]);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'drawer':
|
|
95
|
+
out.push([ESC, 0x70, op.pin === 2 ? 0 : 1, clamp(profile.drawer.on, 0, 255), clamp(profile.drawer.off, 0, 255)]);
|
|
96
|
+
break;
|
|
97
|
+
case 'beep':
|
|
98
|
+
if (profile.features.beep)
|
|
99
|
+
out.push([ESC, 0x42, clamp(op.times, 1, 9), clamp(op.duration, 1, 9)]);
|
|
100
|
+
break;
|
|
101
|
+
case 'barcode': {
|
|
102
|
+
const o = op.options;
|
|
103
|
+
out.push([GS, 0x68, clamp(o.height ?? 80, 1, 255)]);
|
|
104
|
+
out.push([GS, 0x77, clamp(o.width ?? 3, 2, 6)]);
|
|
105
|
+
const pos = o.position ?? 'below';
|
|
106
|
+
out.push([GS, 0x48, pos === 'none' ? 0 : pos === 'above' ? 1 : pos === 'below' ? 2 : 3]);
|
|
107
|
+
out.push([GS, 0x66, (o.font ?? 'a') === 'a' ? 0 : 1]);
|
|
108
|
+
const data = Buffer.from(op.data, 'ascii');
|
|
109
|
+
const m = BARCODE_CODE[op.type];
|
|
110
|
+
if (op.type === 'CODE128') {
|
|
111
|
+
const body = Buffer.concat([Buffer.from('{B', 'ascii'), data]);
|
|
112
|
+
out.push([GS, 0x6b, m, body.length], body);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
out.push([GS, 0x6b, m, data.length], data);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'qr':
|
|
120
|
+
if (!profile.features.nativeQr) {
|
|
121
|
+
throw new Error(`retail-print-kit: profile ${profile.id} has no native QR; raster QR is not implemented yet`);
|
|
122
|
+
}
|
|
123
|
+
pushQr(out, op.data, op.options);
|
|
124
|
+
break;
|
|
125
|
+
case 'raster': {
|
|
126
|
+
if (!profile.features.raster)
|
|
127
|
+
throw new Error(`retail-print-kit: profile ${profile.id} cannot print raster images`);
|
|
128
|
+
const bytesPerRow = Math.ceil(op.width / 8);
|
|
129
|
+
out.push([GS, 0x76, 0x30, 0, bytesPerRow & 0xff, bytesPerRow >> 8, op.height & 0xff, op.height >> 8], op.bits);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'raw':
|
|
133
|
+
out.push(op.bytes);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out.toBuffer();
|
|
138
|
+
}
|
|
139
|
+
function pushQr(out, data, options) {
|
|
140
|
+
const payload = Buffer.from(data, 'utf8');
|
|
141
|
+
const model = options.model === 1 ? 49 : 50;
|
|
142
|
+
const size = clamp(options.size ?? 6, 1, 16);
|
|
143
|
+
const ec = QR_CORRECTION[options.correction ?? 'M'];
|
|
144
|
+
out.push([GS, 0x28, 0x6b, 4, 0, 49, 65, model, 0]);
|
|
145
|
+
out.push([GS, 0x28, 0x6b, 3, 0, 49, 67, size]);
|
|
146
|
+
out.push([GS, 0x28, 0x6b, 3, 0, 49, 69, ec]);
|
|
147
|
+
const len = payload.length + 3;
|
|
148
|
+
out.push([GS, 0x28, 0x6b, len & 0xff, len >> 8, 49, 80, 48], payload);
|
|
149
|
+
out.push([GS, 0x28, 0x6b, 3, 0, 49, 81, 48]);
|
|
150
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Transport } from '../transports/types';
|
|
2
|
+
import { Profile } from '../profiles/types';
|
|
3
|
+
export interface Identity {
|
|
4
|
+
maker: string | null;
|
|
5
|
+
model: string | null;
|
|
6
|
+
firmware: string | null;
|
|
7
|
+
set: 'escpos' | 'star-line' | 'unknown';
|
|
8
|
+
profile: Profile | null;
|
|
9
|
+
}
|
|
10
|
+
/** USB vendor ids of receipt printer makers; used before any byte is sent. */
|
|
11
|
+
export declare const USB_VENDORS: Record<number, {
|
|
12
|
+
maker: string;
|
|
13
|
+
profile: Profile;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function profileForUsb(vendorId: number, productId?: number): Profile;
|
|
16
|
+
/** Picks a profile from the model string an ESC/POS printer reports with GS I. */
|
|
17
|
+
export declare function profileForModel(maker: string | null, model: string | null): Profile;
|
|
18
|
+
/**
|
|
19
|
+
* Asks an open transport what is on the other end: ESC/POS `GS I 66/67/65` (maker, model,
|
|
20
|
+
* firmware), then a Star Line status request when ESC/POS stays silent.
|
|
21
|
+
*/
|
|
22
|
+
export declare function identify(transport: Transport, timeoutMs?: number): Promise<Identity>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.USB_VENDORS = void 0;
|
|
4
|
+
exports.profileForUsb = profileForUsb;
|
|
5
|
+
exports.profileForModel = profileForModel;
|
|
6
|
+
exports.identify = identify;
|
|
7
|
+
const profiles_1 = require("../profiles");
|
|
8
|
+
/** USB vendor ids of receipt printer makers; used before any byte is sent. */
|
|
9
|
+
exports.USB_VENDORS = {
|
|
10
|
+
0x04b8: { maker: 'Epson', profile: profiles_1.epsonTmT88 },
|
|
11
|
+
0x0519: { maker: 'Star Micronics', profile: profiles_1.starTsp650 },
|
|
12
|
+
0x1504: { maker: 'Bixolon', profile: profiles_1.genericEscPos },
|
|
13
|
+
0x1d90: { maker: 'Citizen', profile: profiles_1.genericEscPos },
|
|
14
|
+
};
|
|
15
|
+
function profileForUsb(vendorId, productId) {
|
|
16
|
+
const v = exports.USB_VENDORS[vendorId];
|
|
17
|
+
if (!v)
|
|
18
|
+
return profiles_1.genericEscPos;
|
|
19
|
+
if (vendorId === 0x0519 && productId !== undefined && productId <= 0x0002)
|
|
20
|
+
return profiles_1.starTsp100;
|
|
21
|
+
return v.profile;
|
|
22
|
+
}
|
|
23
|
+
/** Picks a profile from the model string an ESC/POS printer reports with GS I. */
|
|
24
|
+
function profileForModel(maker, model) {
|
|
25
|
+
const m = (model ?? '').toUpperCase();
|
|
26
|
+
if (/T88/.test(m))
|
|
27
|
+
return profiles_1.epsonTmT88;
|
|
28
|
+
if (/T20/.test(m))
|
|
29
|
+
return profiles_1.epsonTmT20;
|
|
30
|
+
if (/M30/.test(m))
|
|
31
|
+
return profiles_1.epsonTmM30;
|
|
32
|
+
if (/^EPSON/i.test(maker ?? ''))
|
|
33
|
+
return profiles_1.epsonTmT88;
|
|
34
|
+
for (const p of Object.values(profiles_1.profiles))
|
|
35
|
+
if (m && p.model.toUpperCase().includes(m))
|
|
36
|
+
return p;
|
|
37
|
+
return profiles_1.genericEscPos;
|
|
38
|
+
}
|
|
39
|
+
async function ask(transport, bytes, timeoutMs) {
|
|
40
|
+
if (!transport.read)
|
|
41
|
+
throw new Error(`retail-print-kit: ${transport.name} transport cannot read from the printer`);
|
|
42
|
+
await transport.write(Buffer.from(bytes));
|
|
43
|
+
try {
|
|
44
|
+
return await transport.read(timeoutMs);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function infoString(reply) {
|
|
51
|
+
if (!reply || reply[0] !== 0x5f)
|
|
52
|
+
return null;
|
|
53
|
+
const end = reply.indexOf(0, 1);
|
|
54
|
+
return reply.subarray(1, end === -1 ? reply.length : end).toString('ascii').trim() || null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Asks an open transport what is on the other end: ESC/POS `GS I 66/67/65` (maker, model,
|
|
58
|
+
* firmware), then a Star Line status request when ESC/POS stays silent.
|
|
59
|
+
*/
|
|
60
|
+
async function identify(transport, timeoutMs = 1500) {
|
|
61
|
+
const maker = infoString(await ask(transport, [0x1d, 0x49, 66], timeoutMs));
|
|
62
|
+
const model = infoString(await ask(transport, [0x1d, 0x49, 67], timeoutMs));
|
|
63
|
+
if (maker || model) {
|
|
64
|
+
const firmware = infoString(await ask(transport, [0x1d, 0x49, 65], timeoutMs));
|
|
65
|
+
return { maker, model, firmware, set: 'escpos', profile: profileForModel(maker, model) };
|
|
66
|
+
}
|
|
67
|
+
const star = await ask(transport, [0x1b, 0x06, 0x01], timeoutMs);
|
|
68
|
+
if (star && star.length >= 2)
|
|
69
|
+
return { maker: 'Star Micronics', model: null, firmware: null, set: 'star-line', profile: profiles_1.starTsp650 };
|
|
70
|
+
return { maker: null, model: null, firmware: null, set: 'unknown', profile: null };
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Bitmap } from './bitmap';
|
|
2
|
+
export interface LoadImageOptions {
|
|
3
|
+
/** Target width in dots; the height keeps the aspect ratio. Defaults to the image's own width. */
|
|
4
|
+
width?: number;
|
|
5
|
+
/** Floyd-Steinberg dithering (photos, gradients). Default false: plain threshold. */
|
|
6
|
+
dither?: boolean;
|
|
7
|
+
threshold?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Loads a PNG / JPEG / WebP / GIF file or buffer and converts it to a 1-bit bitmap, scaled to
|
|
11
|
+
* `width` dots. Node only; needs the optional `@napi-rs/canvas` (or `canvas`) package.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadImage(source: string | Uint8Array, options?: LoadImageOptions): Promise<Bitmap>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadImage = loadImage;
|
|
4
|
+
const bitmap_1 = require("./bitmap");
|
|
5
|
+
/**
|
|
6
|
+
* Loads a PNG / JPEG / WebP / GIF file or buffer and converts it to a 1-bit bitmap, scaled to
|
|
7
|
+
* `width` dots. Node only; needs the optional `@napi-rs/canvas` (or `canvas`) package.
|
|
8
|
+
*/
|
|
9
|
+
async function loadImage(source, options = {}) {
|
|
10
|
+
let mod = null;
|
|
11
|
+
for (const name of ['@napi-rs/canvas', 'canvas']) {
|
|
12
|
+
try {
|
|
13
|
+
mod = require(name);
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* try the next */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!mod)
|
|
21
|
+
throw new Error('retail-print-kit: loadImage needs @napi-rs/canvas; run `npm install @napi-rs/canvas`');
|
|
22
|
+
const img = await mod.loadImage(source);
|
|
23
|
+
const width = Math.max(1, Math.round(options.width ?? img.width));
|
|
24
|
+
const height = Math.max(1, Math.round((img.height * width) / img.width));
|
|
25
|
+
const canvas = mod.createCanvas(width, height);
|
|
26
|
+
const ctx = canvas.getContext('2d');
|
|
27
|
+
ctx.fillStyle = '#fff';
|
|
28
|
+
ctx.fillRect(0, 0, width, height);
|
|
29
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
30
|
+
const data = ctx.getImageData(0, 0, width, height).data;
|
|
31
|
+
return options.dither ? (0, bitmap_1.ditherRgba)(data, width, height) : (0, bitmap_1.fromRgba)(data, width, height, options.threshold ?? 128);
|
|
32
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type Align = 'left' | 'center' | 'right';
|
|
2
|
+
export type Font = 'a' | 'b';
|
|
3
|
+
export type BarcodeType = 'CODE128' | 'EAN13' | 'EAN8' | 'UPC_A' | 'UPC_E' | 'CODE39' | 'ITF' | 'CODABAR';
|
|
4
|
+
export type BarcodePosition = 'none' | 'above' | 'below' | 'both';
|
|
5
|
+
export interface StyleFlags {
|
|
6
|
+
bold?: boolean;
|
|
7
|
+
underline?: 0 | 1 | 2;
|
|
8
|
+
italic?: boolean;
|
|
9
|
+
invert?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface BarcodeOptions {
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
position?: BarcodePosition;
|
|
15
|
+
font?: Font;
|
|
16
|
+
includeParity?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface QrOptions {
|
|
19
|
+
size?: number;
|
|
20
|
+
correction?: 'L' | 'M' | 'Q' | 'H';
|
|
21
|
+
model?: 1 | 2;
|
|
22
|
+
}
|
|
23
|
+
export type Op = {
|
|
24
|
+
kind: 'init';
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'text';
|
|
27
|
+
text: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'newline';
|
|
30
|
+
count: number;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'feed';
|
|
33
|
+
lines: number;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'align';
|
|
36
|
+
align: Align;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'font';
|
|
39
|
+
font: Font;
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'size';
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'style';
|
|
46
|
+
style: StyleFlags;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'lineSpacing';
|
|
49
|
+
dots: number | null;
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'cut';
|
|
52
|
+
partial: boolean;
|
|
53
|
+
feed: number;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'drawer';
|
|
56
|
+
pin: 2 | 5;
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'beep';
|
|
59
|
+
times: number;
|
|
60
|
+
duration: number;
|
|
61
|
+
} | {
|
|
62
|
+
kind: 'barcode';
|
|
63
|
+
data: string;
|
|
64
|
+
type: BarcodeType;
|
|
65
|
+
options: BarcodeOptions;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'qr';
|
|
68
|
+
data: string;
|
|
69
|
+
options: QrOptions;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'raster';
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
bits: Uint8Array;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'raw';
|
|
77
|
+
bytes: Uint8Array;
|
|
78
|
+
};
|
package/dist/core/ops.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Transport } from '../transports/types';
|
|
2
|
+
import { Profile } from '../profiles/types';
|
|
3
|
+
import { EncodeContext } from './encode';
|
|
4
|
+
import { OpBuilder } from './builder';
|
|
5
|
+
import { PrinterStatus } from './status';
|
|
6
|
+
export * from './builder';
|
|
7
|
+
export interface PrinterOptions extends EncodeContext {
|
|
8
|
+
/** Send ESC @ and the profile code page on open. Default true. */
|
|
9
|
+
initOnOpen?: boolean;
|
|
10
|
+
/** Milliseconds to wait for a status reply. Default 1500. */
|
|
11
|
+
statusTimeout?: number;
|
|
12
|
+
}
|
|
13
|
+
/** An `OpBuilder` bound to a transport: `open()`, record ops, `close()` sends them. */
|
|
14
|
+
export declare class Printer extends OpBuilder {
|
|
15
|
+
readonly transport: Transport;
|
|
16
|
+
private readonly options;
|
|
17
|
+
private opened;
|
|
18
|
+
constructor(transport: Transport, profile: Profile, options?: PrinterOptions);
|
|
19
|
+
open(): Promise<this>;
|
|
20
|
+
/** Encodes and sends everything recorded so far, keeping the connection open. */
|
|
21
|
+
flush(): Promise<this>;
|
|
22
|
+
/** Flushes, then closes the transport. */
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
/** Renders a template with data and appends the result. */
|
|
25
|
+
render(template: import('../template/types').Template, data: unknown): this;
|
|
26
|
+
/** HTML preview of the recorded ops. */
|
|
27
|
+
toHtml(options?: import('../preview/html').HtmlPreviewOptions): string;
|
|
28
|
+
/** The bytes that would be sent for the recorded ops; useful for tests and previews. */
|
|
29
|
+
toBuffer(): Buffer;
|
|
30
|
+
/**
|
|
31
|
+
* Asks the printer for its state (ESC/POS DLE EOT 1..4). Flushes pending ops first so the
|
|
32
|
+
* reply is not mixed with receipt data. Needs a transport with `read`.
|
|
33
|
+
*/
|
|
34
|
+
status(): Promise<PrinterStatus>;
|
|
35
|
+
}
|