@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.
@@ -0,0 +1,270 @@
1
+ import { PrintSettings, BlockPrinter } from './core.js';
2
+
3
+ /**
4
+ * @ai-matrx/print/labels — the label-sheet engine.
5
+ *
6
+ * ONE module carrying the three matrx-frontend files this lane was extracted
7
+ * from, ported behaviour-verbatim (2026-08-29 state):
8
+ * lib/label-print/label-templates.ts → the template registry
9
+ * lib/label-print/qr-labels-printer.ts → qrLabelsPrinter / printQrLabelSheet
10
+ * lib/label-print/qr-labels-pdf.ts → downloadLabelsPdf
11
+ *
12
+ * Warehouse-grade QR labels on standard 8.5" × 11" label stock (Avery and
13
+ * compatibles) AND on continuous/die-cut roll stock. ONE generic grid renderer
14
+ * driven by the LabelTemplate registry — geometry is data, never per-stock CSS.
15
+ *
16
+ * Fidelity rules (the reasons this exists):
17
+ * - The print window is a fresh, unauthenticated document: every QR image is
18
+ * inlined as a data URI, never fetched.
19
+ * - Spec-compliant quiet zone: the `qrcode` lib renders 4 modules of white
20
+ * margin INSIDE each data URI (its default; we pass it explicitly).
21
+ * - `print-color-adjust: exact` on code cells so the browser never "helps".
22
+ * - Modules are crisp: each QR is rasterized at 300 dpi for its printed size,
23
+ * and `image-rendering: pixelated` stops resampling blur.
24
+ * - Captions auto-shrink to fit their box (FIT_TEXT pattern) — they never
25
+ * overflow onto the neighboring label.
26
+ * - `startAtLabel` inserts LEADING blank cells so a partially-used sheet can
27
+ * be reused; trailing cells pad out the last page.
28
+ *
29
+ * THE IMAGE RULE — the one deliberate exception to "data URIs only": a label's
30
+ * optional `imageUrl` is a DURABLE http(s) URL and is emitted as a plain
31
+ * `<img src>`. Anything that is not http(s) is SKIPPED rather than rendered.
32
+ * The QR itself is never a URL; it is always an inlined data URI.
33
+ *
34
+ * SEAM INVERSIONS versus the matrx-frontend originals (the only changes to the
35
+ * ported behaviour):
36
+ * 1. `@/lib/block-print/block-print-utils` → `./core` (identical export names).
37
+ * 2. The deferred `import("qrcode")` + `QRCode.toDataURL(...)` call is now
38
+ * `generateQrDataUrl` from `./qr`, which performs the same lazy import at
39
+ * call time and maps 1:1 — errorCorrectionLevel → ecLevel, margin →
40
+ * quietZoneModules, width → widthPx. The original also passed
41
+ * `color: { dark: "#000000", light: "#ffffff" }`, which is byte-for-byte the
42
+ * `qrcode` default, so dropping it changes no output.
43
+ * 3. LP_FIT_SCRIPT is KEPT VERBATIM rather than replaced by core's
44
+ * `buildFitTextScript`. The two scripts are NOT behaviourally identical for
45
+ * label cells: core's steps and writes font sizes in **pt** (and pre-assigns
46
+ * `size + "pt"` from a px-derived number before measuring) with a **6pt**
47
+ * floor, while the label script steps and writes **px** with a **4px** floor
48
+ * and no pre-assignment. On a 9pt caption inside a 0.28in box those two
49
+ * produce different final sizes, so swapping them would regress captions on
50
+ * the small Avery stocks.
51
+ *
52
+ * Docs: lib/label-print/FEATURE.md (matrx-frontend), NOTES-CHANGELOG.md.
53
+ */
54
+
55
+ /**
56
+ * Label-sheet template registry — the geometry source of truth for QR label
57
+ * printing on standard 8.5" × 11" label stock (Avery and compatibles) and on
58
+ * roll stock.
59
+ *
60
+ * ONE generic grid renderer is driven entirely by these numbers — never a
61
+ * hand-written CSS block per stock. All dimensions in inches, taken from
62
+ * Avery's published templates:
63
+ * - horizontal pitch = labelWIn + gutterXIn; vertical pitch = labelHIn + gutterYIn
64
+ * - a template is valid when marginLeft*2 + cols*labelW + (cols-1)*gutterX = sheetW
65
+ * (same vertically) — `assertTemplateGeometry` checks this in dev.
66
+ */
67
+ interface LabelTemplate {
68
+ id: string;
69
+ name: string;
70
+ /** Stock code as printed on the box (e.g. "Avery 5160"). */
71
+ stockCode: string;
72
+ sheetWIn: number;
73
+ sheetHIn: number;
74
+ marginTopIn: number;
75
+ marginLeftIn: number;
76
+ labelWIn: number;
77
+ labelHIn: number;
78
+ cols: number;
79
+ rows: number;
80
+ /** Horizontal gap BETWEEN columns (not the outer margin). */
81
+ gutterXIn: number;
82
+ /** Vertical gap BETWEEN rows. */
83
+ gutterYIn: number;
84
+ /** Corner radius; equal to labelWIn/2 for round labels. */
85
+ cornerRadiusIn?: number;
86
+ /** True round label (die-cut circle) — preview/calibration draw a circle. */
87
+ round?: boolean;
88
+ /**
89
+ * Stock family. Absent means "sheet" — a multi-up page of labels. "roll" is
90
+ * a continuous/die-cut roll where the sheet IS the label: margins 0, a 1×1
91
+ * grid, and `@page` sized to the single label.
92
+ */
93
+ kind?: "sheet" | "roll";
94
+ }
95
+ /**
96
+ * Seeded templates. Sheet geometry verified against Avery's published template
97
+ * dimensions (avery.com template pages for each SKU):
98
+ *
99
+ * 5160 — see AVERY_5160 above.
100
+ * 5163 — 2" × 4" shipping, 10-up (2×5). Top/bottom 0.5", side 0.15625",
101
+ * 0.1875" column gutter, zero row gutter.
102
+ * 5164 — 3⅓" × 4" shipping, 6-up (2×3). Same horizontal grid as 5163.
103
+ * 22806 — 2" × 2" rounded square, 12-up (3×4). Margins 0.5" sides /
104
+ * 0.75" top, 0.75" column gutter, 0.5" row gutter — i.e. Avery's
105
+ * published 2.75" horizontal pitch and 2.5" vertical pitch. (The
106
+ * matrx-frontend original had 0.375"/0.375" here and tiled only 7.5"
107
+ * of the 8.5" sheet; see NOTES-CHANGELOG.md.)
108
+ * 22807 — 2" round, 12-up (3×4). Same grid as 22806, circular die-cut.
109
+ *
110
+ * Roll stock (`kind: "roll"`) — the sheet IS the label, so every roll row is a
111
+ * 1×1 grid with zero margins and zero gutters, and its die-cut size is given
112
+ * as width × height in the feed orientation:
113
+ *
114
+ * DK-1202 — Brother die-cut shipping, 62mm × 100mm = 2.4" × 3.9".
115
+ * DK-11201 — Brother die-cut address, 29mm × 90mm = 1.14" × 3.5".
116
+ * 30252 — DYMO address, 1⅛" × 3½".
117
+ * 4x6 — the generic 4" × 6" thermal shipping label.
118
+ */
119
+ declare const LABEL_TEMPLATES: readonly LabelTemplate[];
120
+ declare function getLabelTemplate(id: string): LabelTemplate | undefined;
121
+ /**
122
+ * Build a `custom` template for stock not in the registry. Callers supply the
123
+ * full geometry; id is fixed to "custom".
124
+ *
125
+ * The geometry spread comes FIRST so an explicitly-present `name: undefined` /
126
+ * `stockCode: undefined` cannot overwrite the resolved default (see
127
+ * NOTES-CHANGELOG.md).
128
+ */
129
+ declare function customLabelTemplate(geometry: Omit<LabelTemplate, "id" | "name" | "stockCode"> & Partial<Pick<LabelTemplate, "name" | "stockCode">>): LabelTemplate;
130
+ /** Dev-time sanity check: the grid must tile the sheet exactly (±1/64"). */
131
+ declare function assertTemplateGeometry(t: LabelTemplate): void;
132
+ interface QrLabel {
133
+ /** The value encoded in the QR code (SKU, URL, id…). */
134
+ qrValue: string;
135
+ /** Bold headline under/next to the code. Defaults to qrValue when captions are on. */
136
+ caption?: string;
137
+ /** Additional small text lines (wide labels only — there is no room on tiny stock). */
138
+ lines?: string[];
139
+ /**
140
+ * A single short, LARGE, bold token (a garment size, a bin letter, a grade).
141
+ * Rendered in the caption zone ABOVE the caption at ~2× the caption size and
142
+ * auto-shrunk by the fit script.
143
+ */
144
+ badge?: string;
145
+ /**
146
+ * Durable http(s) image URL rendered in the lines zone with
147
+ * `object-fit: contain`. Anything that is not http(s) is skipped — THE IMAGE
148
+ * RULE. Ignored by the PDF lane, which cannot fetch a remote image.
149
+ */
150
+ imageUrl?: string;
151
+ }
152
+ interface QrLabelPrintData {
153
+ labels: QrLabel[];
154
+ /** Default template; the dialog's variant picker overrides it. */
155
+ templateId: string;
156
+ /** Optional custom-stock geometry used when templateId === "custom". */
157
+ customTemplate?: LabelTemplate;
158
+ }
159
+ type QrEcLevel = "L" | "M" | "Q";
160
+ interface QrLabelSettings {
161
+ startAtLabel: number;
162
+ copiesPerCode: number;
163
+ showCaption: boolean;
164
+ showLines: boolean;
165
+ rangeFrom: number;
166
+ rangeTo: number;
167
+ ecLevel: QrEcLevel;
168
+ calibrationPage: boolean;
169
+ }
170
+ declare function generateQrDataUri(value: string, ecLevel: QrEcLevel, sizeInches: number): Promise<string>;
171
+ interface CellLayout {
172
+ /** true = QR left, text right; false = QR on top, caption below. */
173
+ wide: boolean;
174
+ qrIn: number;
175
+ padIn: number;
176
+ /** Height reserved above the caption for a `badge`; 0 when there is none. */
177
+ badgeZoneIn: number;
178
+ }
179
+ declare function computeCellLayout(t: LabelTemplate, showCaption: boolean, hasBadge?: boolean): CellLayout;
180
+ declare function expandLabels(labels: QrLabel[], s: QrLabelSettings): QrLabel[];
181
+ /** Print the grid outline + crop marks only — verify alignment before burning labels. */
182
+ declare function printCalibrationSheet(template: LabelTemplate): void;
183
+ /**
184
+ * The printed-module floor a 300 dpi thermal printer and a warehouse scanner
185
+ * can both resolve. Below this a symbol prints, looks fine on screen, and
186
+ * refuses to scan on the dock.
187
+ */
188
+ declare const MIN_SCANNABLE_MODULE_MM = 0.4;
189
+ interface ScannabilityReport {
190
+ ok: boolean;
191
+ version: number;
192
+ moduleSizeMm: number;
193
+ minModuleSizeMm: number;
194
+ message?: string;
195
+ }
196
+ /**
197
+ * Will a payload of `payloadByteLen` bytes actually scan off this stock?
198
+ *
199
+ * The printed side comes from the SAME layout brain the printer uses
200
+ * (`computeCellLayout`), evaluated with captions ON — the smaller, and
201
+ * therefore conservative, QR box. Module size divides that side by the symbol's
202
+ * modules PLUS the two 4-module quiet zones the data URI carries inside it.
203
+ *
204
+ * Never throws: an over-capacity payload comes back as `ok: false` with
205
+ * `version: 0` and the capacity error as the message.
206
+ */
207
+ declare function assertScannable(template: LabelTemplate, payloadByteLen: number, ecLevel?: QrEcLevel): ScannabilityReport;
208
+ /** Core render path — also used by the demo page's direct print entry point. */
209
+ declare function printQrLabelSheet(data: QrLabelPrintData, variantId?: string, rawSettings?: PrintSettings): Promise<void>;
210
+ declare const qrLabelsPrinter: BlockPrinter;
211
+ /**
212
+ * Generate a PDF of QR labels for the given template and hand it to the
213
+ * browser as a download. Same data seam as the printer: `{ labels, templateId }`.
214
+ */
215
+ declare function downloadLabelsPdf(data: QrLabelPrintData, templateId?: string, rawSettings?: PrintSettings, filename?: string): Promise<void>;
216
+ /** Where an element's text comes from: a row column, or a fixed string. */
217
+ type LabelField = {
218
+ field: string;
219
+ } | {
220
+ literal: string;
221
+ };
222
+ type LabelElement = {
223
+ kind: "qr";
224
+ source: LabelField;
225
+ } | {
226
+ kind: "text";
227
+ source: LabelField;
228
+ role: "title" | "detail" | "badge";
229
+ /**
230
+ * Cap on how many lines the value may occupy. The value is split on
231
+ * newlines; beyond `maxLines` the extra lines are dropped and the last
232
+ * kept line gains an ellipsis.
233
+ */
234
+ maxLines?: number;
235
+ }
236
+ /** Durable http(s) URL only — THE IMAGE RULE. A non-http value is SKIPPED. */
237
+ | {
238
+ kind: "image";
239
+ source: LabelField;
240
+ };
241
+ interface LabelFormat {
242
+ id: string;
243
+ name: string;
244
+ elements: LabelElement[];
245
+ }
246
+ /**
247
+ * Map rows through a format into the printer's label shape.
248
+ *
249
+ * - the qr element's resolved value → `qrValue` (first one wins)
250
+ * - the FIRST title-role text → `caption`
251
+ * - the FIRST badge-role text → `badge`
252
+ * - detail-role texts, in element order → `lines[]`
253
+ * - the first http(s) image element → `imageUrl`
254
+ *
255
+ * A row missing a `field` resolves to "" and that element is skipped.
256
+ */
257
+ declare function applyLabelFormat(format: LabelFormat, rows: Record<string, string>[]): {
258
+ labels: QrLabelPrintData["labels"];
259
+ };
260
+ /**
261
+ * The shipped formats. Every preset reads the QR payload from the `qrValue`
262
+ * column, so a row is always `{ qrValue, …format-specific columns }`.
263
+ *
264
+ * NOTE on `asset-spec`: the cell renderer auto-shrinks only the caption/badge
265
+ * (`.lp-fit`); detail lines are fixed 8pt, so the title never loses size to a
266
+ * long spec list.
267
+ */
268
+ declare const LABEL_FORMAT_PRESETS: readonly LabelFormat[];
269
+
270
+ export { LABEL_FORMAT_PRESETS, LABEL_TEMPLATES, type LabelElement, type LabelField, type LabelFormat, type LabelTemplate, MIN_SCANNABLE_MODULE_MM, type QrEcLevel, type QrLabel, type QrLabelPrintData, type QrLabelSettings, type ScannabilityReport, applyLabelFormat, assertScannable, assertTemplateGeometry, computeCellLayout, customLabelTemplate, downloadLabelsPdf, expandLabels, generateQrDataUri, getLabelTemplate, printCalibrationSheet, printQrLabelSheet, qrLabelsPrinter };