@ai-matrx/print 0.1.0 → 0.2.1

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/barcode.cjs +182 -0
  3. package/dist/barcode.cjs.map +1 -0
  4. package/dist/barcode.d.cts +94 -0
  5. package/dist/barcode.d.ts +94 -0
  6. package/dist/barcode.js +151 -0
  7. package/dist/barcode.js.map +1 -0
  8. package/dist/booklet.cjs +203 -0
  9. package/dist/booklet.cjs.map +1 -0
  10. package/dist/booklet.d.cts +93 -0
  11. package/dist/booklet.d.ts +93 -0
  12. package/dist/booklet.js +180 -0
  13. package/dist/booklet.js.map +1 -0
  14. package/dist/core.cjs +6 -5
  15. package/dist/core.cjs.map +1 -1
  16. package/dist/core.d.cts +25 -4
  17. package/dist/core.d.ts +25 -4
  18. package/dist/core.js +6 -5
  19. package/dist/core.js.map +1 -1
  20. package/dist/flashcards.cjs +18 -65
  21. package/dist/flashcards.cjs.map +1 -1
  22. package/dist/flashcards.js +18 -65
  23. package/dist/flashcards.js.map +1 -1
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +4 -1
  26. package/dist/index.d.ts +4 -1
  27. package/dist/labels.cjs +10 -11
  28. package/dist/labels.cjs.map +1 -1
  29. package/dist/labels.d.cts +3 -3
  30. package/dist/labels.d.ts +3 -3
  31. package/dist/labels.js +10 -11
  32. package/dist/labels.js.map +1 -1
  33. package/dist/qr-styled.cjs +219 -0
  34. package/dist/qr-styled.cjs.map +1 -0
  35. package/dist/qr-styled.d.cts +150 -0
  36. package/dist/qr-styled.d.ts +150 -0
  37. package/dist/qr-styled.js +188 -0
  38. package/dist/qr-styled.js.map +1 -0
  39. package/dist/react.cjs +23 -18
  40. package/dist/react.cjs.map +1 -1
  41. package/dist/react.d.cts +26 -3
  42. package/dist/react.d.ts +26 -3
  43. package/dist/react.js +23 -18
  44. package/dist/react.js.map +1 -1
  45. package/package.json +34 -2
@@ -0,0 +1,188 @@
1
+ // src/qr-styled.ts
2
+ var STYLED_QR_MIN_EC_LEVEL = "Q";
3
+ var DEFAULT_STYLED_QR_EC_LEVEL = "H";
4
+ var DEFAULT_STYLED_QR_WIDTH_PX = 512;
5
+ var DEFAULT_STYLED_QR_LOGO_RATIO = 0.2;
6
+ var MAX_STYLED_QR_LOGO_RATIO = 0.3;
7
+ var STYLED_QR_QUIET_ZONE_RATIO = 0.08;
8
+ var DEFAULT_STYLED_QR_COLOR = "#000000";
9
+ var DEFAULT_STYLED_QR_BACKGROUND = "#ffffff";
10
+ function assertValue(value) {
11
+ if (typeof value !== "string" || value.length === 0) {
12
+ throw new Error("Styled QR value must be a non-empty string.");
13
+ }
14
+ return value;
15
+ }
16
+ function resolveStyledEcLevel(ecLevel) {
17
+ if (ecLevel === void 0) return DEFAULT_STYLED_QR_EC_LEVEL;
18
+ if (ecLevel === "Q" || ecLevel === "H") return ecLevel;
19
+ return STYLED_QR_MIN_EC_LEVEL;
20
+ }
21
+ function resolveWidthPx(widthPx) {
22
+ if (widthPx === void 0) return DEFAULT_STYLED_QR_WIDTH_PX;
23
+ if (typeof widthPx !== "number" || !Number.isFinite(widthPx) || widthPx <= 0) {
24
+ throw new Error(`Styled QR widthPx must be a positive number, got ${String(widthPx)}.`);
25
+ }
26
+ return Math.round(widthPx);
27
+ }
28
+ function resolveLogoSizeRatio(logoSizeRatio) {
29
+ if (typeof logoSizeRatio !== "number" || !Number.isFinite(logoSizeRatio) || logoSizeRatio <= 0) {
30
+ return DEFAULT_STYLED_QR_LOGO_RATIO;
31
+ }
32
+ return Math.min(MAX_STYLED_QR_LOGO_RATIO, logoSizeRatio);
33
+ }
34
+ function assertLogoDataUrl(logoDataUrl) {
35
+ if (typeof logoDataUrl !== "string" || logoDataUrl.trim() === "") {
36
+ throw new Error("Styled QR logoDataUrl must be a non-empty string when provided.");
37
+ }
38
+ const trimmed = logoDataUrl.trim();
39
+ if (!/^data:/i.test(trimmed)) {
40
+ throw new Error(
41
+ `Styled QR logo must be a data: URL \u2014 got "${trimmed.slice(0, 48)}". DATA URL ONLY: the print window is unauthenticated so a remote image may not load at all, and a cross-origin image taints the canvas, which makes the PNG export throw. Inline the logo as a data: URL (e.g. data:image/png;base64,...) before passing it.`
42
+ );
43
+ }
44
+ return trimmed;
45
+ }
46
+ var HEX_COLOR = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
47
+ function assertHexColor(color, field) {
48
+ if (typeof color !== "string" || !HEX_COLOR.test(color.trim())) {
49
+ throw new Error(
50
+ `Styled QR ${field} must be a hex colour like "#000000" or "#fff", got "${String(color)}". Named colours, rgb() and hsl() are not accepted.`
51
+ );
52
+ }
53
+ return color.trim();
54
+ }
55
+ function buildStyledQrLibOptions(value, drawType, opts) {
56
+ assertValue(value);
57
+ const widthPx = resolveWidthPx(opts?.widthPx);
58
+ const color = opts?.color === void 0 ? DEFAULT_STYLED_QR_COLOR : assertHexColor(opts.color, "color");
59
+ const backgroundColor = opts?.backgroundColor === void 0 ? DEFAULT_STYLED_QR_BACKGROUND : assertHexColor(opts.backgroundColor, "backgroundColor");
60
+ const built = {
61
+ type: drawType,
62
+ width: widthPx,
63
+ height: widthPx,
64
+ margin: Math.round(widthPx * STYLED_QR_QUIET_ZONE_RATIO),
65
+ data: value,
66
+ qrOptions: { errorCorrectionLevel: resolveStyledEcLevel(opts?.ecLevel) },
67
+ dotsOptions: { color },
68
+ backgroundOptions: { color: backgroundColor }
69
+ };
70
+ if (opts?.dotStyle !== void 0) built.dotsOptions.type = opts.dotStyle;
71
+ if (opts?.cornerStyle !== void 0) built.cornersSquareOptions = { type: opts.cornerStyle, color };
72
+ if (opts?.logoDataUrl !== void 0) {
73
+ built.image = assertLogoDataUrl(opts.logoDataUrl);
74
+ built.imageOptions = {
75
+ imageSize: resolveLogoSizeRatio(opts.logoSizeRatio),
76
+ // Clearing the modules under the logo is what keeps the mark legible;
77
+ // the forced Q/H error correction is what pays for the hole.
78
+ hideBackgroundDots: true,
79
+ margin: 0,
80
+ // Belt and braces: the data: URL cannot taint the canvas anyway.
81
+ crossOrigin: "anonymous"
82
+ };
83
+ }
84
+ return built;
85
+ }
86
+ var BROWSER_REQUIRED = "styled QR generation requires a browser environment \u2014 use @ai-matrx/print/qr for server-side codes";
87
+ function assertBrowser() {
88
+ if (typeof document === "undefined" || typeof window === "undefined") {
89
+ throw new Error(BROWSER_REQUIRED);
90
+ }
91
+ }
92
+ async function loadQrCodeStyling() {
93
+ const mod = await import("qr-code-styling");
94
+ return mod.default;
95
+ }
96
+ function isBlobLike(raw) {
97
+ if (typeof raw !== "object" || raw === null) return false;
98
+ const candidate = raw;
99
+ return typeof candidate.slice === "function" && typeof candidate.size === "number";
100
+ }
101
+ function readBlobWith(blob, read) {
102
+ if (typeof FileReader === "undefined") {
103
+ throw new Error(BROWSER_REQUIRED);
104
+ }
105
+ return new Promise((resolve, reject) => {
106
+ const reader = new FileReader();
107
+ reader.onload = () => resolve(reader.result);
108
+ reader.onerror = () => reject(reader.error ?? new Error("Styled QR blob could not be read."));
109
+ read(reader);
110
+ });
111
+ }
112
+ async function blobToBytes(blob) {
113
+ if (typeof blob.arrayBuffer === "function") {
114
+ return new Uint8Array(await blob.arrayBuffer());
115
+ }
116
+ const result = await readBlobWith(blob, (reader) => reader.readAsArrayBuffer(blob));
117
+ if (!(result instanceof ArrayBuffer)) {
118
+ throw new Error("Styled QR blob could not be read as bytes.");
119
+ }
120
+ return new Uint8Array(result);
121
+ }
122
+ async function blobToText(blob) {
123
+ if (typeof blob.text === "function") {
124
+ return blob.text();
125
+ }
126
+ const result = await readBlobWith(blob, (reader) => reader.readAsText(blob));
127
+ if (typeof result !== "string") {
128
+ throw new Error("Styled QR blob could not be read as text.");
129
+ }
130
+ return result;
131
+ }
132
+ function bytesToBase64(bytes) {
133
+ let binary = "";
134
+ const CHUNK = 32768;
135
+ for (let i = 0; i < bytes.length; i += CHUNK) {
136
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
137
+ }
138
+ if (typeof btoa !== "function") {
139
+ throw new Error(BROWSER_REQUIRED);
140
+ }
141
+ return btoa(binary);
142
+ }
143
+ async function rawToUint8Array(raw, extension) {
144
+ if (raw === null || raw === void 0) {
145
+ throw new Error(`Styled QR ${extension} generation produced no data.`);
146
+ }
147
+ if (isBlobLike(raw)) {
148
+ return blobToBytes(raw);
149
+ }
150
+ if (raw instanceof Uint8Array) return raw;
151
+ if (raw instanceof ArrayBuffer) return new Uint8Array(raw);
152
+ throw new Error(`Styled QR ${extension} generation returned an unexpected payload type.`);
153
+ }
154
+ async function generateStyledQrDataUrl(value, opts) {
155
+ const options = buildStyledQrLibOptions(value, "canvas", opts);
156
+ assertBrowser();
157
+ const QRCodeStyling = await loadQrCodeStyling();
158
+ const raw = await new QRCodeStyling(options).getRawData("png");
159
+ const bytes = await rawToUint8Array(raw, "PNG");
160
+ return `data:image/png;base64,${bytesToBase64(bytes)}`;
161
+ }
162
+ async function generateStyledQrSvg(value, opts) {
163
+ const options = buildStyledQrLibOptions(value, "svg", opts);
164
+ assertBrowser();
165
+ const QRCodeStyling = await loadQrCodeStyling();
166
+ const raw = await new QRCodeStyling(options).getRawData("svg");
167
+ if (isBlobLike(raw)) {
168
+ return blobToText(raw);
169
+ }
170
+ const bytes = await rawToUint8Array(raw, "SVG");
171
+ return new TextDecoder().decode(bytes);
172
+ }
173
+ export {
174
+ DEFAULT_STYLED_QR_EC_LEVEL,
175
+ DEFAULT_STYLED_QR_LOGO_RATIO,
176
+ DEFAULT_STYLED_QR_WIDTH_PX,
177
+ MAX_STYLED_QR_LOGO_RATIO,
178
+ STYLED_QR_MIN_EC_LEVEL,
179
+ STYLED_QR_QUIET_ZONE_RATIO,
180
+ assertHexColor,
181
+ assertLogoDataUrl,
182
+ buildStyledQrLibOptions,
183
+ generateStyledQrDataUrl,
184
+ generateStyledQrSvg,
185
+ resolveLogoSizeRatio,
186
+ resolveStyledEcLevel
187
+ };
188
+ //# sourceMappingURL=qr-styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/qr-styled.ts"],"sourcesContent":["/**\n * @ai-matrx/print/qr-styled — BRANDED / STYLED QR GENERATION.\n *\n * This is the clothing-tag lane. `@ai-matrx/print/qr` is the warehouse lane: it\n * draws a plain, maximally-boring symbol anywhere JavaScript runs (Node\n * included) and is what every label, carton and dock-door code should use. This\n * module exists for the one job that one cannot do — putting a brand mark in the\n * middle of the code and colouring the modules — and it pays for that with two\n * real restrictions that are NOT style choices:\n *\n * 1. **High error correction is FORCED.** A centre logo OCCLUDES MODULES. The\n * symbol only survives because Reed–Solomon recovery covers the hole, so a\n * styled code is restricted to \"Q\" (25%) or \"H\" (30%), and defaults to \"H\".\n * A caller passing \"L\" or \"M\" is **CLAMPED UP to \"Q\"** — never thrown at,\n * mirroring the quiet-zone clamp in `qr.ts`: we would rather print a\n * slightly denser symbol than one that stops scanning the moment the logo\n * lands on it.\n * 2. **The logo is a DATA URL, or it is refused.** Print windows are\n * unauthenticated `about:blank` documents — a remote `https://` logo would\n * both fail to load behind any cookie/auth wall and TAINT THE CANVAS, at\n * which point `toDataURL()` throws a SecurityError and the whole sheet dies.\n * Non-`data:` URLs are rejected up front with an error naming the rule\n * rather than failing mysteriously at draw time.\n *\n * BROWSER-ONLY AT CALL TIME. `qr-code-styling` draws through DOM + canvas/SVG,\n * so both functions throw a clear Error in a non-DOM environment and point at\n * `@ai-matrx/print/qr` for the server-side lane. The MODULE ITSELF STAYS INERT\n * everywhere: `qr-code-styling` is only ever pulled in via `await import(...)`\n * inside a function body, so importing this file from a React Server Component\n * (or the tarball canary, which imports it in bare Node) costs nothing and\n * throws nothing. No module-level mutable state.\n */\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\n/** The only two error correction levels a styled (logo-bearing) code may use. */\nexport type StyledQrEcLevel = \"Q\" | \"H\";\n\n/** Module shapes. These are passed straight through to `dotsOptions.type`. */\nexport type StyledQrDotStyle = \"square\" | \"dots\" | \"rounded\" | \"classy\" | \"extra-rounded\";\n\n/** Finder-pattern (corner square) shapes — straight through to `cornersSquareOptions.type`. */\nexport type StyledQrCornerStyle = \"square\" | \"dot\" | \"extra-rounded\";\n\nexport interface StyledQrOptions {\n /** Error correction. Default \"H\". \"L\"/\"M\" are CLAMPED UP to \"Q\" — a logo eats modules. */\n ecLevel?: StyledQrEcLevel;\n /** Rendered width (and height) in pixels. Default 512. */\n widthPx?: number;\n /** Centre image. **DATA URL ONLY** — a remote URL is rejected (unauthenticated print window + canvas tainting). */\n logoDataUrl?: string;\n /** Logo side as a fraction of the symbol. Default 0.2, CLAMPED to <= 0.3 for scannability. */\n logoSizeRatio?: number;\n /** Module shape. Default \"square\". */\n dotStyle?: StyledQrDotStyle;\n /** Module colour. Hex only (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`). Default \"#000000\". */\n color?: string;\n /** Background colour. Same hex rule. Default \"#ffffff\". */\n backgroundColor?: string;\n /** Finder-pattern shape. Defaults to following `dotStyle`'s library default (omitted). */\n cornerStyle?: StyledQrCornerStyle;\n}\n\n/** Styled codes never drop below this. */\nexport const STYLED_QR_MIN_EC_LEVEL: StyledQrEcLevel = \"Q\";\n\n/** The default for every styled code — maximum recovery, because the logo is always eating modules. */\nexport const DEFAULT_STYLED_QR_EC_LEVEL: StyledQrEcLevel = \"H\";\n\n/** Default rendered width in pixels. */\nexport const DEFAULT_STYLED_QR_WIDTH_PX = 512;\n\n/** Default logo side as a fraction of the symbol. */\nexport const DEFAULT_STYLED_QR_LOGO_RATIO = 0.2;\n\n/**\n * Hard ceiling on the logo. Past roughly 30% of the symbol side even \"H\" (30%\n * recovery) cannot reconstruct what the image covered, so a bigger request is\n * clamped rather than honoured.\n */\nexport const MAX_STYLED_QR_LOGO_RATIO = 0.3;\n\n/**\n * Quiet zone, as a fraction of the rendered width.\n *\n * `qr-code-styling`'s `margin` is measured in PIXELS, not modules, and the\n * module count is not known until the symbol is encoded — so an exact 4-module\n * quiet zone cannot be requested up front. 8% of the width is a deliberately\n * generous approximation: for a typical version-4-to-8 symbol (33–49 modules\n * plus quiet zone) it lands at or above the ISO/IEC 18004 §6.3.8 four-module\n * minimum. Erring large costs a slightly smaller symbol inside the same box;\n * erring small costs a code that fails at the scanner.\n */\nexport const STYLED_QR_QUIET_ZONE_RATIO = 0.08;\n\nconst DEFAULT_STYLED_QR_COLOR = \"#000000\";\nconst DEFAULT_STYLED_QR_BACKGROUND = \"#ffffff\";\n\n// ─── Validation / clamps ─────────────────────────────────────────────────────\n\nfunction assertValue(value: string): string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new Error(\"Styled QR value must be a non-empty string.\");\n }\n return value;\n}\n\n/**\n * The EC clamp. Unset → \"H\". \"Q\"/\"H\" pass. ANYTHING else (including the \"L\"/\"M\"\n * a loosely-typed JS caller can still hand us) is raised to \"Q\" and never\n * throws — same posture as the quiet-zone clamp in `qr.ts`.\n */\nexport function resolveStyledEcLevel(ecLevel: StyledQrEcLevel | undefined): StyledQrEcLevel {\n if (ecLevel === undefined) return DEFAULT_STYLED_QR_EC_LEVEL;\n if (ecLevel === \"Q\" || ecLevel === \"H\") return ecLevel;\n return STYLED_QR_MIN_EC_LEVEL;\n}\n\nfunction resolveWidthPx(widthPx: number | undefined): number {\n if (widthPx === undefined) return DEFAULT_STYLED_QR_WIDTH_PX;\n if (typeof widthPx !== \"number\" || !Number.isFinite(widthPx) || widthPx <= 0) {\n throw new Error(`Styled QR widthPx must be a positive number, got ${String(widthPx)}.`);\n }\n return Math.round(widthPx);\n}\n\n/** Default 0.2; clamped to (0, 0.3]. A non-numeric/non-positive value falls back to the default. */\nexport function resolveLogoSizeRatio(logoSizeRatio: number | undefined): number {\n if (typeof logoSizeRatio !== \"number\" || !Number.isFinite(logoSizeRatio) || logoSizeRatio <= 0) {\n return DEFAULT_STYLED_QR_LOGO_RATIO;\n }\n return Math.min(MAX_STYLED_QR_LOGO_RATIO, logoSizeRatio);\n}\n\n/**\n * THE LOGO RULE: a `data:` URL, or nothing.\n *\n * Print windows are unauthenticated and canvas export refuses to serialize a\n * canvas a cross-origin image has touched, so a remote logo is not \"worse\", it\n * is broken. Rejected loudly here rather than silently at draw time.\n */\nexport function assertLogoDataUrl(logoDataUrl: string): string {\n if (typeof logoDataUrl !== \"string\" || logoDataUrl.trim() === \"\") {\n throw new Error(\"Styled QR logoDataUrl must be a non-empty string when provided.\");\n }\n const trimmed = logoDataUrl.trim();\n if (!/^data:/i.test(trimmed)) {\n throw new Error(\n `Styled QR logo must be a data: URL — got \"${trimmed.slice(0, 48)}\". ` +\n \"DATA URL ONLY: the print window is unauthenticated so a remote image may not load at all, \" +\n \"and a cross-origin image taints the canvas, which makes the PNG export throw. \" +\n \"Inline the logo as a data: URL (e.g. data:image/png;base64,...) before passing it.\",\n );\n }\n return trimmed;\n}\n\nconst HEX_COLOR = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;\n\n/** Hex only — `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`. Named/rgb()/hsl() colours are refused. */\nexport function assertHexColor(color: string, field: string): string {\n if (typeof color !== \"string\" || !HEX_COLOR.test(color.trim())) {\n throw new Error(\n `Styled QR ${field} must be a hex colour like \"#000000\" or \"#fff\", got \"${String(color)}\". ` +\n \"Named colours, rgb() and hsl() are not accepted.\",\n );\n }\n return color.trim();\n}\n\n// ─── Option assembly ─────────────────────────────────────────────────────────\n\n/**\n * The shape handed to `new QRCodeStyling(...)`. Declared structurally rather\n * than imported from `qr-code-styling`, so this module's own types never depend\n * on that package's (which pulls `jsdom`/`canvas` types in through its .d.ts).\n *\n * `dotStyle` and `cornerStyle` need NO remapping: every value in our contract is\n * spelled identically in the library's `DotType` / `CornerSquareType` unions.\n */\nexport interface StyledQrLibOptions {\n type: \"canvas\" | \"svg\";\n width: number;\n height: number;\n margin: number;\n data: string;\n image?: string;\n qrOptions: { errorCorrectionLevel: StyledQrEcLevel };\n imageOptions?: { imageSize: number; hideBackgroundDots: boolean; margin: number; crossOrigin: string };\n dotsOptions: { type?: StyledQrDotStyle; color: string };\n cornersSquareOptions?: { type: StyledQrCornerStyle; color: string };\n backgroundOptions: { color: string };\n}\n\n/**\n * Pure option builder — every clamp and every gate runs here, which is what\n * makes them testable without a canvas.\n */\nexport function buildStyledQrLibOptions(\n value: string,\n drawType: \"canvas\" | \"svg\",\n opts?: StyledQrOptions,\n): StyledQrLibOptions {\n assertValue(value);\n\n const widthPx = resolveWidthPx(opts?.widthPx);\n const color = opts?.color === undefined ? DEFAULT_STYLED_QR_COLOR : assertHexColor(opts.color, \"color\");\n const backgroundColor =\n opts?.backgroundColor === undefined\n ? DEFAULT_STYLED_QR_BACKGROUND\n : assertHexColor(opts.backgroundColor, \"backgroundColor\");\n\n const built: StyledQrLibOptions = {\n type: drawType,\n width: widthPx,\n height: widthPx,\n margin: Math.round(widthPx * STYLED_QR_QUIET_ZONE_RATIO),\n data: value,\n qrOptions: { errorCorrectionLevel: resolveStyledEcLevel(opts?.ecLevel) },\n dotsOptions: { color },\n backgroundOptions: { color: backgroundColor },\n };\n\n // exactOptionalPropertyTypes: omit keys entirely rather than assigning undefined.\n if (opts?.dotStyle !== undefined) built.dotsOptions.type = opts.dotStyle;\n if (opts?.cornerStyle !== undefined) built.cornersSquareOptions = { type: opts.cornerStyle, color };\n if (opts?.logoDataUrl !== undefined) {\n built.image = assertLogoDataUrl(opts.logoDataUrl);\n built.imageOptions = {\n imageSize: resolveLogoSizeRatio(opts.logoSizeRatio),\n // Clearing the modules under the logo is what keeps the mark legible;\n // the forced Q/H error correction is what pays for the hole.\n hideBackgroundDots: true,\n margin: 0,\n // Belt and braces: the data: URL cannot taint the canvas anyway.\n crossOrigin: \"anonymous\",\n };\n }\n\n return built;\n}\n\n// ─── Browser gate + draw ─────────────────────────────────────────────────────\n\nconst BROWSER_REQUIRED =\n \"styled QR generation requires a browser environment — use @ai-matrx/print/qr for server-side codes\";\n\nfunction assertBrowser(): void {\n if (typeof document === \"undefined\" || typeof window === \"undefined\") {\n throw new Error(BROWSER_REQUIRED);\n }\n}\n\n/** Minimal structural view of what we actually use from the library. */\ninterface QrCodeStylingInstance {\n getRawData(extension: \"png\" | \"svg\"): Promise<unknown>;\n}\ntype QrCodeStylingCtor = new (options: StyledQrLibOptions) => QrCodeStylingInstance;\n\n/** Lazy import — this is the ONLY place `qr-code-styling` is pulled in. */\nasync function loadQrCodeStyling(): Promise<QrCodeStylingCtor> {\n const mod = await import(\"qr-code-styling\");\n return mod.default as unknown as QrCodeStylingCtor;\n}\n\n/**\n * Duck-typed rather than `instanceof Blob`: the Blob `qr-code-styling` hands\n * back comes from ITS window, which is not necessarily this realm's — and, in\n * jsdom, is a Blob whose prototype carries only `slice`/`size`/`type`, with\n * neither `arrayBuffer()` nor `text()`. That is exactly why the readers below\n * fall back to `FileReader`.\n */\nfunction isBlobLike(raw: unknown): raw is Blob {\n if (typeof raw !== \"object\" || raw === null) return false;\n const candidate = raw as { slice?: unknown; size?: unknown };\n return typeof candidate.slice === \"function\" && typeof candidate.size === \"number\";\n}\n\nfunction readBlobWith(blob: Blob, read: (reader: FileReader) => void): Promise<FileReader[\"result\"]> {\n if (typeof FileReader === \"undefined\") {\n throw new Error(BROWSER_REQUIRED);\n }\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result);\n reader.onerror = () => reject(reader.error ?? new Error(\"Styled QR blob could not be read.\"));\n read(reader);\n });\n}\n\nasync function blobToBytes(blob: Blob): Promise<Uint8Array> {\n if (typeof blob.arrayBuffer === \"function\") {\n return new Uint8Array(await blob.arrayBuffer());\n }\n const result = await readBlobWith(blob, (reader) => reader.readAsArrayBuffer(blob));\n if (!(result instanceof ArrayBuffer)) {\n throw new Error(\"Styled QR blob could not be read as bytes.\");\n }\n return new Uint8Array(result);\n}\n\nasync function blobToText(blob: Blob): Promise<string> {\n if (typeof blob.text === \"function\") {\n return blob.text();\n }\n const result = await readBlobWith(blob, (reader) => reader.readAsText(blob));\n if (typeof result !== \"string\") {\n throw new Error(\"Styled QR blob could not be read as text.\");\n }\n return result;\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let binary = \"\";\n const CHUNK = 0x8000;\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));\n }\n if (typeof btoa !== \"function\") {\n throw new Error(BROWSER_REQUIRED);\n }\n return btoa(binary);\n}\n\nasync function rawToUint8Array(raw: unknown, extension: string): Promise<Uint8Array> {\n if (raw === null || raw === undefined) {\n throw new Error(`Styled QR ${extension} generation produced no data.`);\n }\n if (isBlobLike(raw)) {\n return blobToBytes(raw);\n }\n if (raw instanceof Uint8Array) return raw;\n if (raw instanceof ArrayBuffer) return new Uint8Array(raw);\n throw new Error(`Styled QR ${extension} generation returned an unexpected payload type.`);\n}\n\n/**\n * PNG data URL. BROWSER-ONLY at call time (see the module header); lazy-imports\n * `qr-code-styling` so this file stays import-inert in Node.\n */\nexport async function generateStyledQrDataUrl(value: string, opts?: StyledQrOptions): Promise<string> {\n const options = buildStyledQrLibOptions(value, \"canvas\", opts);\n assertBrowser();\n\n const QRCodeStyling = await loadQrCodeStyling();\n const raw = await new QRCodeStyling(options).getRawData(\"png\");\n const bytes = await rawToUint8Array(raw, \"PNG\");\n return `data:image/png;base64,${bytesToBase64(bytes)}`;\n}\n\n/** SVG markup string. Same clamps, same gates, same lazy import. */\nexport async function generateStyledQrSvg(value: string, opts?: StyledQrOptions): Promise<string> {\n const options = buildStyledQrLibOptions(value, \"svg\", opts);\n assertBrowser();\n\n const QRCodeStyling = await loadQrCodeStyling();\n const raw = await new QRCodeStyling(options).getRawData(\"svg\");\n if (isBlobLike(raw)) {\n return blobToText(raw);\n }\n const bytes = await rawToUint8Array(raw, \"SVG\");\n return new TextDecoder().decode(bytes);\n}\n"],"mappings":"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","names":[]}
package/dist/react.cjs CHANGED
@@ -46,8 +46,8 @@ var import_tailwind_merge = require("tailwind-merge");
46
46
  function escapeHtml(str) {
47
47
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
48
48
  }
49
- function openPrintWindow(htmlDocument, filename = "print") {
50
- const win = window.open("", "_blank", "width=920,height=720,scrollbars=yes");
49
+ function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "width=920,height=720,scrollbars=yes") {
50
+ const win = window.open("", "_blank", windowFeatures);
51
51
  if (!win) {
52
52
  const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
53
53
  const url = URL.createObjectURL(blob);
@@ -57,12 +57,13 @@ function openPrintWindow(htmlDocument, filename = "print") {
57
57
  document.body.appendChild(a);
58
58
  a.click();
59
59
  document.body.removeChild(a);
60
- URL.revokeObjectURL(url);
61
- return;
60
+ setTimeout(() => URL.revokeObjectURL(url), 0);
61
+ return "downloaded";
62
62
  }
63
63
  win.document.open();
64
64
  win.document.write(htmlDocument);
65
65
  win.document.close();
66
+ return "opened";
66
67
  }
67
68
 
68
69
  // src/qr.ts
@@ -590,7 +591,7 @@ ${bodyHtml}
590
591
  ${withFitScript ? LP_FIT_SCRIPT : ""}
591
592
  </body>
592
593
  </html>`;
593
- openPrintWindow(html, title);
594
+ return openPrintWindow(html, title);
594
595
  }
595
596
  function expandLabels(labels, s) {
596
597
  const from = s.rangeFrom > 0 ? s.rangeFrom : 1;
@@ -628,7 +629,7 @@ function buildCalibrationBody(t) {
628
629
  return `<div class="lp-page">${banner}${marks}${cells.join("\n")}</div>`;
629
630
  }
630
631
  function printCalibrationSheet(template) {
631
- openSheetWindow(
632
+ return openSheetWindow(
632
633
  template,
633
634
  buildCalibrationBody(template),
634
635
  "Label calibration",
@@ -695,18 +696,16 @@ async function printQrLabelSheet(data, variantId, rawSettings) {
695
696
  const t = resolveTemplate(data, variantId);
696
697
  const s = resolveSettings(rawSettings);
697
698
  if (s.calibrationPage) {
698
- printCalibrationSheet(t);
699
- return;
699
+ return printCalibrationSheet(t);
700
700
  }
701
701
  const labels = expandLabels(data.labels ?? [], s);
702
702
  if (!labels.length) {
703
- openSheetWindow(
703
+ return openSheetWindow(
704
704
  t,
705
705
  `<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>`,
706
706
  "QR labels",
707
707
  false
708
708
  );
709
- return;
710
709
  }
711
710
  const hasBadge = labels.some((l) => (l.badge ?? "").trim() !== "");
712
711
  const layout = computeCellLayout(t, s.showCaption, hasBadge);
@@ -736,7 +735,7 @@ async function printQrLabelSheet(data, variantId, rawSettings) {
736
735
  const tail = cells.length % perPage;
737
736
  if (tail > 0)
738
737
  for (let i = 0; i < perPage - tail; i++) cells.push({ html: "" });
739
- openSheetWindow(
738
+ return openSheetWindow(
740
739
  t,
741
740
  renderPages(t, cells),
742
741
  "QR labels",
@@ -1136,7 +1135,8 @@ function visibleSettings(settings, variantId) {
1136
1135
  function PrintOptionsContent({
1137
1136
  printer,
1138
1137
  data,
1139
- onClose
1138
+ onClose,
1139
+ onPrinted
1140
1140
  }) {
1141
1141
  const [selectedVariant, setSelectedVariant] = (0, import_react.useState)(
1142
1142
  printer.variants[0]?.id ?? ""
@@ -1160,7 +1160,8 @@ function PrintOptionsContent({
1160
1160
  setIsPrinting(true);
1161
1161
  try {
1162
1162
  const settings = useDefaults ? buildDefaultSettings(allSettings) : settingValues;
1163
- await printer.print(data, variantId, settings);
1163
+ const outcome = await printer.print(data, variantId, settings);
1164
+ onPrinted?.(outcome);
1164
1165
  } finally {
1165
1166
  setIsPrinting(false);
1166
1167
  onClose();
@@ -1388,7 +1389,8 @@ function PrintOptionsDialog({
1388
1389
  printer,
1389
1390
  data,
1390
1391
  open,
1391
- onOpenChange
1392
+ onOpenChange,
1393
+ onPrinted
1392
1394
  }) {
1393
1395
  const isMobile = useIsMobile();
1394
1396
  const handleClose = () => onOpenChange(false);
@@ -1415,7 +1417,8 @@ function PrintOptionsDialog({
1415
1417
  {
1416
1418
  printer,
1417
1419
  data,
1418
- onClose: handleClose
1420
+ onClose: handleClose,
1421
+ onPrinted
1419
1422
  }
1420
1423
  ) })
1421
1424
  ] }) });
@@ -1433,18 +1436,20 @@ function PrintOptionsDialog({
1433
1436
  {
1434
1437
  printer,
1435
1438
  data,
1436
- onClose: handleClose
1439
+ onClose: handleClose,
1440
+ onPrinted
1437
1441
  }
1438
1442
  ) })
1439
1443
  ] }) });
1440
1444
  }
1441
- function usePrintOptions(printer, data) {
1445
+ function usePrintOptions(printer, data, onPrinted) {
1442
1446
  const [open, setOpen] = (0, import_react.useState)(false);
1443
1447
  const triggerPrint = async () => {
1444
1448
  if (!printer) return;
1445
1449
  const hasOptions = printer.variants.length > 0 || (printer.settings?.length ?? 0) > 0;
1446
1450
  if (!hasOptions) {
1447
- await printer.print(data);
1451
+ const outcome = await printer.print(data);
1452
+ onPrinted?.(outcome);
1448
1453
  } else {
1449
1454
  setOpen(true);
1450
1455
  }