@bobfrankston/brother-label 1.0.13 → 1.1.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.
package/render.d.ts CHANGED
@@ -1,40 +1,17 @@
1
1
  /**
2
- * HTML to bitmap rendering module
3
- * Standalone utility for converting HTML to PNG images
2
+ * Backward-compat shim. The HTML rendering logic now lives in
3
+ * @bobfrankston/label-core. This file just re-exports those entry points so
4
+ * existing deep imports keep working.
4
5
  */
5
- export interface RenderOptions {
6
+ export { renderHtmlString, renderHtmlFile, renderHtmlUrl, closeBrowser, type RenderOptions, } from "@bobfrankston/label-core";
7
+ import { renderHtmlFile as _renderHtmlFile } from "@bobfrankston/label-core";
8
+ /** @deprecated Use renderHtmlFile from @bobfrankston/label-core. */
9
+ export declare const renderHtml: typeof _renderHtmlFile;
10
+ /** @deprecated Render to a file by calling renderHtmlFile then fs.writeFileSync. */
11
+ export declare function renderHtmlToFile(htmlPath: string, outputPath: string, options: {
6
12
  width?: number;
7
13
  height: number;
8
14
  tapeMm?: number;
9
15
  deviceScaleFactor?: number;
10
- keepScale?: boolean;
11
- }
12
- /**
13
- * Close the shared browser instance
14
- */
15
- export declare function closeBrowser(): Promise<void>;
16
- /**
17
- * Render HTML file to PNG buffer
18
- * Processes <img qr="..."> tags to inline base64 QR codes
19
- */
20
- export declare function renderHtmlFile(htmlPath: string, options: RenderOptions): Promise<Buffer>;
21
- /**
22
- * Render HTML string to PNG buffer
23
- * Processes <img qr="..."> tags to inline base64 QR codes via DOM
24
- * @param html - HTML content string
25
- * @param options - Render dimensions (width auto-detected if omitted)
26
- * @param basePath - Optional base path for resolving relative resources
27
- */
28
- export declare function renderHtmlString(html: string, options: RenderOptions, basePath?: string): Promise<Buffer>;
29
- /**
30
- * Render HTML from URL to PNG buffer
31
- * Processes <img qr="..."> tags to inline base64 QR codes via DOM
32
- * If width is not specified, auto-detects from content
33
- */
34
- export declare function renderHtmlUrl(url: string, options: RenderOptions): Promise<Buffer>;
35
- /**
36
- * Render HTML file and save to PNG file
37
- */
38
- export declare function renderHtmlToFile(htmlPath: string, outputPath: string, options: RenderOptions): Promise<void>;
39
- export declare const renderHtml: typeof renderHtmlFile;
16
+ }): Promise<void>;
40
17
  //# sourceMappingURL=render.d.ts.map
package/render.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["render.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,aAAa;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AA+ED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAKlD;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9F;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqE/G;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA+DxF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAClC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,GACvB,OAAO,CAAC,IAAI,CAAC,CAGf;AAGD,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["render.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACH,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,YAAY,EACZ,KAAK,aAAa,GACrB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,cAAc,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE7E,oEAAoE;AACpE,eAAO,MAAM,UAAU,wBAAkB,CAAC;AAE1C,oFAAoF;AACpF,wBAAsB,gBAAgB,CAClC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,GACzF,OAAO,CAAC,IAAI,CAAC,CAIf"}
package/render.js CHANGED
@@ -1,240 +1,16 @@
1
1
  /**
2
- * HTML to bitmap rendering module
3
- * Standalone utility for converting HTML to PNG images
4
- */
5
- import puppeteer from "puppeteer";
6
- import * as path from "path";
7
- import * as fs from "fs";
8
- import { Jimp } from "jimp";
9
- import QRCode from "qrcode";
10
- let browserInstance = null;
11
- const DEFAULT_SCALE = 3; // Render at 3x for quality, then scale down
12
- const CSS_DPI = 96; // CSS interprets mm at 96 DPI
13
- const MM_PER_INCH = 25.4;
14
- /**
15
- * Generate QR code data URLs for all qr attributes found in page
16
- * Returns map of qr data -> data URL
17
- */
18
- async function generateQrDataUrls(qrValues) {
19
- const map = new Map();
20
- for (const data of qrValues) {
21
- if (!map.has(data)) {
22
- const dataUrl = await QRCode.toDataURL(data, {
23
- type: "image/png",
24
- margin: 0,
25
- errorCorrectionLevel: "M",
26
- color: { dark: "#000000", light: "#ffffff" },
27
- });
28
- map.set(data, dataUrl);
29
- }
30
- }
31
- return map;
32
- }
33
- /**
34
- * Process <img qr="..."> elements in page via DOM manipulation
35
- */
36
- async function processQrElements(page) {
37
- // Get all qr attribute values from the page
38
- const qrValues = await page.evaluate(() => {
39
- const imgs = document.querySelectorAll("img[qr]");
40
- return Array.from(imgs).map(img => img.getAttribute("qr") || "");
41
- });
42
- if (qrValues.length === 0)
43
- return;
44
- // Generate QR codes
45
- const qrMap = await generateQrDataUrls(qrValues);
46
- // Convert map to object for passing to page context
47
- const qrUrls = {};
48
- qrMap.forEach((url, data) => { qrUrls[data] = url; });
49
- // Replace qr attributes with src in the DOM
50
- await page.evaluate((urls) => {
51
- const imgs = document.querySelectorAll("img[qr]");
52
- imgs.forEach(img => {
53
- const qrData = img.getAttribute("qr");
54
- if (qrData && urls[qrData]) {
55
- img.setAttribute("src", urls[qrData]);
56
- img.removeAttribute("qr");
57
- }
58
- });
59
- }, qrUrls);
60
- }
61
- /**
62
- * Scale image buffer down to target dimensions
63
- */
64
- async function scaleDown(buffer, targetWidth, targetHeight) {
65
- const image = await Jimp.read(buffer);
66
- image.resize({ w: targetWidth, h: targetHeight });
67
- return image.getBuffer("image/png");
68
- }
69
- /**
70
- * Get or create a shared browser instance for better performance
71
- */
72
- async function getBrowser() {
73
- if (!browserInstance || !browserInstance.connected) {
74
- browserInstance = await puppeteer.launch({ headless: true });
75
- }
76
- return browserInstance;
77
- }
78
- /**
79
- * Close the shared browser instance
80
- */
81
- export async function closeBrowser() {
82
- if (browserInstance) {
83
- await browserInstance.close();
84
- browserInstance = null;
85
- }
86
- }
87
- /**
88
- * Render HTML file to PNG buffer
89
- * Processes <img qr="..."> tags to inline base64 QR codes
90
- */
91
- export async function renderHtmlFile(htmlPath, options) {
92
- const absolutePath = path.resolve(htmlPath);
93
- if (!fs.existsSync(absolutePath)) {
94
- throw new Error(`HTML file not found: ${absolutePath}`);
95
- }
96
- return renderHtmlUrl(`file://${absolutePath}`, options);
97
- }
98
- /**
99
- * Render HTML string to PNG buffer
100
- * Processes <img qr="..."> tags to inline base64 QR codes via DOM
101
- * @param html - HTML content string
102
- * @param options - Render dimensions (width auto-detected if omitted)
103
- * @param basePath - Optional base path for resolving relative resources
104
- */
105
- export async function renderHtmlString(html, options, basePath) {
106
- const browser = await getBrowser();
107
- const page = await browser.newPage();
108
- const scaleFactor = options.deviceScaleFactor ?? DEFAULT_SCALE;
109
- const autoWidth = !options.width || options.width === 0;
110
- // If tapeMm provided, use CSS-equivalent viewport height; otherwise use target height directly
111
- const cssHeight = options.tapeMm
112
- ? Math.round(options.tapeMm * CSS_DPI / MM_PER_INCH)
113
- : options.height;
114
- const targetHeight = options.height;
115
- try {
116
- // Use large initial width for auto-detection, or specified width
117
- const initialWidth = autoWidth ? 4000 : options.width;
118
- await page.setViewport({
119
- width: initialWidth,
120
- height: cssHeight,
121
- deviceScaleFactor: scaleFactor,
122
- });
123
- if (basePath) {
124
- // Set base URL for relative resource resolution
125
- const baseUrl = `file://${path.resolve(basePath)}/`;
126
- await page.goto(baseUrl, { waitUntil: "domcontentloaded" });
127
- await page.setContent(html, { waitUntil: "networkidle0" });
128
- }
129
- else {
130
- await page.setContent(html, { waitUntil: "networkidle0" });
131
- }
132
- // Process <img qr="..."> elements
133
- await processQrElements(page);
134
- // Auto-detect content width if needed
135
- let finalCssWidth = initialWidth;
136
- if (autoWidth) {
137
- finalCssWidth = await page.evaluate(() => {
138
- // Try first child of body (likely the main container)
139
- const firstChild = document.body.firstElementChild;
140
- if (firstChild) {
141
- const rect = firstChild.getBoundingClientRect();
142
- if (rect.width > 0 && rect.width < 3000) {
143
- return Math.ceil(rect.width);
144
- }
145
- }
146
- // Fall back to scroll width (actual content width)
147
- return document.body.scrollWidth;
148
- });
149
- await page.setViewport({
150
- width: finalCssWidth,
151
- height: cssHeight,
152
- deviceScaleFactor: scaleFactor,
153
- });
154
- }
155
- const pngBuffer = await page.screenshot({ type: "png" });
156
- // Scale to target dimensions
157
- // If tapeMm was used, scale from CSS dimensions to target 300 DPI dimensions
158
- const scale = targetHeight / cssHeight;
159
- const finalWidth = Math.round(finalCssWidth * scale);
160
- if (scaleFactor > 1 || options.tapeMm) {
161
- return scaleDown(Buffer.from(pngBuffer), finalWidth, targetHeight);
162
- }
163
- return Buffer.from(pngBuffer);
164
- }
165
- finally {
166
- await page.close();
167
- }
168
- }
169
- /**
170
- * Render HTML from URL to PNG buffer
171
- * Processes <img qr="..."> tags to inline base64 QR codes via DOM
172
- * If width is not specified, auto-detects from content
173
- */
174
- export async function renderHtmlUrl(url, options) {
175
- const browser = await getBrowser();
176
- const page = await browser.newPage();
177
- const scaleFactor = options.deviceScaleFactor ?? DEFAULT_SCALE;
178
- const autoWidth = !options.width || options.width === 0;
179
- // If tapeMm provided, use CSS-equivalent viewport height; otherwise use target height directly
180
- const cssHeight = options.tapeMm
181
- ? Math.round(options.tapeMm * CSS_DPI / MM_PER_INCH)
182
- : options.height;
183
- const targetHeight = options.height;
184
- try {
185
- // Use large initial width for auto-detection, or specified width
186
- const initialWidth = autoWidth ? 4000 : options.width;
187
- await page.setViewport({
188
- width: initialWidth,
189
- height: cssHeight,
190
- deviceScaleFactor: scaleFactor,
191
- });
192
- await page.goto(url, { waitUntil: "networkidle0" });
193
- // Process <img qr="..."> elements
194
- await processQrElements(page);
195
- // Auto-detect content width if needed
196
- let finalCssWidth = initialWidth;
197
- if (autoWidth) {
198
- finalCssWidth = await page.evaluate(() => {
199
- // Try first child of body (likely the main container)
200
- const firstChild = document.body.firstElementChild;
201
- if (firstChild) {
202
- const rect = firstChild.getBoundingClientRect();
203
- if (rect.width > 0 && rect.width < 3000) {
204
- return Math.ceil(rect.width);
205
- }
206
- }
207
- // Fall back to scroll width (actual content width)
208
- return document.body.scrollWidth;
209
- });
210
- // Resize viewport to actual content width
211
- await page.setViewport({
212
- width: finalCssWidth,
213
- height: cssHeight,
214
- deviceScaleFactor: scaleFactor,
215
- });
216
- }
217
- const pngBuffer = await page.screenshot({ type: "png" });
218
- // Scale to target dimensions
219
- // If tapeMm was used, scale from CSS dimensions to target 300 DPI dimensions
220
- const scale = targetHeight / cssHeight;
221
- const finalWidth = Math.round(finalCssWidth * scale);
222
- if (scaleFactor > 1 || options.tapeMm) {
223
- return scaleDown(Buffer.from(pngBuffer), finalWidth, targetHeight);
224
- }
225
- return Buffer.from(pngBuffer);
226
- }
227
- finally {
228
- await page.close();
229
- }
230
- }
231
- /**
232
- * Render HTML file and save to PNG file
233
- */
2
+ * Backward-compat shim. The HTML rendering logic now lives in
3
+ * @bobfrankston/label-core. This file just re-exports those entry points so
4
+ * existing deep imports keep working.
5
+ */
6
+ export { renderHtmlString, renderHtmlFile, renderHtmlUrl, closeBrowser, } from "@bobfrankston/label-core";
7
+ import { renderHtmlFile as _renderHtmlFile } from "@bobfrankston/label-core";
8
+ /** @deprecated Use renderHtmlFile from @bobfrankston/label-core. */
9
+ export const renderHtml = _renderHtmlFile;
10
+ /** @deprecated Render to a file by calling renderHtmlFile then fs.writeFileSync. */
234
11
  export async function renderHtmlToFile(htmlPath, outputPath, options) {
235
- const buffer = await renderHtmlFile(htmlPath, options);
12
+ const fs = await import("fs");
13
+ const buffer = await _renderHtmlFile(htmlPath, options);
236
14
  fs.writeFileSync(outputPath, buffer);
237
15
  }
238
- // Legacy export for compatibility
239
- export const renderHtml = renderHtmlFile;
240
16
  //# sourceMappingURL=render.js.map
package/render.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["render.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * HTML to bitmap rendering module\r\n * Standalone utility for converting HTML to PNG images\r\n */\r\n\r\nimport puppeteer, { Browser } from \"puppeteer\";\r\nimport * as path from \"path\";\r\nimport * as fs from \"fs\";\r\nimport { Jimp } from \"jimp\";\r\nimport QRCode from \"qrcode\";\r\n\r\nexport interface RenderOptions {\r\n width?: number; // If omitted or 0, auto-detect from content\r\n height: number; // Target output height in pixels (300 DPI)\r\n tapeMm?: number; // Tape size in mm - if set, viewport matches CSS mm interpretation\r\n deviceScaleFactor?: number; // Default 3 for high quality (288 DPI effective)\r\n keepScale?: boolean; // If true, don't scale down - keep full resolution output\r\n}\r\n\r\nlet browserInstance: Browser | null = null;\r\n\r\nconst DEFAULT_SCALE = 3; // Render at 3x for quality, then scale down\r\nconst CSS_DPI = 96; // CSS interprets mm at 96 DPI\r\nconst MM_PER_INCH = 25.4;\r\n\r\n/**\r\n * Generate QR code data URLs for all qr attributes found in page\r\n * Returns map of qr data -> data URL\r\n */\r\nasync function generateQrDataUrls(qrValues: string[]): Promise<Map<string, string>> {\r\n const map = new Map<string, string>();\r\n for (const data of qrValues) {\r\n if (!map.has(data)) {\r\n const dataUrl = await QRCode.toDataURL(data, {\r\n type: \"image/png\",\r\n margin: 0,\r\n errorCorrectionLevel: \"M\",\r\n color: { dark: \"#000000\", light: \"#ffffff\" },\r\n });\r\n map.set(data, dataUrl);\r\n }\r\n }\r\n return map;\r\n}\r\n\r\n/**\r\n * Process <img qr=\"...\"> elements in page via DOM manipulation\r\n */\r\nasync function processQrElements(page: puppeteer.Page): Promise<void> {\r\n // Get all qr attribute values from the page\r\n const qrValues = await page.evaluate(() => {\r\n const imgs = document.querySelectorAll(\"img[qr]\");\r\n return Array.from(imgs).map(img => img.getAttribute(\"qr\") || \"\");\r\n });\r\n\r\n if (qrValues.length === 0) return;\r\n\r\n // Generate QR codes\r\n const qrMap = await generateQrDataUrls(qrValues);\r\n\r\n // Convert map to object for passing to page context\r\n const qrUrls: Record<string, string> = {};\r\n qrMap.forEach((url, data) => { qrUrls[data] = url; });\r\n\r\n // Replace qr attributes with src in the DOM\r\n await page.evaluate((urls: Record<string, string>) => {\r\n const imgs = document.querySelectorAll(\"img[qr]\");\r\n imgs.forEach(img => {\r\n const qrData = img.getAttribute(\"qr\");\r\n if (qrData && urls[qrData]) {\r\n img.setAttribute(\"src\", urls[qrData]);\r\n img.removeAttribute(\"qr\");\r\n }\r\n });\r\n }, qrUrls);\r\n}\r\n\r\n/**\r\n * Scale image buffer down to target dimensions\r\n */\r\nasync function scaleDown(buffer: Buffer, targetWidth: number, targetHeight: number): Promise<Buffer> {\r\n const image = await Jimp.read(buffer);\r\n image.resize({ w: targetWidth, h: targetHeight });\r\n return image.getBuffer(\"image/png\");\r\n}\r\n\r\n/**\r\n * Get or create a shared browser instance for better performance\r\n */\r\nasync function getBrowser(): Promise<Browser> {\r\n if (!browserInstance || !browserInstance.connected) {\r\n browserInstance = await puppeteer.launch({ headless: true });\r\n }\r\n return browserInstance;\r\n}\r\n\r\n/**\r\n * Close the shared browser instance\r\n */\r\nexport async function closeBrowser(): Promise<void> {\r\n if (browserInstance) {\r\n await browserInstance.close();\r\n browserInstance = null;\r\n }\r\n}\r\n\r\n/**\r\n * Render HTML file to PNG buffer\r\n * Processes <img qr=\"...\"> tags to inline base64 QR codes\r\n */\r\nexport async function renderHtmlFile(htmlPath: string, options: RenderOptions): Promise<Buffer> {\r\n const absolutePath = path.resolve(htmlPath);\r\n if (!fs.existsSync(absolutePath)) {\r\n throw new Error(`HTML file not found: ${absolutePath}`);\r\n }\r\n return renderHtmlUrl(`file://${absolutePath}`, options);\r\n}\r\n\r\n/**\r\n * Render HTML string to PNG buffer\r\n * Processes <img qr=\"...\"> tags to inline base64 QR codes via DOM\r\n * @param html - HTML content string\r\n * @param options - Render dimensions (width auto-detected if omitted)\r\n * @param basePath - Optional base path for resolving relative resources\r\n */\r\nexport async function renderHtmlString(html: string, options: RenderOptions, basePath?: string): Promise<Buffer> {\r\n const browser = await getBrowser();\r\n const page = await browser.newPage();\r\n const scaleFactor = options.deviceScaleFactor ?? DEFAULT_SCALE;\r\n const autoWidth = !options.width || options.width === 0;\r\n\r\n // If tapeMm provided, use CSS-equivalent viewport height; otherwise use target height directly\r\n const cssHeight = options.tapeMm\r\n ? Math.round(options.tapeMm * CSS_DPI / MM_PER_INCH)\r\n : options.height;\r\n const targetHeight = options.height;\r\n\r\n try {\r\n // Use large initial width for auto-detection, or specified width\r\n const initialWidth = autoWidth ? 4000 : options.width!;\r\n await page.setViewport({\r\n width: initialWidth,\r\n height: cssHeight,\r\n deviceScaleFactor: scaleFactor,\r\n });\r\n\r\n if (basePath) {\r\n // Set base URL for relative resource resolution\r\n const baseUrl = `file://${path.resolve(basePath)}/`;\r\n await page.goto(baseUrl, { waitUntil: \"domcontentloaded\" });\r\n await page.setContent(html, { waitUntil: \"networkidle0\" });\r\n } else {\r\n await page.setContent(html, { waitUntil: \"networkidle0\" });\r\n }\r\n\r\n // Process <img qr=\"...\"> elements\r\n await processQrElements(page);\r\n\r\n // Auto-detect content width if needed\r\n let finalCssWidth = initialWidth;\r\n if (autoWidth) {\r\n finalCssWidth = await page.evaluate(() => {\r\n // Try first child of body (likely the main container)\r\n const firstChild = document.body.firstElementChild as HTMLElement;\r\n if (firstChild) {\r\n const rect = firstChild.getBoundingClientRect();\r\n if (rect.width > 0 && rect.width < 3000) {\r\n return Math.ceil(rect.width);\r\n }\r\n }\r\n // Fall back to scroll width (actual content width)\r\n return document.body.scrollWidth;\r\n });\r\n await page.setViewport({\r\n width: finalCssWidth,\r\n height: cssHeight,\r\n deviceScaleFactor: scaleFactor,\r\n });\r\n }\r\n\r\n const pngBuffer = await page.screenshot({ type: \"png\" });\r\n\r\n // Scale to target dimensions\r\n // If tapeMm was used, scale from CSS dimensions to target 300 DPI dimensions\r\n const scale = targetHeight / cssHeight;\r\n const finalWidth = Math.round(finalCssWidth * scale);\r\n\r\n if (scaleFactor > 1 || options.tapeMm) {\r\n return scaleDown(Buffer.from(pngBuffer), finalWidth, targetHeight);\r\n }\r\n return Buffer.from(pngBuffer);\r\n } finally {\r\n await page.close();\r\n }\r\n}\r\n\r\n/**\r\n * Render HTML from URL to PNG buffer\r\n * Processes <img qr=\"...\"> tags to inline base64 QR codes via DOM\r\n * If width is not specified, auto-detects from content\r\n */\r\nexport async function renderHtmlUrl(url: string, options: RenderOptions): Promise<Buffer> {\r\n const browser = await getBrowser();\r\n const page = await browser.newPage();\r\n const scaleFactor = options.deviceScaleFactor ?? DEFAULT_SCALE;\r\n const autoWidth = !options.width || options.width === 0;\r\n\r\n // If tapeMm provided, use CSS-equivalent viewport height; otherwise use target height directly\r\n const cssHeight = options.tapeMm\r\n ? Math.round(options.tapeMm * CSS_DPI / MM_PER_INCH)\r\n : options.height;\r\n const targetHeight = options.height;\r\n\r\n try {\r\n // Use large initial width for auto-detection, or specified width\r\n const initialWidth = autoWidth ? 4000 : options.width!;\r\n await page.setViewport({\r\n width: initialWidth,\r\n height: cssHeight,\r\n deviceScaleFactor: scaleFactor,\r\n });\r\n\r\n await page.goto(url, { waitUntil: \"networkidle0\" });\r\n\r\n // Process <img qr=\"...\"> elements\r\n await processQrElements(page);\r\n\r\n // Auto-detect content width if needed\r\n let finalCssWidth = initialWidth;\r\n if (autoWidth) {\r\n finalCssWidth = await page.evaluate(() => {\r\n // Try first child of body (likely the main container)\r\n const firstChild = document.body.firstElementChild as HTMLElement;\r\n if (firstChild) {\r\n const rect = firstChild.getBoundingClientRect();\r\n if (rect.width > 0 && rect.width < 3000) {\r\n return Math.ceil(rect.width);\r\n }\r\n }\r\n // Fall back to scroll width (actual content width)\r\n return document.body.scrollWidth;\r\n });\r\n // Resize viewport to actual content width\r\n await page.setViewport({\r\n width: finalCssWidth,\r\n height: cssHeight,\r\n deviceScaleFactor: scaleFactor,\r\n });\r\n }\r\n\r\n const pngBuffer = await page.screenshot({ type: \"png\" });\r\n\r\n // Scale to target dimensions\r\n // If tapeMm was used, scale from CSS dimensions to target 300 DPI dimensions\r\n const scale = targetHeight / cssHeight;\r\n const finalWidth = Math.round(finalCssWidth * scale);\r\n\r\n if (scaleFactor > 1 || options.tapeMm) {\r\n return scaleDown(Buffer.from(pngBuffer), finalWidth, targetHeight);\r\n }\r\n return Buffer.from(pngBuffer);\r\n } finally {\r\n await page.close();\r\n }\r\n}\r\n\r\n/**\r\n * Render HTML file and save to PNG file\r\n */\r\nexport async function renderHtmlToFile(\r\n htmlPath: string,\r\n outputPath: string,\r\n options: RenderOptions\r\n): Promise<void> {\r\n const buffer = await renderHtmlFile(htmlPath, options);\r\n fs.writeFileSync(outputPath, buffer);\r\n}\r\n\r\n// Legacy export for compatibility\r\nexport const renderHtml = renderHtmlFile;\r\n"]}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["render.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACH,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,YAAY,GAEf,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,cAAc,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE7E,oEAAoE;AACpE,MAAM,CAAC,MAAM,UAAU,GAAG,eAAe,CAAC;AAE1C,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAClC,QAAgB,EAChB,UAAkB,EAClB,OAAwF;IAExF,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACxD,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC","sourcesContent":["/**\n * Backward-compat shim. The HTML rendering logic now lives in\n * @bobfrankston/label-core. This file just re-exports those entry points so\n * existing deep imports keep working.\n */\n\nexport {\n renderHtmlString,\n renderHtmlFile,\n renderHtmlUrl,\n closeBrowser,\n type RenderOptions,\n} from \"@bobfrankston/label-core\";\n\nimport { renderHtmlFile as _renderHtmlFile } from \"@bobfrankston/label-core\";\n\n/** @deprecated Use renderHtmlFile from @bobfrankston/label-core. */\nexport const renderHtml = _renderHtmlFile;\n\n/** @deprecated Render to a file by calling renderHtmlFile then fs.writeFileSync. */\nexport async function renderHtmlToFile(\n htmlPath: string,\n outputPath: string,\n options: { width?: number; height: number; tapeMm?: number; deviceScaleFactor?: number }\n): Promise<void> {\n const fs = await import(\"fs\");\n const buffer = await _renderHtmlFile(htmlPath, options);\n fs.writeFileSync(outputPath, buffer);\n}\n"]}
package/render.ts CHANGED
@@ -1,280 +1,29 @@
1
- /**
2
- * HTML to bitmap rendering module
3
- * Standalone utility for converting HTML to PNG images
4
- */
5
-
6
- import puppeteer, { Browser } from "puppeteer";
7
- import * as path from "path";
8
- import * as fs from "fs";
9
- import { Jimp } from "jimp";
10
- import QRCode from "qrcode";
11
-
12
- export interface RenderOptions {
13
- width?: number; // If omitted or 0, auto-detect from content
14
- height: number; // Target output height in pixels (300 DPI)
15
- tapeMm?: number; // Tape size in mm - if set, viewport matches CSS mm interpretation
16
- deviceScaleFactor?: number; // Default 3 for high quality (288 DPI effective)
17
- keepScale?: boolean; // If true, don't scale down - keep full resolution output
18
- }
19
-
20
- let browserInstance: Browser | null = null;
21
-
22
- const DEFAULT_SCALE = 3; // Render at 3x for quality, then scale down
23
- const CSS_DPI = 96; // CSS interprets mm at 96 DPI
24
- const MM_PER_INCH = 25.4;
25
-
26
- /**
27
- * Generate QR code data URLs for all qr attributes found in page
28
- * Returns map of qr data -> data URL
29
- */
30
- async function generateQrDataUrls(qrValues: string[]): Promise<Map<string, string>> {
31
- const map = new Map<string, string>();
32
- for (const data of qrValues) {
33
- if (!map.has(data)) {
34
- const dataUrl = await QRCode.toDataURL(data, {
35
- type: "image/png",
36
- margin: 0,
37
- errorCorrectionLevel: "M",
38
- color: { dark: "#000000", light: "#ffffff" },
39
- });
40
- map.set(data, dataUrl);
41
- }
42
- }
43
- return map;
44
- }
45
-
46
- /**
47
- * Process <img qr="..."> elements in page via DOM manipulation
48
- */
49
- async function processQrElements(page: puppeteer.Page): Promise<void> {
50
- // Get all qr attribute values from the page
51
- const qrValues = await page.evaluate(() => {
52
- const imgs = document.querySelectorAll("img[qr]");
53
- return Array.from(imgs).map(img => img.getAttribute("qr") || "");
54
- });
55
-
56
- if (qrValues.length === 0) return;
57
-
58
- // Generate QR codes
59
- const qrMap = await generateQrDataUrls(qrValues);
60
-
61
- // Convert map to object for passing to page context
62
- const qrUrls: Record<string, string> = {};
63
- qrMap.forEach((url, data) => { qrUrls[data] = url; });
64
-
65
- // Replace qr attributes with src in the DOM
66
- await page.evaluate((urls: Record<string, string>) => {
67
- const imgs = document.querySelectorAll("img[qr]");
68
- imgs.forEach(img => {
69
- const qrData = img.getAttribute("qr");
70
- if (qrData && urls[qrData]) {
71
- img.setAttribute("src", urls[qrData]);
72
- img.removeAttribute("qr");
73
- }
74
- });
75
- }, qrUrls);
76
- }
77
-
78
- /**
79
- * Scale image buffer down to target dimensions
80
- */
81
- async function scaleDown(buffer: Buffer, targetWidth: number, targetHeight: number): Promise<Buffer> {
82
- const image = await Jimp.read(buffer);
83
- image.resize({ w: targetWidth, h: targetHeight });
84
- return image.getBuffer("image/png");
85
- }
86
-
87
- /**
88
- * Get or create a shared browser instance for better performance
89
- */
90
- async function getBrowser(): Promise<Browser> {
91
- if (!browserInstance || !browserInstance.connected) {
92
- browserInstance = await puppeteer.launch({ headless: true });
93
- }
94
- return browserInstance;
95
- }
96
-
97
- /**
98
- * Close the shared browser instance
99
- */
100
- export async function closeBrowser(): Promise<void> {
101
- if (browserInstance) {
102
- await browserInstance.close();
103
- browserInstance = null;
104
- }
105
- }
106
-
107
- /**
108
- * Render HTML file to PNG buffer
109
- * Processes <img qr="..."> tags to inline base64 QR codes
110
- */
111
- export async function renderHtmlFile(htmlPath: string, options: RenderOptions): Promise<Buffer> {
112
- const absolutePath = path.resolve(htmlPath);
113
- if (!fs.existsSync(absolutePath)) {
114
- throw new Error(`HTML file not found: ${absolutePath}`);
115
- }
116
- return renderHtmlUrl(`file://${absolutePath}`, options);
117
- }
118
-
119
- /**
120
- * Render HTML string to PNG buffer
121
- * Processes <img qr="..."> tags to inline base64 QR codes via DOM
122
- * @param html - HTML content string
123
- * @param options - Render dimensions (width auto-detected if omitted)
124
- * @param basePath - Optional base path for resolving relative resources
125
- */
126
- export async function renderHtmlString(html: string, options: RenderOptions, basePath?: string): Promise<Buffer> {
127
- const browser = await getBrowser();
128
- const page = await browser.newPage();
129
- const scaleFactor = options.deviceScaleFactor ?? DEFAULT_SCALE;
130
- const autoWidth = !options.width || options.width === 0;
131
-
132
- // If tapeMm provided, use CSS-equivalent viewport height; otherwise use target height directly
133
- const cssHeight = options.tapeMm
134
- ? Math.round(options.tapeMm * CSS_DPI / MM_PER_INCH)
135
- : options.height;
136
- const targetHeight = options.height;
137
-
138
- try {
139
- // Use large initial width for auto-detection, or specified width
140
- const initialWidth = autoWidth ? 4000 : options.width!;
141
- await page.setViewport({
142
- width: initialWidth,
143
- height: cssHeight,
144
- deviceScaleFactor: scaleFactor,
145
- });
146
-
147
- if (basePath) {
148
- // Set base URL for relative resource resolution
149
- const baseUrl = `file://${path.resolve(basePath)}/`;
150
- await page.goto(baseUrl, { waitUntil: "domcontentloaded" });
151
- await page.setContent(html, { waitUntil: "networkidle0" });
152
- } else {
153
- await page.setContent(html, { waitUntil: "networkidle0" });
154
- }
155
-
156
- // Process <img qr="..."> elements
157
- await processQrElements(page);
158
-
159
- // Auto-detect content width if needed
160
- let finalCssWidth = initialWidth;
161
- if (autoWidth) {
162
- finalCssWidth = await page.evaluate(() => {
163
- // Try first child of body (likely the main container)
164
- const firstChild = document.body.firstElementChild as HTMLElement;
165
- if (firstChild) {
166
- const rect = firstChild.getBoundingClientRect();
167
- if (rect.width > 0 && rect.width < 3000) {
168
- return Math.ceil(rect.width);
169
- }
170
- }
171
- // Fall back to scroll width (actual content width)
172
- return document.body.scrollWidth;
173
- });
174
- await page.setViewport({
175
- width: finalCssWidth,
176
- height: cssHeight,
177
- deviceScaleFactor: scaleFactor,
178
- });
179
- }
180
-
181
- const pngBuffer = await page.screenshot({ type: "png" });
182
-
183
- // Scale to target dimensions
184
- // If tapeMm was used, scale from CSS dimensions to target 300 DPI dimensions
185
- const scale = targetHeight / cssHeight;
186
- const finalWidth = Math.round(finalCssWidth * scale);
187
-
188
- if (scaleFactor > 1 || options.tapeMm) {
189
- return scaleDown(Buffer.from(pngBuffer), finalWidth, targetHeight);
190
- }
191
- return Buffer.from(pngBuffer);
192
- } finally {
193
- await page.close();
194
- }
195
- }
196
-
197
- /**
198
- * Render HTML from URL to PNG buffer
199
- * Processes <img qr="..."> tags to inline base64 QR codes via DOM
200
- * If width is not specified, auto-detects from content
201
- */
202
- export async function renderHtmlUrl(url: string, options: RenderOptions): Promise<Buffer> {
203
- const browser = await getBrowser();
204
- const page = await browser.newPage();
205
- const scaleFactor = options.deviceScaleFactor ?? DEFAULT_SCALE;
206
- const autoWidth = !options.width || options.width === 0;
207
-
208
- // If tapeMm provided, use CSS-equivalent viewport height; otherwise use target height directly
209
- const cssHeight = options.tapeMm
210
- ? Math.round(options.tapeMm * CSS_DPI / MM_PER_INCH)
211
- : options.height;
212
- const targetHeight = options.height;
213
-
214
- try {
215
- // Use large initial width for auto-detection, or specified width
216
- const initialWidth = autoWidth ? 4000 : options.width!;
217
- await page.setViewport({
218
- width: initialWidth,
219
- height: cssHeight,
220
- deviceScaleFactor: scaleFactor,
221
- });
222
-
223
- await page.goto(url, { waitUntil: "networkidle0" });
224
-
225
- // Process <img qr="..."> elements
226
- await processQrElements(page);
227
-
228
- // Auto-detect content width if needed
229
- let finalCssWidth = initialWidth;
230
- if (autoWidth) {
231
- finalCssWidth = await page.evaluate(() => {
232
- // Try first child of body (likely the main container)
233
- const firstChild = document.body.firstElementChild as HTMLElement;
234
- if (firstChild) {
235
- const rect = firstChild.getBoundingClientRect();
236
- if (rect.width > 0 && rect.width < 3000) {
237
- return Math.ceil(rect.width);
238
- }
239
- }
240
- // Fall back to scroll width (actual content width)
241
- return document.body.scrollWidth;
242
- });
243
- // Resize viewport to actual content width
244
- await page.setViewport({
245
- width: finalCssWidth,
246
- height: cssHeight,
247
- deviceScaleFactor: scaleFactor,
248
- });
249
- }
250
-
251
- const pngBuffer = await page.screenshot({ type: "png" });
252
-
253
- // Scale to target dimensions
254
- // If tapeMm was used, scale from CSS dimensions to target 300 DPI dimensions
255
- const scale = targetHeight / cssHeight;
256
- const finalWidth = Math.round(finalCssWidth * scale);
257
-
258
- if (scaleFactor > 1 || options.tapeMm) {
259
- return scaleDown(Buffer.from(pngBuffer), finalWidth, targetHeight);
260
- }
261
- return Buffer.from(pngBuffer);
262
- } finally {
263
- await page.close();
264
- }
265
- }
266
-
267
- /**
268
- * Render HTML file and save to PNG file
269
- */
270
- export async function renderHtmlToFile(
271
- htmlPath: string,
272
- outputPath: string,
273
- options: RenderOptions
274
- ): Promise<void> {
275
- const buffer = await renderHtmlFile(htmlPath, options);
276
- fs.writeFileSync(outputPath, buffer);
277
- }
278
-
279
- // Legacy export for compatibility
280
- export const renderHtml = renderHtmlFile;
1
+ /**
2
+ * Backward-compat shim. The HTML rendering logic now lives in
3
+ * @bobfrankston/label-core. This file just re-exports those entry points so
4
+ * existing deep imports keep working.
5
+ */
6
+
7
+ export {
8
+ renderHtmlString,
9
+ renderHtmlFile,
10
+ renderHtmlUrl,
11
+ closeBrowser,
12
+ type RenderOptions,
13
+ } from "@bobfrankston/label-core";
14
+
15
+ import { renderHtmlFile as _renderHtmlFile } from "@bobfrankston/label-core";
16
+
17
+ /** @deprecated Use renderHtmlFile from @bobfrankston/label-core. */
18
+ export const renderHtml = _renderHtmlFile;
19
+
20
+ /** @deprecated Render to a file by calling renderHtmlFile then fs.writeFileSync. */
21
+ export async function renderHtmlToFile(
22
+ htmlPath: string,
23
+ outputPath: string,
24
+ options: { width?: number; height: number; tapeMm?: number; deviceScaleFactor?: number }
25
+ ): Promise<void> {
26
+ const fs = await import("fs");
27
+ const buffer = await _renderHtmlFile(htmlPath, options);
28
+ fs.writeFileSync(outputPath, buffer);
29
+ }