@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/README.md +63 -3
- package/api.d.ts +27 -16
- package/api.d.ts.map +1 -1
- package/api.js +301 -361
- package/api.js.map +1 -1
- package/api.ts +427 -505
- package/cli.d.ts +1 -1
- package/cli.js +239 -129
- package/cli.js.map +1 -1
- package/cli.ts +301 -184
- package/index.d.ts +2 -1
- package/index.d.ts.map +1 -1
- package/index.js +4 -2
- package/index.js.map +1 -1
- package/index.ts +40 -21
- package/package.json +18 -3
- package/render.d.ts +10 -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/brother-print.d.ts +0 -3
- package/brother-print.d.ts.map +0 -1
- package/brother-print.js +0 -629
- package/brother-print.js.map +0 -1
- package/brother-print.ts +0 -697
package/render.d.ts
CHANGED
|
@@ -1,40 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
+
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
|
+
}
|