@bobfrankston/brother-label 1.0.14 → 1.1.2
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/README.md +45 -2
- package/api.d.ts +26 -26
- package/api.d.ts.map +1 -1
- package/api.js +299 -464
- package/api.js.map +1 -1
- package/api.ts +429 -615
- package/cli.d.ts +1 -1
- package/cli.js +232 -262
- package/cli.js.map +1 -1
- package/cli.ts +301 -344
- package/index.d.ts +3 -1
- package/index.d.ts.map +1 -1
- package/index.js +4 -2
- package/index.js.map +1 -1
- package/index.ts +43 -24
- package/package.json +16 -1
- package/render.d.ts +11 -33
- package/render.d.ts.map +1 -1
- package/render.js +11 -235
- package/render.js.map +1 -1
- package/render.ts +29 -280
- package/tsconfig.json +25 -19
package/render.ts
CHANGED
|
@@ -1,280 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
} from "@bobfrankston/label-core";
|
|
13
|
+
export type { RenderOptions } 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
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"
|
|
9
|
-
"forceConsistentCasingInFileNames": true,
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"inlineSources": true,
|
|
15
|
+
"strictNullChecks": false,
|
|
16
|
+
"noImplicitAny": true,
|
|
17
|
+
"noImplicitReturns": false,
|
|
18
|
+
"noImplicitThis": true,
|
|
19
|
+
"newLine": "lf",
|
|
20
|
+
"outDir": ".",
|
|
21
|
+
"rootDir": "."
|
|
22
|
+
},
|
|
23
|
+
"include": ["*.ts"],
|
|
24
|
+
"exclude": ["node_modules", "cruft", ".git", "tests", "prev"]
|
|
25
|
+
}
|