qrlayout-core 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shashidhar Naik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # qrlayout-core
2
+
3
+ Print-ready QR label layout engine. Build a label template with text/QR/image blocks, then export as PNG, PDF, or ZPL.
4
+
5
+ > [!NOTE]
6
+ > This package was previously published as `@shashi089/qrlayout-core`. Please update your dependencies to `qrlayout-core`.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install qrlayout-core
12
+ ```
13
+
14
+ ## Quick start
15
+
16
+ ```ts
17
+ import { StickerPrinter, type StickerLayout } from "qrlayout-core";
18
+
19
+ const layout: StickerLayout = {
20
+ id: "layout-1",
21
+ name: "Badge",
22
+ width: 100,
23
+ height: 60,
24
+ unit: "mm",
25
+ elements: [
26
+ { id: "title", type: "text", x: 0, y: 5, w: 100, h: 10, content: "CONFERENCE PASS" },
27
+ { id: "name", type: "text", x: 5, y: 25, w: 60, h: 10, content: "{{name}}" },
28
+ { id: "qr", type: "qr", x: 70, y: 20, w: 25, h: 25, content: "{{uuid}}" }
29
+ ]
30
+ };
31
+
32
+ const data = { name: "John Doe", uuid: "https://example.com" };
33
+ const printer = new StickerPrinter();
34
+
35
+ // Browser canvas preview
36
+ const canvas = document.getElementById("preview-canvas") as HTMLCanvasElement;
37
+ await printer.renderToCanvas(layout, data, canvas);
38
+
39
+ // PNG
40
+ const png = await printer.renderToDataURL(layout, data, { format: "png" });
41
+
42
+ // PDF
43
+ const pdf = await printer.exportToPDF(layout, [data]);
44
+ pdf.save("stickers.pdf");
45
+
46
+ // ZPL
47
+ const zpl = printer.exportToZPL(layout, [data]);
48
+ console.log(zpl[0]);
49
+ ```
50
+
51
+ ## Layout schema
52
+
53
+ ```ts
54
+ type Unit = "mm" | "px" | "cm" | "in";
55
+ type ElementType = "text" | "qr" | "image";
56
+
57
+ interface StickerElement {
58
+ id: string;
59
+ type: ElementType;
60
+ x: number;
61
+ y: number;
62
+ w: number;
63
+ h: number;
64
+ content: string; // supports {{placeholders}}
65
+ style?: {
66
+ fontFamily?: string;
67
+ fontSize?: number;
68
+ fontWeight?: string | number;
69
+ textAlign?: "left" | "center" | "right";
70
+ color?: string;
71
+ backgroundColor?: string;
72
+ };
73
+ }
74
+
75
+ interface StickerLayout {
76
+ id: string;
77
+ name: string;
78
+ width: number;
79
+ height: number;
80
+ unit: Unit;
81
+ elements: StickerElement[];
82
+ backgroundColor?: string;
83
+ backgroundImage?: string;
84
+ }
85
+ ```
86
+
87
+ ## Outputs
88
+
89
+ - `renderToCanvas` for live preview in the browser
90
+ - `renderToDataURL` for PNG/JPEG/WebP export
91
+ - `exportToPDF` for print-ready PDF (optional)
92
+ - `exportToZPL` for Zebra printers
93
+
94
+ ## Optional PDF export
95
+
96
+ PDF export is isolated in a separate entry so users can avoid the `jspdf` dependency unless needed.
97
+
98
+ ```ts
99
+ import { exportToPDF } from "qrlayout-core/pdf";
100
+ ```
101
+
102
+ If `jspdf` is not installed, `StickerPrinter.exportToPDF()` will throw a clear error.
103
+
104
+ ## Notes
105
+
106
+ - Text alignment is horizontal only (left/center/right).
107
+ - For ZPL, images are not embedded (text and QR only).
108
+ - When using external images in the browser, CORS headers are required or the canvas export will fail.
@@ -0,0 +1,3 @@
1
+ export * from "./layout/schema";
2
+ export * from "./printer/StickerPrinter";
3
+ export * from "./qr/generator";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./layout/schema";
2
+ export * from "./printer/StickerPrinter";
3
+ export * from "./qr/generator";
@@ -0,0 +1,32 @@
1
+ export type Unit = "mm" | "px" | "cm" | "in";
2
+ export type ElementType = "text" | "qr" | "image";
3
+ export interface ElementStyle {
4
+ fontFamily?: string;
5
+ fontSize?: number;
6
+ fontWeight?: string | number;
7
+ textAlign?: "left" | "center" | "right";
8
+ color?: string;
9
+ backgroundColor?: string;
10
+ }
11
+ export interface StickerElement {
12
+ id: string;
13
+ type: ElementType;
14
+ x: number;
15
+ y: number;
16
+ w: number;
17
+ h: number;
18
+ content: string;
19
+ style?: ElementStyle;
20
+ }
21
+ export type StickerData = Record<string, unknown>;
22
+ export interface StickerLayout {
23
+ id: string;
24
+ name: string;
25
+ width: number;
26
+ height: number;
27
+ unit: Unit;
28
+ elements: StickerElement[];
29
+ backgroundColor?: string;
30
+ backgroundImage?: string;
31
+ }
32
+ export type ImageFormat = "png" | "jpeg" | "jpg" | "webp";
@@ -0,0 +1 @@
1
+ export {};
package/dist/pdf.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import jsPDF from "jspdf";
2
+ import { StickerLayout } from "./layout/schema";
3
+ export type PdfDoc = InstanceType<typeof jsPDF>;
4
+ export declare function exportToPDF(layout: StickerLayout, dataList: Record<string, any>[]): Promise<PdfDoc>;
package/dist/pdf.js ADDED
@@ -0,0 +1,92 @@
1
+ import jsPDF from "jspdf";
2
+ import { generateQR } from "./qr/generator";
3
+ function parseContent(content, data) {
4
+ return content.replace(/\{\{(.*?)\}\}/g, (_, key) => {
5
+ const trimmedKey = String(key).trim();
6
+ return data[trimmedKey] !== undefined ? String(data[trimmedKey]) : "";
7
+ });
8
+ }
9
+ async function resolveDataUrl(src) {
10
+ if (!src)
11
+ return undefined;
12
+ if (src.startsWith("data:"))
13
+ return src;
14
+ if (typeof fetch === "undefined" || typeof FileReader === "undefined")
15
+ return undefined;
16
+ try {
17
+ const res = await fetch(src);
18
+ const blob = await res.blob();
19
+ return await new Promise((resolve, reject) => {
20
+ const reader = new FileReader();
21
+ reader.onloadend = () => resolve(reader.result);
22
+ reader.onerror = () => reject(reader.error);
23
+ reader.readAsDataURL(blob);
24
+ });
25
+ }
26
+ catch (err) {
27
+ console.warn("Could not resolve data URL", err);
28
+ return undefined;
29
+ }
30
+ }
31
+ export async function exportToPDF(layout, dataList) {
32
+ const validUnits = ["pt", "px", "in", "mm", "cm"];
33
+ const pdfUnit = validUnits.includes(layout.unit) ? layout.unit : "mm";
34
+ const doc = new jsPDF({
35
+ orientation: layout.width > layout.height ? "l" : "p",
36
+ unit: pdfUnit,
37
+ format: [layout.width, layout.height]
38
+ });
39
+ for (let i = 0; i < dataList.length; i++) {
40
+ if (i > 0)
41
+ doc.addPage([layout.width, layout.height], layout.width > layout.height ? "l" : "p");
42
+ const data = dataList[i];
43
+ if (layout.backgroundColor) {
44
+ doc.setFillColor(layout.backgroundColor);
45
+ doc.rect(0, 0, layout.width, layout.height, "F");
46
+ }
47
+ if (layout.backgroundImage) {
48
+ const dataUrl = await resolveDataUrl(layout.backgroundImage);
49
+ if (dataUrl) {
50
+ doc.addImage(dataUrl, "PNG", 0, 0, layout.width, layout.height);
51
+ }
52
+ }
53
+ for (const element of layout.elements) {
54
+ const filledContent = parseContent(element.content, data);
55
+ const { x, y, w, h } = element;
56
+ if (element.type === "qr") {
57
+ if (filledContent) {
58
+ const qrUrl = await generateQR(filledContent);
59
+ doc.addImage(qrUrl, "PNG", x, y, w, h);
60
+ }
61
+ }
62
+ else if (element.type === "image") {
63
+ if (filledContent) {
64
+ try {
65
+ doc.addImage(filledContent, "PNG", x, y, w, h);
66
+ }
67
+ catch (e) {
68
+ console.warn("Could not add image to PDF", e);
69
+ }
70
+ }
71
+ }
72
+ else if (element.type === "text") {
73
+ const style = element.style || {};
74
+ const fontSize = style.fontSize || 12;
75
+ const color = style.color || "#000000";
76
+ doc.setFontSize(fontSize);
77
+ doc.setTextColor(color);
78
+ let drawX = x;
79
+ const align = style.textAlign || "left";
80
+ if (align === "center")
81
+ drawX = x + w / 2;
82
+ if (align === "right")
83
+ drawX = x + w;
84
+ doc.text(filledContent, drawX, y, {
85
+ baseline: "top",
86
+ align: align
87
+ });
88
+ }
89
+ }
90
+ }
91
+ return doc;
92
+ }
@@ -0,0 +1,19 @@
1
+ import { StickerLayout, StickerData, ImageFormat } from "../layout/schema";
2
+ import type { PdfDoc } from "../pdf";
3
+ export interface DataUrlOptions {
4
+ format?: ImageFormat;
5
+ quality?: number;
6
+ canvas?: HTMLCanvasElement;
7
+ }
8
+ export declare class StickerPrinter {
9
+ private toPx;
10
+ private parseContent;
11
+ renderToCanvas(layout: StickerLayout, data: StickerData, canvas: HTMLCanvasElement): Promise<void>;
12
+ renderToDataURL(layout: StickerLayout, data: StickerData, options?: DataUrlOptions): Promise<string>;
13
+ exportImages(layout: StickerLayout, dataList: StickerData[], options?: DataUrlOptions): Promise<string[]>;
14
+ private drawText;
15
+ private drawImage;
16
+ private createCanvas;
17
+ exportToPDF(layout: StickerLayout, dataList: Record<string, any>[]): Promise<PdfDoc>;
18
+ exportToZPL(layout: StickerLayout, dataList: Record<string, any>[]): string[];
19
+ }
@@ -0,0 +1,192 @@
1
+ import { generateQR } from "../qr/generator";
2
+ export class StickerPrinter {
3
+ // Helper to convert units to Pixels (Canvas) and Points (PDF)
4
+ // We'll use 96 dpi for screen/canvas calculations
5
+ toPx(value, unit) {
6
+ switch (unit) {
7
+ case "mm": return (value * 96) / 25.4;
8
+ case "cm": return (value * 96) / 2.54;
9
+ case "in": return value * 96;
10
+ case "px":
11
+ default: return value;
12
+ }
13
+ }
14
+ // Parse variable content like "{{name}}"
15
+ parseContent(content, data) {
16
+ return content.replace(/\{\{(.*?)\}\}/g, (_, key) => {
17
+ const trimmedKey = key.trim();
18
+ return data[trimmedKey] !== undefined ? String(data[trimmedKey]) : "";
19
+ });
20
+ }
21
+ // --- HTML Canvas Renderer (Preview & Image Export) ---
22
+ async renderToCanvas(layout, data, canvas) {
23
+ const ctx = canvas.getContext("2d");
24
+ if (!ctx)
25
+ throw new Error("Canvas context not available");
26
+ // Setup Canvas Size
27
+ canvas.width = this.toPx(layout.width, layout.unit);
28
+ canvas.height = this.toPx(layout.height, layout.unit);
29
+ // Clear & Background
30
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
31
+ if (layout.backgroundColor) {
32
+ ctx.fillStyle = layout.backgroundColor;
33
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
34
+ }
35
+ if (layout.backgroundImage) {
36
+ await this.drawImage(ctx, layout.backgroundImage, 0, 0, canvas.width, canvas.height);
37
+ }
38
+ // Render Elements
39
+ for (const element of layout.elements) {
40
+ const x = this.toPx(element.x, layout.unit);
41
+ const y = this.toPx(element.y, layout.unit);
42
+ const w = this.toPx(element.w, layout.unit);
43
+ const h = this.toPx(element.h, layout.unit);
44
+ const filledContent = this.parseContent(element.content, data);
45
+ if (element.type === "qr") {
46
+ if (filledContent) {
47
+ const qrUrl = await generateQR(filledContent);
48
+ await this.drawImage(ctx, qrUrl, x, y, w, h);
49
+ }
50
+ }
51
+ else if (element.type === "text") {
52
+ this.drawText(ctx, element, filledContent, x, y, w, h);
53
+ }
54
+ else if (element.type === "image") {
55
+ if (filledContent) { // Assume content is URL
56
+ await this.drawImage(ctx, filledContent, x, y, w, h);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ async renderToDataURL(layout, data, options) {
62
+ const format = ((options === null || options === void 0 ? void 0 : options.format) || "png").toLowerCase();
63
+ const mime = format === "jpg" ? "image/jpeg" : `image/${format}`;
64
+ const canvas = (options === null || options === void 0 ? void 0 : options.canvas) || this.createCanvas();
65
+ await this.renderToCanvas(layout, data, canvas);
66
+ return canvas.toDataURL(mime, options === null || options === void 0 ? void 0 : options.quality);
67
+ }
68
+ async exportImages(layout, dataList, options) {
69
+ const results = [];
70
+ for (const data of dataList) {
71
+ results.push(await this.renderToDataURL(layout, data, options));
72
+ }
73
+ return results;
74
+ }
75
+ drawText(ctx, el, text, x, y, w, h) {
76
+ const style = el.style || {};
77
+ const fontSize = style.fontSize || 12;
78
+ const fontFamily = style.fontFamily || "sans-serif";
79
+ const fontWeight = style.fontWeight || "normal";
80
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
81
+ ctx.fillStyle = style.color || "#000";
82
+ ctx.textBaseline = "top";
83
+ ctx.textAlign = style.textAlign || "left";
84
+ // Handle Alignment X adjusment
85
+ let drawX = x;
86
+ if (style.textAlign === "center")
87
+ drawX = x + w / 2;
88
+ if (style.textAlign === "right")
89
+ drawX = x + w;
90
+ ctx.fillText(text, drawX, y);
91
+ }
92
+ drawImage(ctx, url, x, y, w, h) {
93
+ return new Promise((resolve) => {
94
+ const img = new Image();
95
+ img.crossOrigin = "anonymous";
96
+ img.onload = () => {
97
+ ctx.drawImage(img, x, y, w, h);
98
+ resolve();
99
+ };
100
+ img.onerror = () => resolve(); // Don't block if image fails
101
+ img.src = url;
102
+ });
103
+ }
104
+ createCanvas() {
105
+ if (typeof document === "undefined") {
106
+ throw new Error("Canvas creation requires a DOM environment. Pass a canvas explicitly when rendering server-side.");
107
+ }
108
+ return document.createElement("canvas");
109
+ }
110
+ // --- PDF Exporter ---
111
+ async exportToPDF(layout, dataList) {
112
+ try {
113
+ const { exportToPDF } = await import("../pdf");
114
+ return await exportToPDF(layout, dataList);
115
+ }
116
+ catch (err) {
117
+ throw new Error("PDF export requires optional dependency 'jspdf'. Install it to use exportToPDF().");
118
+ }
119
+ }
120
+ // --- ZPL Exporter ---
121
+ exportToZPL(layout, dataList) {
122
+ const dpi = 203; // Standard Zebra DPI
123
+ const dpmm = 8; // dots per mm
124
+ // Helper to convert to dots
125
+ const toDots = (val, unit) => {
126
+ let mm = 0;
127
+ switch (unit) {
128
+ case "mm":
129
+ mm = val;
130
+ break;
131
+ case "cm":
132
+ mm = val * 10;
133
+ break;
134
+ case "in":
135
+ mm = val * 25.4;
136
+ break;
137
+ case "px":
138
+ mm = val * (25.4 / 96);
139
+ break;
140
+ default: mm = val;
141
+ }
142
+ return Math.round(mm * dpmm);
143
+ };
144
+ const results = [];
145
+ for (const data of dataList) {
146
+ let zpl = "^XA\n"; // Start Format
147
+ // Label Length (optional but good practice)
148
+ // ^LL<length in dots>
149
+ const heightDots = toDots(layout.height, layout.unit);
150
+ const widthDots = toDots(layout.width, layout.unit);
151
+ zpl += `^PW${widthDots}\n`;
152
+ zpl += `^LL${heightDots}\n`;
153
+ for (const element of layout.elements) {
154
+ const filledContent = this.parseContent(element.content, data);
155
+ const x = toDots(element.x, layout.unit);
156
+ const y = toDots(element.y, layout.unit);
157
+ zpl += `^FO${x},${y}`;
158
+ if (element.type === "text") {
159
+ // Font mapping is tricky. We'll use Scalable Font 0 (^A0)
160
+ // Height in dots.
161
+ const style = element.style || {};
162
+ const fontSizePt = style.fontSize || 12;
163
+ // Approximate pt to dots conversion (1 pt = 1/72 inch). 203 DPI.
164
+ // dots = pt * (203/72) ~ pt * 2.8
165
+ const fontHeightDots = Math.round(fontSizePt * 2.8);
166
+ zpl += `^A0N,${fontHeightDots},${fontHeightDots}`;
167
+ zpl += `^FD${filledContent}^FS\n`;
168
+ }
169
+ else if (element.type === "qr") {
170
+ // ^BQN,2,height
171
+ // ZPL QR codes are controlled by magnification factor mostly.
172
+ const w = toDots(element.w, layout.unit);
173
+ // Mag factor 1-10. Approximate based on width?
174
+ // Let's assume a reasonable default magnification or calculate roughly.
175
+ // ^BQa,b,c,d,e
176
+ // ^BQN,2,height
177
+ let mag = 2;
178
+ if (w > 100)
179
+ mag = 4;
180
+ if (w > 200)
181
+ mag = 6;
182
+ zpl += `^BQN,2,${mag}`;
183
+ zpl += `^FDQA,${filledContent}^FS\n`;
184
+ }
185
+ // Images are very hard in pure ZPL text (need hex conversion), skipping for simple implementation
186
+ }
187
+ zpl += "^XZ"; // End Format
188
+ results.push(zpl);
189
+ }
190
+ return results;
191
+ }
192
+ }
@@ -0,0 +1 @@
1
+ export declare function generateQR(text: string): Promise<string>;
@@ -0,0 +1,16 @@
1
+ import QRCode from "qrcode";
2
+ const qrCache = new Map();
3
+ export async function generateQR(text) {
4
+ if (qrCache.has(text)) {
5
+ return qrCache.get(text);
6
+ }
7
+ try {
8
+ const url = await QRCode.toDataURL(text);
9
+ qrCache.set(text, url);
10
+ return url;
11
+ }
12
+ catch (err) {
13
+ console.error("Error generating QR code", err);
14
+ return "";
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "qrlayout-core",
3
+ "version": "1.0.0",
4
+ "description": "QR sticker layout builder/renderer/exporter core",
5
+ "author": "Shashidhar Naik <shashidharnaik8@gmail.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/shashi089/qr-code-layout-generate-tool.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "type": "module",
13
+ "main": "dist/index.js",
14
+ "module": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./pdf": {
22
+ "types": "./dist/pdf.d.ts",
23
+ "import": "./dist/pdf.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json"
36
+ },
37
+ "sideEffects": false,
38
+ "dependencies": {
39
+ "qrcode": "^1.5.4"
40
+ },
41
+ "optionalDependencies": {
42
+ "jspdf": "^3.0.4"
43
+ },
44
+ "devDependencies": {
45
+ "@types/jspdf": "^1.3.3",
46
+ "@types/qrcode": "^1.5.6",
47
+ "typescript": "^5.3.3"
48
+ }
49
+ }