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