@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.ts
CHANGED
|
@@ -1,505 +1,427 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brother Label Printer API
|
|
3
|
-
* Programmatic interface for printing labels on Brother P-touch printers
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
async function
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
$
|
|
378
|
-
$
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
$
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
$
|
|
385
|
-
|
|
386
|
-
$
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
$
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
$pageWidth = $labelLengthMicrons / $MICRONS_PER_INCH * 96
|
|
430
|
-
$pageHeight = $tapeWidthMicrons / $MICRONS_PER_INCH * 96
|
|
431
|
-
|
|
432
|
-
$package = [System.IO.Packaging.Package]::Open($xpsPath, [System.IO.FileMode]::Create)
|
|
433
|
-
$xpsDoc = New-Object System.Windows.Xps.Packaging.XpsDocument($package)
|
|
434
|
-
$writer = [System.Windows.Xps.Packaging.XpsDocument]::CreateXpsDocumentWriter($xpsDoc)
|
|
435
|
-
|
|
436
|
-
$visual = New-Object System.Windows.Media.DrawingVisual
|
|
437
|
-
$dc = $visual.RenderOpen()
|
|
438
|
-
|
|
439
|
-
$bitmapImg = New-Object System.Windows.Media.Imaging.BitmapImage
|
|
440
|
-
$bitmapImg.BeginInit()
|
|
441
|
-
$bitmapImg.UriSource = New-Object System.Uri('${tempPath.replace(/\\/g, "\\\\")}')
|
|
442
|
-
$bitmapImg.EndInit()
|
|
443
|
-
|
|
444
|
-
$yOffset = ($pageHeight - $imgHeightWpf) / 2
|
|
445
|
-
$xOffset = 2 / 25.4 * 96 # 2mm left margin in WPF units
|
|
446
|
-
$rect = New-Object System.Windows.Rect($xOffset, $yOffset, $imgWidthWpf, $imgHeightWpf)
|
|
447
|
-
$dc.DrawImage($bitmapImg, $rect)
|
|
448
|
-
$dc.Close()
|
|
449
|
-
|
|
450
|
-
$writer.Write($visual, $ticket)
|
|
451
|
-
|
|
452
|
-
$xpsDoc.Close()
|
|
453
|
-
$package.Close()
|
|
454
|
-
$img.Dispose()
|
|
455
|
-
|
|
456
|
-
$xpsDocForPrint = New-Object System.Windows.Xps.Packaging.XpsDocument($xpsPath, [System.IO.FileAccess]::Read)
|
|
457
|
-
$seq = $xpsDocForPrint.GetFixedDocumentSequence()
|
|
458
|
-
|
|
459
|
-
$xpsWriter = [System.Printing.PrintQueue]::CreateXpsDocumentWriter($queue)
|
|
460
|
-
$xpsWriter.Write($seq, $ticket)
|
|
461
|
-
|
|
462
|
-
$xpsDocForPrint.Close()
|
|
463
|
-
Remove-Item $xpsPath -ErrorAction SilentlyContinue
|
|
464
|
-
`;
|
|
465
|
-
|
|
466
|
-
const ps = spawn("powershell", ["-Command", psScript], { stdio: "pipe" });
|
|
467
|
-
|
|
468
|
-
let stdout = "";
|
|
469
|
-
let stderr = "";
|
|
470
|
-
ps.stdout.on("data", (data) => { stdout += data.toString(); });
|
|
471
|
-
ps.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
472
|
-
|
|
473
|
-
ps.on("close", (code) => {
|
|
474
|
-
if (code === 0) {
|
|
475
|
-
resolve();
|
|
476
|
-
} else {
|
|
477
|
-
reject(new Error(`Print failed: ${stderr}`));
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
ps.on("error", (err) => {
|
|
482
|
-
reject(new Error(`Failed to print: ${err.message}`));
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
} finally {
|
|
486
|
-
if (fs.existsSync(tempPath)) {
|
|
487
|
-
fs.unlinkSync(tempPath);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Main API functions
|
|
493
|
-
export async function render(options: PrintOptions): Promise<Buffer> {
|
|
494
|
-
validateOptions(options);
|
|
495
|
-
const { tape } = resolveSettings(options);
|
|
496
|
-
return renderContent(options, tape);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
export async function print(options: PrintOptions): Promise<PrintResult> {
|
|
500
|
-
validateOptions(options);
|
|
501
|
-
const { tape, printer } = resolveSettings(options);
|
|
502
|
-
const image = await renderContent(options, tape);
|
|
503
|
-
await printBuffer(image, printer, tape);
|
|
504
|
-
return { image };
|
|
505
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Brother Label Printer API
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as os from "os";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import {
|
|
16
|
+
LabelPrinter,
|
|
17
|
+
PrintOptions as CorePrintOptions,
|
|
18
|
+
SegmentOptions as CoreSegmentOptions,
|
|
19
|
+
PrintResult as CorePrintResult,
|
|
20
|
+
PrinterStatus,
|
|
21
|
+
PrinterInfo as CorePrinterInfo,
|
|
22
|
+
PrinterConfig as CorePrinterConfig,
|
|
23
|
+
MediaInfo,
|
|
24
|
+
Segment,
|
|
25
|
+
WaitOptions,
|
|
26
|
+
validateContent,
|
|
27
|
+
resolveContent,
|
|
28
|
+
renderSegments as coreRenderSegments,
|
|
29
|
+
listPrinters as coreListPrinters,
|
|
30
|
+
getPrinterStatus,
|
|
31
|
+
waitOnline as coreWaitOnline,
|
|
32
|
+
runPowerShellOrThrow,
|
|
33
|
+
readJsonConfig,
|
|
34
|
+
mergeJsonConfig,
|
|
35
|
+
} from "@bobfrankston/label-core";
|
|
36
|
+
|
|
37
|
+
/* ----- Brother-specific types ----- */
|
|
38
|
+
|
|
39
|
+
export type TapeSize = 6 | 9 | 12 | 18 | 24;
|
|
40
|
+
export type Orientation = "landscape" | "portrait";
|
|
41
|
+
|
|
42
|
+
/** Backward-compat: brother-label's config kept the `defaultTape` name. */
|
|
43
|
+
export interface PrinterConfig {
|
|
44
|
+
defaultTape?: TapeSize;
|
|
45
|
+
defaultPrinter?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PrinterInfo {
|
|
49
|
+
name: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Brother-specific PrintOptions: same as core but with tape alias for media. */
|
|
53
|
+
export interface PrintOptions extends Omit<CorePrintOptions, "media"> {
|
|
54
|
+
tape?: TapeSize; /** Tape size in mm — alias for core.media */
|
|
55
|
+
orientation?: Orientation; /** Reserved (Brother PT prints landscape only) */
|
|
56
|
+
length?: number; /** Reserved — explicit length in mm */
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SegmentOptions extends Omit<CoreSegmentOptions, "media"> {
|
|
60
|
+
tape?: TapeSize;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PrintResult {
|
|
64
|
+
image: Buffer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type { Segment };
|
|
68
|
+
|
|
69
|
+
/* ----- Constants ----- */
|
|
70
|
+
|
|
71
|
+
const CONFIG_PATH = path.join(os.homedir(), ".brother-label.json");
|
|
72
|
+
const DEFAULT_PRINTER = "Brother PT-P710BT";
|
|
73
|
+
const DEFAULT_PRINTER_PATTERN = /brother|^pt-|^ql-/i;
|
|
74
|
+
const PRINT_DPI = 300;
|
|
75
|
+
const VALID_TAPES: TapeSize[] = [6, 9, 12, 18, 24];
|
|
76
|
+
|
|
77
|
+
/** Tape sizes: width in pixels @ 300 DPI = continuous direction; height = perpendicular. */
|
|
78
|
+
const TAPE_SIZES: Record<TapeSize, { width: number; height: number }> = {
|
|
79
|
+
6: { width: 1137, height: 56 },
|
|
80
|
+
9: { width: 1137, height: 84 },
|
|
81
|
+
12: { width: 1137, height: 113 },
|
|
82
|
+
18: { width: 1137, height: 169 },
|
|
83
|
+
24: { width: 1137, height: 213 },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Brother CustomMediaSize names + tape widths in microns (XPS PrintTicket). */
|
|
87
|
+
const MEDIA_OPTIONS: Record<TapeSize, { name: string; width: number }> = {
|
|
88
|
+
6: { name: "CustomMediaSize257", width: 5900 },
|
|
89
|
+
9: { name: "CustomMediaSize258", width: 9000 },
|
|
90
|
+
12: { name: "CustomMediaSize259", width: 11900 },
|
|
91
|
+
18: { name: "CustomMediaSize260", width: 18100 },
|
|
92
|
+
24: { name: "CustomMediaSize261", width: 24000 },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/* ----- Config (backward-compat — note "defaultTape" stays in JSON) ----- */
|
|
96
|
+
|
|
97
|
+
export function getConfig(): PrinterConfig {
|
|
98
|
+
const raw: any = readJsonConfig(CONFIG_PATH);
|
|
99
|
+
if (typeof raw.defaultTape === "string") {
|
|
100
|
+
raw.defaultTape = parseInt(raw.defaultTape.replace("mm", ""), 10) as TapeSize;
|
|
101
|
+
}
|
|
102
|
+
return raw;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function setConfig(config: Partial<PrinterConfig>): void {
|
|
106
|
+
mergeJsonConfig(CONFIG_PATH, config);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getConfigPath(): string {
|
|
110
|
+
return CONFIG_PATH;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ----- Printer listing (backward-compat shape: { name }[]) ----- */
|
|
114
|
+
|
|
115
|
+
export async function listPrinters(): Promise<PrinterInfo[]> {
|
|
116
|
+
const ps = await coreListPrinters(DEFAULT_PRINTER_PATTERN);
|
|
117
|
+
return ps.map(p => ({ name: p.name }));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Auto-detect tape size from printer's default print ticket. Returns null if
|
|
122
|
+
* detection fails. Brother's driver embeds the loaded tape's CustomMediaSize
|
|
123
|
+
* name in the default PrintTicket XML.
|
|
124
|
+
*/
|
|
125
|
+
export async function detectTapeSize(printerName?: string): Promise<TapeSize> {
|
|
126
|
+
const config = getConfig();
|
|
127
|
+
const printer = printerName ?? config.defaultPrinter ?? DEFAULT_PRINTER;
|
|
128
|
+
const script = `
|
|
129
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
130
|
+
Add-Type -AssemblyName System.Printing | Out-Null
|
|
131
|
+
try {
|
|
132
|
+
$server = New-Object System.Printing.LocalPrintServer
|
|
133
|
+
$queue = $server.GetPrintQueue('${printer.replace(/'/g, "''")}')
|
|
134
|
+
$ticket = $queue.DefaultPrintTicket
|
|
135
|
+
$stream = $ticket.GetXmlStream()
|
|
136
|
+
$reader = New-Object System.IO.StreamReader($stream)
|
|
137
|
+
Write-Output $reader.ReadToEnd()
|
|
138
|
+
$reader.Close()
|
|
139
|
+
} catch {
|
|
140
|
+
Write-Output "ERROR"
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
try {
|
|
144
|
+
const out = await runPowerShellOrThrow(script);
|
|
145
|
+
for (const [size, media] of Object.entries(MEDIA_OPTIONS)) {
|
|
146
|
+
if (out.includes(media.name)) return parseInt(size, 10) as TapeSize;
|
|
147
|
+
}
|
|
148
|
+
} catch { /* fall through */ }
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ----- Brother LabelPrinter implementation ----- */
|
|
153
|
+
|
|
154
|
+
class BrotherPrinterImpl implements LabelPrinter {
|
|
155
|
+
readonly driverName = "brother";
|
|
156
|
+
|
|
157
|
+
getConfig(): CorePrinterConfig {
|
|
158
|
+
const c = getConfig();
|
|
159
|
+
return {
|
|
160
|
+
defaultPrinter: c.defaultPrinter,
|
|
161
|
+
defaultMedia: c.defaultTape !== undefined ? String(c.defaultTape) : undefined,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
setConfig(config: Partial<CorePrinterConfig>): void {
|
|
165
|
+
const update: Partial<PrinterConfig> = {};
|
|
166
|
+
if (config.defaultPrinter !== undefined) update.defaultPrinter = config.defaultPrinter;
|
|
167
|
+
if (config.defaultMedia !== undefined) {
|
|
168
|
+
const t = parseInt(config.defaultMedia, 10) as TapeSize;
|
|
169
|
+
if (VALID_TAPES.includes(t)) update.defaultTape = t;
|
|
170
|
+
}
|
|
171
|
+
setConfig(update);
|
|
172
|
+
}
|
|
173
|
+
getConfigPath(): string {
|
|
174
|
+
return CONFIG_PATH;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
listMedia(): MediaInfo[] {
|
|
178
|
+
return VALID_TAPES.map(t => ({
|
|
179
|
+
id: String(t),
|
|
180
|
+
name: `${t}mm tape`,
|
|
181
|
+
widthPx: TAPE_SIZES[t].height, /** "media width" (perpendicular to feed) */
|
|
182
|
+
lengthPx: 0, /** continuous tape — length determined by content */
|
|
183
|
+
continuous: true,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
getMedia(id: string): MediaInfo {
|
|
187
|
+
const t = parseInt(id.replace("mm", ""), 10) as TapeSize;
|
|
188
|
+
if (!VALID_TAPES.includes(t)) return null;
|
|
189
|
+
return {
|
|
190
|
+
id: String(t), name: `${t}mm tape`,
|
|
191
|
+
widthPx: TAPE_SIZES[t].height, lengthPx: 0, continuous: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async detectMedia(printerName?: string): Promise<MediaInfo> {
|
|
195
|
+
const t = await detectTapeSize(printerName);
|
|
196
|
+
return t ? this.getMedia(String(t)) : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async listPrinters(): Promise<CorePrinterInfo[]> {
|
|
200
|
+
return coreListPrinters(DEFAULT_PRINTER_PATTERN);
|
|
201
|
+
}
|
|
202
|
+
async getStatus(printerName?: string): Promise<PrinterStatus> {
|
|
203
|
+
const printer = printerName ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
204
|
+
return getPrinterStatus(printer);
|
|
205
|
+
}
|
|
206
|
+
async waitOnline(printerName?: string, opts: WaitOptions = {}): Promise<PrinterStatus> {
|
|
207
|
+
const printer = printerName ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
208
|
+
return coreWaitOnline(printer, opts, DEFAULT_PRINTER_PATTERN);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async render(opts: CorePrintOptions): Promise<Buffer> {
|
|
212
|
+
validateContent(opts);
|
|
213
|
+
const tape = await this.resolveTape(opts.media);
|
|
214
|
+
return resolveContent(opts, TAPE_SIZES[tape].height, tape);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async print(opts: CorePrintOptions): Promise<CorePrintResult> {
|
|
218
|
+
validateContent(opts);
|
|
219
|
+
const printer = opts.printer ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
220
|
+
const tape = await this.resolveTape(opts.media);
|
|
221
|
+
const image = await resolveContent(opts, TAPE_SIZES[tape].height, tape);
|
|
222
|
+
await ensureOnline(printer, opts);
|
|
223
|
+
await printBuffer(image, printer, tape);
|
|
224
|
+
return { image, media: this.getMedia(String(tape)), printer };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async renderSegments(segments: Segment[], opts: CoreSegmentOptions = {}): Promise<Buffer> {
|
|
228
|
+
const tape = await this.resolveTape(opts.media);
|
|
229
|
+
return coreRenderSegments(segments, TAPE_SIZES[tape].height, opts.textHeight, opts.space);
|
|
230
|
+
}
|
|
231
|
+
async printSegments(segments: Segment[], opts: CoreSegmentOptions = {}): Promise<CorePrintResult> {
|
|
232
|
+
const printer = opts.printer ?? this.getConfig().defaultPrinter ?? DEFAULT_PRINTER;
|
|
233
|
+
const tape = await this.resolveTape(opts.media);
|
|
234
|
+
const image = await coreRenderSegments(segments, TAPE_SIZES[tape].height, opts.textHeight, opts.space);
|
|
235
|
+
await ensureOnline(printer, opts);
|
|
236
|
+
await printBuffer(image, printer, tape);
|
|
237
|
+
return { image, media: this.getMedia(String(tape)), printer };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private async resolveTape(media?: string): Promise<TapeSize> {
|
|
241
|
+
if (media !== undefined) {
|
|
242
|
+
const t = parseInt(media.replace("mm", ""), 10) as TapeSize;
|
|
243
|
+
if (!VALID_TAPES.includes(t)) throw new Error(`Invalid tape size: ${media}. Valid: ${VALID_TAPES.join(", ")}`);
|
|
244
|
+
return t;
|
|
245
|
+
}
|
|
246
|
+
const cfg = getConfig();
|
|
247
|
+
if (cfg.defaultTape) return cfg.defaultTape;
|
|
248
|
+
const detected = await detectTapeSize();
|
|
249
|
+
return detected ?? 24;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Public Brother singleton (implements LabelPrinter). */
|
|
254
|
+
export const brotherPrinter: LabelPrinter = new BrotherPrinterImpl();
|
|
255
|
+
|
|
256
|
+
/* ----- Backward-compatible function exports ----- */
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Translate brother-label PrintOptions (with tape) to core (with media).
|
|
260
|
+
*/
|
|
261
|
+
function toCore(opts: PrintOptions): CorePrintOptions {
|
|
262
|
+
const out: CorePrintOptions = { ...opts };
|
|
263
|
+
if (opts.tape !== undefined && out.media === undefined) {
|
|
264
|
+
out.media = String(opts.tape);
|
|
265
|
+
}
|
|
266
|
+
delete (out as any).tape;
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function toCoreSeg(opts: SegmentOptions = {}): CoreSegmentOptions {
|
|
270
|
+
const out: CoreSegmentOptions = { ...opts };
|
|
271
|
+
if (opts.tape !== undefined && out.media === undefined) out.media = String(opts.tape);
|
|
272
|
+
delete (out as any).tape;
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function render(options: PrintOptions): Promise<Buffer> {
|
|
277
|
+
return brotherPrinter.render(toCore(options));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function print(options: PrintOptions): Promise<PrintResult> {
|
|
281
|
+
const r = await brotherPrinter.print(toCore(options));
|
|
282
|
+
return { image: r.image };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function renderSegments(segments: Segment[], tape?: TapeSize, textHeight?: string, space?: string): Promise<Buffer> {
|
|
286
|
+
return brotherPrinter.renderSegments(segments, {
|
|
287
|
+
media: tape !== undefined ? String(tape) : undefined,
|
|
288
|
+
textHeight, space,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function printSegments(segments: Segment[], options?: SegmentOptions): Promise<PrintResult> {
|
|
293
|
+
const r = await brotherPrinter.printSegments(segments, toCoreSeg(options));
|
|
294
|
+
return { image: r.image };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ----- Brother XPS print path (kept here, Brother-specific) ----- */
|
|
298
|
+
|
|
299
|
+
async function ensureOnline(printer: string, opts: { wait?: boolean; waitTimeoutMs?: number; waitIntervalMs?: number; onWaiting?: CorePrintOptions["onWaiting"]; log?: CorePrintOptions["log"] }): Promise<void> {
|
|
300
|
+
const status = await getPrinterStatus(printer);
|
|
301
|
+
if (status.online) return;
|
|
302
|
+
if (opts.wait === false) {
|
|
303
|
+
throw new Error(`Printer "${printer}" is offline (${status.statusText}${status.error ? ", " + status.error : ""}). Use wait:true to wait, or pick another printer.`);
|
|
304
|
+
}
|
|
305
|
+
if (opts.log) opts.log(`[brother-label] ${printer} offline (${status.statusText}); waiting...`);
|
|
306
|
+
await coreWaitOnline(printer, {
|
|
307
|
+
timeoutMs: opts.waitTimeoutMs,
|
|
308
|
+
intervalMs: opts.waitIntervalMs,
|
|
309
|
+
onWaiting: opts.onWaiting,
|
|
310
|
+
log: opts.log,
|
|
311
|
+
}, DEFAULT_PRINTER_PATTERN);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Print a single image buffer using the Brother XPS PrintTicket path.
|
|
316
|
+
* Builds an XPS document with a custom CustomMediaSize for the tape, then
|
|
317
|
+
* sends it to the print queue.
|
|
318
|
+
*/
|
|
319
|
+
async function printBuffer(imageBuffer: Buffer, printer: string, tape: TapeSize): Promise<void> {
|
|
320
|
+
const media = MEDIA_OPTIONS[tape];
|
|
321
|
+
const tempPath = path.join(os.tmpdir(), `label-${Date.now()}.png`);
|
|
322
|
+
fs.writeFileSync(tempPath, imageBuffer);
|
|
323
|
+
const psPath = tempPath.replace(/\\/g, "\\\\");
|
|
324
|
+
|
|
325
|
+
const script = `
|
|
326
|
+
$ErrorActionPreference = 'Stop'
|
|
327
|
+
Add-Type -AssemblyName System.Drawing
|
|
328
|
+
Add-Type -AssemblyName System.Printing
|
|
329
|
+
Add-Type -AssemblyName ReachFramework
|
|
330
|
+
Add-Type -AssemblyName PresentationCore
|
|
331
|
+
|
|
332
|
+
$img = [System.Drawing.Image]::FromFile('${psPath}')
|
|
333
|
+
|
|
334
|
+
$DPI = ${PRINT_DPI}
|
|
335
|
+
$MICRONS_PER_INCH = 25400
|
|
336
|
+
|
|
337
|
+
$labelLengthMicrons = [int]($img.Width / $DPI * $MICRONS_PER_INCH) + 3000
|
|
338
|
+
$tapeWidthMicrons = ${media.width}
|
|
339
|
+
|
|
340
|
+
$imgWidthWpf = $img.Width / $DPI * 96
|
|
341
|
+
$imgHeightWpf = $img.Height / $DPI * 96
|
|
342
|
+
|
|
343
|
+
$ticketXml = @"
|
|
344
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
345
|
+
<psf:PrintTicket xmlns:psf="http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework"
|
|
346
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1"
|
|
347
|
+
xmlns:psk="http://schemas.microsoft.com/windows/2003/08/printing/printschemakeywords"
|
|
348
|
+
xmlns:ns0001="http://schemas.brother.info/mfc/printing/2006/11/printschemakeywords">
|
|
349
|
+
<psf:Feature name="psk:PageMediaSize">
|
|
350
|
+
<psf:Option name="ns0001:${media.name}">
|
|
351
|
+
<psf:ScoredProperty name="psk:MediaSizeWidth">
|
|
352
|
+
<psf:Value xsi:type="xsd:integer">${media.width}</psf:Value>
|
|
353
|
+
</psf:ScoredProperty>
|
|
354
|
+
<psf:ScoredProperty name="psk:MediaSizeHeight">
|
|
355
|
+
<psf:ParameterRef name="psk:PageMediaSizeMediaSizeHeight" />
|
|
356
|
+
</psf:ScoredProperty>
|
|
357
|
+
</psf:Option>
|
|
358
|
+
</psf:Feature>
|
|
359
|
+
<psf:ParameterInit name="psk:PageMediaSizeMediaSizeHeight">
|
|
360
|
+
<psf:Value xsi:type="xsd:integer">$labelLengthMicrons</psf:Value>
|
|
361
|
+
</psf:ParameterInit>
|
|
362
|
+
<psf:Feature name="psk:PageOrientation">
|
|
363
|
+
<psf:Option name="psk:Landscape" />
|
|
364
|
+
</psf:Feature>
|
|
365
|
+
<psf:Feature name="ns0001:PageRollFeedToEndOfSheet">
|
|
366
|
+
<psf:Option name="ns0001:FeedToImageEdge" />
|
|
367
|
+
</psf:Feature>
|
|
368
|
+
<psf:Feature name="psk:PageMediaType">
|
|
369
|
+
<psf:Option name="psk:Label" />
|
|
370
|
+
</psf:Feature>
|
|
371
|
+
<psf:Feature name="ns0001:JobRollCutAtEndOfJob">
|
|
372
|
+
<psf:Option name="ns0001:Cut" />
|
|
373
|
+
</psf:Feature>
|
|
374
|
+
</psf:PrintTicket>
|
|
375
|
+
"@
|
|
376
|
+
|
|
377
|
+
$xmlBytes = [System.Text.Encoding]::UTF8.GetBytes($ticketXml)
|
|
378
|
+
$memStream = New-Object System.IO.MemoryStream(,$xmlBytes)
|
|
379
|
+
$ticket = New-Object System.Printing.PrintTicket($memStream)
|
|
380
|
+
|
|
381
|
+
$server = New-Object System.Printing.LocalPrintServer
|
|
382
|
+
$queue = $server.GetPrintQueue('${printer.replace(/'/g, "''")}')
|
|
383
|
+
|
|
384
|
+
$xpsPath = [System.IO.Path]::GetTempFileName() + ".xps"
|
|
385
|
+
|
|
386
|
+
$pageWidth = $labelLengthMicrons / $MICRONS_PER_INCH * 96
|
|
387
|
+
$pageHeight = $tapeWidthMicrons / $MICRONS_PER_INCH * 96
|
|
388
|
+
|
|
389
|
+
$package = [System.IO.Packaging.Package]::Open($xpsPath, [System.IO.FileMode]::Create)
|
|
390
|
+
$xpsDoc = New-Object System.Windows.Xps.Packaging.XpsDocument($package)
|
|
391
|
+
$writer = [System.Windows.Xps.Packaging.XpsDocument]::CreateXpsDocumentWriter($xpsDoc)
|
|
392
|
+
|
|
393
|
+
$visual = New-Object System.Windows.Media.DrawingVisual
|
|
394
|
+
$dc = $visual.RenderOpen()
|
|
395
|
+
|
|
396
|
+
$bitmapImg = New-Object System.Windows.Media.Imaging.BitmapImage
|
|
397
|
+
$bitmapImg.BeginInit()
|
|
398
|
+
$bitmapImg.UriSource = New-Object System.Uri('${psPath}')
|
|
399
|
+
$bitmapImg.EndInit()
|
|
400
|
+
|
|
401
|
+
$yOffset = ($pageHeight - $imgHeightWpf) / 2
|
|
402
|
+
$xOffset = 2 / 25.4 * 96
|
|
403
|
+
$rect = New-Object System.Windows.Rect($xOffset, $yOffset, $imgWidthWpf, $imgHeightWpf)
|
|
404
|
+
$dc.DrawImage($bitmapImg, $rect)
|
|
405
|
+
$dc.Close()
|
|
406
|
+
|
|
407
|
+
$writer.Write($visual, $ticket)
|
|
408
|
+
|
|
409
|
+
$xpsDoc.Close()
|
|
410
|
+
$package.Close()
|
|
411
|
+
$img.Dispose()
|
|
412
|
+
|
|
413
|
+
$xpsDocForPrint = New-Object System.Windows.Xps.Packaging.XpsDocument($xpsPath, [System.IO.FileAccess]::Read)
|
|
414
|
+
$seq = $xpsDocForPrint.GetFixedDocumentSequence()
|
|
415
|
+
|
|
416
|
+
$xpsWriter = [System.Printing.PrintQueue]::CreateXpsDocumentWriter($queue)
|
|
417
|
+
$xpsWriter.Write($seq, $ticket)
|
|
418
|
+
|
|
419
|
+
$xpsDocForPrint.Close()
|
|
420
|
+
Remove-Item $xpsPath -ErrorAction SilentlyContinue
|
|
421
|
+
`;
|
|
422
|
+
try {
|
|
423
|
+
await runPowerShellOrThrow(script);
|
|
424
|
+
} finally {
|
|
425
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
426
|
+
}
|
|
427
|
+
}
|