retail-print-kit 0.2.2 → 0.3.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/CHANGELOG.md +41 -0
- package/README.md +74 -3
- package/dist/label-design/builtins-data.d.ts +2 -0
- package/dist/label-design/builtins-data.js +1631 -0
- package/dist/label-design/builtins.d.ts +13 -0
- package/dist/label-design/builtins.js +124 -0
- package/dist/label-design/encode.d.ts +11 -0
- package/dist/label-design/encode.js +151 -0
- package/dist/label-design/fields.d.ts +30 -0
- package/dist/label-design/fields.js +115 -0
- package/dist/label-design/fonts.d.ts +6 -0
- package/dist/label-design/fonts.js +35 -0
- package/dist/label-design/image.d.ts +4 -0
- package/dist/label-design/image.js +48 -0
- package/dist/label-design/index.d.ts +12 -0
- package/dist/label-design/index.js +72 -0
- package/dist/label-design/printer-text.d.ts +12 -0
- package/dist/label-design/printer-text.js +18 -0
- package/dist/label-design/render.d.ts +30 -0
- package/dist/label-design/render.js +419 -0
- package/dist/label-design/types.d.ts +137 -0
- package/dist/label-design/types.js +2 -0
- package/dist/label-design/units.d.ts +5 -0
- package/dist/label-design/units.js +17 -0
- package/dist/label-design/validate.d.ts +15 -0
- package/dist/label-design/validate.js +97 -0
- package/dist/label-designer/designer.d.ts +26 -0
- package/dist/label-designer/designer.js +1311 -0
- package/dist/label-designer/index.d.ts +4 -0
- package/dist/label-designer/index.js +10 -0
- package/dist/label-designer/preview.d.ts +14 -0
- package/dist/label-designer/preview.js +73 -0
- package/dist/label-designer/styles.d.ts +1 -0
- package/dist/label-designer/styles.js +4 -0
- package/dist/label-fonts/FONTS.md +11 -0
- package/dist/label-fonts/archivo-narrow-400.woff2 +0 -0
- package/dist/label-fonts/archivo-narrow-700.woff2 +0 -0
- package/dist/label-fonts/archivo-narrow-OFL.txt +93 -0
- package/dist/label-fonts/arimo-400-italic.woff2 +0 -0
- package/dist/label-fonts/arimo-400.woff2 +0 -0
- package/dist/label-fonts/arimo-700.woff2 +0 -0
- package/dist/label-fonts/arimo-OFL.txt +93 -0
- package/dist/label-fonts/oswald-500.woff2 +0 -0
- package/dist/label-fonts/oswald-700.woff2 +0 -0
- package/dist/label-fonts/oswald-OFL.txt +93 -0
- package/dist/label-fonts/roboto-slab-400.woff2 +0 -0
- package/dist/label-fonts/roboto-slab-700.woff2 +0 -0
- package/dist/label-fonts/roboto-slab-LICENSE.txt +202 -0
- package/package.json +39 -5
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PrinterLanguage } from './types';
|
|
2
|
+
export declare function cleanForPrinter(value: any, language: PrinterLanguage): string;
|
|
3
|
+
export declare function fitText(fit: {
|
|
4
|
+
left: number;
|
|
5
|
+
right: number;
|
|
6
|
+
middle: number;
|
|
7
|
+
size: number;
|
|
8
|
+
}, text: string): {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
size: number;
|
|
12
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cleanForPrinter = cleanForPrinter;
|
|
4
|
+
exports.fitText = fitText;
|
|
5
|
+
function cleanForPrinter(value, language) {
|
|
6
|
+
const text = String(value).replace(/[\r\n]+/g, ' ');
|
|
7
|
+
return language === 'zpl' ? text.replace(/[\^~]/g, '') : text;
|
|
8
|
+
}
|
|
9
|
+
function fitText(fit, text) {
|
|
10
|
+
const units = Array.from(String(text)).reduce((sum, ch) => sum + (ch === '.' || ch === ',' ? 0.26 : 0.52), 0) || 1;
|
|
11
|
+
const room = fit.right - fit.left;
|
|
12
|
+
const size = Math.max(8, Math.min(fit.size, Math.floor(room / units)));
|
|
13
|
+
return {
|
|
14
|
+
x: fit.left + Math.max(0, Math.round((room - size * units) / 2)),
|
|
15
|
+
y: Math.max(0, Math.round(fit.middle - size * 0.5)),
|
|
16
|
+
size
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DesignImage, LabelData, LabelDesign, LabelElement, LabelWarning, Rasterize, Symbology } from './types';
|
|
2
|
+
export interface CanvasLike {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
getContext(kind: '2d'): any;
|
|
6
|
+
}
|
|
7
|
+
export type CanvasFactory = (width: number, height: number) => CanvasLike;
|
|
8
|
+
export declare function domCanvasFactory(): CanvasFactory | undefined;
|
|
9
|
+
export declare function barcodeProblem(symbology: Symbology, value: string): string | null;
|
|
10
|
+
export declare function barcodeWidthMm(el: LabelElement, value: string, dpi: number): number;
|
|
11
|
+
export interface DrawOptions {
|
|
12
|
+
ppm: number;
|
|
13
|
+
stock?: boolean;
|
|
14
|
+
background?: string;
|
|
15
|
+
canvasFactory?: CanvasFactory;
|
|
16
|
+
}
|
|
17
|
+
/** Draws the whole label on `canvas` at `ppm` pixels per millimetre and returns what does not fit. */
|
|
18
|
+
export declare function drawLabel(canvas: CanvasLike, design: LabelDesign, data: LabelData, opts: DrawOptions): LabelWarning[];
|
|
19
|
+
/** The label as the printer prints it: drawn at the printer's dots, then black or white. */
|
|
20
|
+
export declare function drawPrinterView(canvas: CanvasLike, design: LabelDesign, data: LabelData, opts?: {
|
|
21
|
+
canvasFactory?: CanvasFactory;
|
|
22
|
+
}): LabelWarning[];
|
|
23
|
+
/**
|
|
24
|
+
* Draws one text, price, QR or image element at printer resolution for `encodeDesignLabel`.
|
|
25
|
+
* Elements without placeholders are drawn once per design object and reused.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createRasterizer(options?: {
|
|
28
|
+
canvasFactory?: CanvasFactory;
|
|
29
|
+
}): Rasterize;
|
|
30
|
+
export declare function imageFromSource(source: any, srcW: number, srcH: number, w: number, h: number, mode: 'dither' | 'threshold', canvasFactory?: CanvasFactory): DesignImage;
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.domCanvasFactory = domCanvasFactory;
|
|
4
|
+
exports.barcodeProblem = barcodeProblem;
|
|
5
|
+
exports.barcodeWidthMm = barcodeWidthMm;
|
|
6
|
+
exports.drawLabel = drawLabel;
|
|
7
|
+
exports.drawPrinterView = drawPrinterView;
|
|
8
|
+
exports.createRasterizer = createRasterizer;
|
|
9
|
+
exports.imageFromSource = imageFromSource;
|
|
10
|
+
const barcode1d_1 = require("../core/barcode1d");
|
|
11
|
+
const bitmap_1 = require("../core/bitmap");
|
|
12
|
+
const fields_1 = require("./fields");
|
|
13
|
+
const image_1 = require("./image");
|
|
14
|
+
const units_1 = require("./units");
|
|
15
|
+
function domCanvasFactory() {
|
|
16
|
+
const doc = globalThis.document;
|
|
17
|
+
if (!doc || typeof doc.createElement !== 'function')
|
|
18
|
+
return undefined;
|
|
19
|
+
return (width, height) => {
|
|
20
|
+
const canvas = doc.createElement('canvas');
|
|
21
|
+
canvas.width = width;
|
|
22
|
+
canvas.height = height;
|
|
23
|
+
return canvas;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function roundPt(pt) {
|
|
27
|
+
return Math.round(pt * 10) / 10;
|
|
28
|
+
}
|
|
29
|
+
function wrapLines(ctx, text, width) {
|
|
30
|
+
const words = text.split(' ');
|
|
31
|
+
const lines = [];
|
|
32
|
+
let cur = '';
|
|
33
|
+
for (const w of words) {
|
|
34
|
+
const t = cur ? `${cur} ${w}` : w;
|
|
35
|
+
if (ctx.measureText(t).width <= width || !cur)
|
|
36
|
+
cur = t;
|
|
37
|
+
else {
|
|
38
|
+
lines.push(cur);
|
|
39
|
+
cur = w;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (cur)
|
|
43
|
+
lines.push(cur);
|
|
44
|
+
return lines;
|
|
45
|
+
}
|
|
46
|
+
function fontCss(el, px) {
|
|
47
|
+
return `${el.italic ? 'italic ' : ''}${el.weight || 400} ${px}px ${(0, fields_1.fontFamily)(el.font)}`;
|
|
48
|
+
}
|
|
49
|
+
function drawText(ctx, el, data, ppm, ox, oy, warn) {
|
|
50
|
+
let text = (0, fields_1.resolveValue)(el.value, data).replace(/[\r\n]+/g, ' ');
|
|
51
|
+
if (el.upper)
|
|
52
|
+
text = text.toUpperCase();
|
|
53
|
+
if (!text)
|
|
54
|
+
return;
|
|
55
|
+
const w = el.w * ppm;
|
|
56
|
+
const h = el.h * ppm;
|
|
57
|
+
let px = ((el.size || 10) / units_1.PT_PER_INCH) * units_1.MM_PER_INCH * ppm;
|
|
58
|
+
ctx.save();
|
|
59
|
+
if (el.inverse) {
|
|
60
|
+
ctx.fillStyle = '#000';
|
|
61
|
+
ctx.fillRect(ox, oy, w, h);
|
|
62
|
+
}
|
|
63
|
+
ctx.fillStyle = el.inverse ? '#fff' : '#000';
|
|
64
|
+
ctx.font = fontCss(el, px);
|
|
65
|
+
let lines = [text];
|
|
66
|
+
if (el.fit === 'wrap') {
|
|
67
|
+
lines = wrapLines(ctx, text, w);
|
|
68
|
+
const max = el.lines || 2;
|
|
69
|
+
if (lines.length > max) {
|
|
70
|
+
lines = lines.slice(0, max);
|
|
71
|
+
warn.push({ elementId: el.id, kind: 'wrap', message: `Text runs past ${max} lines` });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (ctx.measureText(text).width > w + 0.5) {
|
|
75
|
+
if (el.fit === 'shrink') {
|
|
76
|
+
const fitted = px * (w / ctx.measureText(text).width) * 0.98;
|
|
77
|
+
warn.push({ elementId: el.id, kind: 'shrink', message: `Text shrunk ${roundPt(el.size || 10)} → ${roundPt(((el.size || 10) * fitted) / px)} pt to fit` });
|
|
78
|
+
px = fitted;
|
|
79
|
+
ctx.font = fontCss(el, px);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
warn.push({ elementId: el.id, kind: 'cut', message: 'Text is cut at the box edge' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
ctx.rect(ox, oy, w, h);
|
|
87
|
+
ctx.clip();
|
|
88
|
+
const lineH = px * 1.12;
|
|
89
|
+
let ty = oy + (el.fit === 'wrap' ? 0 : Math.max(0, (h - lines.length * lineH) / 2)) + px * 0.86;
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const lw = ctx.measureText(line).width;
|
|
92
|
+
const lx = el.align === 'center' ? ox + (w - lw) / 2 : el.align === 'right' ? ox + w - lw : ox;
|
|
93
|
+
ctx.fillText(line, lx, ty);
|
|
94
|
+
ty += lineH;
|
|
95
|
+
}
|
|
96
|
+
ctx.restore();
|
|
97
|
+
}
|
|
98
|
+
function drawPrice(ctx, el, data, ppm, ox, oy, warn) {
|
|
99
|
+
const raw = (0, fields_1.resolveValue)(el.value, data).trim();
|
|
100
|
+
if (!raw)
|
|
101
|
+
return;
|
|
102
|
+
const amount = Number(raw.replace(/[$,]/g, ''));
|
|
103
|
+
const w = el.w * ppm;
|
|
104
|
+
const h = el.h * ppm;
|
|
105
|
+
let px = ((el.size || 24) / units_1.PT_PER_INCH) * units_1.MM_PER_INCH * ppm;
|
|
106
|
+
const weight = el.weight || 700;
|
|
107
|
+
let parts;
|
|
108
|
+
if (!Number.isFinite(amount)) {
|
|
109
|
+
parts = [{ t: raw, s: 1 }];
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const [d, c] = Math.abs(amount).toFixed(2).split('.');
|
|
113
|
+
const dollars = d.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
114
|
+
if (el.format === 'super')
|
|
115
|
+
parts = [{ t: '$', s: 0.5, up: true }, { t: dollars, s: 1 }, { t: c, s: 0.5, up: true }];
|
|
116
|
+
else if (el.format === 'nosign')
|
|
117
|
+
parts = [{ t: `${dollars}.${c}`, s: 1 }];
|
|
118
|
+
else
|
|
119
|
+
parts = [{ t: `$${dollars}.${c}`, s: 1 }];
|
|
120
|
+
}
|
|
121
|
+
const family = (0, fields_1.fontFamily)(el.font);
|
|
122
|
+
const measure = (size) => parts.reduce((a, p) => {
|
|
123
|
+
ctx.font = `${weight} ${size * p.s}px ${family}`;
|
|
124
|
+
return a + ctx.measureText(p.t).width + (p.up ? size * 0.04 : 0);
|
|
125
|
+
}, 0);
|
|
126
|
+
let width = measure(px);
|
|
127
|
+
if (width > w) {
|
|
128
|
+
const fitted = px * (w / width) * 0.98;
|
|
129
|
+
warn.push({ elementId: el.id, kind: 'shrink', message: `Price shrunk ${roundPt(el.size || 24)} → ${roundPt(((el.size || 24) * fitted) / px)} pt to fit` });
|
|
130
|
+
px = fitted;
|
|
131
|
+
width = measure(px);
|
|
132
|
+
}
|
|
133
|
+
let cx = el.align === 'center' ? ox + (w - width) / 2 : el.align === 'right' ? ox + w - width : ox;
|
|
134
|
+
const base = oy + (h + px * 0.72) / 2;
|
|
135
|
+
ctx.save();
|
|
136
|
+
ctx.fillStyle = '#000';
|
|
137
|
+
for (const p of parts) {
|
|
138
|
+
const s = px * p.s;
|
|
139
|
+
ctx.font = `${weight} ${s}px ${family}`;
|
|
140
|
+
const py = p.up ? base - px * 0.72 + s * 0.72 : base;
|
|
141
|
+
ctx.fillText(p.t, cx, py);
|
|
142
|
+
const tw = ctx.measureText(p.t).width;
|
|
143
|
+
if (el.strike)
|
|
144
|
+
ctx.fillRect(cx, base - px * 0.32, tw, Math.max(1, px * 0.07));
|
|
145
|
+
cx += tw + (p.up ? px * 0.04 : 0);
|
|
146
|
+
}
|
|
147
|
+
ctx.restore();
|
|
148
|
+
}
|
|
149
|
+
function checkDigitOk(digits) {
|
|
150
|
+
let sum = 0;
|
|
151
|
+
for (let i = 0; i < digits.length - 1; i++)
|
|
152
|
+
sum += Number(digits[digits.length - 2 - i]) * (i % 2 === 0 ? 3 : 1);
|
|
153
|
+
return (10 - (sum % 10)) % 10 === Number(digits[digits.length - 1]);
|
|
154
|
+
}
|
|
155
|
+
function barcodeProblem(symbology, value) {
|
|
156
|
+
if (symbology === 'CODE128')
|
|
157
|
+
return /^[\x20-\x7e]+$/.test(value) ? null : 'needs printable ASCII';
|
|
158
|
+
const [min, max] = symbology === 'EAN13' ? [12, 13] : [11, 12];
|
|
159
|
+
if (!/^\d+$/.test(value) || value.length < min || value.length > max)
|
|
160
|
+
return `needs ${min} or ${max} digits`;
|
|
161
|
+
if (value.length === max && !checkDigitOk(value))
|
|
162
|
+
return 'has a wrong check digit';
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const moduleCache = new Map();
|
|
166
|
+
function barcodeRuns(symbology, value) {
|
|
167
|
+
const key = `${symbology}|${value}`;
|
|
168
|
+
if (moduleCache.has(key))
|
|
169
|
+
return moduleCache.get(key);
|
|
170
|
+
let runs = null;
|
|
171
|
+
if (!barcodeProblem(symbology, value)) {
|
|
172
|
+
try {
|
|
173
|
+
runs = (0, barcode1d_1.pattern)(symbology, value);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
runs = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (moduleCache.size > 2000)
|
|
180
|
+
moduleCache.clear();
|
|
181
|
+
moduleCache.set(key, runs);
|
|
182
|
+
return runs;
|
|
183
|
+
}
|
|
184
|
+
function barcodeWidthMm(el, value, dpi) {
|
|
185
|
+
const runs = barcodeRuns(el.symbology || 'UPC_A', value);
|
|
186
|
+
return runs ? (0, units_1.dotsToMm)(runs.reduce((a, b) => a + b, 0) * (el.module || 2), dpi) : 0;
|
|
187
|
+
}
|
|
188
|
+
function drawBarcode(ctx, el, data, ppm, dpi, ox, oy, warn) {
|
|
189
|
+
const value = (0, fields_1.resolveValue)(el.value, data).trim();
|
|
190
|
+
const symbology = el.symbology || 'UPC_A';
|
|
191
|
+
if (!value) {
|
|
192
|
+
warn.push({ elementId: el.id, kind: 'missing', message: 'No UPC, barcode left off' });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const runs = barcodeRuns(symbology, value);
|
|
196
|
+
if (!runs) {
|
|
197
|
+
warn.push({ elementId: el.id, kind: 'barcode', message: `"${value}" is not a valid ${symbology} code` });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const hri = el.hri !== false;
|
|
201
|
+
const barsH = el.h * (hri ? 0.78 : 1) * ppm;
|
|
202
|
+
const modulePx = (0, units_1.dotsToMm)(el.module || 2, dpi) * ppm;
|
|
203
|
+
const widthPx = runs.reduce((a, b) => a + b, 0) * modulePx;
|
|
204
|
+
ctx.save();
|
|
205
|
+
ctx.fillStyle = '#000';
|
|
206
|
+
let x = ox;
|
|
207
|
+
let bar = true;
|
|
208
|
+
for (const run of runs) {
|
|
209
|
+
if (bar)
|
|
210
|
+
ctx.fillRect(x, oy, run * modulePx, barsH);
|
|
211
|
+
x += run * modulePx;
|
|
212
|
+
bar = !bar;
|
|
213
|
+
}
|
|
214
|
+
if (widthPx > el.w * ppm + 0.5)
|
|
215
|
+
warn.push({ elementId: el.id, kind: 'cut', message: 'Barcode is wider than its box' });
|
|
216
|
+
if (hri) {
|
|
217
|
+
const px = el.h * 0.18 * ppm;
|
|
218
|
+
ctx.font = `500 ${px}px ${(0, fields_1.fontFamily)('sans')}`;
|
|
219
|
+
const tw = ctx.measureText(value).width;
|
|
220
|
+
ctx.fillText(value, ox + (widthPx - tw) / 2, oy + el.h * ppm - px * 0.1);
|
|
221
|
+
}
|
|
222
|
+
ctx.restore();
|
|
223
|
+
}
|
|
224
|
+
function drawQr(ctx, el, data, ppm, ox, oy, warn) {
|
|
225
|
+
const value = (0, fields_1.resolveValue)(el.value, data).trim();
|
|
226
|
+
if (!value) {
|
|
227
|
+
warn.push({ elementId: el.id, kind: 'missing', message: 'QR code has no data' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const q = (0, bitmap_1.qrBitmap)(value, { size: 1, correction: 'M' });
|
|
231
|
+
const cell = (Math.min(el.w, el.h) * ppm) / q.width;
|
|
232
|
+
const rowBytes = Math.ceil(q.width / 8);
|
|
233
|
+
ctx.save();
|
|
234
|
+
ctx.fillStyle = '#000';
|
|
235
|
+
for (let y = 0; y < q.height; y++) {
|
|
236
|
+
for (let x = 0; x < q.width; x++) {
|
|
237
|
+
if (q.bits[y * rowBytes + (x >> 3)] & (0x80 >> (x & 7))) {
|
|
238
|
+
ctx.fillRect(ox + Math.floor(x * cell), oy + Math.floor(y * cell), Math.ceil(cell), Math.ceil(cell));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
ctx.restore();
|
|
243
|
+
}
|
|
244
|
+
function drawImage(ctx, el, ppm, ox, oy, factory, warn) {
|
|
245
|
+
const image = el.image;
|
|
246
|
+
if (!image || !image.bits) {
|
|
247
|
+
warn.push({ elementId: el.id, kind: 'missing', message: 'No image uploaded yet' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const raw = (0, image_1.decodeImageBits)(image);
|
|
251
|
+
if (!raw)
|
|
252
|
+
return;
|
|
253
|
+
const width = Math.max(1, Math.round(el.w * ppm));
|
|
254
|
+
const height = Math.max(1, Math.round(el.h * ppm));
|
|
255
|
+
const target = ox === 0 && oy === 0 && !factory ? ctx : null;
|
|
256
|
+
const off = target ? null : factory ? factory(width, height) : null;
|
|
257
|
+
const g = target || (off && off.getContext('2d'));
|
|
258
|
+
if (!g)
|
|
259
|
+
return;
|
|
260
|
+
const out = g.createImageData(width, height);
|
|
261
|
+
const px = out.data;
|
|
262
|
+
const rowBytes = Math.ceil(image.w / 8);
|
|
263
|
+
for (let y = 0; y < height; y++) {
|
|
264
|
+
const sy = Math.min(image.h - 1, Math.floor((y * image.h) / height));
|
|
265
|
+
for (let x = 0; x < width; x++) {
|
|
266
|
+
const sx = Math.min(image.w - 1, Math.floor((x * image.w) / width));
|
|
267
|
+
const v = raw[sy * rowBytes + (sx >> 3)] & (0x80 >> (sx & 7)) ? 0 : 255;
|
|
268
|
+
const i = (y * width + x) * 4;
|
|
269
|
+
px[i] = v;
|
|
270
|
+
px[i + 1] = v;
|
|
271
|
+
px[i + 2] = v;
|
|
272
|
+
px[i + 3] = 255;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
g.putImageData(out, 0, 0);
|
|
276
|
+
if (off) {
|
|
277
|
+
ctx.save();
|
|
278
|
+
ctx.imageSmoothingEnabled = false;
|
|
279
|
+
ctx.drawImage(off, ox, oy);
|
|
280
|
+
ctx.restore();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function drawShape(ctx, el, ppm, ox, oy) {
|
|
284
|
+
if (el.type === 'box') {
|
|
285
|
+
const t = Math.max(1, (el.stroke || 0.3) * ppm);
|
|
286
|
+
ctx.strokeStyle = '#000';
|
|
287
|
+
ctx.lineWidth = t;
|
|
288
|
+
ctx.strokeRect(ox + t / 2, oy + t / 2, el.w * ppm - t, el.h * ppm - t);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
ctx.fillStyle = '#000';
|
|
292
|
+
ctx.fillRect(ox, oy, Math.max(1, el.w * ppm), Math.max(1, el.h * ppm));
|
|
293
|
+
}
|
|
294
|
+
/** Draws the whole label on `canvas` at `ppm` pixels per millimetre and returns what does not fit. */
|
|
295
|
+
function drawLabel(canvas, design, data, opts) {
|
|
296
|
+
const ppm = opts.ppm;
|
|
297
|
+
canvas.width = Math.max(1, Math.round(design.widthMm * ppm));
|
|
298
|
+
canvas.height = Math.max(1, Math.round(design.heightMm * ppm));
|
|
299
|
+
const ctx = canvas.getContext('2d');
|
|
300
|
+
const factory = opts.canvasFactory || domCanvasFactory();
|
|
301
|
+
ctx.fillStyle = opts.background || '#fff';
|
|
302
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
303
|
+
if (design.stock && opts.stock !== false) {
|
|
304
|
+
ctx.fillStyle = design.stock.color;
|
|
305
|
+
ctx.fillRect(design.stock.x * ppm, design.stock.y * ppm, design.stock.w * ppm, design.stock.h * ppm);
|
|
306
|
+
}
|
|
307
|
+
const warn = [];
|
|
308
|
+
for (const el of design.elements || []) {
|
|
309
|
+
try {
|
|
310
|
+
if (!(0, fields_1.isVisible)(el, data))
|
|
311
|
+
continue;
|
|
312
|
+
const ox = el.x * ppm;
|
|
313
|
+
const oy = el.y * ppm;
|
|
314
|
+
if (el.type === 'text')
|
|
315
|
+
drawText(ctx, el, data, ppm, ox, oy, warn);
|
|
316
|
+
else if (el.type === 'price')
|
|
317
|
+
drawPrice(ctx, el, data, ppm, ox, oy, warn);
|
|
318
|
+
else if (el.type === 'barcode')
|
|
319
|
+
drawBarcode(ctx, el, data, ppm, design.dpi, ox, oy, warn);
|
|
320
|
+
else if (el.type === 'qr')
|
|
321
|
+
drawQr(ctx, el, data, ppm, ox, oy, warn);
|
|
322
|
+
else if (el.type === 'image')
|
|
323
|
+
drawImage(ctx, el, ppm, ox, oy, factory, warn);
|
|
324
|
+
else if (el.type === 'fill' || el.type === 'line' || el.type === 'box')
|
|
325
|
+
drawShape(ctx, el, ppm, ox, oy);
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return warn;
|
|
332
|
+
}
|
|
333
|
+
/** The label as the printer prints it: drawn at the printer's dots, then black or white. */
|
|
334
|
+
function drawPrinterView(canvas, design, data, opts = {}) {
|
|
335
|
+
const warn = drawLabel(canvas, design, data, { ppm: design.dpi / units_1.MM_PER_INCH, stock: false, canvasFactory: opts.canvasFactory });
|
|
336
|
+
const ctx = canvas.getContext('2d');
|
|
337
|
+
const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
338
|
+
for (let i = 0; i < img.data.length; i += 4) {
|
|
339
|
+
const l = img.data[i] * 0.299 + img.data[i + 1] * 0.587 + img.data[i + 2] * 0.114;
|
|
340
|
+
const v = l < 150 ? 0 : 255;
|
|
341
|
+
img.data[i] = v;
|
|
342
|
+
img.data[i + 1] = v;
|
|
343
|
+
img.data[i + 2] = v;
|
|
344
|
+
}
|
|
345
|
+
ctx.putImageData(img, 0, 0);
|
|
346
|
+
return warn;
|
|
347
|
+
}
|
|
348
|
+
function toBitmap(ctx, width, height) {
|
|
349
|
+
const img = ctx.getImageData(0, 0, width, height).data;
|
|
350
|
+
const rowBytes = Math.ceil(width / 8);
|
|
351
|
+
const bits = new Uint8Array(rowBytes * height);
|
|
352
|
+
for (let y = 0; y < height; y++) {
|
|
353
|
+
for (let x = 0; x < width; x++) {
|
|
354
|
+
const i = (y * width + x) * 4;
|
|
355
|
+
const l = img[i] * 0.299 + img[i + 1] * 0.587 + img[i + 2] * 0.114;
|
|
356
|
+
if (img[i + 3] > 0 && l < 150)
|
|
357
|
+
bits[y * rowBytes + (x >> 3)] |= 0x80 >> (x & 7);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { width, height, bits };
|
|
361
|
+
}
|
|
362
|
+
function isStatic(el) {
|
|
363
|
+
return el.type === 'image' || !/\{\{\w+\}\}/.test(String(el.value || ''));
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Draws one text, price, QR or image element at printer resolution for `encodeDesignLabel`.
|
|
367
|
+
* Elements without placeholders are drawn once per design object and reused.
|
|
368
|
+
*/
|
|
369
|
+
function createRasterizer(options = {}) {
|
|
370
|
+
const factory = options.canvasFactory || domCanvasFactory();
|
|
371
|
+
if (!factory)
|
|
372
|
+
throw new Error('retail-print-kit: createRasterizer needs a canvasFactory outside a browser');
|
|
373
|
+
const cache = new WeakMap();
|
|
374
|
+
const draw = (el, data, dpi) => {
|
|
375
|
+
if (el.type !== 'text' && el.type !== 'price' && el.type !== 'qr' && el.type !== 'image')
|
|
376
|
+
return null;
|
|
377
|
+
const ppm = dpi / units_1.MM_PER_INCH;
|
|
378
|
+
const width = Math.max(1, Math.round(el.w * ppm));
|
|
379
|
+
const height = Math.max(1, Math.round(el.h * ppm));
|
|
380
|
+
const canvas = factory(width, height);
|
|
381
|
+
const ctx = canvas.getContext('2d');
|
|
382
|
+
ctx.fillStyle = '#fff';
|
|
383
|
+
ctx.fillRect(0, 0, width, height);
|
|
384
|
+
const warn = [];
|
|
385
|
+
if (el.type === 'text')
|
|
386
|
+
drawText(ctx, el, data, ppm, 0, 0, warn);
|
|
387
|
+
else if (el.type === 'price')
|
|
388
|
+
drawPrice(ctx, el, data, ppm, 0, 0, warn);
|
|
389
|
+
else if (el.type === 'qr')
|
|
390
|
+
drawQr(ctx, el, data, ppm, 0, 0, warn);
|
|
391
|
+
else
|
|
392
|
+
drawImage(ctx, el, ppm, 0, 0, undefined, warn);
|
|
393
|
+
return toBitmap(ctx, width, height);
|
|
394
|
+
};
|
|
395
|
+
return (el, data, dpi) => {
|
|
396
|
+
if (!isStatic(el))
|
|
397
|
+
return draw(el, data, dpi);
|
|
398
|
+
const hit = cache.get(el);
|
|
399
|
+
if (hit && hit.dpi === dpi)
|
|
400
|
+
return hit.bitmap;
|
|
401
|
+
const bitmap = draw(el, data, dpi);
|
|
402
|
+
cache.set(el, { dpi, bitmap });
|
|
403
|
+
return bitmap;
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function imageFromSource(source, srcW, srcH, w, h, mode, canvasFactory) {
|
|
407
|
+
const factory = canvasFactory || domCanvasFactory();
|
|
408
|
+
if (!factory)
|
|
409
|
+
throw new Error('retail-print-kit: imageFromSource needs a canvasFactory outside a browser');
|
|
410
|
+
const canvas = factory(w, h);
|
|
411
|
+
const g = canvas.getContext('2d');
|
|
412
|
+
g.fillStyle = '#fff';
|
|
413
|
+
g.fillRect(0, 0, w, h);
|
|
414
|
+
const scale = Math.min(w / srcW, h / srcH);
|
|
415
|
+
const dw = srcW * scale;
|
|
416
|
+
const dh = srcH * scale;
|
|
417
|
+
g.drawImage(source, (w - dw) / 2, (h - dh) / 2, dw, dh);
|
|
418
|
+
return (0, image_1.imageFromRgba)(g.getImageData(0, 0, w, h).data, w, h, mode);
|
|
419
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export type ElementType = 'text' | 'price' | 'barcode' | 'qr' | 'box' | 'fill' | 'line' | 'image';
|
|
2
|
+
export type PrinterLanguage = 'ezpl' | 'zpl';
|
|
3
|
+
export type Symbology = 'UPC_A' | 'EAN13' | 'CODE128';
|
|
4
|
+
export interface DesignImage {
|
|
5
|
+
w: number;
|
|
6
|
+
h: number;
|
|
7
|
+
bits: string;
|
|
8
|
+
}
|
|
9
|
+
export interface NativeFit {
|
|
10
|
+
left: number;
|
|
11
|
+
right: number;
|
|
12
|
+
middle: number;
|
|
13
|
+
size: number;
|
|
14
|
+
}
|
|
15
|
+
export interface LabelElement {
|
|
16
|
+
id: string;
|
|
17
|
+
type: ElementType;
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
w: number;
|
|
21
|
+
h: number;
|
|
22
|
+
value?: string;
|
|
23
|
+
font?: string;
|
|
24
|
+
size?: number;
|
|
25
|
+
weight?: number;
|
|
26
|
+
italic?: boolean;
|
|
27
|
+
align?: 'left' | 'center' | 'right';
|
|
28
|
+
fit?: 'cut' | 'wrap' | 'shrink';
|
|
29
|
+
lines?: number;
|
|
30
|
+
upper?: boolean;
|
|
31
|
+
inverse?: boolean;
|
|
32
|
+
format?: 'super' | 'plain' | 'nosign';
|
|
33
|
+
strike?: boolean;
|
|
34
|
+
symbology?: Symbology;
|
|
35
|
+
hri?: boolean;
|
|
36
|
+
module?: number;
|
|
37
|
+
stroke?: number;
|
|
38
|
+
when?: string;
|
|
39
|
+
native?: {
|
|
40
|
+
ezpl?: string;
|
|
41
|
+
zpl?: string;
|
|
42
|
+
};
|
|
43
|
+
nativeFit?: NativeFit;
|
|
44
|
+
image?: DesignImage;
|
|
45
|
+
}
|
|
46
|
+
export interface EzplHeader {
|
|
47
|
+
q: string;
|
|
48
|
+
w: string;
|
|
49
|
+
h: string;
|
|
50
|
+
s: string;
|
|
51
|
+
r?: string;
|
|
52
|
+
shift?: string;
|
|
53
|
+
gap?: string;
|
|
54
|
+
includeDateTimeLines?: boolean;
|
|
55
|
+
addDateTimeTwice?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** A label design, version 1. Saved as JSON; every app that renders or prints it reads the same shape. */
|
|
58
|
+
export interface LabelDesign {
|
|
59
|
+
version: 1;
|
|
60
|
+
kind: 'label';
|
|
61
|
+
widthMm: number;
|
|
62
|
+
heightMm: number;
|
|
63
|
+
dpi: number;
|
|
64
|
+
printer?: PrinterLanguage;
|
|
65
|
+
gapMm?: number;
|
|
66
|
+
stock?: {
|
|
67
|
+
color: string;
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
w: number;
|
|
71
|
+
h: number;
|
|
72
|
+
} | null;
|
|
73
|
+
source?: string;
|
|
74
|
+
nativeHeader?: {
|
|
75
|
+
ezpl?: EzplHeader;
|
|
76
|
+
zpl?: string[];
|
|
77
|
+
};
|
|
78
|
+
limits?: {
|
|
79
|
+
itemName?: number;
|
|
80
|
+
brand?: number;
|
|
81
|
+
};
|
|
82
|
+
elements: LabelElement[];
|
|
83
|
+
}
|
|
84
|
+
export interface LabelData {
|
|
85
|
+
[field: string]: any;
|
|
86
|
+
}
|
|
87
|
+
export interface LabelWarning {
|
|
88
|
+
elementId: string;
|
|
89
|
+
kind: 'cut' | 'wrap' | 'shrink' | 'missing' | 'barcode';
|
|
90
|
+
message: string;
|
|
91
|
+
}
|
|
92
|
+
export interface FieldOption {
|
|
93
|
+
key: string;
|
|
94
|
+
label: string;
|
|
95
|
+
icon: string;
|
|
96
|
+
sample: string;
|
|
97
|
+
}
|
|
98
|
+
export interface BuiltinElement {
|
|
99
|
+
type: 'text' | 'barcode';
|
|
100
|
+
x?: number;
|
|
101
|
+
y?: number;
|
|
102
|
+
value: string;
|
|
103
|
+
native: string;
|
|
104
|
+
fit?: NativeFit;
|
|
105
|
+
}
|
|
106
|
+
export interface BuiltinVariant {
|
|
107
|
+
limits: {
|
|
108
|
+
itemName: number;
|
|
109
|
+
brand: number;
|
|
110
|
+
};
|
|
111
|
+
header: any;
|
|
112
|
+
elements: BuiltinElement[];
|
|
113
|
+
}
|
|
114
|
+
/** A fixed layout printed with the printer's own fonts, kept byte for byte as it always printed. */
|
|
115
|
+
export interface BuiltinTemplate {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
version: number;
|
|
119
|
+
kind: 'label';
|
|
120
|
+
builtin: true;
|
|
121
|
+
size: string;
|
|
122
|
+
layout: 'single' | 'dual';
|
|
123
|
+
priceTypes: string[];
|
|
124
|
+
widthMm: number;
|
|
125
|
+
heightMm: number;
|
|
126
|
+
dpi: number;
|
|
127
|
+
printers: {
|
|
128
|
+
ezpl?: BuiltinVariant;
|
|
129
|
+
zpl?: BuiltinVariant;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export interface ElementBitmap {
|
|
133
|
+
width: number;
|
|
134
|
+
height: number;
|
|
135
|
+
bits: Uint8Array;
|
|
136
|
+
}
|
|
137
|
+
export type Rasterize = (el: LabelElement, data: LabelData, dpi: number) => ElementBitmap | null;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const MM_PER_INCH = 25.4;
|
|
2
|
+
export declare const PT_PER_INCH = 72;
|
|
3
|
+
export declare function mmToDots(mm: number, dpi: number): number;
|
|
4
|
+
export declare function dotsToMm(dots: number, dpi: number): number;
|
|
5
|
+
export declare function snapToDot(mm: number, dpi: number): number;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PT_PER_INCH = exports.MM_PER_INCH = void 0;
|
|
4
|
+
exports.mmToDots = mmToDots;
|
|
5
|
+
exports.dotsToMm = dotsToMm;
|
|
6
|
+
exports.snapToDot = snapToDot;
|
|
7
|
+
exports.MM_PER_INCH = 25.4;
|
|
8
|
+
exports.PT_PER_INCH = 72;
|
|
9
|
+
function mmToDots(mm, dpi) {
|
|
10
|
+
return Math.round((Number(mm) / exports.MM_PER_INCH) * dpi);
|
|
11
|
+
}
|
|
12
|
+
function dotsToMm(dots, dpi) {
|
|
13
|
+
return (dots / dpi) * exports.MM_PER_INCH;
|
|
14
|
+
}
|
|
15
|
+
function snapToDot(mm, dpi) {
|
|
16
|
+
return dotsToMm(mmToDots(mm, dpi), dpi);
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const DESIGN_LIMITS: {
|
|
2
|
+
maxBytes: number;
|
|
3
|
+
maxElements: number;
|
|
4
|
+
valueMax: number;
|
|
5
|
+
sizeMm: [number, number];
|
|
6
|
+
dpis: number[];
|
|
7
|
+
printers: string[];
|
|
8
|
+
elementTypes: string[];
|
|
9
|
+
gapMm: [number, number];
|
|
10
|
+
imageMaxDots: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function imageError(image: any): string | null;
|
|
13
|
+
export declare function elementError(element: any, index: number, widthMm: number, heightMm: number): string | null;
|
|
14
|
+
/** The first reason a design cannot be saved or printed, or null when it is fine. */
|
|
15
|
+
export declare function validateDesign(design: any): string | null;
|