@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/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,387 +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
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// Ignore config errors
|
|
39
|
+
const raw = readJsonConfig(CONFIG_PATH);
|
|
40
|
+
if (typeof raw.defaultTape === "string") {
|
|
41
|
+
raw.defaultTape = parseInt(raw.defaultTape.replace("mm", ""), 10);
|
|
47
42
|
}
|
|
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
|
-
|
|
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"
|
|
77
|
+
}
|
|
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
|
+
}
|
|
97
85
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const width = Math.round(tapeHeight * (aspectWidth / aspectHeight));
|
|
101
|
-
return { width, height };
|
|
86
|
+
catch { /* fall through */ }
|
|
87
|
+
return null;
|
|
102
88
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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");
|
|
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
|
+
};
|
|
116
98
|
}
|
|
117
|
-
|
|
118
|
-
|
|
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);
|
|
119
109
|
}
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
getConfigPath() {
|
|
111
|
+
return CONFIG_PATH;
|
|
122
112
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
// Parse text height spec to pixels
|
|
133
|
-
function parseTextHeight(spec, tapeHeightPx) {
|
|
134
|
-
const trimmed = spec.trim().toLowerCase();
|
|
135
|
-
if (trimmed.endsWith("%")) {
|
|
136
|
-
const pct = parseFloat(trimmed.slice(0, -1));
|
|
137
|
-
if (isNaN(pct) || pct <= 0)
|
|
138
|
-
throw new Error(`Invalid text height: ${spec}`);
|
|
139
|
-
return Math.round(tapeHeightPx * pct / 100);
|
|
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
|
+
}));
|
|
140
121
|
}
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
return
|
|
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
|
+
};
|
|
146
130
|
}
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
throw new Error(`Invalid text height: ${spec}`);
|
|
151
|
-
return Math.round(inches * PRINT_DPI);
|
|
131
|
+
async detectMedia(printerName) {
|
|
132
|
+
const t = await detectTapeSize(printerName);
|
|
133
|
+
return t ? this.getMedia(String(t)) : null;
|
|
152
134
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Render text to image buffer
|
|
156
|
-
async function renderText(text, tape, textHeightSpec) {
|
|
157
|
-
const tapeSize = TAPE_SIZES[tape];
|
|
158
|
-
const targetHeight = textHeightSpec
|
|
159
|
-
? Math.min(parseTextHeight(textHeightSpec, tapeSize.height), tapeSize.height)
|
|
160
|
-
: tapeSize.height;
|
|
161
|
-
const lines = text.split("\\n").join("\n");
|
|
162
|
-
// Load largest font for quality
|
|
163
|
-
const fontDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "node_modules/@jimp/plugin-print/fonts/open-sans");
|
|
164
|
-
const fontPath = path.join(fontDir, "open-sans-128-black", "open-sans-128-black.fnt");
|
|
165
|
-
const font = await loadFont(fontPath);
|
|
166
|
-
// Measure text
|
|
167
|
-
const textWidth = measureText(font, lines);
|
|
168
|
-
const textHeight = measureTextHeight(font, lines, textWidth + 100);
|
|
169
|
-
// Create temp image for text
|
|
170
|
-
const padding = 10;
|
|
171
|
-
const imgWidth = textWidth + padding * 2;
|
|
172
|
-
const imgHeight = textHeight + padding * 2;
|
|
173
|
-
const tempImage = new Jimp({ width: imgWidth, height: imgHeight, color: 0xffffffff });
|
|
174
|
-
tempImage.print({ font, x: padding, y: padding, text: lines });
|
|
175
|
-
// Scale to fit tape height
|
|
176
|
-
const scale = targetHeight / imgHeight;
|
|
177
|
-
const finalWidth = Math.round(imgWidth * scale);
|
|
178
|
-
const finalHeight = Math.round(imgHeight * scale);
|
|
179
|
-
tempImage.resize({ w: finalWidth, h: finalHeight });
|
|
180
|
-
// Create final image with padding
|
|
181
|
-
const canvasHeight = tapeSize.height;
|
|
182
|
-
const hPadding = Math.round(canvasHeight * 0.2);
|
|
183
|
-
const outputWidth = finalWidth + hPadding * 2;
|
|
184
|
-
const image = new Jimp({ width: outputWidth, height: canvasHeight, color: 0xffffffff });
|
|
185
|
-
const xOffset = hPadding;
|
|
186
|
-
const yOffset = Math.round((canvasHeight - finalHeight) / 2);
|
|
187
|
-
image.composite(tempImage, xOffset, yOffset);
|
|
188
|
-
return image.getBuffer("image/png");
|
|
189
|
-
}
|
|
190
|
-
// Render QR code to image buffer
|
|
191
|
-
async function renderQr(data, tape, labelText) {
|
|
192
|
-
const tapeSize = TAPE_SIZES[tape];
|
|
193
|
-
const targetHeight = tapeSize.height;
|
|
194
|
-
const qrSize = Math.floor(targetHeight * 0.95);
|
|
195
|
-
// Margins in pixels at PRINT_DPI (3mm left, 4mm right)
|
|
196
|
-
const pxPerMm = PRINT_DPI / 25.4;
|
|
197
|
-
const leftMargin = Math.round(3 * pxPerMm);
|
|
198
|
-
const rightMargin = Math.round(4 * pxPerMm);
|
|
199
|
-
// Generate QR code at high resolution
|
|
200
|
-
const qrBuffer = await QRCode.toBuffer(data, {
|
|
201
|
-
type: "png",
|
|
202
|
-
width: qrSize,
|
|
203
|
-
margin: 0,
|
|
204
|
-
errorCorrectionLevel: "M",
|
|
205
|
-
color: { dark: "#000000", light: "#ffffff" },
|
|
206
|
-
});
|
|
207
|
-
const qrImage = await Jimp.read(qrBuffer);
|
|
208
|
-
if (!labelText) {
|
|
209
|
-
// QR only
|
|
210
|
-
const outputWidth = leftMargin + qrSize + rightMargin;
|
|
211
|
-
const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });
|
|
212
|
-
const yOffset = Math.floor((targetHeight - qrSize) / 2);
|
|
213
|
-
image.composite(qrImage, leftMargin, yOffset);
|
|
214
|
-
return image.getBuffer("image/png");
|
|
135
|
+
async listPrinters() {
|
|
136
|
+
return coreListPrinters(DEFAULT_PRINTER_PATTERN);
|
|
215
137
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const font = await loadFont(fontPath);
|
|
220
|
-
const textWidth = measureText(font, labelText);
|
|
221
|
-
const textHeight = measureTextHeight(font, labelText, textWidth + 50);
|
|
222
|
-
// Scale text to fit beside QR
|
|
223
|
-
const maxTextHeight = targetHeight * 0.8;
|
|
224
|
-
const textScale = Math.min(1, maxTextHeight / textHeight);
|
|
225
|
-
const scaledTextWidth = Math.round(textWidth * textScale);
|
|
226
|
-
const scaledTextHeight = Math.round(textHeight * textScale);
|
|
227
|
-
// Create text image
|
|
228
|
-
const textImg = new Jimp({ width: textWidth + 20, height: textHeight + 20, color: 0xffffffff });
|
|
229
|
-
textImg.print({ font, x: 10, y: 10, text: labelText });
|
|
230
|
-
if (textScale < 1) {
|
|
231
|
-
textImg.resize({ w: scaledTextWidth, h: scaledTextHeight });
|
|
138
|
+
async getStatus(printerName) {
|
|
139
|
+
const printer = printerName ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
140
|
+
return getPrinterStatus(printer);
|
|
232
141
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });
|
|
237
|
-
const qrY = Math.floor((targetHeight - qrSize) / 2);
|
|
238
|
-
image.composite(qrImage, leftMargin, qrY);
|
|
239
|
-
const textY = Math.floor((targetHeight - scaledTextHeight) / 2);
|
|
240
|
-
image.composite(textImg, leftMargin + qrSize + gap, textY);
|
|
241
|
-
return image.getBuffer("image/png");
|
|
242
|
-
}
|
|
243
|
-
// Render content to image buffer
|
|
244
|
-
async function renderContent(options, tape) {
|
|
245
|
-
const tapeSize = TAPE_SIZES[tape];
|
|
246
|
-
if (options.text !== undefined) {
|
|
247
|
-
return renderText(options.text, tape, options.textHeight);
|
|
142
|
+
async waitOnline(printerName, opts = {}) {
|
|
143
|
+
const printer = printerName ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
144
|
+
return coreWaitOnline(printer, opts, DEFAULT_PRINTER_PATTERN);
|
|
248
145
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
146
|
+
async render(opts) {
|
|
147
|
+
validateContent(opts);
|
|
148
|
+
const tape = await this.resolveTape(opts.media);
|
|
149
|
+
return resolveContent(opts, TAPE_SIZES[tape].height, tape);
|
|
252
150
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
await
|
|
257
|
-
|
|
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 };
|
|
258
159
|
}
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
await closeBrowser();
|
|
263
|
-
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);
|
|
264
163
|
}
|
|
265
|
-
|
|
266
|
-
|
|
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 };
|
|
267
171
|
}
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
270
184
|
}
|
|
271
|
-
|
|
272
|
-
|
|
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);
|
|
273
196
|
}
|
|
274
|
-
|
|
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 };
|
|
275
223
|
}
|
|
276
|
-
|
|
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.`);
|
|
231
|
+
}
|
|
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);
|
|
240
|
+
}
|
|
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
|
+
*/
|
|
277
246
|
async function printBuffer(imageBuffer, printer, tape) {
|
|
278
247
|
const media = MEDIA_OPTIONS[tape];
|
|
279
248
|
const tempPath = path.join(os.tmpdir(), `label-${Date.now()}.png`);
|
|
280
249
|
fs.writeFileSync(tempPath, imageBuffer);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
Add-Type -AssemblyName System.Drawing
|
|
285
|
-
Add-Type -AssemblyName System.Printing
|
|
286
|
-
Add-Type -AssemblyName ReachFramework
|
|
287
|
-
Add-Type -AssemblyName PresentationCore
|
|
288
|
-
|
|
289
|
-
$img = [System.Drawing.Image]::FromFile('${
|
|
290
|
-
|
|
291
|
-
$DPI = ${PRINT_DPI}
|
|
292
|
-
$MICRONS_PER_INCH = 25400
|
|
293
|
-
|
|
294
|
-
$labelLengthMicrons = [int]($img.Width / $DPI * $MICRONS_PER_INCH) + 3000
|
|
295
|
-
$tapeWidthMicrons = ${media.width}
|
|
296
|
-
|
|
297
|
-
$imgWidthWpf = $img.Width / $DPI * 96
|
|
298
|
-
$imgHeightWpf = $img.Height / $DPI * 96
|
|
299
|
-
|
|
300
|
-
$ticketXml = @"
|
|
301
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
302
|
-
<psf:PrintTicket xmlns:psf="http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework"
|
|
303
|
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1"
|
|
304
|
-
xmlns:psk="http://schemas.microsoft.com/windows/2003/08/printing/printschemakeywords"
|
|
305
|
-
xmlns:ns0001="http://schemas.brother.info/mfc/printing/2006/11/printschemakeywords">
|
|
306
|
-
<psf:Feature name="psk:PageMediaSize">
|
|
307
|
-
<psf:Option name="ns0001:${media.name}">
|
|
308
|
-
<psf:ScoredProperty name="psk:MediaSizeWidth">
|
|
309
|
-
<psf:Value xsi:type="xsd:integer">${media.width}</psf:Value>
|
|
310
|
-
</psf:ScoredProperty>
|
|
311
|
-
<psf:ScoredProperty name="psk:MediaSizeHeight">
|
|
312
|
-
<psf:ParameterRef name="psk:PageMediaSizeMediaSizeHeight" />
|
|
313
|
-
</psf:ScoredProperty>
|
|
314
|
-
</psf:Option>
|
|
315
|
-
</psf:Feature>
|
|
316
|
-
<psf:ParameterInit name="psk:PageMediaSizeMediaSizeHeight">
|
|
317
|
-
<psf:Value xsi:type="xsd:integer">$labelLengthMicrons</psf:Value>
|
|
318
|
-
</psf:ParameterInit>
|
|
319
|
-
<psf:Feature name="psk:PageOrientation">
|
|
320
|
-
<psf:Option name="psk:Landscape" />
|
|
321
|
-
</psf:Feature>
|
|
322
|
-
<psf:Feature name="ns0001:PageRollFeedToEndOfSheet">
|
|
323
|
-
<psf:Option name="ns0001:FeedToImageEdge" />
|
|
324
|
-
</psf:Feature>
|
|
325
|
-
<psf:Feature name="psk:PageMediaType">
|
|
326
|
-
<psf:Option name="psk:Label" />
|
|
327
|
-
</psf:Feature>
|
|
328
|
-
<psf:Feature name="ns0001:JobRollCutAtEndOfJob">
|
|
329
|
-
<psf:Option name="ns0001:Cut" />
|
|
330
|
-
</psf:Feature>
|
|
331
|
-
</psf:PrintTicket>
|
|
332
|
-
"@
|
|
333
|
-
|
|
334
|
-
$xmlBytes = [System.Text.Encoding]::UTF8.GetBytes($ticketXml)
|
|
335
|
-
$memStream = New-Object System.IO.MemoryStream(,$xmlBytes)
|
|
336
|
-
$ticket = New-Object System.Printing.PrintTicket($memStream)
|
|
337
|
-
|
|
338
|
-
$server = New-Object System.Printing.LocalPrintServer
|
|
339
|
-
$queue = $server.GetPrintQueue('${printer}')
|
|
340
|
-
|
|
341
|
-
$xpsPath = [System.IO.Path]::GetTempFileName() + ".xps"
|
|
342
|
-
|
|
343
|
-
$pageWidth = $labelLengthMicrons / $MICRONS_PER_INCH * 96
|
|
344
|
-
$pageHeight = $tapeWidthMicrons / $MICRONS_PER_INCH * 96
|
|
345
|
-
|
|
346
|
-
$package = [System.IO.Packaging.Package]::Open($xpsPath, [System.IO.FileMode]::Create)
|
|
347
|
-
$xpsDoc = New-Object System.Windows.Xps.Packaging.XpsDocument($package)
|
|
348
|
-
$writer = [System.Windows.Xps.Packaging.XpsDocument]::CreateXpsDocumentWriter($xpsDoc)
|
|
349
|
-
|
|
350
|
-
$visual = New-Object System.Windows.Media.DrawingVisual
|
|
351
|
-
$dc = $visual.RenderOpen()
|
|
352
|
-
|
|
353
|
-
$bitmapImg = New-Object System.Windows.Media.Imaging.BitmapImage
|
|
354
|
-
$bitmapImg.BeginInit()
|
|
355
|
-
$bitmapImg.UriSource = New-Object System.Uri('${
|
|
356
|
-
$bitmapImg.EndInit()
|
|
357
|
-
|
|
358
|
-
$yOffset = ($pageHeight - $imgHeightWpf) / 2
|
|
359
|
-
$xOffset = 2 / 25.4 * 96
|
|
360
|
-
$rect = New-Object System.Windows.Rect($xOffset, $yOffset, $imgWidthWpf, $imgHeightWpf)
|
|
361
|
-
$dc.DrawImage($bitmapImg, $rect)
|
|
362
|
-
$dc.Close()
|
|
363
|
-
|
|
364
|
-
$writer.Write($visual, $ticket)
|
|
365
|
-
|
|
366
|
-
$xpsDoc.Close()
|
|
367
|
-
$package.Close()
|
|
368
|
-
$img.Dispose()
|
|
369
|
-
|
|
370
|
-
$xpsDocForPrint = New-Object System.Windows.Xps.Packaging.XpsDocument($xpsPath, [System.IO.FileAccess]::Read)
|
|
371
|
-
$seq = $xpsDocForPrint.GetFixedDocumentSequence()
|
|
372
|
-
|
|
373
|
-
$xpsWriter = [System.Printing.PrintQueue]::CreateXpsDocumentWriter($queue)
|
|
374
|
-
$xpsWriter.Write($seq, $ticket)
|
|
375
|
-
|
|
376
|
-
$xpsDocForPrint.Close()
|
|
377
|
-
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
|
|
378
347
|
`;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
let stderr = "";
|
|
382
|
-
ps.stdout.on("data", (data) => { stdout += data.toString(); });
|
|
383
|
-
ps.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
384
|
-
ps.on("close", (code) => {
|
|
385
|
-
if (code === 0) {
|
|
386
|
-
resolve();
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
reject(new Error(`Print failed: ${stderr}`));
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
ps.on("error", (err) => {
|
|
393
|
-
reject(new Error(`Failed to print: ${err.message}`));
|
|
394
|
-
});
|
|
395
|
-
});
|
|
348
|
+
try {
|
|
349
|
+
await runPowerShellOrThrow(script);
|
|
396
350
|
}
|
|
397
351
|
finally {
|
|
398
|
-
if (fs.existsSync(tempPath))
|
|
352
|
+
if (fs.existsSync(tempPath))
|
|
399
353
|
fs.unlinkSync(tempPath);
|
|
400
|
-
}
|
|
401
354
|
}
|
|
402
355
|
}
|
|
403
|
-
// Main API functions
|
|
404
|
-
export async function render(options) {
|
|
405
|
-
validateOptions(options);
|
|
406
|
-
const { tape } = resolveSettings(options);
|
|
407
|
-
return renderContent(options, tape);
|
|
408
|
-
}
|
|
409
|
-
export async function print(options) {
|
|
410
|
-
validateOptions(options);
|
|
411
|
-
const { tape, printer } = resolveSettings(options);
|
|
412
|
-
const image = await renderContent(options, tape);
|
|
413
|
-
await printBuffer(image, printer, tape);
|
|
414
|
-
return { image };
|
|
415
|
-
}
|
|
416
356
|
//# sourceMappingURL=api.js.map
|