@ai-matrx/print 0.3.2 → 0.4.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 +36 -0
- package/dist/certificate.cjs +746 -0
- package/dist/certificate.cjs.map +1 -0
- package/dist/certificate.d.cts +101 -0
- package/dist/certificate.d.ts +101 -0
- package/dist/certificate.js +723 -0
- package/dist/certificate.js.map +1 -0
- package/dist/education.cjs +928 -0
- package/dist/education.cjs.map +1 -0
- package/dist/education.d.cts +79 -0
- package/dist/education.d.ts +79 -0
- package/dist/education.js +905 -0
- package/dist/education.js.map +1 -0
- package/dist/exam.cjs +648 -0
- package/dist/exam.cjs.map +1 -0
- package/dist/exam.d.cts +69 -0
- package/dist/exam.d.ts +69 -0
- package/dist/exam.js +625 -0
- package/dist/exam.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/zpl.cjs +1039 -0
- package/dist/zpl.cjs.map +1 -0
- package/dist/zpl.d.cts +160 -0
- package/dist/zpl.d.ts +160 -0
- package/dist/zpl.js +1006 -0
- package/dist/zpl.js.map +1 -0
- package/package.json +41 -1
package/dist/zpl.js
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
// src/core.ts
|
|
2
|
+
function escapeHtml(str) {
|
|
3
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4
|
+
}
|
|
5
|
+
function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "width=920,height=720,scrollbars=yes") {
|
|
6
|
+
const win = window.open("", "_blank", windowFeatures);
|
|
7
|
+
if (!win) {
|
|
8
|
+
const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
|
|
9
|
+
const url = URL.createObjectURL(blob);
|
|
10
|
+
const a = document.createElement("a");
|
|
11
|
+
a.href = url;
|
|
12
|
+
a.download = `${filename.replace(/\s+/g, "-").toLowerCase()}.html`;
|
|
13
|
+
document.body.appendChild(a);
|
|
14
|
+
a.click();
|
|
15
|
+
document.body.removeChild(a);
|
|
16
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
17
|
+
return "downloaded";
|
|
18
|
+
}
|
|
19
|
+
win.document.open();
|
|
20
|
+
win.document.write(htmlDocument);
|
|
21
|
+
win.document.close();
|
|
22
|
+
return "opened";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/qr.ts
|
|
26
|
+
var QR_MIN_QUIET_ZONE_MODULES = 4;
|
|
27
|
+
var DEFAULT_QR_EC_LEVEL = "M";
|
|
28
|
+
var QR_MIN_VERSION = 1;
|
|
29
|
+
var QR_MAX_VERSION = 40;
|
|
30
|
+
var BYTE_CAPACITY = [
|
|
31
|
+
/* v1 */
|
|
32
|
+
[17, 14, 11, 7],
|
|
33
|
+
/* v2 */
|
|
34
|
+
[32, 26, 20, 14],
|
|
35
|
+
/* v3 */
|
|
36
|
+
[53, 42, 32, 24],
|
|
37
|
+
/* v4 */
|
|
38
|
+
[78, 62, 46, 34],
|
|
39
|
+
/* v5 */
|
|
40
|
+
[106, 84, 60, 44],
|
|
41
|
+
/* v6 */
|
|
42
|
+
[134, 106, 74, 58],
|
|
43
|
+
/* v7 */
|
|
44
|
+
[154, 122, 86, 64],
|
|
45
|
+
/* v8 */
|
|
46
|
+
[192, 152, 108, 84],
|
|
47
|
+
/* v9 */
|
|
48
|
+
[230, 180, 130, 98],
|
|
49
|
+
/* v10 */
|
|
50
|
+
[271, 213, 151, 119],
|
|
51
|
+
/* v11 */
|
|
52
|
+
[321, 251, 177, 137],
|
|
53
|
+
/* v12 */
|
|
54
|
+
[367, 287, 203, 155],
|
|
55
|
+
/* v13 */
|
|
56
|
+
[425, 331, 241, 177],
|
|
57
|
+
/* v14 */
|
|
58
|
+
[458, 362, 258, 194],
|
|
59
|
+
/* v15 */
|
|
60
|
+
[520, 412, 292, 220],
|
|
61
|
+
/* v16 */
|
|
62
|
+
[586, 450, 322, 250],
|
|
63
|
+
/* v17 */
|
|
64
|
+
[644, 504, 364, 280],
|
|
65
|
+
/* v18 */
|
|
66
|
+
[718, 560, 394, 310],
|
|
67
|
+
/* v19 */
|
|
68
|
+
[792, 624, 442, 338],
|
|
69
|
+
/* v20 */
|
|
70
|
+
[858, 666, 482, 382],
|
|
71
|
+
/* v21 */
|
|
72
|
+
[929, 711, 509, 403],
|
|
73
|
+
/* v22 */
|
|
74
|
+
[1003, 779, 565, 439],
|
|
75
|
+
/* v23 */
|
|
76
|
+
[1091, 857, 611, 461],
|
|
77
|
+
/* v24 */
|
|
78
|
+
[1171, 911, 661, 511],
|
|
79
|
+
/* v25 */
|
|
80
|
+
[1273, 997, 715, 535],
|
|
81
|
+
/* v26 */
|
|
82
|
+
[1367, 1059, 751, 593],
|
|
83
|
+
/* v27 */
|
|
84
|
+
[1465, 1125, 805, 625],
|
|
85
|
+
/* v28 */
|
|
86
|
+
[1528, 1190, 868, 658],
|
|
87
|
+
/* v29 */
|
|
88
|
+
[1628, 1264, 908, 698],
|
|
89
|
+
/* v30 */
|
|
90
|
+
[1732, 1370, 982, 742],
|
|
91
|
+
/* v31 */
|
|
92
|
+
[1840, 1452, 1030, 790],
|
|
93
|
+
/* v32 */
|
|
94
|
+
[1952, 1538, 1112, 842],
|
|
95
|
+
/* v33 */
|
|
96
|
+
[2068, 1628, 1168, 898],
|
|
97
|
+
/* v34 */
|
|
98
|
+
[2188, 1722, 1228, 958],
|
|
99
|
+
/* v35 */
|
|
100
|
+
[2303, 1809, 1283, 983],
|
|
101
|
+
/* v36 */
|
|
102
|
+
[2431, 1911, 1351, 1051],
|
|
103
|
+
/* v37 */
|
|
104
|
+
[2563, 1989, 1423, 1093],
|
|
105
|
+
/* v38 */
|
|
106
|
+
[2699, 2099, 1499, 1139],
|
|
107
|
+
/* v39 */
|
|
108
|
+
[2809, 2213, 1579, 1219],
|
|
109
|
+
/* v40 */
|
|
110
|
+
[2953, 2331, 1663, 1273]
|
|
111
|
+
];
|
|
112
|
+
var EC_COLUMN = { L: 0, M: 1, Q: 2, H: 3 };
|
|
113
|
+
function assertEcLevel(ecLevel) {
|
|
114
|
+
const column = EC_COLUMN[ecLevel];
|
|
115
|
+
if (column === void 0) {
|
|
116
|
+
throw new Error(`Unknown QR error correction level "${String(ecLevel)}". Expected one of "L", "M", "Q", "H".`);
|
|
117
|
+
}
|
|
118
|
+
return column;
|
|
119
|
+
}
|
|
120
|
+
function assertVersion(version) {
|
|
121
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < QR_MIN_VERSION || version > QR_MAX_VERSION) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`QR version must be an integer between ${QR_MIN_VERSION} and ${QR_MAX_VERSION}, got ${String(version)}.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return version;
|
|
127
|
+
}
|
|
128
|
+
function qrByteCapacity(version, ecLevel) {
|
|
129
|
+
const column = assertEcLevel(ecLevel);
|
|
130
|
+
const row = BYTE_CAPACITY[assertVersion(version) - 1];
|
|
131
|
+
if (row === void 0) {
|
|
132
|
+
throw new Error(`No QR capacity row for version ${version}.`);
|
|
133
|
+
}
|
|
134
|
+
return row[column];
|
|
135
|
+
}
|
|
136
|
+
function qrModuleCount(version) {
|
|
137
|
+
return 17 + 4 * assertVersion(version);
|
|
138
|
+
}
|
|
139
|
+
function minQrVersion(payloadByteLen, ecLevel) {
|
|
140
|
+
const column = assertEcLevel(ecLevel);
|
|
141
|
+
if (typeof payloadByteLen !== "number" || !Number.isInteger(payloadByteLen) || payloadByteLen < 0) {
|
|
142
|
+
throw new Error(`QR payload byte length must be a non-negative integer, got ${String(payloadByteLen)}.`);
|
|
143
|
+
}
|
|
144
|
+
for (let version = QR_MIN_VERSION; version <= QR_MAX_VERSION; version += 1) {
|
|
145
|
+
const row = BYTE_CAPACITY[version - 1];
|
|
146
|
+
if (row !== void 0 && row[column] >= payloadByteLen) {
|
|
147
|
+
return version;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const max = qrByteCapacity(QR_MAX_VERSION, ecLevel);
|
|
151
|
+
throw new Error(
|
|
152
|
+
`QR payload of ${payloadByteLen} bytes does not fit any QR version at error correction level "${ecLevel}" (version ${QR_MAX_VERSION} holds ${max} bytes). Shorten the payload \u2014 a short resolver URL is the intended fix \u2014 or, only for a large clean code, drop to a lower error correction level.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
function resolveEcLevel(ecLevel) {
|
|
156
|
+
if (ecLevel === void 0) return DEFAULT_QR_EC_LEVEL;
|
|
157
|
+
assertEcLevel(ecLevel);
|
|
158
|
+
return ecLevel;
|
|
159
|
+
}
|
|
160
|
+
function resolveQuietZone(quietZoneModules) {
|
|
161
|
+
if (typeof quietZoneModules !== "number" || !Number.isFinite(quietZoneModules)) {
|
|
162
|
+
return QR_MIN_QUIET_ZONE_MODULES;
|
|
163
|
+
}
|
|
164
|
+
return Math.max(QR_MIN_QUIET_ZONE_MODULES, Math.floor(quietZoneModules));
|
|
165
|
+
}
|
|
166
|
+
function resolveWidthPx(widthPx) {
|
|
167
|
+
if (widthPx === void 0) return void 0;
|
|
168
|
+
if (typeof widthPx !== "number" || !Number.isFinite(widthPx) || widthPx <= 0) {
|
|
169
|
+
throw new Error(`QR widthPx must be a positive number, got ${String(widthPx)}.`);
|
|
170
|
+
}
|
|
171
|
+
return Math.round(widthPx);
|
|
172
|
+
}
|
|
173
|
+
function assertValue(value) {
|
|
174
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
175
|
+
throw new Error("QR value must be a non-empty string.");
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
async function generateQrDataUrl(value, opts) {
|
|
180
|
+
assertValue(value);
|
|
181
|
+
const width = resolveWidthPx(opts?.widthPx);
|
|
182
|
+
const options = {
|
|
183
|
+
errorCorrectionLevel: resolveEcLevel(opts?.ecLevel),
|
|
184
|
+
margin: resolveQuietZone(opts?.quietZoneModules)
|
|
185
|
+
};
|
|
186
|
+
if (width !== void 0) options.width = width;
|
|
187
|
+
const QRCode = await import("qrcode");
|
|
188
|
+
return QRCode.toDataURL(value, options);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/labels.ts
|
|
192
|
+
var AVERY_5160 = {
|
|
193
|
+
id: "avery-5160",
|
|
194
|
+
name: 'Address 1" \xD7 2\u215D" \u2014 30 per sheet',
|
|
195
|
+
stockCode: "Avery 5160 / 8160",
|
|
196
|
+
sheetWIn: 8.5,
|
|
197
|
+
sheetHIn: 11,
|
|
198
|
+
marginTopIn: 0.5,
|
|
199
|
+
marginLeftIn: 0.1875,
|
|
200
|
+
labelWIn: 2.625,
|
|
201
|
+
labelHIn: 1,
|
|
202
|
+
cols: 3,
|
|
203
|
+
rows: 10,
|
|
204
|
+
gutterXIn: 0.125,
|
|
205
|
+
gutterYIn: 0,
|
|
206
|
+
cornerRadiusIn: 0.1
|
|
207
|
+
};
|
|
208
|
+
var LABEL_TEMPLATES = [
|
|
209
|
+
AVERY_5160,
|
|
210
|
+
{
|
|
211
|
+
id: "avery-5163",
|
|
212
|
+
name: 'Shipping 2" \xD7 4" \u2014 10 per sheet',
|
|
213
|
+
stockCode: "Avery 5163 / 8163",
|
|
214
|
+
sheetWIn: 8.5,
|
|
215
|
+
sheetHIn: 11,
|
|
216
|
+
marginTopIn: 0.5,
|
|
217
|
+
marginLeftIn: 0.15625,
|
|
218
|
+
labelWIn: 4,
|
|
219
|
+
labelHIn: 2,
|
|
220
|
+
cols: 2,
|
|
221
|
+
rows: 5,
|
|
222
|
+
gutterXIn: 0.1875,
|
|
223
|
+
gutterYIn: 0,
|
|
224
|
+
cornerRadiusIn: 0.125
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "avery-5164",
|
|
228
|
+
name: 'Shipping 3\u2153" \xD7 4" \u2014 6 per sheet',
|
|
229
|
+
stockCode: "Avery 5164 / 8164",
|
|
230
|
+
sheetWIn: 8.5,
|
|
231
|
+
sheetHIn: 11,
|
|
232
|
+
marginTopIn: 0.5,
|
|
233
|
+
marginLeftIn: 0.15625,
|
|
234
|
+
labelWIn: 4,
|
|
235
|
+
labelHIn: 3.3333333,
|
|
236
|
+
cols: 2,
|
|
237
|
+
rows: 3,
|
|
238
|
+
gutterXIn: 0.1875,
|
|
239
|
+
gutterYIn: 0,
|
|
240
|
+
cornerRadiusIn: 0.125
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "avery-22806",
|
|
244
|
+
name: 'Square 2" \xD7 2" \u2014 12 per sheet',
|
|
245
|
+
stockCode: "Avery 22806",
|
|
246
|
+
sheetWIn: 8.5,
|
|
247
|
+
sheetHIn: 11,
|
|
248
|
+
marginTopIn: 0.75,
|
|
249
|
+
marginLeftIn: 0.5,
|
|
250
|
+
labelWIn: 2,
|
|
251
|
+
labelHIn: 2,
|
|
252
|
+
cols: 3,
|
|
253
|
+
rows: 4,
|
|
254
|
+
gutterXIn: 0.75,
|
|
255
|
+
gutterYIn: 0.5,
|
|
256
|
+
cornerRadiusIn: 0.09375
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "avery-22807",
|
|
260
|
+
name: 'Round 2" \u2014 12 per sheet',
|
|
261
|
+
stockCode: "Avery 22807",
|
|
262
|
+
sheetWIn: 8.5,
|
|
263
|
+
sheetHIn: 11,
|
|
264
|
+
marginTopIn: 0.75,
|
|
265
|
+
marginLeftIn: 0.5,
|
|
266
|
+
labelWIn: 2,
|
|
267
|
+
labelHIn: 2,
|
|
268
|
+
cols: 3,
|
|
269
|
+
rows: 4,
|
|
270
|
+
gutterXIn: 0.75,
|
|
271
|
+
gutterYIn: 0.5,
|
|
272
|
+
cornerRadiusIn: 1,
|
|
273
|
+
round: true
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "brother-dk-1202",
|
|
277
|
+
name: 'Shipping roll 2.4" \xD7 3.9" \u2014 die-cut',
|
|
278
|
+
stockCode: "Brother DK-1202",
|
|
279
|
+
sheetWIn: 2.4,
|
|
280
|
+
sheetHIn: 3.9,
|
|
281
|
+
marginTopIn: 0,
|
|
282
|
+
marginLeftIn: 0,
|
|
283
|
+
labelWIn: 2.4,
|
|
284
|
+
labelHIn: 3.9,
|
|
285
|
+
cols: 1,
|
|
286
|
+
rows: 1,
|
|
287
|
+
gutterXIn: 0,
|
|
288
|
+
gutterYIn: 0,
|
|
289
|
+
kind: "roll"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "brother-dk-11201",
|
|
293
|
+
name: 'Address roll 1.14" \xD7 3.5" \u2014 die-cut',
|
|
294
|
+
stockCode: "Brother DK-11201",
|
|
295
|
+
sheetWIn: 1.14,
|
|
296
|
+
sheetHIn: 3.5,
|
|
297
|
+
marginTopIn: 0,
|
|
298
|
+
marginLeftIn: 0,
|
|
299
|
+
labelWIn: 1.14,
|
|
300
|
+
labelHIn: 3.5,
|
|
301
|
+
cols: 1,
|
|
302
|
+
rows: 1,
|
|
303
|
+
gutterXIn: 0,
|
|
304
|
+
gutterYIn: 0,
|
|
305
|
+
kind: "roll"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "dymo-30252",
|
|
309
|
+
name: 'Address roll 1\u215B" \xD7 3\xBD"',
|
|
310
|
+
stockCode: "DYMO 30252",
|
|
311
|
+
sheetWIn: 1.125,
|
|
312
|
+
sheetHIn: 3.5,
|
|
313
|
+
marginTopIn: 0,
|
|
314
|
+
marginLeftIn: 0,
|
|
315
|
+
labelWIn: 1.125,
|
|
316
|
+
labelHIn: 3.5,
|
|
317
|
+
cols: 1,
|
|
318
|
+
rows: 1,
|
|
319
|
+
gutterXIn: 0,
|
|
320
|
+
gutterYIn: 0,
|
|
321
|
+
kind: "roll"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: "roll-4x6",
|
|
325
|
+
name: 'Shipping roll 4" \xD7 6"',
|
|
326
|
+
stockCode: "Thermal 4x6",
|
|
327
|
+
sheetWIn: 4,
|
|
328
|
+
sheetHIn: 6,
|
|
329
|
+
marginTopIn: 0,
|
|
330
|
+
marginLeftIn: 0,
|
|
331
|
+
labelWIn: 4,
|
|
332
|
+
labelHIn: 6,
|
|
333
|
+
cols: 1,
|
|
334
|
+
rows: 1,
|
|
335
|
+
gutterXIn: 0,
|
|
336
|
+
gutterYIn: 0,
|
|
337
|
+
kind: "roll"
|
|
338
|
+
}
|
|
339
|
+
];
|
|
340
|
+
function getLabelTemplate(id) {
|
|
341
|
+
return LABEL_TEMPLATES.find((t) => t.id === id);
|
|
342
|
+
}
|
|
343
|
+
function assertTemplateGeometry(t) {
|
|
344
|
+
const w = t.marginLeftIn * 2 + t.cols * t.labelWIn + (t.cols - 1) * t.gutterXIn;
|
|
345
|
+
const h = t.marginTopIn * 2 + t.rows * t.labelHIn + (t.rows - 1) * t.gutterYIn;
|
|
346
|
+
const tol = 1 / 64;
|
|
347
|
+
if (Math.abs(w - t.sheetWIn) > tol || Math.abs(h - t.sheetHIn) > tol) {
|
|
348
|
+
console.error(
|
|
349
|
+
`[label-print] Template ${t.id} does not tile its sheet: grid ${w.toFixed(4)}\xD7${h.toFixed(4)}in vs sheet ${t.sheetWIn}\xD7${t.sheetHIn}in`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (typeof process === "undefined" || true) {
|
|
354
|
+
LABEL_TEMPLATES.forEach(assertTemplateGeometry);
|
|
355
|
+
}
|
|
356
|
+
var DEFAULTS = {
|
|
357
|
+
startAtLabel: 1,
|
|
358
|
+
copiesPerCode: 1,
|
|
359
|
+
showCaption: true,
|
|
360
|
+
showLines: true,
|
|
361
|
+
rangeFrom: 0,
|
|
362
|
+
rangeTo: 0,
|
|
363
|
+
ecLevel: "M",
|
|
364
|
+
calibrationPage: false
|
|
365
|
+
};
|
|
366
|
+
function resolveSettings(s) {
|
|
367
|
+
const num = (v, d, min = 0) => typeof v === "number" && !Number.isNaN(v) ? Math.max(min, v) : d;
|
|
368
|
+
const ec = s?.ecLevel;
|
|
369
|
+
return {
|
|
370
|
+
startAtLabel: num(s?.startAtLabel, DEFAULTS.startAtLabel, 1),
|
|
371
|
+
copiesPerCode: num(s?.copiesPerCode, DEFAULTS.copiesPerCode, 1),
|
|
372
|
+
showCaption: s?.showCaption ?? DEFAULTS.showCaption,
|
|
373
|
+
showLines: s?.showLines ?? DEFAULTS.showLines,
|
|
374
|
+
rangeFrom: num(s?.rangeFrom, 0),
|
|
375
|
+
rangeTo: num(s?.rangeTo, 0),
|
|
376
|
+
ecLevel: ec === "L" || ec === "Q" ? ec : "M",
|
|
377
|
+
calibrationPage: s?.calibrationPage ?? false
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
var PRINT_DPI = 300;
|
|
381
|
+
var QUIET_ZONE_MODULES = 4;
|
|
382
|
+
async function generateQrDataUri(value, ecLevel, sizeInches) {
|
|
383
|
+
return generateQrDataUrl(value, {
|
|
384
|
+
ecLevel,
|
|
385
|
+
quietZoneModules: QUIET_ZONE_MODULES,
|
|
386
|
+
widthPx: Math.round(sizeInches * PRINT_DPI)
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function computeCellLayout(t, showCaption, hasBadge = false) {
|
|
390
|
+
const padIn = Math.min(0.08, t.labelHIn * 0.06);
|
|
391
|
+
const wide = t.labelWIn / t.labelHIn >= 1.8;
|
|
392
|
+
const badgeZoneIn = hasBadge ? Math.min(0.4, t.labelHIn * 0.22) : 0;
|
|
393
|
+
if (wide) {
|
|
394
|
+
return { wide, qrIn: t.labelHIn - padIn * 2, padIn, badgeZoneIn };
|
|
395
|
+
}
|
|
396
|
+
const inset = t.round ? t.labelWIn * 0.15 : 0;
|
|
397
|
+
const captionZone = showCaption ? Math.min(0.28, t.labelHIn * 0.16) : 0;
|
|
398
|
+
const qrIn = Math.min(
|
|
399
|
+
t.labelWIn - padIn * 2 - inset * 2,
|
|
400
|
+
t.labelHIn - padIn * 2 - inset * 2 - captionZone - badgeZoneIn
|
|
401
|
+
);
|
|
402
|
+
return { wide, qrIn, padIn, badgeZoneIn };
|
|
403
|
+
}
|
|
404
|
+
function sheetStyles(t) {
|
|
405
|
+
const radius = t.round ? `${t.labelWIn / 2}in` : t.cornerRadiusIn ? `${t.cornerRadiusIn}in` : "0";
|
|
406
|
+
return `
|
|
407
|
+
@page { size: ${t.sheetWIn}in ${t.sheetHIn}in; margin: 0; }
|
|
408
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
409
|
+
html, body { width: ${t.sheetWIn}in; background: #fff; color: #000;
|
|
410
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
|
|
411
|
+
|
|
412
|
+
.lp-page {
|
|
413
|
+
width: ${t.sheetWIn}in; height: ${t.sheetHIn}in;
|
|
414
|
+
position: relative; overflow: hidden;
|
|
415
|
+
page-break-after: always; break-after: page;
|
|
416
|
+
border-bottom: 1px solid #e2e8f0; /* screen-only separator */
|
|
417
|
+
}
|
|
418
|
+
.lp-page:last-child { page-break-after: auto; break-after: auto; border-bottom: none; }
|
|
419
|
+
|
|
420
|
+
.lp-cell {
|
|
421
|
+
position: absolute;
|
|
422
|
+
width: ${t.labelWIn}in; height: ${t.labelHIn}in;
|
|
423
|
+
border-radius: ${radius};
|
|
424
|
+
overflow: hidden;
|
|
425
|
+
-webkit-print-color-adjust: exact; print-color-adjust: exact;
|
|
426
|
+
}
|
|
427
|
+
.lp-qr {
|
|
428
|
+
display: block;
|
|
429
|
+
image-rendering: pixelated; /* no resampling blur on crisp modules */
|
|
430
|
+
}
|
|
431
|
+
.lp-cell-wide { display: flex; align-items: center; }
|
|
432
|
+
.lp-cell-stacked { display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|
433
|
+
|
|
434
|
+
.lp-text { flex: 1 1 auto; min-width: 0; overflow: hidden; align-self: stretch;
|
|
435
|
+
display: flex; flex-direction: column; justify-content: center; }
|
|
436
|
+
.lp-fit-wrap { overflow: hidden; }
|
|
437
|
+
.lp-caption { font-weight: 700; line-height: 1.15; word-break: break-word; }
|
|
438
|
+
.lp-line { line-height: 1.25; word-break: break-word; }
|
|
439
|
+
.lp-caption-stacked { text-align: center; width: 100%; }
|
|
440
|
+
|
|
441
|
+
/* Badge \u2014 the LARGE token above the caption (garment size, bin letter). */
|
|
442
|
+
.lp-badge { font-weight: 800; line-height: 1.05; word-break: break-word;
|
|
443
|
+
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
444
|
+
.lp-badge-stacked { text-align: center; width: 100%; }
|
|
445
|
+
|
|
446
|
+
/* Durable http(s) artwork in the lines zone. */
|
|
447
|
+
.lp-img { display: block; width: 100%; object-fit: contain; }
|
|
448
|
+
|
|
449
|
+
/* Calibration */
|
|
450
|
+
.lp-cal-cell { border: 0.5pt dashed #94a3b8; background: transparent; }
|
|
451
|
+
.lp-cal-label { position: absolute; inset: 0; display: flex; align-items: center;
|
|
452
|
+
justify-content: center; font-size: 7pt; color: #94a3b8; text-align: center; padding: 4px; }
|
|
453
|
+
.lp-crop { position: absolute; background: #94a3b8;
|
|
454
|
+
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
455
|
+
|
|
456
|
+
@media print {
|
|
457
|
+
html, body { height: auto; overflow: visible; }
|
|
458
|
+
.lp-page { border-bottom: none; }
|
|
459
|
+
.screen-only { display: none !important; }
|
|
460
|
+
}
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
function cellPosition(t, index) {
|
|
464
|
+
const col = index % t.cols;
|
|
465
|
+
const row = Math.floor(index / t.cols);
|
|
466
|
+
return {
|
|
467
|
+
x: t.marginLeftIn + col * (t.labelWIn + t.gutterXIn),
|
|
468
|
+
y: t.marginTopIn + row * (t.labelHIn + t.gutterYIn)
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
var LP_FIT_SCRIPT = `
|
|
472
|
+
<script>
|
|
473
|
+
(function () {
|
|
474
|
+
function fit(el, box) {
|
|
475
|
+
var size = parseFloat(window.getComputedStyle(el).fontSize);
|
|
476
|
+
var min = 4;
|
|
477
|
+
while (size > min && (el.scrollWidth > box.clientWidth + 1 || el.scrollHeight > box.clientHeight + 1)) {
|
|
478
|
+
size -= 0.5;
|
|
479
|
+
el.style.fontSize = size + "px";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function run() {
|
|
483
|
+
var els = document.querySelectorAll(".lp-fit-wrap .lp-fit");
|
|
484
|
+
for (var i = 0; i < els.length; i++) fit(els[i], els[i].parentElement);
|
|
485
|
+
}
|
|
486
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", run);
|
|
487
|
+
else setTimeout(run, 0);
|
|
488
|
+
})();
|
|
489
|
+
<\/script>`;
|
|
490
|
+
function isHttpUrl(value) {
|
|
491
|
+
return typeof value === "string" && /^https?:\/\//i.test(value.trim());
|
|
492
|
+
}
|
|
493
|
+
function buildLabelCellHtml(label, qrDataUri, t, layout, s) {
|
|
494
|
+
const qr = qrDataUri ? `<img class="lp-qr" src="${qrDataUri}" alt="" style="width:${layout.qrIn}in;height:${layout.qrIn}in;" />` : "";
|
|
495
|
+
const captionText = s.showCaption ? label.caption ?? label.qrValue : "";
|
|
496
|
+
const badgeText = label.badge?.trim() ? label.badge.trim() : "";
|
|
497
|
+
if (layout.wide) {
|
|
498
|
+
const lines = s.showLines && label.lines?.length ? label.lines.filter((l) => l.trim()).map(
|
|
499
|
+
(l) => `<div class="lp-line" style="font-size:8pt;">${escapeHtml(l)}</div>`
|
|
500
|
+
).join("") : "";
|
|
501
|
+
const caption2 = captionText ? `<div class="lp-fit-wrap" style="max-height:${(t.labelHIn * 0.3).toFixed(3)}in;"><div class="lp-fit lp-caption" style="font-size:12pt;">${escapeHtml(captionText)}</div></div>` : "";
|
|
502
|
+
const badge2 = badgeText ? `<div class="lp-fit-wrap" style="max-height:${layout.badgeZoneIn.toFixed(3)}in;"><div class="lp-fit lp-badge" style="font-size:24pt;">${escapeHtml(badgeText)}</div></div>` : "";
|
|
503
|
+
const image = isHttpUrl(label.imageUrl) ? `<img class="lp-img" src="${escapeHtml(label.imageUrl)}" alt="" style="height:${(t.labelHIn * 0.35).toFixed(3)}in;" />` : "";
|
|
504
|
+
return `<div class="lp-cell-wide" style="height:100%;padding:${layout.padIn}in;gap:${layout.padIn}in;">
|
|
505
|
+
${qr}
|
|
506
|
+
<div class="lp-text">${badge2}${caption2}${lines}${image}</div>
|
|
507
|
+
</div>`;
|
|
508
|
+
}
|
|
509
|
+
const caption = captionText ? `<div class="lp-fit-wrap lp-caption-stacked" style="height:${Math.min(0.28, t.labelHIn * 0.16).toFixed(3)}in;"><div class="lp-fit lp-caption" style="font-size:9pt;">${escapeHtml(captionText)}</div></div>` : "";
|
|
510
|
+
const badge = badgeText ? `<div class="lp-fit-wrap lp-badge-stacked" style="height:${layout.badgeZoneIn.toFixed(3)}in;"><div class="lp-fit lp-badge" style="font-size:18pt;">${escapeHtml(badgeText)}</div></div>` : "";
|
|
511
|
+
return `<div class="lp-cell-stacked" style="height:100%;padding:${layout.padIn}in;">
|
|
512
|
+
${qr}
|
|
513
|
+
${badge}
|
|
514
|
+
${caption}
|
|
515
|
+
</div>`;
|
|
516
|
+
}
|
|
517
|
+
function renderPages(t, cells) {
|
|
518
|
+
const perPage = t.cols * t.rows;
|
|
519
|
+
const pages = [];
|
|
520
|
+
for (let p = 0; p < Math.max(1, Math.ceil(cells.length / perPage)); p++) {
|
|
521
|
+
const batch = cells.slice(p * perPage, (p + 1) * perPage);
|
|
522
|
+
const cellHtml = batch.map((c, i) => {
|
|
523
|
+
const { x, y } = cellPosition(t, i);
|
|
524
|
+
return `<div class="lp-cell" style="left:${x}in;top:${y}in;">${c.html}</div>`;
|
|
525
|
+
}).join("\n");
|
|
526
|
+
pages.push(`<div class="lp-page">${cellHtml}</div>`);
|
|
527
|
+
}
|
|
528
|
+
return pages.join("\n");
|
|
529
|
+
}
|
|
530
|
+
function openSheetWindow(t, bodyHtml, title, withFitScript, warning) {
|
|
531
|
+
const warningHtml = warning ? `<div class="screen-only lp-warning" style="font-family:sans-serif;font-size:12px;font-weight:600;padding:9px 16px;background:#fef2f2;color:#991b1b;border-bottom:1px solid #fecaca;">${escapeHtml(warning)}</div>` : "";
|
|
532
|
+
const html = `<!DOCTYPE html>
|
|
533
|
+
<html lang="en">
|
|
534
|
+
<head>
|
|
535
|
+
<meta charset="UTF-8">
|
|
536
|
+
<title>${escapeHtml(title)} \u2014 ${escapeHtml(t.stockCode)}</title>
|
|
537
|
+
<style>${sheetStyles(t)}</style>
|
|
538
|
+
</head>
|
|
539
|
+
<body>
|
|
540
|
+
${warningHtml}
|
|
541
|
+
<div class="screen-only" style="font-family:sans-serif;font-size:12px;padding:10px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
|
542
|
+
<button onclick="window.print()" style="padding:7px 18px;background:#111;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">Print / Save PDF</button>
|
|
543
|
+
<button onclick="window.close()" style="padding:7px 14px;background:#f1f5f9;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;cursor:pointer;">Close</button>
|
|
544
|
+
<span style="color:#64748b;font-size:11px;">${escapeHtml(t.stockCode)} \xB7 ${escapeHtml(t.name)} \xB7 set printer to <strong>100% scale (no "fit to page"), no margins, Portrait</strong></span>
|
|
545
|
+
</div>
|
|
546
|
+
${bodyHtml}
|
|
547
|
+
${withFitScript ? LP_FIT_SCRIPT : ""}
|
|
548
|
+
</body>
|
|
549
|
+
</html>`;
|
|
550
|
+
return openPrintWindow(html, title);
|
|
551
|
+
}
|
|
552
|
+
function expandLabels(labels, s) {
|
|
553
|
+
const from = s.rangeFrom > 0 ? s.rangeFrom : 1;
|
|
554
|
+
const to = s.rangeTo > 0 ? Math.min(s.rangeTo, labels.length) : labels.length;
|
|
555
|
+
const subset = labels.slice(from - 1, to);
|
|
556
|
+
if (s.copiesPerCode <= 1) return subset;
|
|
557
|
+
const out = [];
|
|
558
|
+
for (const l of subset)
|
|
559
|
+
for (let i = 0; i < s.copiesPerCode; i++) out.push(l);
|
|
560
|
+
return out;
|
|
561
|
+
}
|
|
562
|
+
function buildCalibrationBody(t) {
|
|
563
|
+
const perPage = t.cols * t.rows;
|
|
564
|
+
const cells = [];
|
|
565
|
+
for (let i = 0; i < perPage; i++) {
|
|
566
|
+
const { x, y } = cellPosition(t, i);
|
|
567
|
+
cells.push(
|
|
568
|
+
`<div class="lp-cell lp-cal-cell" style="left:${x}in;top:${y}in;"><div class="lp-cal-label">${i + 1}</div></div>`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const gridR = t.sheetWIn - t.marginLeftIn;
|
|
572
|
+
const gridB = t.sheetHIn - t.marginTopIn;
|
|
573
|
+
const mark = (x, y, h) => `<div class="lp-crop" style="left:${x}in;top:${y}in;width:${h ? 0.25 : 0.0104}in;height:${h ? 0.0104 : 0.25}in;"></div>`;
|
|
574
|
+
const marks = [
|
|
575
|
+
mark(t.marginLeftIn - 0.25, t.marginTopIn, true),
|
|
576
|
+
mark(t.marginLeftIn, t.marginTopIn - 0.25, false),
|
|
577
|
+
mark(gridR, t.marginTopIn, true),
|
|
578
|
+
mark(gridR - 0.0104, t.marginTopIn - 0.25, false),
|
|
579
|
+
mark(t.marginLeftIn - 0.25, gridB - 0.0104, true),
|
|
580
|
+
mark(t.marginLeftIn, gridB, false),
|
|
581
|
+
mark(gridR, gridB - 0.0104, true),
|
|
582
|
+
mark(gridR - 0.0104, gridB, false)
|
|
583
|
+
].join("");
|
|
584
|
+
const banner = `<div style="position:absolute;left:0;right:0;top:${Math.max(0, t.marginTopIn - 0.3).toFixed(2)}in;text-align:center;font-size:7pt;color:#94a3b8;">Calibration \u2014 ${escapeHtml(t.stockCode)} \xB7 hold against your label sheet: every outline must sit on a label</div>`;
|
|
585
|
+
return `<div class="lp-page">${banner}${marks}${cells.join("\n")}</div>`;
|
|
586
|
+
}
|
|
587
|
+
function printCalibrationSheet(template) {
|
|
588
|
+
return openSheetWindow(
|
|
589
|
+
template,
|
|
590
|
+
buildCalibrationBody(template),
|
|
591
|
+
"Label calibration",
|
|
592
|
+
false
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
var MM_PER_IN = 25.4;
|
|
596
|
+
var MIN_SCANNABLE_MODULE_MM = 0.4;
|
|
597
|
+
function assertScannable(template, payloadByteLen, ecLevel = DEFAULTS.ecLevel) {
|
|
598
|
+
const layout = computeCellLayout(template, true);
|
|
599
|
+
const printedSideMm = layout.qrIn * MM_PER_IN;
|
|
600
|
+
let version;
|
|
601
|
+
try {
|
|
602
|
+
version = minQrVersion(payloadByteLen, ecLevel);
|
|
603
|
+
} catch (err) {
|
|
604
|
+
return {
|
|
605
|
+
ok: false,
|
|
606
|
+
version: 0,
|
|
607
|
+
moduleSizeMm: 0,
|
|
608
|
+
minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,
|
|
609
|
+
message: err instanceof Error ? err.message : String(err)
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const modulesPerSide = qrModuleCount(version) + QUIET_ZONE_MODULES * 2;
|
|
613
|
+
const moduleSizeMm = printedSideMm / modulesPerSide;
|
|
614
|
+
if (moduleSizeMm >= MIN_SCANNABLE_MODULE_MM) {
|
|
615
|
+
return {
|
|
616
|
+
ok: true,
|
|
617
|
+
version,
|
|
618
|
+
moduleSizeMm,
|
|
619
|
+
minModuleSizeMm: MIN_SCANNABLE_MODULE_MM
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
ok: false,
|
|
624
|
+
version,
|
|
625
|
+
moduleSizeMm,
|
|
626
|
+
minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,
|
|
627
|
+
message: `A ${payloadByteLen}-byte payload needs a version ${version} QR, which prints at ${moduleSizeMm.toFixed(3)}mm per module on ${template.stockCode} \u2014 below the ${MIN_SCANNABLE_MODULE_MM}mm floor a scanner can resolve. Shorten the payload (a short resolver URL is the intended fix) or move to larger label stock.`
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function batchScannabilityWarning(t, labels, ecLevel) {
|
|
631
|
+
const encoder = new TextEncoder();
|
|
632
|
+
let worst = -1;
|
|
633
|
+
for (const l of labels) {
|
|
634
|
+
if (!l.qrValue) continue;
|
|
635
|
+
const len = encoder.encode(l.qrValue).length;
|
|
636
|
+
if (len > worst) worst = len;
|
|
637
|
+
}
|
|
638
|
+
if (worst < 0) return void 0;
|
|
639
|
+
const report = assertScannable(t, worst, ecLevel);
|
|
640
|
+
return report.ok ? void 0 : report.message;
|
|
641
|
+
}
|
|
642
|
+
function resolveTemplate(data, variantId) {
|
|
643
|
+
if (variantId && variantId !== "custom") {
|
|
644
|
+
const t = getLabelTemplate(variantId);
|
|
645
|
+
if (t) return t;
|
|
646
|
+
}
|
|
647
|
+
if (data.templateId === "custom" && data.customTemplate)
|
|
648
|
+
return data.customTemplate;
|
|
649
|
+
return getLabelTemplate(data.templateId) ?? data.customTemplate ?? AVERY_5160;
|
|
650
|
+
}
|
|
651
|
+
async function printQrLabelSheet(data, variantId, rawSettings) {
|
|
652
|
+
const t = resolveTemplate(data, variantId);
|
|
653
|
+
const s = resolveSettings(rawSettings);
|
|
654
|
+
if (s.calibrationPage) {
|
|
655
|
+
return printCalibrationSheet(t);
|
|
656
|
+
}
|
|
657
|
+
const labels = expandLabels(data.labels ?? [], s);
|
|
658
|
+
if (!labels.length) {
|
|
659
|
+
return openSheetWindow(
|
|
660
|
+
t,
|
|
661
|
+
`<div class="lp-page"><div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:11pt;">No labels to print.</div></div>`,
|
|
662
|
+
"QR labels",
|
|
663
|
+
false
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const hasBadge = labels.some((l) => (l.badge ?? "").trim() !== "");
|
|
667
|
+
const layout = computeCellLayout(t, s.showCaption, hasBadge);
|
|
668
|
+
const uriByValue = /* @__PURE__ */ new Map();
|
|
669
|
+
for (const l of labels) {
|
|
670
|
+
if (l.qrValue && !uriByValue.has(l.qrValue)) {
|
|
671
|
+
uriByValue.set(
|
|
672
|
+
l.qrValue,
|
|
673
|
+
await generateQrDataUri(l.qrValue, s.ecLevel, layout.qrIn)
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const leadingBlanks = Math.max(0, s.startAtLabel - 1) % (t.cols * t.rows);
|
|
678
|
+
const cells = [
|
|
679
|
+
...Array.from({ length: leadingBlanks }, () => ({ html: "" })),
|
|
680
|
+
...labels.map((l) => ({
|
|
681
|
+
html: buildLabelCellHtml(
|
|
682
|
+
l,
|
|
683
|
+
uriByValue.get(l.qrValue) ?? "",
|
|
684
|
+
t,
|
|
685
|
+
layout,
|
|
686
|
+
s
|
|
687
|
+
)
|
|
688
|
+
}))
|
|
689
|
+
];
|
|
690
|
+
const perPage = t.cols * t.rows;
|
|
691
|
+
const tail = cells.length % perPage;
|
|
692
|
+
if (tail > 0)
|
|
693
|
+
for (let i = 0; i < perPage - tail; i++) cells.push({ html: "" });
|
|
694
|
+
return openSheetWindow(
|
|
695
|
+
t,
|
|
696
|
+
renderPages(t, cells),
|
|
697
|
+
"QR labels",
|
|
698
|
+
true,
|
|
699
|
+
batchScannabilityWarning(t, labels, s.ecLevel)
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
var qrLabelsPrinter = {
|
|
703
|
+
label: "Print QR label sheet",
|
|
704
|
+
variants: LABEL_TEMPLATES.map((t) => ({
|
|
705
|
+
id: t.id,
|
|
706
|
+
label: t.name,
|
|
707
|
+
description: t.stockCode
|
|
708
|
+
})),
|
|
709
|
+
settings: [
|
|
710
|
+
{
|
|
711
|
+
type: "number",
|
|
712
|
+
id: "startAtLabel",
|
|
713
|
+
label: "Start at label #",
|
|
714
|
+
description: "Skip already-used positions on a partial sheet",
|
|
715
|
+
defaultValue: 1,
|
|
716
|
+
min: 1
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
type: "number",
|
|
720
|
+
id: "copiesPerCode",
|
|
721
|
+
label: "Copies per code",
|
|
722
|
+
defaultValue: 1,
|
|
723
|
+
min: 1,
|
|
724
|
+
max: 999
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
type: "boolean",
|
|
728
|
+
id: "showCaption",
|
|
729
|
+
label: "Show caption",
|
|
730
|
+
description: "Print the caption (or the code value) beside each QR",
|
|
731
|
+
defaultValue: true
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
type: "boolean",
|
|
735
|
+
id: "showLines",
|
|
736
|
+
label: "Show detail lines",
|
|
737
|
+
description: "Extra text lines on wide labels",
|
|
738
|
+
defaultValue: true
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
type: "range",
|
|
742
|
+
id: "labelRange",
|
|
743
|
+
label: "Label range",
|
|
744
|
+
description: "Print only labels N to M (blank = all)",
|
|
745
|
+
fromId: "rangeFrom",
|
|
746
|
+
toId: "rangeTo",
|
|
747
|
+
defaultFrom: 0,
|
|
748
|
+
defaultTo: 0
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
type: "select",
|
|
752
|
+
id: "ecLevel",
|
|
753
|
+
label: "Error correction",
|
|
754
|
+
description: "Higher survives more damage; M is the warehouse default",
|
|
755
|
+
options: [
|
|
756
|
+
{ value: "L", label: "L" },
|
|
757
|
+
{ value: "M", label: "M" },
|
|
758
|
+
{ value: "Q", label: "Q" }
|
|
759
|
+
],
|
|
760
|
+
defaultValue: "M"
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
type: "boolean",
|
|
764
|
+
id: "calibrationPage",
|
|
765
|
+
label: "Calibration test page",
|
|
766
|
+
description: "Print outlines + crop marks only to verify alignment",
|
|
767
|
+
defaultValue: false
|
|
768
|
+
}
|
|
769
|
+
],
|
|
770
|
+
print: (data, variantId, settings) => printQrLabelSheet(data, variantId, settings)
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
// src/zpl.ts
|
|
774
|
+
var ZPL_MIN_MAGNIFICATION = 1;
|
|
775
|
+
var ZPL_MAX_MAGNIFICATION = 10;
|
|
776
|
+
var ZPL_QUIET_ZONE_MODULES = 4;
|
|
777
|
+
var DEFAULT_ZPL_DPI = 203;
|
|
778
|
+
var DEFAULT_ZPL_EC_LEVEL = "M";
|
|
779
|
+
var MM_PER_IN2 = 25.4;
|
|
780
|
+
var ZPL_SHEET_STOCK_MESSAGE = "ZPL is for roll stock \u2014 use printQrLabelSheet for sheets";
|
|
781
|
+
function resolveOptions(opts) {
|
|
782
|
+
const dpi = opts?.dpi ?? DEFAULT_ZPL_DPI;
|
|
783
|
+
if (dpi !== 203 && dpi !== 300) {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`ZPL dpi must be 203 (8 dots/mm) or 300 (12 dots/mm), got ${String(dpi)}.`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const copies = opts?.copies ?? 1;
|
|
789
|
+
if (!Number.isInteger(copies) || copies < 1 || copies > 9999) {
|
|
790
|
+
throw new Error(`ZPL copies must be an integer between 1 and 9999, got ${String(copies)}.`);
|
|
791
|
+
}
|
|
792
|
+
const rawDarkness = opts?.darkness;
|
|
793
|
+
let darkness;
|
|
794
|
+
if (rawDarkness !== void 0) {
|
|
795
|
+
if (!Number.isInteger(rawDarkness) || rawDarkness < -30 || rawDarkness > 30) {
|
|
796
|
+
throw new Error(
|
|
797
|
+
`ZPL darkness (^MD) must be an integer between -30 and 30, got ${String(rawDarkness)}.`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
darkness = rawDarkness;
|
|
801
|
+
}
|
|
802
|
+
const ecLevel = opts?.ecLevel ?? DEFAULT_ZPL_EC_LEVEL;
|
|
803
|
+
if (ecLevel !== "L" && ecLevel !== "M" && ecLevel !== "Q" && ecLevel !== "H") {
|
|
804
|
+
throw new Error(
|
|
805
|
+
`ZPL QR error correction must be "L", "M", "Q" or "H", got ${String(ecLevel)}.`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
return { dpi, darkness, copies, ecLevel };
|
|
809
|
+
}
|
|
810
|
+
function assertRollStock(template) {
|
|
811
|
+
if (template.kind !== "roll") {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`${ZPL_SHEET_STOCK_MESSAGE} (template "${template.id}" \u2014 ${template.stockCode} \u2014 is ${template.cols}\xD7${template.rows} sheet stock, which has no ZPL page model).`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
var HEX = "0123456789ABCDEF";
|
|
818
|
+
function zplFieldData(text) {
|
|
819
|
+
if (typeof text !== "string") {
|
|
820
|
+
throw new Error(`ZPL field data must be a string, got ${String(text)}.`);
|
|
821
|
+
}
|
|
822
|
+
if (text === "") return "";
|
|
823
|
+
const bytes = new TextEncoder().encode(text);
|
|
824
|
+
let out = "";
|
|
825
|
+
for (const byte of bytes) {
|
|
826
|
+
const safe = byte >= 32 && byte <= 126 && byte !== 94 && byte !== 126 && byte !== 95;
|
|
827
|
+
if (safe) {
|
|
828
|
+
out += String.fromCharCode(byte);
|
|
829
|
+
} else {
|
|
830
|
+
out += `_${HEX[byte >> 4 & 15] ?? "0"}${HEX[byte & 15] ?? "0"}`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return out;
|
|
834
|
+
}
|
|
835
|
+
function dots(inches, dpi) {
|
|
836
|
+
return Math.round(inches * dpi);
|
|
837
|
+
}
|
|
838
|
+
function clampMagnification(value) {
|
|
839
|
+
if (!Number.isFinite(value)) return ZPL_MIN_MAGNIFICATION;
|
|
840
|
+
return Math.min(ZPL_MAX_MAGNIFICATION, Math.max(ZPL_MIN_MAGNIFICATION, Math.floor(value)));
|
|
841
|
+
}
|
|
842
|
+
function payloadByteLength(value) {
|
|
843
|
+
return new TextEncoder().encode(value).length;
|
|
844
|
+
}
|
|
845
|
+
function magnificationFor(qrBoxDots, payloadBytes, ecLevel) {
|
|
846
|
+
const version = minQrVersion(payloadBytes, ecLevel);
|
|
847
|
+
const modulesPerSide = qrModuleCount(version) + ZPL_QUIET_ZONE_MODULES * 2;
|
|
848
|
+
const magnification = clampMagnification(qrBoxDots / modulesPerSide);
|
|
849
|
+
return { version, magnification, drawnDots: modulesPerSide * magnification };
|
|
850
|
+
}
|
|
851
|
+
function captionZoneIn(t, wide) {
|
|
852
|
+
return wide ? t.labelHIn * 0.3 : Math.min(0.28, t.labelHIn * 0.16);
|
|
853
|
+
}
|
|
854
|
+
function textField(x, y, widthDots, fontHeightDots, justify, text) {
|
|
855
|
+
return `^FO${x},${y}^A0N,${fontHeightDots},0^FB${widthDots},1,0,${justify}^FH^FD${zplFieldData(text)}^FS`;
|
|
856
|
+
}
|
|
857
|
+
function qrField(x, y, magnification, ecLevel, value) {
|
|
858
|
+
return `^FO${x},${y}^BQN,2,${magnification}^FH^FD${ecLevel}A,${zplFieldData(value)}^FS`;
|
|
859
|
+
}
|
|
860
|
+
function cleanLines(lines) {
|
|
861
|
+
if (lines === void 0) return [];
|
|
862
|
+
return lines.map((l) => l.trim()).filter((l) => l !== "");
|
|
863
|
+
}
|
|
864
|
+
function labelToZpl(template, label, opts) {
|
|
865
|
+
assertRollStock(template);
|
|
866
|
+
const o = resolveOptions(opts);
|
|
867
|
+
const captionText = (label.caption ?? label.qrValue).trim();
|
|
868
|
+
const badgeText = (label.badge ?? "").trim();
|
|
869
|
+
const lines = cleanLines(label.lines);
|
|
870
|
+
const hasQr = typeof label.qrValue === "string" && label.qrValue !== "";
|
|
871
|
+
const layout = computeCellLayout(template, captionText !== "", badgeText !== "");
|
|
872
|
+
const labelWDots = dots(template.labelWIn, o.dpi);
|
|
873
|
+
const labelHDots = dots(template.labelHIn, o.dpi);
|
|
874
|
+
const padDots = dots(layout.padIn, o.dpi);
|
|
875
|
+
const qrBoxDots = hasQr ? dots(layout.qrIn, o.dpi) : 0;
|
|
876
|
+
const badgeZoneDots = badgeText !== "" ? dots(layout.badgeZoneIn, o.dpi) : 0;
|
|
877
|
+
const captionZoneDots = captionText !== "" ? dots(captionZoneIn(template, layout.wide), o.dpi) : 0;
|
|
878
|
+
const lineFontDots = Math.max(8, Math.round(o.dpi * 8 / 72));
|
|
879
|
+
const lineStepDots = Math.round(lineFontDots * 1.25);
|
|
880
|
+
const linesHDots = lines.length * lineStepDots;
|
|
881
|
+
const badgeFontDots = Math.max(10, Math.round(badgeZoneDots * 0.8));
|
|
882
|
+
const captionFontDots = Math.max(8, Math.round(captionZoneDots * 0.72));
|
|
883
|
+
const commands = [];
|
|
884
|
+
commands.push("^XA");
|
|
885
|
+
commands.push("^CI28");
|
|
886
|
+
commands.push("^LH0,0");
|
|
887
|
+
commands.push(`^PW${dots(template.labelWIn, o.dpi)}`);
|
|
888
|
+
commands.push(`^LL${dots(template.labelHIn, o.dpi)}`);
|
|
889
|
+
if (o.darkness !== void 0) {
|
|
890
|
+
commands.push(`^MD${o.darkness}`);
|
|
891
|
+
}
|
|
892
|
+
const fit = hasQr ? magnificationFor(qrBoxDots, payloadByteLength(label.qrValue), o.ecLevel) : { version: 0, magnification: 0, drawnDots: 0 };
|
|
893
|
+
const quietDots = fit.magnification * ZPL_QUIET_ZONE_MODULES;
|
|
894
|
+
if (layout.wide) {
|
|
895
|
+
const qrY = Math.max(padDots, Math.round((labelHDots - fit.drawnDots) / 2));
|
|
896
|
+
if (hasQr) {
|
|
897
|
+
commands.push(
|
|
898
|
+
qrField(padDots + quietDots, qrY + quietDots, fit.magnification, o.ecLevel, label.qrValue)
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
const textX = padDots + fit.drawnDots + padDots;
|
|
902
|
+
const textW = Math.max(1, labelWDots - textX - padDots);
|
|
903
|
+
let y = padDots;
|
|
904
|
+
if (badgeText !== "") {
|
|
905
|
+
commands.push(textField(textX, y, textW, badgeFontDots, "L", badgeText));
|
|
906
|
+
y += badgeZoneDots;
|
|
907
|
+
}
|
|
908
|
+
if (captionText !== "") {
|
|
909
|
+
commands.push(textField(textX, y, textW, captionFontDots, "L", captionText));
|
|
910
|
+
y += captionZoneDots;
|
|
911
|
+
}
|
|
912
|
+
for (const line of lines) {
|
|
913
|
+
commands.push(textField(textX, y, textW, lineFontDots, "L", line));
|
|
914
|
+
y += lineStepDots;
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
const contentH = fit.drawnDots + badgeZoneDots + captionZoneDots + linesHDots;
|
|
918
|
+
const textW = Math.max(1, labelWDots - padDots * 2);
|
|
919
|
+
let y = Math.max(padDots, Math.round((labelHDots - contentH) / 2));
|
|
920
|
+
if (hasQr) {
|
|
921
|
+
const qrX = Math.max(0, Math.round((labelWDots - fit.drawnDots) / 2));
|
|
922
|
+
commands.push(
|
|
923
|
+
qrField(qrX + quietDots, y + quietDots, fit.magnification, o.ecLevel, label.qrValue)
|
|
924
|
+
);
|
|
925
|
+
y += fit.drawnDots;
|
|
926
|
+
}
|
|
927
|
+
if (badgeText !== "") {
|
|
928
|
+
commands.push(textField(padDots, y, textW, badgeFontDots, "C", badgeText));
|
|
929
|
+
y += badgeZoneDots;
|
|
930
|
+
}
|
|
931
|
+
if (captionText !== "") {
|
|
932
|
+
commands.push(textField(padDots, y, textW, captionFontDots, "C", captionText));
|
|
933
|
+
y += captionZoneDots;
|
|
934
|
+
}
|
|
935
|
+
for (const line of lines) {
|
|
936
|
+
commands.push(textField(padDots, y, textW, lineFontDots, "C", line));
|
|
937
|
+
y += lineStepDots;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (o.copies > 1) {
|
|
941
|
+
commands.push(`^PQ${o.copies},0,0,N`);
|
|
942
|
+
}
|
|
943
|
+
commands.push("^XZ");
|
|
944
|
+
return commands.join("\n");
|
|
945
|
+
}
|
|
946
|
+
function labelsToZpl(template, labels, opts) {
|
|
947
|
+
assertRollStock(template);
|
|
948
|
+
resolveOptions(opts);
|
|
949
|
+
if (labels.length === 0) return "";
|
|
950
|
+
return labels.map((label) => labelToZpl(template, label, opts)).join("\n");
|
|
951
|
+
}
|
|
952
|
+
function assertZplScannable(template, payloadByteLen, opts) {
|
|
953
|
+
const fail = (message, version2 = 0, moduleSizeMm2 = 0) => ({
|
|
954
|
+
ok: false,
|
|
955
|
+
version: version2,
|
|
956
|
+
moduleSizeMm: moduleSizeMm2,
|
|
957
|
+
minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,
|
|
958
|
+
message
|
|
959
|
+
});
|
|
960
|
+
if (template.kind !== "roll") {
|
|
961
|
+
return fail(
|
|
962
|
+
`${ZPL_SHEET_STOCK_MESSAGE} (template "${template.id}" \u2014 ${template.stockCode} \u2014 is sheet stock).`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
let o;
|
|
966
|
+
try {
|
|
967
|
+
o = resolveOptions(opts);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
970
|
+
}
|
|
971
|
+
const layout = computeCellLayout(template, true);
|
|
972
|
+
const qrBoxDots = dots(layout.qrIn, o.dpi);
|
|
973
|
+
let fit;
|
|
974
|
+
try {
|
|
975
|
+
fit = magnificationFor(qrBoxDots, payloadByteLen, o.ecLevel);
|
|
976
|
+
} catch (err) {
|
|
977
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
978
|
+
}
|
|
979
|
+
const { version, magnification } = fit;
|
|
980
|
+
const moduleSizeMm = magnification * MM_PER_IN2 / o.dpi;
|
|
981
|
+
if (moduleSizeMm >= MIN_SCANNABLE_MODULE_MM) {
|
|
982
|
+
return {
|
|
983
|
+
ok: true,
|
|
984
|
+
version,
|
|
985
|
+
moduleSizeMm,
|
|
986
|
+
minModuleSizeMm: MIN_SCANNABLE_MODULE_MM
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
return fail(
|
|
990
|
+
`A ${payloadByteLen}-byte payload needs a version ${version} QR, which ZPL prints at magnification ${magnification} (${moduleSizeMm.toFixed(3)}mm per module) on ${template.stockCode} at ${o.dpi}dpi \u2014 below the ${MIN_SCANNABLE_MODULE_MM}mm floor a scanner can resolve. Shorten the payload (a short resolver URL is the intended fix), move to larger roll stock, or print on a 300dpi head.`,
|
|
991
|
+
version,
|
|
992
|
+
moduleSizeMm
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
export {
|
|
996
|
+
DEFAULT_ZPL_DPI,
|
|
997
|
+
ZPL_MAX_MAGNIFICATION,
|
|
998
|
+
ZPL_MIN_MAGNIFICATION,
|
|
999
|
+
ZPL_QUIET_ZONE_MODULES,
|
|
1000
|
+
ZPL_SHEET_STOCK_MESSAGE,
|
|
1001
|
+
assertZplScannable,
|
|
1002
|
+
labelToZpl,
|
|
1003
|
+
labelsToZpl,
|
|
1004
|
+
zplFieldData
|
|
1005
|
+
};
|
|
1006
|
+
//# sourceMappingURL=zpl.js.map
|