@bobfrankston/brother-label 1.0.12 → 1.0.14

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 CHANGED
@@ -35,6 +35,14 @@ Use `<img qr="data">` to embed QR codes directly in HTML - they're converted to
35
35
  <img qr="any data here" class="my-qr">
36
36
  ```
37
37
 
38
+ ### Multi-segment labels
39
+ Combine multiple `-text` and `-qr` segments on a single label, printed side-by-side in order:
40
+ ```bash
41
+ brother-print -text "Hello" -qr "https://example.com"
42
+ brother-print -text "Top\nBottom" -qr "https://example.com" -text "More text"
43
+ brother-print -t "Label" -q "data" -o combined.png
44
+ ```
45
+
38
46
  ### Print image
39
47
  ```bash
40
48
  brother-print image photo.png
@@ -48,7 +56,7 @@ brother-print preview "Test Label" -o preview.png
48
56
  ### Configuration
49
57
  ```bash
50
58
  brother-print config --show # Show current config
51
- brother-print config -t 12mm # Set default tape size
59
+ brother-print config -s 12 # Set default tape size
52
60
  brother-print config -p "Brother PT-P710BT" # Set default printer
53
61
  brother-print list # List Brother printers
54
62
  ```
@@ -56,10 +64,17 @@ brother-print list # List Brother printers
56
64
  ### Options
57
65
  | Option | Description |
58
66
  |--------|-------------|
67
+ | `-t, --text` | Force input as literal text; repeatable for multi-segment labels |
68
+ | `-q, --qr` | QR code segment; repeatable for multi-segment labels |
69
+ | `-s, --tape <size>` | Tape size: 6, 9, 12, 18, 24 (mm) |
59
70
  | `-p, --printer <name>` | Printer name |
60
- | `-t, --tape <size>` | Tape size: 6mm, 9mm, 12mm, 18mm, 24mm, or 'auto' |
61
- | `-l, --label <text>` | Text label beside QR code (qr command only) |
62
71
  | `-o, --output <file>` | Save to file instead of printing |
72
+ | `-a, --aspect <ratio>` | Aspect ratio width:height for HTML (e.g., 3.5:2) |
73
+ | `-H, --height <size>` | Text height: 12mm, .5in, or 50% (of tape height) |
74
+ | `-w, --html` | Force input as HTML file path |
75
+ | `-i, --image` | Force input as image file path |
76
+
77
+ Single-hyphen long options are supported: `-text` works the same as `--text`, `-tape` as `--tape`, etc. Single letters are strict: `-t` means `--text`, not the start of `-tape`.
63
78
 
64
79
  ## API Usage
65
80
 
@@ -175,6 +190,8 @@ interface PrintResult {
175
190
  |----------|-------------|
176
191
  | `print(options)` | Render and print a label |
177
192
  | `render(options)` | Render label to PNG buffer |
193
+ | `renderSegments(segments, tape?, textHeight?)` | Render multiple text/qr segments side-by-side |
194
+ | `printSegments(segments, options?)` | Render and print multiple segments |
178
195
  | `getConfig()` | Get current configuration |
179
196
  | `setConfig(config)` | Set default tape/printer |
180
197
  | `getConfigPath()` | Get config file path |
package/api.d.ts CHANGED
@@ -26,14 +26,26 @@ export interface PrintOptions {
26
26
  basePath?: string;
27
27
  aspect?: string;
28
28
  qrLabel?: string;
29
+ textHeight?: string;
29
30
  }
30
31
  export interface PrintResult {
31
32
  image: Buffer;
32
33
  }
34
+ export interface Segment {
35
+ type: "text" | "qr";
36
+ value: string;
37
+ }
33
38
  export declare function getConfig(): PrinterConfig;
34
39
  export declare function setConfig(config: Partial<PrinterConfig>): void;
35
40
  export declare function getConfigPath(): string;
36
41
  export declare function listPrinters(): Promise<PrinterInfo[]>;
37
42
  export declare function render(options: PrintOptions): Promise<Buffer>;
38
43
  export declare function print(options: PrintOptions): Promise<PrintResult>;
44
+ export declare function renderSegments(segments: Segment[], tape?: TapeSize, textHeight?: string, space?: string): Promise<Buffer>;
45
+ export declare function printSegments(segments: Segment[], options?: {
46
+ tape?: TapeSize;
47
+ printer?: string;
48
+ textHeight?: string;
49
+ space?: string;
50
+ }): Promise<PrintResult>;
39
51
  //# sourceMappingURL=api.d.ts.map
package/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAYH,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAC5C,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,UAAU,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC1B,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAEzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;IAGZ,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAC;CACjB;AA0BD,wBAAgB,SAAS,IAAI,aAAa,CAczC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAI9D;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAGD,wBAAsB,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAuB3D;AAsVD,wBAAsB,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAInE;AAED,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAMvE"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAYH,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAC5C,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,UAAU,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC1B,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAEzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;IAGZ,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACjB;AA0BD,wBAAgB,SAAS,IAAI,aAAa,CAczC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAI9D;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAGD,wBAAsB,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAuB3D;AA2YD,wBAAsB,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAInE;AAED,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAMvE;AAGD,wBAAsB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA+D/H;AAGD,wBAAsB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAOnK"}
package/api.js CHANGED
@@ -129,10 +129,68 @@ function resolveSettings(options) {
129
129
  printer: options.printer ?? config.defaultPrinter ?? DEFAULT_PRINTER,
130
130
  };
131
131
  }
132
+ // Parse a size spec (12px, 1mm, .2in, 50%) to pixels
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);
140
+ }
141
+ if (trimmed.endsWith("%")) {
142
+ const pct = parseFloat(trimmed.slice(0, -1));
143
+ if (isNaN(pct) || pct < 0)
144
+ throw new Error(`Invalid size: ${spec}`);
145
+ return Math.round(referencePx * pct / 100);
146
+ }
147
+ if (trimmed.endsWith("mm")) {
148
+ const mm = parseFloat(trimmed.slice(0, -2));
149
+ if (isNaN(mm) || mm < 0)
150
+ throw new Error(`Invalid size: ${spec}`);
151
+ return Math.round(mm * PRINT_DPI / 25.4);
152
+ }
153
+ if (trimmed.endsWith("in")) {
154
+ const inches = parseFloat(trimmed.slice(0, -2));
155
+ if (isNaN(inches) || inches < 0)
156
+ throw new Error(`Invalid size: ${spec}`);
157
+ return Math.round(inches * PRINT_DPI);
158
+ }
159
+ // Bare number → pixels
160
+ const px = parseFloat(trimmed);
161
+ if (!isNaN(px) && px >= 0)
162
+ return Math.round(px);
163
+ throw new Error(`Invalid size: ${spec}. Use "12px", "1mm", ".2in", or "50%"`);
164
+ }
165
+ // Parse text height spec to pixels
166
+ function parseTextHeight(spec, tapeHeightPx) {
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);
173
+ }
174
+ if (trimmed.endsWith("mm")) {
175
+ const mm = parseFloat(trimmed.slice(0, -2));
176
+ if (isNaN(mm) || mm <= 0)
177
+ throw new Error(`Invalid text height: ${spec}`);
178
+ return Math.round(mm * PRINT_DPI / 25.4);
179
+ }
180
+ if (trimmed.endsWith("in")) {
181
+ const inches = parseFloat(trimmed.slice(0, -2));
182
+ if (isNaN(inches) || inches <= 0)
183
+ throw new Error(`Invalid text height: ${spec}`);
184
+ return Math.round(inches * PRINT_DPI);
185
+ }
186
+ throw new Error(`Invalid text height: ${spec}. Use "12mm", ".5in", or "50%"`);
187
+ }
132
188
  // Render text to image buffer
133
- async function renderText(text, tape) {
189
+ async function renderText(text, tape, textHeightSpec) {
134
190
  const tapeSize = TAPE_SIZES[tape];
135
- const targetHeight = tapeSize.height;
191
+ const targetHeight = textHeightSpec
192
+ ? Math.min(parseTextHeight(textHeightSpec, tapeSize.height), tapeSize.height)
193
+ : tapeSize.height;
136
194
  const lines = text.split("\\n").join("\n");
137
195
  // Load largest font for quality
138
196
  const fontDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "node_modules/@jimp/plugin-print/fonts/open-sans");
@@ -153,11 +211,12 @@ async function renderText(text, tape) {
153
211
  const finalHeight = Math.round(imgHeight * scale);
154
212
  tempImage.resize({ w: finalWidth, h: finalHeight });
155
213
  // Create final image with padding
156
- const hPadding = Math.round(targetHeight * 0.2);
214
+ const canvasHeight = tapeSize.height;
215
+ const hPadding = Math.round(canvasHeight * 0.2);
157
216
  const outputWidth = finalWidth + hPadding * 2;
158
- const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });
217
+ const image = new Jimp({ width: outputWidth, height: canvasHeight, color: 0xffffffff });
159
218
  const xOffset = hPadding;
160
- const yOffset = Math.round((targetHeight - finalHeight) / 2);
219
+ const yOffset = Math.round((canvasHeight - finalHeight) / 2);
161
220
  image.composite(tempImage, xOffset, yOffset);
162
221
  return image.getBuffer("image/png");
163
222
  }
@@ -218,11 +277,11 @@ async function renderQr(data, tape, labelText) {
218
277
  async function renderContent(options, tape) {
219
278
  const tapeSize = TAPE_SIZES[tape];
220
279
  if (options.text !== undefined) {
221
- return renderText(options.text, tape);
280
+ return renderText(options.text, tape, options.textHeight);
222
281
  }
223
282
  if (options.textFile !== undefined) {
224
283
  const text = fs.readFileSync(options.textFile, "utf-8");
225
- return renderText(text, tape);
284
+ return renderText(text, tape, options.textHeight);
226
285
  }
227
286
  if (options.html !== undefined) {
228
287
  const { width, height } = getHtmlDimensions(tapeSize.height, options.aspect);
@@ -387,4 +446,76 @@ export async function print(options) {
387
446
  await printBuffer(image, printer, tape);
388
447
  return { image };
389
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
+ }
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
+ }
390
521
  //# sourceMappingURL=api.js.map
package/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["api.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * Brother Label Printer API\r\n * Programmatic interface for printing labels on Brother P-touch printers\r\n */\r\n\r\nimport * as fs from \"fs\";\r\nimport * as path from \"path\";\r\nimport { fileURLToPath } from \"url\";\r\nimport * as os from \"os\";\r\nimport { renderHtmlFile, renderHtmlString, closeBrowser } from \"./render.js\";\r\nimport { Jimp, loadFont, measureText, measureTextHeight } from \"jimp\";\r\nimport { spawn } from \"child_process\";\r\nimport QRCode from \"qrcode\";\r\n\r\n// Types\r\nexport type TapeSize = 6 | 9 | 12 | 18 | 24;\r\nexport type Orientation = \"landscape\" | \"portrait\";\r\n\r\nexport interface PrinterConfig {\r\n defaultTape?: TapeSize;\r\n defaultPrinter?: string;\r\n}\r\n\r\nexport interface PrinterInfo {\r\n name: string;\r\n}\r\n\r\nexport interface PrintOptions {\r\n // Content (exactly one required)\r\n text?: string;\r\n html?: string;\r\n htmlPath?: string;\r\n textFile?: string;\r\n imagePath?: string;\r\n imageBuffer?: Buffer;\r\n qr?: string; // QR code data to encode\r\n\r\n // Settings\r\n tape?: TapeSize;\r\n printer?: string;\r\n orientation?: Orientation;\r\n length?: number; // explicit length in mm\r\n basePath?: string; // base path for inline HTML resources\r\n aspect?: string; // width:height ratio for HTML (e.g., \"3.5:2\")\r\n qrLabel?: string; // optional text label beside QR code\r\n}\r\n\r\nexport interface PrintResult {\r\n image: Buffer;\r\n}\r\n\r\n// Constants\r\nconst CONFIG_PATH = path.join(os.homedir(), \".brother-label.json\");\r\nconst DEFAULT_PRINTER = \"Brother PT-P710BT\";\r\nconst PRINT_DPI = 300; // High resolution for quality output\r\n\r\n// Tape sizes: height is printable area in pixels at 300 DPI\r\nconst TAPE_SIZES: Record<TapeSize, { width: number; height: number }> = {\r\n 6: { width: 1137, height: 56 }, // 3.79\" x 0.19\" @ 300 DPI\r\n 9: { width: 1137, height: 84 }, // 3.79\" x 0.28\" @ 300 DPI\r\n 12: { width: 1137, height: 113 }, // 3.79\" x 0.38\" @ 300 DPI\r\n 18: { width: 1137, height: 169 }, // 3.79\" x 0.56\" @ 300 DPI\r\n 24: { width: 1137, height: 213 }, // 3.79\" x 0.71\" @ 300 DPI\r\n};\r\n\r\n// Brother CustomMediaSize names and widths in microns\r\nconst MEDIA_OPTIONS: Record<TapeSize, { name: string; width: number }> = {\r\n 6: { name: \"CustomMediaSize257\", width: 5900 },\r\n 9: { name: \"CustomMediaSize258\", width: 9000 },\r\n 12: { name: \"CustomMediaSize259\", width: 11900 },\r\n 18: { name: \"CustomMediaSize260\", width: 18100 },\r\n 24: { name: \"CustomMediaSize261\", width: 24000 },\r\n};\r\n\r\n// Config functions\r\nexport function getConfig(): PrinterConfig {\r\n try {\r\n if (fs.existsSync(CONFIG_PATH)) {\r\n const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, \"utf-8\"));\r\n // Handle legacy format where tape was stored as \"24mm\" string\r\n if (typeof raw.defaultTape === \"string\") {\r\n raw.defaultTape = parseInt(raw.defaultTape.replace(\"mm\", \"\"), 10) as TapeSize;\r\n }\r\n return raw;\r\n }\r\n } catch {\r\n // Ignore config errors\r\n }\r\n return {};\r\n}\r\n\r\nexport function setConfig(config: Partial<PrinterConfig>): void {\r\n const current = getConfig();\r\n const merged = { ...current, ...config };\r\n fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));\r\n}\r\n\r\nexport function getConfigPath(): string {\r\n return CONFIG_PATH;\r\n}\r\n\r\n// Printer listing\r\nexport async function listPrinters(): Promise<PrinterInfo[]> {\r\n return new Promise((resolve, reject) => {\r\n const ps = spawn(\"powershell\", [\r\n \"-Command\",\r\n \"Get-Printer | Where-Object { $_.Name -like '*Brother*' -or $_.Name -like '*PT*' -or $_.Name -like '*QL*' } | Select-Object -ExpandProperty Name\"\r\n ], { stdio: \"pipe\" });\r\n\r\n let stdout = \"\";\r\n let stderr = \"\";\r\n ps.stdout.on(\"data\", (data) => { stdout += data.toString(); });\r\n ps.stderr.on(\"data\", (data) => { stderr += data.toString(); });\r\n\r\n ps.on(\"close\", (code) => {\r\n if (code !== 0) {\r\n reject(new Error(`Failed to list printers: ${stderr}`));\r\n return;\r\n }\r\n const printers = stdout.trim().split(\"\\n\")\r\n .filter(p => p.trim())\r\n .map(name => ({ name: name.trim() }));\r\n resolve(printers);\r\n });\r\n });\r\n}\r\n\r\n// Calculate HTML viewport dimensions from tape height and optional aspect ratio\r\nfunction getHtmlDimensions(tapeHeight: number, aspect?: string): { width?: number; height: number } {\r\n if (!aspect) {\r\n // Auto-detect width from content\r\n return { width: undefined, height: tapeHeight };\r\n }\r\n\r\n // Parse aspect ratio \"width:height\" or \"width/height\" (e.g., \"3.5:2\" or \"3.5/2\")\r\n const separator = aspect.includes(\"/\") ? \"/\" : \":\";\r\n const parts = aspect.split(separator);\r\n if (parts.length !== 2) {\r\n throw new Error(`Invalid aspect ratio: ${aspect}. Use format \"width:height\" or \"width/height\"`);\r\n }\r\n const aspectWidth = parseFloat(parts[0]);\r\n const aspectHeight = parseFloat(parts[1]);\r\n if (isNaN(aspectWidth) || isNaN(aspectHeight) || aspectHeight === 0) {\r\n throw new Error(`Invalid aspect ratio: ${aspect}. Use format \"width:height\" (e.g., \"3.5:2\")`);\r\n }\r\n\r\n // Scale so height fits tape, width is proportional\r\n const height = tapeHeight;\r\n const width = Math.round(tapeHeight * (aspectWidth / aspectHeight));\r\n return { width, height };\r\n}\r\n\r\n// Validation\r\nfunction validateOptions(options: PrintOptions): void {\r\n const contentFields = [\r\n options.text,\r\n options.html,\r\n options.htmlPath,\r\n options.textFile,\r\n options.imagePath,\r\n options.imageBuffer,\r\n options.qr,\r\n ].filter(f => f !== undefined);\r\n\r\n if (contentFields.length === 0) {\r\n throw new Error(\"No content provided. Specify one of: text, html, htmlPath, textFile, imagePath, imageBuffer, qr\");\r\n }\r\n if (contentFields.length > 1) {\r\n throw new Error(\"Multiple content options provided. Specify exactly one.\");\r\n }\r\n\r\n if (options.tape !== undefined && !TAPE_SIZES[options.tape]) {\r\n throw new Error(`Invalid tape size: ${options.tape}. Valid sizes: 6, 9, 12, 18, 24`);\r\n }\r\n}\r\n\r\n// Resolve effective settings from options + config + defaults\r\nfunction resolveSettings(options: PrintOptions): { tape: TapeSize; printer: string } {\r\n const config = getConfig();\r\n return {\r\n tape: options.tape ?? config.defaultTape ?? 24,\r\n printer: options.printer ?? config.defaultPrinter ?? DEFAULT_PRINTER,\r\n };\r\n}\r\n\r\n// Render text to image buffer\r\nasync function renderText(text: string, tape: TapeSize): Promise<Buffer> {\r\n const tapeSize = TAPE_SIZES[tape];\r\n const targetHeight = tapeSize.height;\r\n const lines = text.split(\"\\\\n\").join(\"\\n\");\r\n\r\n // Load largest font for quality\r\n const fontDir = path.join(path.dirname(fileURLToPath(import.meta.url)), \"node_modules/@jimp/plugin-print/fonts/open-sans\");\r\n const fontPath = path.join(fontDir, \"open-sans-128-black\", \"open-sans-128-black.fnt\");\r\n const font = await loadFont(fontPath);\r\n\r\n // Measure text\r\n const textWidth = measureText(font, lines);\r\n const textHeight = measureTextHeight(font, lines, textWidth + 100);\r\n\r\n // Create temp image for text\r\n const padding = 10;\r\n const imgWidth = textWidth + padding * 2;\r\n const imgHeight = textHeight + padding * 2;\r\n\r\n const tempImage = new Jimp({ width: imgWidth, height: imgHeight, color: 0xffffffff });\r\n tempImage.print({ font, x: padding, y: padding, text: lines });\r\n\r\n // Scale to fit tape height\r\n const scale = targetHeight / imgHeight;\r\n const finalWidth = Math.round(imgWidth * scale);\r\n const finalHeight = Math.round(imgHeight * scale);\r\n tempImage.resize({ w: finalWidth, h: finalHeight });\r\n\r\n // Create final image with padding\r\n const hPadding = Math.round(targetHeight * 0.2);\r\n const outputWidth = finalWidth + hPadding * 2;\r\n const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });\r\n\r\n const xOffset = hPadding;\r\n const yOffset = Math.round((targetHeight - finalHeight) / 2);\r\n image.composite(tempImage, xOffset, yOffset);\r\n\r\n return image.getBuffer(\"image/png\");\r\n}\r\n\r\n// Render QR code to image buffer\r\nasync function renderQr(data: string, tape: TapeSize, labelText?: string): Promise<Buffer> {\r\n const tapeSize = TAPE_SIZES[tape];\r\n const targetHeight = tapeSize.height;\r\n const qrSize = Math.floor(targetHeight * 0.95);\r\n\r\n // Margins in pixels at PRINT_DPI (3mm left, 4mm right)\r\n const pxPerMm = PRINT_DPI / 25.4;\r\n const leftMargin = Math.round(3 * pxPerMm);\r\n const rightMargin = Math.round(4 * pxPerMm);\r\n\r\n // Generate QR code at high resolution\r\n const qrBuffer = await QRCode.toBuffer(data, {\r\n type: \"png\",\r\n width: qrSize,\r\n margin: 0,\r\n errorCorrectionLevel: \"M\",\r\n color: { dark: \"#000000\", light: \"#ffffff\" },\r\n });\r\n\r\n const qrImage = await Jimp.read(qrBuffer);\r\n\r\n if (!labelText) {\r\n // QR only\r\n const outputWidth = leftMargin + qrSize + rightMargin;\r\n const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });\r\n const yOffset = Math.floor((targetHeight - qrSize) / 2);\r\n image.composite(qrImage, leftMargin, yOffset);\r\n return image.getBuffer(\"image/png\");\r\n }\r\n\r\n // QR + text label\r\n const fontDir = path.join(path.dirname(fileURLToPath(import.meta.url)), \"node_modules/@jimp/plugin-print/fonts/open-sans\");\r\n const fontPath = path.join(fontDir, \"open-sans-64-black\", \"open-sans-64-black.fnt\");\r\n const font = await loadFont(fontPath);\r\n\r\n const textWidth = measureText(font, labelText);\r\n const textHeight = measureTextHeight(font, labelText, textWidth + 50);\r\n\r\n // Scale text to fit beside QR\r\n const maxTextHeight = targetHeight * 0.8;\r\n const textScale = Math.min(1, maxTextHeight / textHeight);\r\n const scaledTextWidth = Math.round(textWidth * textScale);\r\n const scaledTextHeight = Math.round(textHeight * textScale);\r\n\r\n // Create text image\r\n const textImg = new Jimp({ width: textWidth + 20, height: textHeight + 20, color: 0xffffffff });\r\n textImg.print({ font, x: 10, y: 10, text: labelText });\r\n if (textScale < 1) {\r\n textImg.resize({ w: scaledTextWidth, h: scaledTextHeight });\r\n }\r\n\r\n // Compose: QR on left, text on right\r\n const gap = Math.floor(targetHeight * 0.15);\r\n const outputWidth = leftMargin + qrSize + gap + scaledTextWidth + rightMargin;\r\n const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });\r\n\r\n const qrY = Math.floor((targetHeight - qrSize) / 2);\r\n image.composite(qrImage, leftMargin, qrY);\r\n\r\n const textY = Math.floor((targetHeight - scaledTextHeight) / 2);\r\n image.composite(textImg, leftMargin + qrSize + gap, textY);\r\n\r\n return image.getBuffer(\"image/png\");\r\n}\r\n\r\n// Render content to image buffer\r\nasync function renderContent(options: PrintOptions, tape: TapeSize): Promise<Buffer> {\r\n const tapeSize = TAPE_SIZES[tape];\r\n\r\n if (options.text !== undefined) {\r\n return renderText(options.text, tape);\r\n }\r\n\r\n if (options.textFile !== undefined) {\r\n const text = fs.readFileSync(options.textFile, \"utf-8\");\r\n return renderText(text, tape);\r\n }\r\n\r\n if (options.html !== undefined) {\r\n const { width, height } = getHtmlDimensions(tapeSize.height, options.aspect);\r\n const buffer = await renderHtmlString(options.html, { width, height, tapeMm: tape }, options.basePath);\r\n await closeBrowser();\r\n return buffer;\r\n }\r\n\r\n if (options.htmlPath !== undefined) {\r\n const { width, height } = getHtmlDimensions(tapeSize.height, options.aspect);\r\n const buffer = await renderHtmlFile(options.htmlPath, { width, height, tapeMm: tape });\r\n await closeBrowser();\r\n return buffer;\r\n }\r\n\r\n if (options.imagePath !== undefined) {\r\n return fs.readFileSync(options.imagePath);\r\n }\r\n\r\n if (options.imageBuffer !== undefined) {\r\n return options.imageBuffer;\r\n }\r\n\r\n if (options.qr !== undefined) {\r\n return renderQr(options.qr, tape, options.qrLabel);\r\n }\r\n\r\n throw new Error(\"No content to render\");\r\n}\r\n\r\n// Print single image buffer to printer\r\nasync function printBuffer(imageBuffer: Buffer, printer: string, tape: TapeSize): Promise<void> {\r\n const media = MEDIA_OPTIONS[tape];\r\n\r\n const tempPath = path.join(os.tmpdir(), `label-${Date.now()}.png`);\r\n fs.writeFileSync(tempPath, imageBuffer);\r\n\r\n try {\r\n await new Promise<void>((resolve, reject) => {\r\n const psScript = `\r\nAdd-Type -AssemblyName System.Drawing\r\nAdd-Type -AssemblyName System.Printing\r\nAdd-Type -AssemblyName ReachFramework\r\nAdd-Type -AssemblyName PresentationCore\r\n\r\n$img = [System.Drawing.Image]::FromFile('${tempPath.replace(/\\\\/g, \"\\\\\\\\\")}')\r\n\r\n$DPI = ${PRINT_DPI}\r\n$MICRONS_PER_INCH = 25400\r\n\r\n$labelLengthMicrons = [int]($img.Width / $DPI * $MICRONS_PER_INCH) + 3000 # +2mm offset +1mm buffer\r\n$tapeWidthMicrons = ${media.width}\r\n\r\n$imgWidthWpf = $img.Width / $DPI * 96\r\n$imgHeightWpf = $img.Height / $DPI * 96\r\n\r\n$ticketXml = @\"\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<psf:PrintTicket xmlns:psf=\"http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework\"\r\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" version=\"1\"\r\n xmlns:psk=\"http://schemas.microsoft.com/windows/2003/08/printing/printschemakeywords\"\r\n xmlns:ns0001=\"http://schemas.brother.info/mfc/printing/2006/11/printschemakeywords\">\r\n <psf:Feature name=\"psk:PageMediaSize\">\r\n <psf:Option name=\"ns0001:${media.name}\">\r\n <psf:ScoredProperty name=\"psk:MediaSizeWidth\">\r\n <psf:Value xsi:type=\"xsd:integer\">${media.width}</psf:Value>\r\n </psf:ScoredProperty>\r\n <psf:ScoredProperty name=\"psk:MediaSizeHeight\">\r\n <psf:ParameterRef name=\"psk:PageMediaSizeMediaSizeHeight\" />\r\n </psf:ScoredProperty>\r\n </psf:Option>\r\n </psf:Feature>\r\n <psf:ParameterInit name=\"psk:PageMediaSizeMediaSizeHeight\">\r\n <psf:Value xsi:type=\"xsd:integer\">$labelLengthMicrons</psf:Value>\r\n </psf:ParameterInit>\r\n <psf:Feature name=\"psk:PageOrientation\">\r\n <psf:Option name=\"psk:Landscape\" />\r\n </psf:Feature>\r\n <psf:Feature name=\"ns0001:PageRollFeedToEndOfSheet\">\r\n <psf:Option name=\"ns0001:FeedToImageEdge\" />\r\n </psf:Feature>\r\n <psf:Feature name=\"psk:PageMediaType\">\r\n <psf:Option name=\"psk:Label\" />\r\n </psf:Feature>\r\n <psf:Feature name=\"ns0001:JobRollCutAtEndOfJob\">\r\n <psf:Option name=\"ns0001:Cut\" />\r\n </psf:Feature>\r\n</psf:PrintTicket>\r\n\"@\r\n\r\n$xmlBytes = [System.Text.Encoding]::UTF8.GetBytes($ticketXml)\r\n$memStream = New-Object System.IO.MemoryStream(,$xmlBytes)\r\n$ticket = New-Object System.Printing.PrintTicket($memStream)\r\n\r\n$server = New-Object System.Printing.LocalPrintServer\r\n$queue = $server.GetPrintQueue('${printer}')\r\n\r\n$xpsPath = [System.IO.Path]::GetTempFileName() + \".xps\"\r\n\r\n$pageWidth = $labelLengthMicrons / $MICRONS_PER_INCH * 96\r\n$pageHeight = $tapeWidthMicrons / $MICRONS_PER_INCH * 96\r\n\r\n$package = [System.IO.Packaging.Package]::Open($xpsPath, [System.IO.FileMode]::Create)\r\n$xpsDoc = New-Object System.Windows.Xps.Packaging.XpsDocument($package)\r\n$writer = [System.Windows.Xps.Packaging.XpsDocument]::CreateXpsDocumentWriter($xpsDoc)\r\n\r\n$visual = New-Object System.Windows.Media.DrawingVisual\r\n$dc = $visual.RenderOpen()\r\n\r\n$bitmapImg = New-Object System.Windows.Media.Imaging.BitmapImage\r\n$bitmapImg.BeginInit()\r\n$bitmapImg.UriSource = New-Object System.Uri('${tempPath.replace(/\\\\/g, \"\\\\\\\\\")}')\r\n$bitmapImg.EndInit()\r\n\r\n$yOffset = ($pageHeight - $imgHeightWpf) / 2\r\n$xOffset = 2 / 25.4 * 96 # 2mm left margin in WPF units\r\n$rect = New-Object System.Windows.Rect($xOffset, $yOffset, $imgWidthWpf, $imgHeightWpf)\r\n$dc.DrawImage($bitmapImg, $rect)\r\n$dc.Close()\r\n\r\n$writer.Write($visual, $ticket)\r\n\r\n$xpsDoc.Close()\r\n$package.Close()\r\n$img.Dispose()\r\n\r\n$xpsDocForPrint = New-Object System.Windows.Xps.Packaging.XpsDocument($xpsPath, [System.IO.FileAccess]::Read)\r\n$seq = $xpsDocForPrint.GetFixedDocumentSequence()\r\n\r\n$xpsWriter = [System.Printing.PrintQueue]::CreateXpsDocumentWriter($queue)\r\n$xpsWriter.Write($seq, $ticket)\r\n\r\n$xpsDocForPrint.Close()\r\nRemove-Item $xpsPath -ErrorAction SilentlyContinue\r\n`;\r\n\r\n const ps = spawn(\"powershell\", [\"-Command\", psScript], { stdio: \"pipe\" });\r\n\r\n let stdout = \"\";\r\n let stderr = \"\";\r\n ps.stdout.on(\"data\", (data) => { stdout += data.toString(); });\r\n ps.stderr.on(\"data\", (data) => { stderr += data.toString(); });\r\n\r\n ps.on(\"close\", (code) => {\r\n if (code === 0) {\r\n resolve();\r\n } else {\r\n reject(new Error(`Print failed: ${stderr}`));\r\n }\r\n });\r\n\r\n ps.on(\"error\", (err) => {\r\n reject(new Error(`Failed to print: ${err.message}`));\r\n });\r\n });\r\n } finally {\r\n if (fs.existsSync(tempPath)) {\r\n fs.unlinkSync(tempPath);\r\n }\r\n }\r\n}\r\n\r\n// Main API functions\r\nexport async function render(options: PrintOptions): Promise<Buffer> {\r\n validateOptions(options);\r\n const { tape } = resolveSettings(options);\r\n return renderContent(options, tape);\r\n}\r\n\r\nexport async function print(options: PrintOptions): Promise<PrintResult> {\r\n validateOptions(options);\r\n const { tape, printer } = resolveSettings(options);\r\n const image = await renderContent(options, tape);\r\n await printBuffer(image, printer, tape);\r\n return { image };\r\n}\r\n"]}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["api.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * Brother Label Printer API\r\n * Programmatic interface for printing labels on Brother P-touch printers\r\n */\r\n\r\nimport * as fs from \"fs\";\r\nimport * as path from \"path\";\r\nimport { fileURLToPath } from \"url\";\r\nimport * as os from \"os\";\r\nimport { renderHtmlFile, renderHtmlString, closeBrowser } from \"./render.js\";\r\nimport { Jimp, loadFont, measureText, measureTextHeight } from \"jimp\";\r\nimport { spawn } from \"child_process\";\r\nimport QRCode from \"qrcode\";\r\n\r\n// Types\r\nexport type TapeSize = 6 | 9 | 12 | 18 | 24;\r\nexport type Orientation = \"landscape\" | \"portrait\";\r\n\r\nexport interface PrinterConfig {\r\n defaultTape?: TapeSize;\r\n defaultPrinter?: string;\r\n}\r\n\r\nexport interface PrinterInfo {\r\n name: string;\r\n}\r\n\r\nexport interface PrintOptions {\r\n // Content (exactly one required)\r\n text?: string;\r\n html?: string;\r\n htmlPath?: string;\r\n textFile?: string;\r\n imagePath?: string;\r\n imageBuffer?: Buffer;\r\n qr?: string; // QR code data to encode\r\n\r\n // Settings\r\n tape?: TapeSize;\r\n printer?: string;\r\n orientation?: Orientation;\r\n length?: number; // explicit length in mm\r\n basePath?: string; // base path for inline HTML resources\r\n aspect?: string; // width:height ratio for HTML (e.g., \"3.5:2\")\r\n qrLabel?: string; // optional text label beside QR code\r\n textHeight?: string; // text height: \"12mm\", \".5in\", \"50%\" (of tape height)\r\n}\r\n\r\nexport interface PrintResult {\r\n image: Buffer;\r\n}\r\n\r\nexport interface Segment {\r\n type: \"text\" | \"qr\";\r\n value: string;\r\n}\r\n\r\n// Constants\r\nconst CONFIG_PATH = path.join(os.homedir(), \".brother-label.json\");\r\nconst DEFAULT_PRINTER = \"Brother PT-P710BT\";\r\nconst PRINT_DPI = 300; // High resolution for quality output\r\n\r\n// Tape sizes: height is printable area in pixels at 300 DPI\r\nconst TAPE_SIZES: Record<TapeSize, { width: number; height: number }> = {\r\n 6: { width: 1137, height: 56 }, // 3.79\" x 0.19\" @ 300 DPI\r\n 9: { width: 1137, height: 84 }, // 3.79\" x 0.28\" @ 300 DPI\r\n 12: { width: 1137, height: 113 }, // 3.79\" x 0.38\" @ 300 DPI\r\n 18: { width: 1137, height: 169 }, // 3.79\" x 0.56\" @ 300 DPI\r\n 24: { width: 1137, height: 213 }, // 3.79\" x 0.71\" @ 300 DPI\r\n};\r\n\r\n// Brother CustomMediaSize names and widths in microns\r\nconst MEDIA_OPTIONS: Record<TapeSize, { name: string; width: number }> = {\r\n 6: { name: \"CustomMediaSize257\", width: 5900 },\r\n 9: { name: \"CustomMediaSize258\", width: 9000 },\r\n 12: { name: \"CustomMediaSize259\", width: 11900 },\r\n 18: { name: \"CustomMediaSize260\", width: 18100 },\r\n 24: { name: \"CustomMediaSize261\", width: 24000 },\r\n};\r\n\r\n// Config functions\r\nexport function getConfig(): PrinterConfig {\r\n try {\r\n if (fs.existsSync(CONFIG_PATH)) {\r\n const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, \"utf-8\"));\r\n // Handle legacy format where tape was stored as \"24mm\" string\r\n if (typeof raw.defaultTape === \"string\") {\r\n raw.defaultTape = parseInt(raw.defaultTape.replace(\"mm\", \"\"), 10) as TapeSize;\r\n }\r\n return raw;\r\n }\r\n } catch {\r\n // Ignore config errors\r\n }\r\n return {};\r\n}\r\n\r\nexport function setConfig(config: Partial<PrinterConfig>): void {\r\n const current = getConfig();\r\n const merged = { ...current, ...config };\r\n fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));\r\n}\r\n\r\nexport function getConfigPath(): string {\r\n return CONFIG_PATH;\r\n}\r\n\r\n// Printer listing\r\nexport async function listPrinters(): Promise<PrinterInfo[]> {\r\n return new Promise((resolve, reject) => {\r\n const ps = spawn(\"powershell\", [\r\n \"-Command\",\r\n \"Get-Printer | Where-Object { $_.Name -like '*Brother*' -or $_.Name -like '*PT*' -or $_.Name -like '*QL*' } | Select-Object -ExpandProperty Name\"\r\n ], { stdio: \"pipe\" });\r\n\r\n let stdout = \"\";\r\n let stderr = \"\";\r\n ps.stdout.on(\"data\", (data) => { stdout += data.toString(); });\r\n ps.stderr.on(\"data\", (data) => { stderr += data.toString(); });\r\n\r\n ps.on(\"close\", (code) => {\r\n if (code !== 0) {\r\n reject(new Error(`Failed to list printers: ${stderr}`));\r\n return;\r\n }\r\n const printers = stdout.trim().split(\"\\n\")\r\n .filter(p => p.trim())\r\n .map(name => ({ name: name.trim() }));\r\n resolve(printers);\r\n });\r\n });\r\n}\r\n\r\n// Calculate HTML viewport dimensions from tape height and optional aspect ratio\r\nfunction getHtmlDimensions(tapeHeight: number, aspect?: string): { width?: number; height: number } {\r\n if (!aspect) {\r\n // Auto-detect width from content\r\n return { width: undefined, height: tapeHeight };\r\n }\r\n\r\n // Parse aspect ratio \"width:height\" or \"width/height\" (e.g., \"3.5:2\" or \"3.5/2\")\r\n const separator = aspect.includes(\"/\") ? \"/\" : \":\";\r\n const parts = aspect.split(separator);\r\n if (parts.length !== 2) {\r\n throw new Error(`Invalid aspect ratio: ${aspect}. Use format \"width:height\" or \"width/height\"`);\r\n }\r\n const aspectWidth = parseFloat(parts[0]);\r\n const aspectHeight = parseFloat(parts[1]);\r\n if (isNaN(aspectWidth) || isNaN(aspectHeight) || aspectHeight === 0) {\r\n throw new Error(`Invalid aspect ratio: ${aspect}. Use format \"width:height\" (e.g., \"3.5:2\")`);\r\n }\r\n\r\n // Scale so height fits tape, width is proportional\r\n const height = tapeHeight;\r\n const width = Math.round(tapeHeight * (aspectWidth / aspectHeight));\r\n return { width, height };\r\n}\r\n\r\n// Validation\r\nfunction validateOptions(options: PrintOptions): void {\r\n const contentFields = [\r\n options.text,\r\n options.html,\r\n options.htmlPath,\r\n options.textFile,\r\n options.imagePath,\r\n options.imageBuffer,\r\n options.qr,\r\n ].filter(f => f !== undefined);\r\n\r\n if (contentFields.length === 0) {\r\n throw new Error(\"No content provided. Specify one of: text, html, htmlPath, textFile, imagePath, imageBuffer, qr\");\r\n }\r\n if (contentFields.length > 1) {\r\n throw new Error(\"Multiple content options provided. Specify exactly one.\");\r\n }\r\n\r\n if (options.tape !== undefined && !TAPE_SIZES[options.tape]) {\r\n throw new Error(`Invalid tape size: ${options.tape}. Valid sizes: 6, 9, 12, 18, 24`);\r\n }\r\n}\r\n\r\n// Resolve effective settings from options + config + defaults\r\nfunction resolveSettings(options: PrintOptions): { tape: TapeSize; printer: string } {\r\n const config = getConfig();\r\n return {\r\n tape: options.tape ?? config.defaultTape ?? 24,\r\n printer: options.printer ?? config.defaultPrinter ?? DEFAULT_PRINTER,\r\n };\r\n}\r\n\r\n// Parse a size spec (12px, 1mm, .2in, 50%) to pixels\r\nfunction parseSize(spec: string, referencePx: number): number {\r\n const trimmed = spec.trim().toLowerCase();\r\n if (trimmed.endsWith(\"px\")) {\r\n const px = parseFloat(trimmed.slice(0, -2));\r\n if (isNaN(px) || px < 0) throw new Error(`Invalid size: ${spec}`);\r\n return Math.round(px);\r\n }\r\n if (trimmed.endsWith(\"%\")) {\r\n const pct = parseFloat(trimmed.slice(0, -1));\r\n if (isNaN(pct) || pct < 0) throw new Error(`Invalid size: ${spec}`);\r\n return Math.round(referencePx * pct / 100);\r\n }\r\n if (trimmed.endsWith(\"mm\")) {\r\n const mm = parseFloat(trimmed.slice(0, -2));\r\n if (isNaN(mm) || mm < 0) throw new Error(`Invalid size: ${spec}`);\r\n return Math.round(mm * PRINT_DPI / 25.4);\r\n }\r\n if (trimmed.endsWith(\"in\")) {\r\n const inches = parseFloat(trimmed.slice(0, -2));\r\n if (isNaN(inches) || inches < 0) throw new Error(`Invalid size: ${spec}`);\r\n return Math.round(inches * PRINT_DPI);\r\n }\r\n // Bare number → pixels\r\n const px = parseFloat(trimmed);\r\n if (!isNaN(px) && px >= 0) return Math.round(px);\r\n throw new Error(`Invalid size: ${spec}. Use \"12px\", \"1mm\", \".2in\", or \"50%\"`);\r\n}\r\n\r\n// Parse text height spec to pixels\r\nfunction parseTextHeight(spec: string, tapeHeightPx: number): number {\r\n const trimmed = spec.trim().toLowerCase();\r\n if (trimmed.endsWith(\"%\")) {\r\n const pct = parseFloat(trimmed.slice(0, -1));\r\n if (isNaN(pct) || pct <= 0) throw new Error(`Invalid text height: ${spec}`);\r\n return Math.round(tapeHeightPx * pct / 100);\r\n }\r\n if (trimmed.endsWith(\"mm\")) {\r\n const mm = parseFloat(trimmed.slice(0, -2));\r\n if (isNaN(mm) || mm <= 0) throw new Error(`Invalid text height: ${spec}`);\r\n return Math.round(mm * PRINT_DPI / 25.4);\r\n }\r\n if (trimmed.endsWith(\"in\")) {\r\n const inches = parseFloat(trimmed.slice(0, -2));\r\n if (isNaN(inches) || inches <= 0) throw new Error(`Invalid text height: ${spec}`);\r\n return Math.round(inches * PRINT_DPI);\r\n }\r\n throw new Error(`Invalid text height: ${spec}. Use \"12mm\", \".5in\", or \"50%\"`);\r\n}\r\n\r\n// Render text to image buffer\r\nasync function renderText(text: string, tape: TapeSize, textHeightSpec?: string): Promise<Buffer> {\r\n const tapeSize = TAPE_SIZES[tape];\r\n const targetHeight = textHeightSpec\r\n ? Math.min(parseTextHeight(textHeightSpec, tapeSize.height), tapeSize.height)\r\n : tapeSize.height;\r\n const lines = text.split(\"\\\\n\").join(\"\\n\");\r\n\r\n // Load largest font for quality\r\n const fontDir = path.join(path.dirname(fileURLToPath(import.meta.url)), \"node_modules/@jimp/plugin-print/fonts/open-sans\");\r\n const fontPath = path.join(fontDir, \"open-sans-128-black\", \"open-sans-128-black.fnt\");\r\n const font = await loadFont(fontPath);\r\n\r\n // Measure text\r\n const textWidth = measureText(font, lines);\r\n const textHeight = measureTextHeight(font, lines, textWidth + 100);\r\n\r\n // Create temp image for text\r\n const padding = 10;\r\n const imgWidth = textWidth + padding * 2;\r\n const imgHeight = textHeight + padding * 2;\r\n\r\n const tempImage = new Jimp({ width: imgWidth, height: imgHeight, color: 0xffffffff });\r\n tempImage.print({ font, x: padding, y: padding, text: lines });\r\n\r\n // Scale to fit tape height\r\n const scale = targetHeight / imgHeight;\r\n const finalWidth = Math.round(imgWidth * scale);\r\n const finalHeight = Math.round(imgHeight * scale);\r\n tempImage.resize({ w: finalWidth, h: finalHeight });\r\n\r\n // Create final image with padding\r\n const canvasHeight = tapeSize.height;\r\n const hPadding = Math.round(canvasHeight * 0.2);\r\n const outputWidth = finalWidth + hPadding * 2;\r\n const image = new Jimp({ width: outputWidth, height: canvasHeight, color: 0xffffffff });\r\n\r\n const xOffset = hPadding;\r\n const yOffset = Math.round((canvasHeight - finalHeight) / 2);\r\n image.composite(tempImage, xOffset, yOffset);\r\n\r\n return image.getBuffer(\"image/png\");\r\n}\r\n\r\n// Render QR code to image buffer\r\nasync function renderQr(data: string, tape: TapeSize, labelText?: string): Promise<Buffer> {\r\n const tapeSize = TAPE_SIZES[tape];\r\n const targetHeight = tapeSize.height;\r\n const qrSize = Math.floor(targetHeight * 0.95);\r\n\r\n // Margins in pixels at PRINT_DPI (3mm left, 4mm right)\r\n const pxPerMm = PRINT_DPI / 25.4;\r\n const leftMargin = Math.round(3 * pxPerMm);\r\n const rightMargin = Math.round(4 * pxPerMm);\r\n\r\n // Generate QR code at high resolution\r\n const qrBuffer = await QRCode.toBuffer(data, {\r\n type: \"png\",\r\n width: qrSize,\r\n margin: 0,\r\n errorCorrectionLevel: \"M\",\r\n color: { dark: \"#000000\", light: \"#ffffff\" },\r\n });\r\n\r\n const qrImage = await Jimp.read(qrBuffer);\r\n\r\n if (!labelText) {\r\n // QR only\r\n const outputWidth = leftMargin + qrSize + rightMargin;\r\n const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });\r\n const yOffset = Math.floor((targetHeight - qrSize) / 2);\r\n image.composite(qrImage, leftMargin, yOffset);\r\n return image.getBuffer(\"image/png\");\r\n }\r\n\r\n // QR + text label\r\n const fontDir = path.join(path.dirname(fileURLToPath(import.meta.url)), \"node_modules/@jimp/plugin-print/fonts/open-sans\");\r\n const fontPath = path.join(fontDir, \"open-sans-64-black\", \"open-sans-64-black.fnt\");\r\n const font = await loadFont(fontPath);\r\n\r\n const textWidth = measureText(font, labelText);\r\n const textHeight = measureTextHeight(font, labelText, textWidth + 50);\r\n\r\n // Scale text to fit beside QR\r\n const maxTextHeight = targetHeight * 0.8;\r\n const textScale = Math.min(1, maxTextHeight / textHeight);\r\n const scaledTextWidth = Math.round(textWidth * textScale);\r\n const scaledTextHeight = Math.round(textHeight * textScale);\r\n\r\n // Create text image\r\n const textImg = new Jimp({ width: textWidth + 20, height: textHeight + 20, color: 0xffffffff });\r\n textImg.print({ font, x: 10, y: 10, text: labelText });\r\n if (textScale < 1) {\r\n textImg.resize({ w: scaledTextWidth, h: scaledTextHeight });\r\n }\r\n\r\n // Compose: QR on left, text on right\r\n const gap = Math.floor(targetHeight * 0.15);\r\n const outputWidth = leftMargin + qrSize + gap + scaledTextWidth + rightMargin;\r\n const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });\r\n\r\n const qrY = Math.floor((targetHeight - qrSize) / 2);\r\n image.composite(qrImage, leftMargin, qrY);\r\n\r\n const textY = Math.floor((targetHeight - scaledTextHeight) / 2);\r\n image.composite(textImg, leftMargin + qrSize + gap, textY);\r\n\r\n return image.getBuffer(\"image/png\");\r\n}\r\n\r\n// Render content to image buffer\r\nasync function renderContent(options: PrintOptions, tape: TapeSize): Promise<Buffer> {\r\n const tapeSize = TAPE_SIZES[tape];\r\n\r\n if (options.text !== undefined) {\r\n return renderText(options.text, tape, options.textHeight);\r\n }\r\n\r\n if (options.textFile !== undefined) {\r\n const text = fs.readFileSync(options.textFile, \"utf-8\");\r\n return renderText(text, tape, options.textHeight);\r\n }\r\n\r\n if (options.html !== undefined) {\r\n const { width, height } = getHtmlDimensions(tapeSize.height, options.aspect);\r\n const buffer = await renderHtmlString(options.html, { width, height, tapeMm: tape }, options.basePath);\r\n await closeBrowser();\r\n return buffer;\r\n }\r\n\r\n if (options.htmlPath !== undefined) {\r\n const { width, height } = getHtmlDimensions(tapeSize.height, options.aspect);\r\n const buffer = await renderHtmlFile(options.htmlPath, { width, height, tapeMm: tape });\r\n await closeBrowser();\r\n return buffer;\r\n }\r\n\r\n if (options.imagePath !== undefined) {\r\n return fs.readFileSync(options.imagePath);\r\n }\r\n\r\n if (options.imageBuffer !== undefined) {\r\n return options.imageBuffer;\r\n }\r\n\r\n if (options.qr !== undefined) {\r\n return renderQr(options.qr, tape, options.qrLabel);\r\n }\r\n\r\n throw new Error(\"No content to render\");\r\n}\r\n\r\n// Print single image buffer to printer\r\nasync function printBuffer(imageBuffer: Buffer, printer: string, tape: TapeSize): Promise<void> {\r\n const media = MEDIA_OPTIONS[tape];\r\n\r\n const tempPath = path.join(os.tmpdir(), `label-${Date.now()}.png`);\r\n fs.writeFileSync(tempPath, imageBuffer);\r\n\r\n try {\r\n await new Promise<void>((resolve, reject) => {\r\n const psScript = `\r\nAdd-Type -AssemblyName System.Drawing\r\nAdd-Type -AssemblyName System.Printing\r\nAdd-Type -AssemblyName ReachFramework\r\nAdd-Type -AssemblyName PresentationCore\r\n\r\n$img = [System.Drawing.Image]::FromFile('${tempPath.replace(/\\\\/g, \"\\\\\\\\\")}')\r\n\r\n$DPI = ${PRINT_DPI}\r\n$MICRONS_PER_INCH = 25400\r\n\r\n$labelLengthMicrons = [int]($img.Width / $DPI * $MICRONS_PER_INCH) + 3000 # +2mm offset +1mm buffer\r\n$tapeWidthMicrons = ${media.width}\r\n\r\n$imgWidthWpf = $img.Width / $DPI * 96\r\n$imgHeightWpf = $img.Height / $DPI * 96\r\n\r\n$ticketXml = @\"\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<psf:PrintTicket xmlns:psf=\"http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework\"\r\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" version=\"1\"\r\n xmlns:psk=\"http://schemas.microsoft.com/windows/2003/08/printing/printschemakeywords\"\r\n xmlns:ns0001=\"http://schemas.brother.info/mfc/printing/2006/11/printschemakeywords\">\r\n <psf:Feature name=\"psk:PageMediaSize\">\r\n <psf:Option name=\"ns0001:${media.name}\">\r\n <psf:ScoredProperty name=\"psk:MediaSizeWidth\">\r\n <psf:Value xsi:type=\"xsd:integer\">${media.width}</psf:Value>\r\n </psf:ScoredProperty>\r\n <psf:ScoredProperty name=\"psk:MediaSizeHeight\">\r\n <psf:ParameterRef name=\"psk:PageMediaSizeMediaSizeHeight\" />\r\n </psf:ScoredProperty>\r\n </psf:Option>\r\n </psf:Feature>\r\n <psf:ParameterInit name=\"psk:PageMediaSizeMediaSizeHeight\">\r\n <psf:Value xsi:type=\"xsd:integer\">$labelLengthMicrons</psf:Value>\r\n </psf:ParameterInit>\r\n <psf:Feature name=\"psk:PageOrientation\">\r\n <psf:Option name=\"psk:Landscape\" />\r\n </psf:Feature>\r\n <psf:Feature name=\"ns0001:PageRollFeedToEndOfSheet\">\r\n <psf:Option name=\"ns0001:FeedToImageEdge\" />\r\n </psf:Feature>\r\n <psf:Feature name=\"psk:PageMediaType\">\r\n <psf:Option name=\"psk:Label\" />\r\n </psf:Feature>\r\n <psf:Feature name=\"ns0001:JobRollCutAtEndOfJob\">\r\n <psf:Option name=\"ns0001:Cut\" />\r\n </psf:Feature>\r\n</psf:PrintTicket>\r\n\"@\r\n\r\n$xmlBytes = [System.Text.Encoding]::UTF8.GetBytes($ticketXml)\r\n$memStream = New-Object System.IO.MemoryStream(,$xmlBytes)\r\n$ticket = New-Object System.Printing.PrintTicket($memStream)\r\n\r\n$server = New-Object System.Printing.LocalPrintServer\r\n$queue = $server.GetPrintQueue('${printer}')\r\n\r\n$xpsPath = [System.IO.Path]::GetTempFileName() + \".xps\"\r\n\r\n$pageWidth = $labelLengthMicrons / $MICRONS_PER_INCH * 96\r\n$pageHeight = $tapeWidthMicrons / $MICRONS_PER_INCH * 96\r\n\r\n$package = [System.IO.Packaging.Package]::Open($xpsPath, [System.IO.FileMode]::Create)\r\n$xpsDoc = New-Object System.Windows.Xps.Packaging.XpsDocument($package)\r\n$writer = [System.Windows.Xps.Packaging.XpsDocument]::CreateXpsDocumentWriter($xpsDoc)\r\n\r\n$visual = New-Object System.Windows.Media.DrawingVisual\r\n$dc = $visual.RenderOpen()\r\n\r\n$bitmapImg = New-Object System.Windows.Media.Imaging.BitmapImage\r\n$bitmapImg.BeginInit()\r\n$bitmapImg.UriSource = New-Object System.Uri('${tempPath.replace(/\\\\/g, \"\\\\\\\\\")}')\r\n$bitmapImg.EndInit()\r\n\r\n$yOffset = ($pageHeight - $imgHeightWpf) / 2\r\n$xOffset = 2 / 25.4 * 96 # 2mm left margin in WPF units\r\n$rect = New-Object System.Windows.Rect($xOffset, $yOffset, $imgWidthWpf, $imgHeightWpf)\r\n$dc.DrawImage($bitmapImg, $rect)\r\n$dc.Close()\r\n\r\n$writer.Write($visual, $ticket)\r\n\r\n$xpsDoc.Close()\r\n$package.Close()\r\n$img.Dispose()\r\n\r\n$xpsDocForPrint = New-Object System.Windows.Xps.Packaging.XpsDocument($xpsPath, [System.IO.FileAccess]::Read)\r\n$seq = $xpsDocForPrint.GetFixedDocumentSequence()\r\n\r\n$xpsWriter = [System.Printing.PrintQueue]::CreateXpsDocumentWriter($queue)\r\n$xpsWriter.Write($seq, $ticket)\r\n\r\n$xpsDocForPrint.Close()\r\nRemove-Item $xpsPath -ErrorAction SilentlyContinue\r\n`;\r\n\r\n const ps = spawn(\"powershell\", [\"-Command\", psScript], { stdio: \"pipe\" });\r\n\r\n let stdout = \"\";\r\n let stderr = \"\";\r\n ps.stdout.on(\"data\", (data) => { stdout += data.toString(); });\r\n ps.stderr.on(\"data\", (data) => { stderr += data.toString(); });\r\n\r\n ps.on(\"close\", (code) => {\r\n if (code === 0) {\r\n resolve();\r\n } else {\r\n reject(new Error(`Print failed: ${stderr}`));\r\n }\r\n });\r\n\r\n ps.on(\"error\", (err) => {\r\n reject(new Error(`Failed to print: ${err.message}`));\r\n });\r\n });\r\n } finally {\r\n if (fs.existsSync(tempPath)) {\r\n fs.unlinkSync(tempPath);\r\n }\r\n }\r\n}\r\n\r\n// Main API functions\r\nexport async function render(options: PrintOptions): Promise<Buffer> {\r\n validateOptions(options);\r\n const { tape } = resolveSettings(options);\r\n return renderContent(options, tape);\r\n}\r\n\r\nexport async function print(options: PrintOptions): Promise<PrintResult> {\r\n validateOptions(options);\r\n const { tape, printer } = resolveSettings(options);\r\n const image = await renderContent(options, tape);\r\n await printBuffer(image, printer, tape);\r\n return { image };\r\n}\r\n\r\n// Render multiple segments side-by-side into a single image\r\nexport async function renderSegments(segments: Segment[], tape?: TapeSize, textHeight?: string, space?: string): Promise<Buffer> {\r\n const config = getConfig();\r\n const effectiveTape: TapeSize = tape ?? config.defaultTape ?? 24;\r\n const tapeSize = TAPE_SIZES[effectiveTape];\r\n\r\n // Render each segment individually\r\n const buffers: Buffer[] = [];\r\n for (const seg of segments) {\r\n if (seg.type === \"text\") {\r\n buffers.push(await renderText(seg.value, effectiveTape, textHeight));\r\n } else {\r\n buffers.push(await renderQr(seg.value, effectiveTape));\r\n }\r\n }\r\n\r\n if (buffers.length === 1) {\r\n return buffers[0];\r\n }\r\n\r\n // Load all rendered images and trim horizontal whitespace\r\n const images: Array<{ img: Awaited<ReturnType<typeof Jimp.read>>; width: number; height: number }> = [];\r\n for (const buf of buffers) {\r\n const img = await Jimp.read(buf);\r\n // Find leftmost and rightmost non-white columns to trim padding\r\n let left = img.width;\r\n let right = 0;\r\n for (let x = 0; x < img.width; x++) {\r\n for (let y = 0; y < img.height; y++) {\r\n const pixel = img.getPixelColor(x, y);\r\n if (pixel !== 0xffffffff) {\r\n if (x < left) left = x;\r\n if (x > right) right = x;\r\n break;\r\n }\r\n }\r\n }\r\n if (right >= left) {\r\n const margin = 4; // small margin around content\r\n const cropLeft = Math.max(0, left - margin);\r\n const cropRight = Math.min(img.width - 1, right + margin);\r\n const cropWidth = cropRight - cropLeft + 1;\r\n img.crop({ x: cropLeft, y: 0, w: cropWidth, h: img.height });\r\n }\r\n images.push({ img, width: img.width, height: img.height });\r\n }\r\n\r\n // Composite left-to-right\r\n const gap = space ? parseSize(space, tapeSize.height) : 2;\r\n let totalWidth = 0;\r\n for (let i = 0; i < images.length; i++) {\r\n totalWidth += images[i].width;\r\n if (i < images.length - 1) totalWidth += gap;\r\n }\r\n\r\n const canvas = new Jimp({ width: totalWidth, height: tapeSize.height, color: 0xffffffff });\r\n let x = 0;\r\n for (let i = 0; i < images.length; i++) {\r\n const yOffset = Math.round((tapeSize.height - images[i].height) / 2);\r\n canvas.composite(images[i].img as any, x, yOffset);\r\n x += images[i].width + gap;\r\n }\r\n\r\n return canvas.getBuffer(\"image/png\");\r\n}\r\n\r\n// Print multiple segments as a single label\r\nexport async function printSegments(segments: Segment[], options?: { tape?: TapeSize; printer?: string; textHeight?: string; space?: string }): Promise<PrintResult> {\r\n const config = getConfig();\r\n const tape: TapeSize = options?.tape ?? config.defaultTape ?? 24;\r\n const printer: string = options?.printer ?? config.defaultPrinter ?? DEFAULT_PRINTER;\r\n const image = await renderSegments(segments, tape, options?.textHeight, options?.space);\r\n await printBuffer(image, printer, tape);\r\n return { image };\r\n}\r\n"]}
package/api.ts CHANGED
@@ -43,12 +43,18 @@ export interface PrintOptions {
43
43
  basePath?: string; // base path for inline HTML resources
44
44
  aspect?: string; // width:height ratio for HTML (e.g., "3.5:2")
45
45
  qrLabel?: string; // optional text label beside QR code
46
+ textHeight?: string; // text height: "12mm", ".5in", "50%" (of tape height)
46
47
  }
47
48
 
48
49
  export interface PrintResult {
49
50
  image: Buffer;
50
51
  }
51
52
 
53
+ export interface Segment {
54
+ type: "text" | "qr";
55
+ value: string;
56
+ }
57
+
52
58
  // Constants
53
59
  const CONFIG_PATH = path.join(os.homedir(), ".brother-label.json");
54
60
  const DEFAULT_PRINTER = "Brother PT-P710BT";
@@ -183,10 +189,62 @@ function resolveSettings(options: PrintOptions): { tape: TapeSize; printer: stri
183
189
  };
184
190
  }
185
191
 
192
+ // Parse a size spec (12px, 1mm, .2in, 50%) to pixels
193
+ function parseSize(spec: string, referencePx: number): number {
194
+ const trimmed = spec.trim().toLowerCase();
195
+ if (trimmed.endsWith("px")) {
196
+ const px = parseFloat(trimmed.slice(0, -2));
197
+ if (isNaN(px) || px < 0) throw new Error(`Invalid size: ${spec}`);
198
+ return Math.round(px);
199
+ }
200
+ if (trimmed.endsWith("%")) {
201
+ const pct = parseFloat(trimmed.slice(0, -1));
202
+ if (isNaN(pct) || pct < 0) throw new Error(`Invalid size: ${spec}`);
203
+ return Math.round(referencePx * pct / 100);
204
+ }
205
+ if (trimmed.endsWith("mm")) {
206
+ const mm = parseFloat(trimmed.slice(0, -2));
207
+ if (isNaN(mm) || mm < 0) throw new Error(`Invalid size: ${spec}`);
208
+ return Math.round(mm * PRINT_DPI / 25.4);
209
+ }
210
+ if (trimmed.endsWith("in")) {
211
+ const inches = parseFloat(trimmed.slice(0, -2));
212
+ if (isNaN(inches) || inches < 0) throw new Error(`Invalid size: ${spec}`);
213
+ return Math.round(inches * PRINT_DPI);
214
+ }
215
+ // Bare number → pixels
216
+ const px = parseFloat(trimmed);
217
+ if (!isNaN(px) && px >= 0) return Math.round(px);
218
+ throw new Error(`Invalid size: ${spec}. Use "12px", "1mm", ".2in", or "50%"`);
219
+ }
220
+
221
+ // Parse text height spec to pixels
222
+ function parseTextHeight(spec: string, tapeHeightPx: number): number {
223
+ const trimmed = spec.trim().toLowerCase();
224
+ if (trimmed.endsWith("%")) {
225
+ const pct = parseFloat(trimmed.slice(0, -1));
226
+ if (isNaN(pct) || pct <= 0) throw new Error(`Invalid text height: ${spec}`);
227
+ return Math.round(tapeHeightPx * pct / 100);
228
+ }
229
+ if (trimmed.endsWith("mm")) {
230
+ const mm = parseFloat(trimmed.slice(0, -2));
231
+ if (isNaN(mm) || mm <= 0) throw new Error(`Invalid text height: ${spec}`);
232
+ return Math.round(mm * PRINT_DPI / 25.4);
233
+ }
234
+ if (trimmed.endsWith("in")) {
235
+ const inches = parseFloat(trimmed.slice(0, -2));
236
+ if (isNaN(inches) || inches <= 0) throw new Error(`Invalid text height: ${spec}`);
237
+ return Math.round(inches * PRINT_DPI);
238
+ }
239
+ throw new Error(`Invalid text height: ${spec}. Use "12mm", ".5in", or "50%"`);
240
+ }
241
+
186
242
  // Render text to image buffer
187
- async function renderText(text: string, tape: TapeSize): Promise<Buffer> {
243
+ async function renderText(text: string, tape: TapeSize, textHeightSpec?: string): Promise<Buffer> {
188
244
  const tapeSize = TAPE_SIZES[tape];
189
- const targetHeight = tapeSize.height;
245
+ const targetHeight = textHeightSpec
246
+ ? Math.min(parseTextHeight(textHeightSpec, tapeSize.height), tapeSize.height)
247
+ : tapeSize.height;
190
248
  const lines = text.split("\\n").join("\n");
191
249
 
192
250
  // Load largest font for quality
@@ -213,12 +271,13 @@ async function renderText(text: string, tape: TapeSize): Promise<Buffer> {
213
271
  tempImage.resize({ w: finalWidth, h: finalHeight });
214
272
 
215
273
  // Create final image with padding
216
- const hPadding = Math.round(targetHeight * 0.2);
274
+ const canvasHeight = tapeSize.height;
275
+ const hPadding = Math.round(canvasHeight * 0.2);
217
276
  const outputWidth = finalWidth + hPadding * 2;
218
- const image = new Jimp({ width: outputWidth, height: targetHeight, color: 0xffffffff });
277
+ const image = new Jimp({ width: outputWidth, height: canvasHeight, color: 0xffffffff });
219
278
 
220
279
  const xOffset = hPadding;
221
- const yOffset = Math.round((targetHeight - finalHeight) / 2);
280
+ const yOffset = Math.round((canvasHeight - finalHeight) / 2);
222
281
  image.composite(tempImage, xOffset, yOffset);
223
282
 
224
283
  return image.getBuffer("image/png");
@@ -295,12 +354,12 @@ async function renderContent(options: PrintOptions, tape: TapeSize): Promise<Buf
295
354
  const tapeSize = TAPE_SIZES[tape];
296
355
 
297
356
  if (options.text !== undefined) {
298
- return renderText(options.text, tape);
357
+ return renderText(options.text, tape, options.textHeight);
299
358
  }
300
359
 
301
360
  if (options.textFile !== undefined) {
302
361
  const text = fs.readFileSync(options.textFile, "utf-8");
303
- return renderText(text, tape);
362
+ return renderText(text, tape, options.textHeight);
304
363
  }
305
364
 
306
365
  if (options.html !== undefined) {
@@ -478,3 +537,79 @@ export async function print(options: PrintOptions): Promise<PrintResult> {
478
537
  await printBuffer(image, printer, tape);
479
538
  return { image };
480
539
  }
540
+
541
+ // Render multiple segments side-by-side into a single image
542
+ export async function renderSegments(segments: Segment[], tape?: TapeSize, textHeight?: string, space?: string): Promise<Buffer> {
543
+ const config = getConfig();
544
+ const effectiveTape: TapeSize = tape ?? config.defaultTape ?? 24;
545
+ const tapeSize = TAPE_SIZES[effectiveTape];
546
+
547
+ // Render each segment individually
548
+ const buffers: Buffer[] = [];
549
+ for (const seg of segments) {
550
+ if (seg.type === "text") {
551
+ buffers.push(await renderText(seg.value, effectiveTape, textHeight));
552
+ } else {
553
+ buffers.push(await renderQr(seg.value, effectiveTape));
554
+ }
555
+ }
556
+
557
+ if (buffers.length === 1) {
558
+ return buffers[0];
559
+ }
560
+
561
+ // Load all rendered images and trim horizontal whitespace
562
+ const images: Array<{ img: Awaited<ReturnType<typeof Jimp.read>>; width: number; height: number }> = [];
563
+ for (const buf of buffers) {
564
+ const img = await Jimp.read(buf);
565
+ // Find leftmost and rightmost non-white columns to trim padding
566
+ let left = img.width;
567
+ let right = 0;
568
+ for (let x = 0; x < img.width; x++) {
569
+ for (let y = 0; y < img.height; y++) {
570
+ const pixel = img.getPixelColor(x, y);
571
+ if (pixel !== 0xffffffff) {
572
+ if (x < left) left = x;
573
+ if (x > right) right = x;
574
+ break;
575
+ }
576
+ }
577
+ }
578
+ if (right >= left) {
579
+ const margin = 4; // small margin around content
580
+ const cropLeft = Math.max(0, left - margin);
581
+ const cropRight = Math.min(img.width - 1, right + margin);
582
+ const cropWidth = cropRight - cropLeft + 1;
583
+ img.crop({ x: cropLeft, y: 0, w: cropWidth, h: img.height });
584
+ }
585
+ images.push({ img, width: img.width, height: img.height });
586
+ }
587
+
588
+ // Composite left-to-right
589
+ const gap = space ? parseSize(space, tapeSize.height) : 2;
590
+ let totalWidth = 0;
591
+ for (let i = 0; i < images.length; i++) {
592
+ totalWidth += images[i].width;
593
+ if (i < images.length - 1) totalWidth += gap;
594
+ }
595
+
596
+ const canvas = new Jimp({ width: totalWidth, height: tapeSize.height, color: 0xffffffff });
597
+ let x = 0;
598
+ for (let i = 0; i < images.length; i++) {
599
+ const yOffset = Math.round((tapeSize.height - images[i].height) / 2);
600
+ canvas.composite(images[i].img as any, x, yOffset);
601
+ x += images[i].width + gap;
602
+ }
603
+
604
+ return canvas.getBuffer("image/png");
605
+ }
606
+
607
+ // Print multiple segments as a single label
608
+ export async function printSegments(segments: Segment[], options?: { tape?: TapeSize; printer?: string; textHeight?: string; space?: string }): Promise<PrintResult> {
609
+ const config = getConfig();
610
+ const tape: TapeSize = options?.tape ?? config.defaultTape ?? 24;
611
+ const printer: string = options?.printer ?? config.defaultPrinter ?? DEFAULT_PRINTER;
612
+ const image = await renderSegments(segments, tape, options?.textHeight, options?.space);
613
+ await printBuffer(image, printer, tape);
614
+ return { image };
615
+ }