@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/api.js
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Brother Label Printer API
|
|
3
|
-
* Programmatic interface for printing labels on Brother P-touch printers
|
|
3
|
+
* Programmatic interface for printing labels on Brother P-touch printers.
|
|
4
|
+
*
|
|
5
|
+
* Implements the unified LabelPrinter interface from @bobfrankston/label-core.
|
|
6
|
+
* All printer-agnostic logic (rendering, segments, clipboard, CLI primitives,
|
|
7
|
+
* online-wait) lives in label-core. This file holds the Brother-specific bits:
|
|
8
|
+
* TAPE_SIZES catalog, MEDIA_OPTIONS (CustomMediaSize* names), XPS PrintTicket
|
|
9
|
+
* builder with the Brother namespace, and detectTapeSize.
|
|
4
10
|
*/
|
|
5
11
|
import * as fs from "fs";
|
|
6
|
-
import * as path from "path";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
12
|
import * as os from "os";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import QRCode from "qrcode";
|
|
13
|
-
// Constants
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { validateContent, resolveContent, renderSegments as coreRenderSegments, listPrinters as coreListPrinters, getPrinterStatus, waitOnline as coreWaitOnline, runPowerShellOrThrow, readJsonConfig, mergeJsonConfig, } from "@bobfrankston/label-core";
|
|
15
|
+
/* ----- Constants ----- */
|
|
14
16
|
const CONFIG_PATH = path.join(os.homedir(), ".brother-label.json");
|
|
15
17
|
const DEFAULT_PRINTER = "Brother PT-P710BT";
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
+
const DEFAULT_PRINTER_PATTERN = /brother|^pt-|^ql-/i;
|
|
19
|
+
const PRINT_DPI = 300;
|
|
20
|
+
const VALID_TAPES = [6, 9, 12, 18, 24];
|
|
21
|
+
/** Tape sizes: width in pixels @ 300 DPI = continuous direction; height = perpendicular. */
|
|
18
22
|
const TAPE_SIZES = {
|
|
19
|
-
6: { width: 1137, height: 56 },
|
|
20
|
-
9: { width: 1137, height: 84 },
|
|
21
|
-
12: { width: 1137, height: 113 },
|
|
22
|
-
18: { width: 1137, height: 169 },
|
|
23
|
-
24: { width: 1137, height: 213 },
|
|
23
|
+
6: { width: 1137, height: 56 },
|
|
24
|
+
9: { width: 1137, height: 84 },
|
|
25
|
+
12: { width: 1137, height: 113 },
|
|
26
|
+
18: { width: 1137, height: 169 },
|
|
27
|
+
24: { width: 1137, height: 213 },
|
|
24
28
|
};
|
|
25
|
-
|
|
29
|
+
/** Brother CustomMediaSize names + tape widths in microns (XPS PrintTicket). */
|
|
26
30
|
const MEDIA_OPTIONS = {
|
|
27
31
|
6: { name: "CustomMediaSize257", width: 5900 },
|
|
28
32
|
9: { name: "CustomMediaSize258", width: 9000 },
|
|
@@ -30,492 +34,323 @@ const MEDIA_OPTIONS = {
|
|
|
30
34
|
18: { name: "CustomMediaSize260", width: 18100 },
|
|
31
35
|
24: { name: "CustomMediaSize261", width: 24000 },
|
|
32
36
|
};
|
|
33
|
-
|
|
37
|
+
/* ----- Config (backward-compat — note "defaultTape" stays in JSON) ----- */
|
|
34
38
|
export function getConfig() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Handle legacy format where tape was stored as "24mm" string
|
|
39
|
-
if (typeof raw.defaultTape === "string") {
|
|
40
|
-
raw.defaultTape = parseInt(raw.defaultTape.replace("mm", ""), 10);
|
|
41
|
-
}
|
|
42
|
-
return raw;
|
|
43
|
-
}
|
|
39
|
+
const raw = readJsonConfig(CONFIG_PATH);
|
|
40
|
+
if (typeof raw.defaultTape === "string") {
|
|
41
|
+
raw.defaultTape = parseInt(raw.defaultTape.replace("mm", ""), 10);
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
// Ignore config errors
|
|
47
|
-
}
|
|
48
|
-
return {};
|
|
43
|
+
return raw;
|
|
49
44
|
}
|
|
50
45
|
export function setConfig(config) {
|
|
51
|
-
|
|
52
|
-
const merged = { ...current, ...config };
|
|
53
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
46
|
+
mergeJsonConfig(CONFIG_PATH, config);
|
|
54
47
|
}
|
|
55
48
|
export function getConfigPath() {
|
|
56
49
|
return CONFIG_PATH;
|
|
57
50
|
}
|
|
58
|
-
|
|
51
|
+
/* ----- Printer listing (backward-compat shape: { name }[]) ----- */
|
|
59
52
|
export async function listPrinters() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"-Command",
|
|
63
|
-
"Get-Printer | Where-Object { $_.Name -like '*Brother*' -or $_.Name -like '*PT*' -or $_.Name -like '*QL*' } | Select-Object -ExpandProperty Name"
|
|
64
|
-
], { stdio: "pipe" });
|
|
65
|
-
let stdout = "";
|
|
66
|
-
let stderr = "";
|
|
67
|
-
ps.stdout.on("data", (data) => { stdout += data.toString(); });
|
|
68
|
-
ps.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
69
|
-
ps.on("close", (code) => {
|
|
70
|
-
if (code !== 0) {
|
|
71
|
-
reject(new Error(`Failed to list printers: ${stderr}`));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const printers = stdout.trim().split("\n")
|
|
75
|
-
.filter(p => p.trim())
|
|
76
|
-
.map(name => ({ name: name.trim() }));
|
|
77
|
-
resolve(printers);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
53
|
+
const ps = await coreListPrinters(DEFAULT_PRINTER_PATTERN);
|
|
54
|
+
return ps.map(p => ({ name: p.name }));
|
|
80
55
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Auto-detect tape size from printer's default print ticket. Returns null if
|
|
58
|
+
* detection fails. Brother's driver embeds the loaded tape's CustomMediaSize
|
|
59
|
+
* name in the default PrintTicket XML.
|
|
60
|
+
*/
|
|
61
|
+
export async function detectTapeSize(printerName) {
|
|
62
|
+
const config = getConfig();
|
|
63
|
+
const printer = printerName ?? config.defaultPrinter ?? DEFAULT_PRINTER;
|
|
64
|
+
const script = `
|
|
65
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
66
|
+
Add-Type -AssemblyName System.Printing | Out-Null
|
|
67
|
+
try {
|
|
68
|
+
$server = New-Object System.Printing.LocalPrintServer
|
|
69
|
+
$queue = $server.GetPrintQueue('${printer.replace(/'/g, "''")}')
|
|
70
|
+
$ticket = $queue.DefaultPrintTicket
|
|
71
|
+
$stream = $ticket.GetXmlStream()
|
|
72
|
+
$reader = New-Object System.IO.StreamReader($stream)
|
|
73
|
+
Write-Output $reader.ReadToEnd()
|
|
74
|
+
$reader.Close()
|
|
75
|
+
} catch {
|
|
76
|
+
Write-Output "ERROR"
|
|
102
77
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
options.imagePath,
|
|
111
|
-
options.imageBuffer,
|
|
112
|
-
options.qr,
|
|
113
|
-
].filter(f => f !== undefined);
|
|
114
|
-
if (contentFields.length === 0) {
|
|
115
|
-
throw new Error("No content provided. Specify one of: text, html, htmlPath, textFile, imagePath, imageBuffer, qr");
|
|
116
|
-
}
|
|
117
|
-
if (contentFields.length > 1) {
|
|
118
|
-
throw new Error("Multiple content options provided. Specify exactly one.");
|
|
119
|
-
}
|
|
120
|
-
if (options.tape !== undefined && !TAPE_SIZES[options.tape]) {
|
|
121
|
-
throw new Error(`Invalid tape size: ${options.tape}. Valid sizes: 6, 9, 12, 18, 24`);
|
|
78
|
+
`;
|
|
79
|
+
try {
|
|
80
|
+
const out = await runPowerShellOrThrow(script);
|
|
81
|
+
for (const [size, media] of Object.entries(MEDIA_OPTIONS)) {
|
|
82
|
+
if (out.includes(media.name))
|
|
83
|
+
return parseInt(size, 10);
|
|
84
|
+
}
|
|
122
85
|
}
|
|
86
|
+
catch { /* fall through */ }
|
|
87
|
+
return null;
|
|
123
88
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
function parseSize(spec, referencePx) {
|
|
134
|
-
const trimmed = spec.trim().toLowerCase();
|
|
135
|
-
if (trimmed.endsWith("px")) {
|
|
136
|
-
const px = parseFloat(trimmed.slice(0, -2));
|
|
137
|
-
if (isNaN(px) || px < 0)
|
|
138
|
-
throw new Error(`Invalid size: ${spec}`);
|
|
139
|
-
return Math.round(px);
|
|
89
|
+
/* ----- Brother LabelPrinter implementation ----- */
|
|
90
|
+
class BrotherPrinterImpl {
|
|
91
|
+
driverName = "brother";
|
|
92
|
+
getConfig() {
|
|
93
|
+
const c = getConfig();
|
|
94
|
+
return {
|
|
95
|
+
defaultPrinter: c.defaultPrinter,
|
|
96
|
+
defaultMedia: c.defaultTape !== undefined ? String(c.defaultTape) : undefined,
|
|
97
|
+
};
|
|
140
98
|
}
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
99
|
+
setConfig(config) {
|
|
100
|
+
const update = {};
|
|
101
|
+
if (config.defaultPrinter !== undefined)
|
|
102
|
+
update.defaultPrinter = config.defaultPrinter;
|
|
103
|
+
if (config.defaultMedia !== undefined) {
|
|
104
|
+
const t = parseInt(config.defaultMedia, 10);
|
|
105
|
+
if (VALID_TAPES.includes(t))
|
|
106
|
+
update.defaultTape = t;
|
|
107
|
+
}
|
|
108
|
+
setConfig(update);
|
|
146
109
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (isNaN(mm) || mm < 0)
|
|
150
|
-
throw new Error(`Invalid size: ${spec}`);
|
|
151
|
-
return Math.round(mm * PRINT_DPI / 25.4);
|
|
110
|
+
getConfigPath() {
|
|
111
|
+
return CONFIG_PATH;
|
|
152
112
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
113
|
+
listMedia() {
|
|
114
|
+
return VALID_TAPES.map(t => ({
|
|
115
|
+
id: String(t),
|
|
116
|
+
name: `${t}mm tape`,
|
|
117
|
+
widthPx: TAPE_SIZES[t].height, /** "media width" (perpendicular to feed) */
|
|
118
|
+
lengthPx: 0, /** continuous tape — length determined by content */
|
|
119
|
+
continuous: true,
|
|
120
|
+
}));
|
|
158
121
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const trimmed = spec.trim().toLowerCase();
|
|
168
|
-
if (trimmed.endsWith("%")) {
|
|
169
|
-
const pct = parseFloat(trimmed.slice(0, -1));
|
|
170
|
-
if (isNaN(pct) || pct <= 0)
|
|
171
|
-
throw new Error(`Invalid text height: ${spec}`);
|
|
172
|
-
return Math.round(tapeHeightPx * pct / 100);
|
|
122
|
+
getMedia(id) {
|
|
123
|
+
const t = parseInt(id.replace("mm", ""), 10);
|
|
124
|
+
if (!VALID_TAPES.includes(t))
|
|
125
|
+
return null;
|
|
126
|
+
return {
|
|
127
|
+
id: String(t), name: `${t}mm tape`,
|
|
128
|
+
widthPx: TAPE_SIZES[t].height, lengthPx: 0, continuous: true,
|
|
129
|
+
};
|
|
173
130
|
}
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
throw new Error(`Invalid text height: ${spec}`);
|
|
178
|
-
return Math.round(mm * PRINT_DPI / 25.4);
|
|
131
|
+
async detectMedia(printerName) {
|
|
132
|
+
const t = await detectTapeSize(printerName);
|
|
133
|
+
return t ? this.getMedia(String(t)) : null;
|
|
179
134
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (isNaN(inches) || inches <= 0)
|
|
183
|
-
throw new Error(`Invalid text height: ${spec}`);
|
|
184
|
-
return Math.round(inches * PRINT_DPI);
|
|
135
|
+
async listPrinters() {
|
|
136
|
+
return coreListPrinters(DEFAULT_PRINTER_PATTERN);
|
|
185
137
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
async function renderText(text, tape, textHeightSpec) {
|
|
190
|
-
const tapeSize = TAPE_SIZES[tape];
|
|
191
|
-
const targetHeight = textHeightSpec
|
|
192
|
-
? Math.min(parseTextHeight(textHeightSpec, tapeSize.height), tapeSize.height)
|
|
193
|
-
: tapeSize.height;
|
|
194
|
-
const lines = text.split("\\n").join("\n");
|
|
195
|
-
// Load largest font for quality
|
|
196
|
-
const fontDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "node_modules/@jimp/plugin-print/fonts/open-sans");
|
|
197
|
-
const fontPath = path.join(fontDir, "open-sans-128-black", "open-sans-128-black.fnt");
|
|
198
|
-
const font = await loadFont(fontPath);
|
|
199
|
-
// Measure text
|
|
200
|
-
const textWidth = measureText(font, lines);
|
|
201
|
-
const textHeight = measureTextHeight(font, lines, textWidth + 100);
|
|
202
|
-
// Create temp image for text
|
|
203
|
-
const padding = 10;
|
|
204
|
-
const imgWidth = textWidth + padding * 2;
|
|
205
|
-
const imgHeight = textHeight + padding * 2;
|
|
206
|
-
const tempImage = new Jimp({ width: imgWidth, height: imgHeight, color: 0xffffffff });
|
|
207
|
-
tempImage.print({ font, x: padding, y: padding, text: lines });
|
|
208
|
-
// Scale to fit tape height
|
|
209
|
-
const scale = targetHeight / imgHeight;
|
|
210
|
-
const finalWidth = Math.round(imgWidth * scale);
|
|
211
|
-
const finalHeight = Math.round(imgHeight * scale);
|
|
212
|
-
tempImage.resize({ w: finalWidth, h: finalHeight });
|
|
213
|
-
// Create final image with padding
|
|
214
|
-
const canvasHeight = tapeSize.height;
|
|
215
|
-
const hPadding = Math.round(canvasHeight * 0.2);
|
|
216
|
-
const outputWidth = finalWidth + hPadding * 2;
|
|
217
|
-
const image = new Jimp({ width: outputWidth, height: canvasHeight, color: 0xffffffff });
|
|
218
|
-
const xOffset = hPadding;
|
|
219
|
-
const yOffset = Math.round((canvasHeight - finalHeight) / 2);
|
|
220
|
-
image.composite(tempImage, xOffset, yOffset);
|
|
221
|
-
return image.getBuffer("image/png");
|
|
222
|
-
}
|
|
223
|
-
// Render QR code to image buffer
|
|
224
|
-
async function renderQr(data, tape, labelText) {
|
|
225
|
-
const tapeSize = TAPE_SIZES[tape];
|
|
226
|
-
const targetHeight = tapeSize.height;
|
|
227
|
-
const qrSize = Math.floor(targetHeight * 0.95);
|
|
228
|
-
// Margins in pixels at PRINT_DPI (3mm left, 4mm right)
|
|
229
|
-
const pxPerMm = PRINT_DPI / 25.4;
|
|
230
|
-
const leftMargin = Math.round(3 * pxPerMm);
|
|
231
|
-
const rightMargin = Math.round(4 * pxPerMm);
|
|
232
|
-
// Generate QR code at high resolution
|
|
233
|
-
const qrBuffer = await QRCode.toBuffer(data, {
|
|
234
|
-
type: "png",
|
|
235
|
-
width: qrSize,
|
|
236
|
-
margin: 0,
|
|
237
|
-
errorCorrectionLevel: "M",
|
|
238
|
-
color: { dark: "#000000", light: "#ffffff" },
|
|
239
|
-
});
|
|
240
|
-
const qrImage = await Jimp.read(qrBuffer);
|
|
241
|
-
if (!labelText) {
|
|
242
|
-
// QR only
|
|
243
|
-
const outputWidth = leftMargin + qrSize + rightMargin;
|
|
244
|
-
const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });
|
|
245
|
-
const yOffset = Math.floor((targetHeight - qrSize) / 2);
|
|
246
|
-
image.composite(qrImage, leftMargin, yOffset);
|
|
247
|
-
return image.getBuffer("image/png");
|
|
138
|
+
async getStatus(printerName) {
|
|
139
|
+
const printer = printerName ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
140
|
+
return getPrinterStatus(printer);
|
|
248
141
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const font = await loadFont(fontPath);
|
|
253
|
-
const textWidth = measureText(font, labelText);
|
|
254
|
-
const textHeight = measureTextHeight(font, labelText, textWidth + 50);
|
|
255
|
-
// Scale text to fit beside QR
|
|
256
|
-
const maxTextHeight = targetHeight * 0.8;
|
|
257
|
-
const textScale = Math.min(1, maxTextHeight / textHeight);
|
|
258
|
-
const scaledTextWidth = Math.round(textWidth * textScale);
|
|
259
|
-
const scaledTextHeight = Math.round(textHeight * textScale);
|
|
260
|
-
// Create text image
|
|
261
|
-
const textImg = new Jimp({ width: textWidth + 20, height: textHeight + 20, color: 0xffffffff });
|
|
262
|
-
textImg.print({ font, x: 10, y: 10, text: labelText });
|
|
263
|
-
if (textScale < 1) {
|
|
264
|
-
textImg.resize({ w: scaledTextWidth, h: scaledTextHeight });
|
|
142
|
+
async waitOnline(printerName, opts = {}) {
|
|
143
|
+
const printer = printerName ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
144
|
+
return coreWaitOnline(printer, opts, DEFAULT_PRINTER_PATTERN);
|
|
265
145
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const qrY = Math.floor((targetHeight - qrSize) / 2);
|
|
271
|
-
image.composite(qrImage, leftMargin, qrY);
|
|
272
|
-
const textY = Math.floor((targetHeight - scaledTextHeight) / 2);
|
|
273
|
-
image.composite(textImg, leftMargin + qrSize + gap, textY);
|
|
274
|
-
return image.getBuffer("image/png");
|
|
275
|
-
}
|
|
276
|
-
// Render content to image buffer
|
|
277
|
-
async function renderContent(options, tape) {
|
|
278
|
-
const tapeSize = TAPE_SIZES[tape];
|
|
279
|
-
if (options.text !== undefined) {
|
|
280
|
-
return renderText(options.text, tape, options.textHeight);
|
|
146
|
+
async render(opts) {
|
|
147
|
+
validateContent(opts);
|
|
148
|
+
const tape = await this.resolveTape(opts.media);
|
|
149
|
+
return resolveContent(opts, TAPE_SIZES[tape].height, tape);
|
|
281
150
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
151
|
+
async print(opts) {
|
|
152
|
+
validateContent(opts);
|
|
153
|
+
const printer = opts.printer ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
154
|
+
const tape = await this.resolveTape(opts.media);
|
|
155
|
+
const image = await resolveContent(opts, TAPE_SIZES[tape].height, tape);
|
|
156
|
+
await ensureOnline(printer, opts);
|
|
157
|
+
await printBuffer(image, printer, tape);
|
|
158
|
+
return { image, media: this.getMedia(String(tape)), printer };
|
|
285
159
|
}
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
await closeBrowser();
|
|
290
|
-
return buffer;
|
|
160
|
+
async renderSegments(segments, opts = {}) {
|
|
161
|
+
const tape = await this.resolveTape(opts.media);
|
|
162
|
+
return coreRenderSegments(segments, TAPE_SIZES[tape].height, opts.textHeight, opts.space);
|
|
291
163
|
}
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
await
|
|
296
|
-
|
|
164
|
+
async printSegments(segments, opts = {}) {
|
|
165
|
+
const printer = opts.printer ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
166
|
+
const tape = await this.resolveTape(opts.media);
|
|
167
|
+
const image = await coreRenderSegments(segments, TAPE_SIZES[tape].height, opts.textHeight, opts.space);
|
|
168
|
+
await ensureOnline(printer, opts);
|
|
169
|
+
await printBuffer(image, printer, tape);
|
|
170
|
+
return { image, media: this.getMedia(String(tape)), printer };
|
|
297
171
|
}
|
|
298
|
-
|
|
299
|
-
|
|
172
|
+
async resolveTape(media) {
|
|
173
|
+
if (media !== undefined) {
|
|
174
|
+
const t = parseInt(media.replace("mm", ""), 10);
|
|
175
|
+
if (!VALID_TAPES.includes(t))
|
|
176
|
+
throw new Error(`Invalid tape size: ${media}. Valid: ${VALID_TAPES.join(", ")}`);
|
|
177
|
+
return t;
|
|
178
|
+
}
|
|
179
|
+
const cfg = getConfig();
|
|
180
|
+
if (cfg.defaultTape)
|
|
181
|
+
return cfg.defaultTape;
|
|
182
|
+
const detected = await detectTapeSize();
|
|
183
|
+
return detected ?? 24;
|
|
300
184
|
}
|
|
301
|
-
|
|
302
|
-
|
|
185
|
+
}
|
|
186
|
+
/** Public Brother singleton (implements LabelPrinter). */
|
|
187
|
+
export const brotherPrinter = new BrotherPrinterImpl();
|
|
188
|
+
/* ----- Backward-compatible function exports ----- */
|
|
189
|
+
/**
|
|
190
|
+
* Translate brother-label PrintOptions (with tape) to core (with media).
|
|
191
|
+
*/
|
|
192
|
+
function toCore(opts) {
|
|
193
|
+
const out = { ...opts };
|
|
194
|
+
if (opts.tape !== undefined && out.media === undefined) {
|
|
195
|
+
out.media = String(opts.tape);
|
|
303
196
|
}
|
|
304
|
-
|
|
305
|
-
|
|
197
|
+
delete out.tape;
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function toCoreSeg(opts = {}) {
|
|
201
|
+
const out = { ...opts };
|
|
202
|
+
if (opts.tape !== undefined && out.media === undefined)
|
|
203
|
+
out.media = String(opts.tape);
|
|
204
|
+
delete out.tape;
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
export async function render(options) {
|
|
208
|
+
return brotherPrinter.render(toCore(options));
|
|
209
|
+
}
|
|
210
|
+
export async function print(options) {
|
|
211
|
+
const r = await brotherPrinter.print(toCore(options));
|
|
212
|
+
return { image: r.image };
|
|
213
|
+
}
|
|
214
|
+
export async function renderSegments(segments, tape, textHeight, space) {
|
|
215
|
+
return brotherPrinter.renderSegments(segments, {
|
|
216
|
+
media: tape !== undefined ? String(tape) : undefined,
|
|
217
|
+
textHeight, space,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
export async function printSegments(segments, options) {
|
|
221
|
+
const r = await brotherPrinter.printSegments(segments, toCoreSeg(options));
|
|
222
|
+
return { image: r.image };
|
|
223
|
+
}
|
|
224
|
+
/* ----- Brother XPS print path (kept here, Brother-specific) ----- */
|
|
225
|
+
async function ensureOnline(printer, opts) {
|
|
226
|
+
const status = await getPrinterStatus(printer);
|
|
227
|
+
if (status.online)
|
|
228
|
+
return;
|
|
229
|
+
if (opts.wait === false) {
|
|
230
|
+
throw new Error(`Printer "${printer}" is offline (${status.statusText}${status.error ? ", " + status.error : ""}). Use wait:true to wait, or pick another printer.`);
|
|
306
231
|
}
|
|
307
|
-
|
|
232
|
+
if (opts.log)
|
|
233
|
+
opts.log(`[brother-label] ${printer} offline (${status.statusText}); waiting...`);
|
|
234
|
+
await coreWaitOnline(printer, {
|
|
235
|
+
timeoutMs: opts.waitTimeoutMs,
|
|
236
|
+
intervalMs: opts.waitIntervalMs,
|
|
237
|
+
onWaiting: opts.onWaiting,
|
|
238
|
+
log: opts.log,
|
|
239
|
+
}, DEFAULT_PRINTER_PATTERN);
|
|
308
240
|
}
|
|
309
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Print a single image buffer using the Brother XPS PrintTicket path.
|
|
243
|
+
* Builds an XPS document with a custom CustomMediaSize for the tape, then
|
|
244
|
+
* sends it to the print queue.
|
|
245
|
+
*/
|
|
310
246
|
async function printBuffer(imageBuffer, printer, tape) {
|
|
311
247
|
const media = MEDIA_OPTIONS[tape];
|
|
312
248
|
const tempPath = path.join(os.tmpdir(), `label-${Date.now()}.png`);
|
|
313
249
|
fs.writeFileSync(tempPath, imageBuffer);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
Add-Type -AssemblyName System.Drawing
|
|
318
|
-
Add-Type -AssemblyName System.Printing
|
|
319
|
-
Add-Type -AssemblyName ReachFramework
|
|
320
|
-
Add-Type -AssemblyName PresentationCore
|
|
321
|
-
|
|
322
|
-
$img = [System.Drawing.Image]::FromFile('${
|
|
323
|
-
|
|
324
|
-
$DPI = ${PRINT_DPI}
|
|
325
|
-
$MICRONS_PER_INCH = 25400
|
|
326
|
-
|
|
327
|
-
$labelLengthMicrons = [int]($img.Width / $DPI * $MICRONS_PER_INCH) + 3000
|
|
328
|
-
$tapeWidthMicrons = ${media.width}
|
|
329
|
-
|
|
330
|
-
$imgWidthWpf = $img.Width / $DPI * 96
|
|
331
|
-
$imgHeightWpf = $img.Height / $DPI * 96
|
|
332
|
-
|
|
333
|
-
$ticketXml = @"
|
|
334
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
335
|
-
<psf:PrintTicket xmlns:psf="http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework"
|
|
336
|
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1"
|
|
337
|
-
xmlns:psk="http://schemas.microsoft.com/windows/2003/08/printing/printschemakeywords"
|
|
338
|
-
xmlns:ns0001="http://schemas.brother.info/mfc/printing/2006/11/printschemakeywords">
|
|
339
|
-
<psf:Feature name="psk:PageMediaSize">
|
|
340
|
-
<psf:Option name="ns0001:${media.name}">
|
|
341
|
-
<psf:ScoredProperty name="psk:MediaSizeWidth">
|
|
342
|
-
<psf:Value xsi:type="xsd:integer">${media.width}</psf:Value>
|
|
343
|
-
</psf:ScoredProperty>
|
|
344
|
-
<psf:ScoredProperty name="psk:MediaSizeHeight">
|
|
345
|
-
<psf:ParameterRef name="psk:PageMediaSizeMediaSizeHeight" />
|
|
346
|
-
</psf:ScoredProperty>
|
|
347
|
-
</psf:Option>
|
|
348
|
-
</psf:Feature>
|
|
349
|
-
<psf:ParameterInit name="psk:PageMediaSizeMediaSizeHeight">
|
|
350
|
-
<psf:Value xsi:type="xsd:integer">$labelLengthMicrons</psf:Value>
|
|
351
|
-
</psf:ParameterInit>
|
|
352
|
-
<psf:Feature name="psk:PageOrientation">
|
|
353
|
-
<psf:Option name="psk:Landscape" />
|
|
354
|
-
</psf:Feature>
|
|
355
|
-
<psf:Feature name="ns0001:PageRollFeedToEndOfSheet">
|
|
356
|
-
<psf:Option name="ns0001:FeedToImageEdge" />
|
|
357
|
-
</psf:Feature>
|
|
358
|
-
<psf:Feature name="psk:PageMediaType">
|
|
359
|
-
<psf:Option name="psk:Label" />
|
|
360
|
-
</psf:Feature>
|
|
361
|
-
<psf:Feature name="ns0001:JobRollCutAtEndOfJob">
|
|
362
|
-
<psf:Option name="ns0001:Cut" />
|
|
363
|
-
</psf:Feature>
|
|
364
|
-
</psf:PrintTicket>
|
|
365
|
-
"@
|
|
366
|
-
|
|
367
|
-
$xmlBytes = [System.Text.Encoding]::UTF8.GetBytes($ticketXml)
|
|
368
|
-
$memStream = New-Object System.IO.MemoryStream(,$xmlBytes)
|
|
369
|
-
$ticket = New-Object System.Printing.PrintTicket($memStream)
|
|
370
|
-
|
|
371
|
-
$server = New-Object System.Printing.LocalPrintServer
|
|
372
|
-
$queue = $server.GetPrintQueue('${printer}')
|
|
373
|
-
|
|
374
|
-
$xpsPath = [System.IO.Path]::GetTempFileName() + ".xps"
|
|
375
|
-
|
|
376
|
-
$pageWidth = $labelLengthMicrons / $MICRONS_PER_INCH * 96
|
|
377
|
-
$pageHeight = $tapeWidthMicrons / $MICRONS_PER_INCH * 96
|
|
378
|
-
|
|
379
|
-
$package = [System.IO.Packaging.Package]::Open($xpsPath, [System.IO.FileMode]::Create)
|
|
380
|
-
$xpsDoc = New-Object System.Windows.Xps.Packaging.XpsDocument($package)
|
|
381
|
-
$writer = [System.Windows.Xps.Packaging.XpsDocument]::CreateXpsDocumentWriter($xpsDoc)
|
|
382
|
-
|
|
383
|
-
$visual = New-Object System.Windows.Media.DrawingVisual
|
|
384
|
-
$dc = $visual.RenderOpen()
|
|
385
|
-
|
|
386
|
-
$bitmapImg = New-Object System.Windows.Media.Imaging.BitmapImage
|
|
387
|
-
$bitmapImg.BeginInit()
|
|
388
|
-
$bitmapImg.UriSource = New-Object System.Uri('${
|
|
389
|
-
$bitmapImg.EndInit()
|
|
390
|
-
|
|
391
|
-
$yOffset = ($pageHeight - $imgHeightWpf) / 2
|
|
392
|
-
$xOffset = 2 / 25.4 * 96
|
|
393
|
-
$rect = New-Object System.Windows.Rect($xOffset, $yOffset, $imgWidthWpf, $imgHeightWpf)
|
|
394
|
-
$dc.DrawImage($bitmapImg, $rect)
|
|
395
|
-
$dc.Close()
|
|
396
|
-
|
|
397
|
-
$writer.Write($visual, $ticket)
|
|
398
|
-
|
|
399
|
-
$xpsDoc.Close()
|
|
400
|
-
$package.Close()
|
|
401
|
-
$img.Dispose()
|
|
402
|
-
|
|
403
|
-
$xpsDocForPrint = New-Object System.Windows.Xps.Packaging.XpsDocument($xpsPath, [System.IO.FileAccess]::Read)
|
|
404
|
-
$seq = $xpsDocForPrint.GetFixedDocumentSequence()
|
|
405
|
-
|
|
406
|
-
$xpsWriter = [System.Printing.PrintQueue]::CreateXpsDocumentWriter($queue)
|
|
407
|
-
$xpsWriter.Write($seq, $ticket)
|
|
408
|
-
|
|
409
|
-
$xpsDocForPrint.Close()
|
|
410
|
-
Remove-Item $xpsPath -ErrorAction SilentlyContinue
|
|
250
|
+
const psPath = tempPath.replace(/\\/g, "\\\\");
|
|
251
|
+
const script = `
|
|
252
|
+
$ErrorActionPreference = 'Stop'
|
|
253
|
+
Add-Type -AssemblyName System.Drawing
|
|
254
|
+
Add-Type -AssemblyName System.Printing
|
|
255
|
+
Add-Type -AssemblyName ReachFramework
|
|
256
|
+
Add-Type -AssemblyName PresentationCore
|
|
257
|
+
|
|
258
|
+
$img = [System.Drawing.Image]::FromFile('${psPath}')
|
|
259
|
+
|
|
260
|
+
$DPI = ${PRINT_DPI}
|
|
261
|
+
$MICRONS_PER_INCH = 25400
|
|
262
|
+
|
|
263
|
+
$labelLengthMicrons = [int]($img.Width / $DPI * $MICRONS_PER_INCH) + 3000
|
|
264
|
+
$tapeWidthMicrons = ${media.width}
|
|
265
|
+
|
|
266
|
+
$imgWidthWpf = $img.Width / $DPI * 96
|
|
267
|
+
$imgHeightWpf = $img.Height / $DPI * 96
|
|
268
|
+
|
|
269
|
+
$ticketXml = @"
|
|
270
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
271
|
+
<psf:PrintTicket xmlns:psf="http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework"
|
|
272
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1"
|
|
273
|
+
xmlns:psk="http://schemas.microsoft.com/windows/2003/08/printing/printschemakeywords"
|
|
274
|
+
xmlns:ns0001="http://schemas.brother.info/mfc/printing/2006/11/printschemakeywords">
|
|
275
|
+
<psf:Feature name="psk:PageMediaSize">
|
|
276
|
+
<psf:Option name="ns0001:${media.name}">
|
|
277
|
+
<psf:ScoredProperty name="psk:MediaSizeWidth">
|
|
278
|
+
<psf:Value xsi:type="xsd:integer">${media.width}</psf:Value>
|
|
279
|
+
</psf:ScoredProperty>
|
|
280
|
+
<psf:ScoredProperty name="psk:MediaSizeHeight">
|
|
281
|
+
<psf:ParameterRef name="psk:PageMediaSizeMediaSizeHeight" />
|
|
282
|
+
</psf:ScoredProperty>
|
|
283
|
+
</psf:Option>
|
|
284
|
+
</psf:Feature>
|
|
285
|
+
<psf:ParameterInit name="psk:PageMediaSizeMediaSizeHeight">
|
|
286
|
+
<psf:Value xsi:type="xsd:integer">$labelLengthMicrons</psf:Value>
|
|
287
|
+
</psf:ParameterInit>
|
|
288
|
+
<psf:Feature name="psk:PageOrientation">
|
|
289
|
+
<psf:Option name="psk:Landscape" />
|
|
290
|
+
</psf:Feature>
|
|
291
|
+
<psf:Feature name="ns0001:PageRollFeedToEndOfSheet">
|
|
292
|
+
<psf:Option name="ns0001:FeedToImageEdge" />
|
|
293
|
+
</psf:Feature>
|
|
294
|
+
<psf:Feature name="psk:PageMediaType">
|
|
295
|
+
<psf:Option name="psk:Label" />
|
|
296
|
+
</psf:Feature>
|
|
297
|
+
<psf:Feature name="ns0001:JobRollCutAtEndOfJob">
|
|
298
|
+
<psf:Option name="ns0001:Cut" />
|
|
299
|
+
</psf:Feature>
|
|
300
|
+
</psf:PrintTicket>
|
|
301
|
+
"@
|
|
302
|
+
|
|
303
|
+
$xmlBytes = [System.Text.Encoding]::UTF8.GetBytes($ticketXml)
|
|
304
|
+
$memStream = New-Object System.IO.MemoryStream(,$xmlBytes)
|
|
305
|
+
$ticket = New-Object System.Printing.PrintTicket($memStream)
|
|
306
|
+
|
|
307
|
+
$server = New-Object System.Printing.LocalPrintServer
|
|
308
|
+
$queue = $server.GetPrintQueue('${printer.replace(/'/g, "''")}')
|
|
309
|
+
|
|
310
|
+
$xpsPath = [System.IO.Path]::GetTempFileName() + ".xps"
|
|
311
|
+
|
|
312
|
+
$pageWidth = $labelLengthMicrons / $MICRONS_PER_INCH * 96
|
|
313
|
+
$pageHeight = $tapeWidthMicrons / $MICRONS_PER_INCH * 96
|
|
314
|
+
|
|
315
|
+
$package = [System.IO.Packaging.Package]::Open($xpsPath, [System.IO.FileMode]::Create)
|
|
316
|
+
$xpsDoc = New-Object System.Windows.Xps.Packaging.XpsDocument($package)
|
|
317
|
+
$writer = [System.Windows.Xps.Packaging.XpsDocument]::CreateXpsDocumentWriter($xpsDoc)
|
|
318
|
+
|
|
319
|
+
$visual = New-Object System.Windows.Media.DrawingVisual
|
|
320
|
+
$dc = $visual.RenderOpen()
|
|
321
|
+
|
|
322
|
+
$bitmapImg = New-Object System.Windows.Media.Imaging.BitmapImage
|
|
323
|
+
$bitmapImg.BeginInit()
|
|
324
|
+
$bitmapImg.UriSource = New-Object System.Uri('${psPath}')
|
|
325
|
+
$bitmapImg.EndInit()
|
|
326
|
+
|
|
327
|
+
$yOffset = ($pageHeight - $imgHeightWpf) / 2
|
|
328
|
+
$xOffset = 2 / 25.4 * 96
|
|
329
|
+
$rect = New-Object System.Windows.Rect($xOffset, $yOffset, $imgWidthWpf, $imgHeightWpf)
|
|
330
|
+
$dc.DrawImage($bitmapImg, $rect)
|
|
331
|
+
$dc.Close()
|
|
332
|
+
|
|
333
|
+
$writer.Write($visual, $ticket)
|
|
334
|
+
|
|
335
|
+
$xpsDoc.Close()
|
|
336
|
+
$package.Close()
|
|
337
|
+
$img.Dispose()
|
|
338
|
+
|
|
339
|
+
$xpsDocForPrint = New-Object System.Windows.Xps.Packaging.XpsDocument($xpsPath, [System.IO.FileAccess]::Read)
|
|
340
|
+
$seq = $xpsDocForPrint.GetFixedDocumentSequence()
|
|
341
|
+
|
|
342
|
+
$xpsWriter = [System.Printing.PrintQueue]::CreateXpsDocumentWriter($queue)
|
|
343
|
+
$xpsWriter.Write($seq, $ticket)
|
|
344
|
+
|
|
345
|
+
$xpsDocForPrint.Close()
|
|
346
|
+
Remove-Item $xpsPath -ErrorAction SilentlyContinue
|
|
411
347
|
`;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
let stderr = "";
|
|
415
|
-
ps.stdout.on("data", (data) => { stdout += data.toString(); });
|
|
416
|
-
ps.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
417
|
-
ps.on("close", (code) => {
|
|
418
|
-
if (code === 0) {
|
|
419
|
-
resolve();
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
reject(new Error(`Print failed: ${stderr}`));
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
ps.on("error", (err) => {
|
|
426
|
-
reject(new Error(`Failed to print: ${err.message}`));
|
|
427
|
-
});
|
|
428
|
-
});
|
|
348
|
+
try {
|
|
349
|
+
await runPowerShellOrThrow(script);
|
|
429
350
|
}
|
|
430
351
|
finally {
|
|
431
|
-
if (fs.existsSync(tempPath))
|
|
352
|
+
if (fs.existsSync(tempPath))
|
|
432
353
|
fs.unlinkSync(tempPath);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
// Main API functions
|
|
437
|
-
export async function render(options) {
|
|
438
|
-
validateOptions(options);
|
|
439
|
-
const { tape } = resolveSettings(options);
|
|
440
|
-
return renderContent(options, tape);
|
|
441
|
-
}
|
|
442
|
-
export async function print(options) {
|
|
443
|
-
validateOptions(options);
|
|
444
|
-
const { tape, printer } = resolveSettings(options);
|
|
445
|
-
const image = await renderContent(options, tape);
|
|
446
|
-
await printBuffer(image, printer, tape);
|
|
447
|
-
return { image };
|
|
448
|
-
}
|
|
449
|
-
// Render multiple segments side-by-side into a single image
|
|
450
|
-
export async function renderSegments(segments, tape, textHeight, space) {
|
|
451
|
-
const config = getConfig();
|
|
452
|
-
const effectiveTape = tape ?? config.defaultTape ?? 24;
|
|
453
|
-
const tapeSize = TAPE_SIZES[effectiveTape];
|
|
454
|
-
// Render each segment individually
|
|
455
|
-
const buffers = [];
|
|
456
|
-
for (const seg of segments) {
|
|
457
|
-
if (seg.type === "text") {
|
|
458
|
-
buffers.push(await renderText(seg.value, effectiveTape, textHeight));
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
buffers.push(await renderQr(seg.value, effectiveTape));
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (buffers.length === 1) {
|
|
465
|
-
return buffers[0];
|
|
466
354
|
}
|
|
467
|
-
// Load all rendered images and trim horizontal whitespace
|
|
468
|
-
const images = [];
|
|
469
|
-
for (const buf of buffers) {
|
|
470
|
-
const img = await Jimp.read(buf);
|
|
471
|
-
// Find leftmost and rightmost non-white columns to trim padding
|
|
472
|
-
let left = img.width;
|
|
473
|
-
let right = 0;
|
|
474
|
-
for (let x = 0; x < img.width; x++) {
|
|
475
|
-
for (let y = 0; y < img.height; y++) {
|
|
476
|
-
const pixel = img.getPixelColor(x, y);
|
|
477
|
-
if (pixel !== 0xffffffff) {
|
|
478
|
-
if (x < left)
|
|
479
|
-
left = x;
|
|
480
|
-
if (x > right)
|
|
481
|
-
right = x;
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
if (right >= left) {
|
|
487
|
-
const margin = 4; // small margin around content
|
|
488
|
-
const cropLeft = Math.max(0, left - margin);
|
|
489
|
-
const cropRight = Math.min(img.width - 1, right + margin);
|
|
490
|
-
const cropWidth = cropRight - cropLeft + 1;
|
|
491
|
-
img.crop({ x: cropLeft, y: 0, w: cropWidth, h: img.height });
|
|
492
|
-
}
|
|
493
|
-
images.push({ img, width: img.width, height: img.height });
|
|
494
|
-
}
|
|
495
|
-
// Composite left-to-right
|
|
496
|
-
const gap = space ? parseSize(space, tapeSize.height) : 2;
|
|
497
|
-
let totalWidth = 0;
|
|
498
|
-
for (let i = 0; i < images.length; i++) {
|
|
499
|
-
totalWidth += images[i].width;
|
|
500
|
-
if (i < images.length - 1)
|
|
501
|
-
totalWidth += gap;
|
|
502
|
-
}
|
|
503
|
-
const canvas = new Jimp({ width: totalWidth, height: tapeSize.height, color: 0xffffffff });
|
|
504
|
-
let x = 0;
|
|
505
|
-
for (let i = 0; i < images.length; i++) {
|
|
506
|
-
const yOffset = Math.round((tapeSize.height - images[i].height) / 2);
|
|
507
|
-
canvas.composite(images[i].img, x, yOffset);
|
|
508
|
-
x += images[i].width + gap;
|
|
509
|
-
}
|
|
510
|
-
return canvas.getBuffer("image/png");
|
|
511
|
-
}
|
|
512
|
-
// Print multiple segments as a single label
|
|
513
|
-
export async function printSegments(segments, options) {
|
|
514
|
-
const config = getConfig();
|
|
515
|
-
const tape = options?.tape ?? config.defaultTape ?? 24;
|
|
516
|
-
const printer = options?.printer ?? config.defaultPrinter ?? DEFAULT_PRINTER;
|
|
517
|
-
const image = await renderSegments(segments, tape, options?.textHeight, options?.space);
|
|
518
|
-
await printBuffer(image, printer, tape);
|
|
519
|
-
return { image };
|
|
520
355
|
}
|
|
521
356
|
//# sourceMappingURL=api.js.map
|