@thermal-label/brother-ql-core 0.3.0 → 0.5.0

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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/data/devices.json +823 -0
  3. package/data/media.json +823 -0
  4. package/dist/__tests__/devices.test.js +112 -31
  5. package/dist/__tests__/devices.test.js.map +1 -1
  6. package/dist/__tests__/media.test.js +251 -1
  7. package/dist/__tests__/media.test.js.map +1 -1
  8. package/dist/__tests__/protocol.test.js +168 -1
  9. package/dist/__tests__/protocol.test.js.map +1 -1
  10. package/dist/__tests__/status.test.js +71 -0
  11. package/dist/__tests__/status.test.js.map +1 -1
  12. package/dist/devices.d.ts +13 -270
  13. package/dist/devices.d.ts.map +1 -1
  14. package/dist/devices.generated.d.ts +696 -0
  15. package/dist/devices.generated.d.ts.map +1 -0
  16. package/dist/devices.generated.js +831 -0
  17. package/dist/devices.generated.js.map +1 -0
  18. package/dist/devices.js +28 -272
  19. package/dist/devices.js.map +1 -1
  20. package/dist/index.d.ts +13 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +10 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/media.d.ts +37 -22
  25. package/dist/media.d.ts.map +1 -1
  26. package/dist/media.generated.d.ts +4 -0
  27. package/dist/media.generated.d.ts.map +1 -0
  28. package/dist/media.generated.js +1640 -0
  29. package/dist/media.generated.js.map +1 -0
  30. package/dist/media.js +74 -281
  31. package/dist/media.js.map +1 -1
  32. package/dist/protocol.d.ts +54 -3
  33. package/dist/protocol.d.ts.map +1 -1
  34. package/dist/protocol.js +117 -18
  35. package/dist/protocol.js.map +1 -1
  36. package/dist/status.d.ts +4 -1
  37. package/dist/status.d.ts.map +1 -1
  38. package/dist/status.js +6 -2
  39. package/dist/status.js.map +1 -1
  40. package/dist/types.d.ts +92 -27
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +13 -9
  43. package/src/__tests__/devices.test.ts +122 -32
  44. package/src/__tests__/media.test.ts +287 -1
  45. package/src/__tests__/protocol.test.ts +209 -0
  46. package/src/__tests__/status.test.ts +87 -0
  47. package/src/devices.generated.ts +840 -0
  48. package/src/devices.ts +30 -272
  49. package/src/index.ts +28 -4
  50. package/src/media.generated.ts +1644 -0
  51. package/src/media.ts +86 -282
  52. package/src/protocol.ts +196 -18
  53. package/src/status.ts +10 -3
  54. package/src/types.ts +93 -27
@@ -13,6 +13,8 @@ import {
13
13
  buildVariousMode,
14
14
  buildExpandedMode,
15
15
  encodeJob,
16
+ encodeJobForEngine,
17
+ type EncoderEngine,
16
18
  } from '../protocol.js';
17
19
  import type { PageData } from '../types.js';
18
20
  import { MEDIA } from '../media.js';
@@ -285,3 +287,210 @@ describe('buildCompression', () => {
285
287
  expect(Array.from(buildCompression(false))).toEqual([0x4d, 0x00]);
286
288
  });
287
289
  });
290
+
291
+ describe('encodeJobForEngine — dispatch + invalidate-byte derivation', () => {
292
+ const TZE_12MM = MEDIA[404]!;
293
+ const DK_62MM = MEDIA[259]!;
294
+
295
+ const NARROW_PT_ENGINE: EncoderEngine = {
296
+ protocol: 'pt-raster',
297
+ headDots: 128,
298
+ capabilities: { autocut: true, mediaDetection: true, highResDpi: 360 },
299
+ };
300
+ const WIDE_PT_ENGINE: EncoderEngine = {
301
+ protocol: 'pt-raster',
302
+ headDots: 560,
303
+ capabilities: { autocut: true, mediaDetection: true, highResDpi: 720 },
304
+ };
305
+ const QL_TWO_COLOR_ENGINE: EncoderEngine = {
306
+ protocol: 'ql-raster',
307
+ headDots: 720,
308
+ capabilities: { autocut: true, mediaDetection: true, twoColor: true },
309
+ };
310
+ const QL_SINGLE_COLOR_ENGINE: EncoderEngine = {
311
+ protocol: 'ql-raster',
312
+ headDots: 720,
313
+ capabilities: { autocut: true, mediaDetection: true },
314
+ };
315
+
316
+ it('PT-raster job uses 200-byte invalidate', () => {
317
+ const bitmap = createBitmap(70, 5);
318
+ const out = encodeJobForEngine([{ bitmap, media: TZE_12MM }], {}, NARROW_PT_ENGINE);
319
+ // After buildRasterMode (4 bytes: 0x1B 0x69 0x61 0x01), invalidate begins.
320
+ expect(out.slice(0, 4)).toEqual(new Uint8Array([0x1b, 0x69, 0x61, 0x01]));
321
+ // Confirm the next 200 bytes are zero (PT invalidate).
322
+ for (let i = 4; i < 204; i++) expect(out[i]).toBe(0);
323
+ // The byte after the 200-zero invalidate should be 0x1B (start of buildInitialize),
324
+ // proving the invalidate is not 400 bytes.
325
+ expect(out[204]).toBe(0x1b);
326
+ });
327
+
328
+ it('QL two-colour engine bumps invalidate to 400 bytes', () => {
329
+ const bitmap = createBitmap(696, 5);
330
+ const out = encodeJobForEngine([{ bitmap, media: DK_62MM }], {}, QL_TWO_COLOR_ENGINE);
331
+ expect(out.slice(0, 4)).toEqual(new Uint8Array([0x1b, 0x69, 0x61, 0x01]));
332
+ for (let i = 4; i < 404; i++) expect(out[i]).toBe(0);
333
+ expect(out[404]).toBe(0x1b);
334
+ });
335
+
336
+ it('QL single-colour engine keeps invalidate at 200 bytes', () => {
337
+ const bitmap = createBitmap(696, 5);
338
+ const out = encodeJobForEngine([{ bitmap, media: DK_62MM }], {}, QL_SINGLE_COLOR_ENGINE);
339
+ for (let i = 4; i < 204; i++) expect(out[i]).toBe(0);
340
+ expect(out[204]).toBe(0x1b);
341
+ });
342
+
343
+ it('PT-raster expanded-mode uses bit 6 (0x40) for high-res', () => {
344
+ const bitmap = createBitmap(70, 5);
345
+ const out = encodeJobForEngine(
346
+ [{ bitmap, media: TZE_12MM, options: { highResolution: true } }],
347
+ {},
348
+ NARROW_PT_ENGINE,
349
+ );
350
+ // Locate the ESC i K command (0x1B 0x69 0x4B FLAGS).
351
+ let idx = -1;
352
+ for (let i = 0; i < out.length - 3; i++) {
353
+ if (out[i] === 0x1b && out[i + 1] === 0x69 && out[i + 2] === 0x4b) {
354
+ idx = i;
355
+ break;
356
+ }
357
+ }
358
+ expect(idx).toBeGreaterThanOrEqual(0);
359
+ expect((out[idx + 3] ?? 0) & 0x40).toBe(0x40);
360
+ expect((out[idx + 3] ?? 0) & 0x10).toBe(0x00);
361
+ });
362
+
363
+ it('PT-raster feed-margin defaults to 14 dots', () => {
364
+ const bitmap = createBitmap(70, 5);
365
+ const out = encodeJobForEngine([{ bitmap, media: TZE_12MM }], {}, NARROW_PT_ENGINE);
366
+ // Locate ESC i d (0x1B 0x69 0x64 LO HI).
367
+ let idx = -1;
368
+ for (let i = 0; i < out.length - 4; i++) {
369
+ if (out[i] === 0x1b && out[i + 1] === 0x69 && out[i + 2] === 0x64) {
370
+ idx = i;
371
+ break;
372
+ }
373
+ }
374
+ expect(idx).toBeGreaterThanOrEqual(0);
375
+ const lo = out[idx + 3] ?? 0;
376
+ const hi = out[idx + 4] ?? 0;
377
+ expect(lo | (hi << 8)).toBe(14);
378
+ });
379
+
380
+ it('QL feed-margin defaults to 35 dots', () => {
381
+ const bitmap = createBitmap(696, 5);
382
+ const out = encodeJobForEngine([{ bitmap, media: DK_62MM }], {}, QL_SINGLE_COLOR_ENGINE);
383
+ let idx = -1;
384
+ for (let i = 0; i < out.length - 4; i++) {
385
+ if (out[i] === 0x1b && out[i + 1] === 0x69 && out[i + 2] === 0x64) {
386
+ idx = i;
387
+ break;
388
+ }
389
+ }
390
+ expect(idx).toBeGreaterThanOrEqual(0);
391
+ const lo = out[idx + 3] ?? 0;
392
+ const hi = out[idx + 4] ?? 0;
393
+ expect(lo | (hi << 8)).toBe(35);
394
+ });
395
+
396
+ it('PT high-res duplicates each raster line', () => {
397
+ const bitmap = createBitmap(70, 3);
398
+ const noHighRes = encodeJobForEngine([{ bitmap, media: TZE_12MM }], {}, NARROW_PT_ENGINE);
399
+ const highRes = encodeJobForEngine(
400
+ [{ bitmap, media: TZE_12MM, options: { highResolution: true } }],
401
+ {},
402
+ NARROW_PT_ENGINE,
403
+ );
404
+ // Counting the 0x67 raster opcodes is the cleanest signal: 3 lines
405
+ // native vs 6 lines high-res.
406
+ const countOp = (buf: Uint8Array, op: number): number => {
407
+ let n = 0;
408
+ for (const b of buf) if (b === op) n++;
409
+ return n;
410
+ };
411
+ // 0x67 happens to also be present in invalidate-zero adjacency rare;
412
+ // we instead compare relative line counts (delta = doubled rows).
413
+ expect(countOp(highRes, 0x67) - countOp(noHighRes, 0x67)).toBe(3);
414
+ });
415
+
416
+ it('PT high-res doubles the feed-margin field', () => {
417
+ const bitmap = createBitmap(70, 3);
418
+ const out = encodeJobForEngine(
419
+ [{ bitmap, media: TZE_12MM, options: { highResolution: true } }],
420
+ {},
421
+ NARROW_PT_ENGINE,
422
+ );
423
+ let idx = -1;
424
+ for (let i = 0; i < out.length - 4; i++) {
425
+ if (out[i] === 0x1b && out[i + 1] === 0x69 && out[i + 2] === 0x64) {
426
+ idx = i;
427
+ break;
428
+ }
429
+ }
430
+ const lo = out[idx + 3] ?? 0;
431
+ const hi = out[idx + 4] ?? 0;
432
+ expect(lo | (hi << 8)).toBe(28);
433
+ });
434
+
435
+ it('encodeJobForEngine resolves narrow vs wide TZe geometry', () => {
436
+ const bitmap = createBitmap(70, 1);
437
+ const narrow = encodeJobForEngine([{ bitmap, media: TZE_12MM }], {}, NARROW_PT_ENGINE);
438
+ const bitmap2 = createBitmap(150, 1);
439
+ const wide = encodeJobForEngine([{ bitmap: bitmap2, media: TZE_12MM }], {}, WIDE_PT_ENGINE);
440
+ // narrow: 128 pin head → row payload 16 bytes per raster line.
441
+ // wide: 560 pin head → row payload 70 bytes per raster line.
442
+ // Find the 0x67 raster opcode and check the LEN byte.
443
+ const findRowLen = (buf: Uint8Array): number => {
444
+ for (let i = 0; i < buf.length - 2; i++) {
445
+ if (buf[i] === 0x67 && buf[i + 1] === 0x00) return buf[i + 2] ?? -1;
446
+ }
447
+ return -1;
448
+ };
449
+ expect(findRowLen(narrow)).toBe(16);
450
+ expect(findRowLen(wide)).toBe(70);
451
+ });
452
+
453
+ it('throws when high-res requested on an engine without highResDpi', () => {
454
+ const bitmap = createBitmap(696, 1);
455
+ expect(() =>
456
+ encodeJobForEngine(
457
+ [{ bitmap, media: DK_62MM, options: { highResolution: true } }],
458
+ {},
459
+ QL_SINGLE_COLOR_ENGINE,
460
+ ),
461
+ ).toThrow(/high-res/);
462
+ });
463
+
464
+ it('PT-E550W rejects autocut with compression disabled (cutter quirk)', () => {
465
+ const bitmap = createBitmap(70, 3);
466
+ expect(() =>
467
+ encodeJobForEngine(
468
+ [{ bitmap, media: TZE_12MM, options: { autoCut: true, compress: false } }],
469
+ {},
470
+ NARROW_PT_ENGINE,
471
+ 'PT-E550W',
472
+ ),
473
+ ).toThrow(/PT-E550W/);
474
+ });
475
+
476
+ it('PT-P750W (same head family, different name) does NOT trigger the E550W cutter quirk', () => {
477
+ const bitmap = createBitmap(70, 3);
478
+ expect(() =>
479
+ encodeJobForEngine(
480
+ [{ bitmap, media: TZE_12MM, options: { autoCut: true, compress: false } }],
481
+ {},
482
+ NARROW_PT_ENGINE,
483
+ 'PT-P750W',
484
+ ),
485
+ ).not.toThrow();
486
+ });
487
+ });
488
+
489
+ describe('encodeJob legacy entry point — back compat', () => {
490
+ it('still produces the original 200-byte single-colour invalidate', () => {
491
+ const bitmap = createBitmap(696, 1);
492
+ const out = encodeJob([{ bitmap, media: MEDIA[259]! }], {});
493
+ for (let i = 4; i < 204; i++) expect(out[i]).toBe(0);
494
+ expect(out[204]).toBe(0x1b);
495
+ });
496
+ });
@@ -9,6 +9,7 @@ function makeStatusBytes(
9
9
  mediaLengthMm: number;
10
10
  mediaTypeByte: number;
11
11
  statusType: number;
12
+ byte25: number;
12
13
  }>,
13
14
  ): Uint8Array {
14
15
  const bytes = new Uint8Array(32);
@@ -22,9 +23,40 @@ function makeStatusBytes(
22
23
  bytes[11] = overrides?.mediaTypeByte ?? 0x0a;
23
24
  bytes[17] = overrides?.mediaLengthMm ?? 0;
24
25
  bytes[18] = overrides?.statusType ?? 0x00;
26
+ bytes[25] = overrides?.byte25 ?? 0;
25
27
  return bytes;
26
28
  }
27
29
 
30
+ /**
31
+ * Verbatim 32-byte captures from scripts/STATUS-CAPTURE.md. These lock
32
+ * in the byte-25 two-color flag against the registry — without them,
33
+ * the DK-22251/DK-22205 detection ambiguity could silently regress.
34
+ */
35
+ function captureBytes(values: readonly number[]): Uint8Array {
36
+ if (values.length !== 32) throw new Error(`expected 32 bytes, got ${values.length.toString()}`);
37
+ return Uint8Array.from(values);
38
+ }
39
+
40
+ const CAPTURE_DK_22251 = captureBytes([
41
+ 0x80, 0x20, 0x42, 0x34, 0x41, 0x30, 0x04, 0x00, 0x00, 0x00, 0x3e, 0x0a, 0x00, 0x00, 0x23, 0x00,
42
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
43
+ ]);
44
+
45
+ const CAPTURE_DK_22205 = captureBytes([
46
+ 0x80, 0x20, 0x42, 0x34, 0x41, 0x30, 0x04, 0x00, 0x00, 0x00, 0x3e, 0x0a, 0x00, 0x00, 0x15, 0x00,
47
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
48
+ ]);
49
+
50
+ const CAPTURE_DK_11201 = captureBytes([
51
+ 0x80, 0x20, 0x42, 0x34, 0x41, 0x30, 0x04, 0x00, 0x00, 0x00, 0x1d, 0x0b, 0x00, 0x00, 0x01, 0x00,
52
+ 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
53
+ ]);
54
+
55
+ const CAPTURE_DK_22214 = captureBytes([
56
+ 0x80, 0x20, 0x42, 0x34, 0x41, 0x30, 0x04, 0x00, 0x00, 0x00, 0x0c, 0x0a, 0x00, 0x00, 0x1a, 0x00,
57
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
58
+ ]);
59
+
28
60
  describe('parseStatus', () => {
29
61
  it('throws on short input', () => {
30
62
  expect(() => parseStatus(new Uint8Array(10))).toThrow('too short');
@@ -95,6 +127,61 @@ describe('parseStatus', () => {
95
127
  const status = parseStatus(makeStatusBytes());
96
128
  expect(status.editorLiteMode).toBe(false);
97
129
  });
130
+
131
+ it('byte 25 bit 7 set → twoColorRoll=true and resolves to DK-22251', () => {
132
+ const status = parseStatus(
133
+ makeStatusBytes({ mediaWidthMm: 62, mediaTypeByte: 0x0a, byte25: 0x81 }),
134
+ );
135
+ expect(status.twoColorRoll).toBe(true);
136
+ expect(status.detectedMedia?.id).toBe(251);
137
+ });
138
+
139
+ it('byte 25 bit 7 clear → twoColorRoll=false and resolves to DK-22205', () => {
140
+ const status = parseStatus(
141
+ makeStatusBytes({ mediaWidthMm: 62, mediaTypeByte: 0x0a, byte25: 0x01 }),
142
+ );
143
+ expect(status.twoColorRoll).toBe(false);
144
+ expect(status.detectedMedia?.id).toBe(259);
145
+ });
146
+
147
+ it('twoColorRoll is omitted when no media is loaded', () => {
148
+ const status = parseStatus(makeStatusBytes({ mediaWidthMm: 0, mediaTypeByte: 0 }));
149
+ expect(status.twoColorRoll).toBeUndefined();
150
+ });
151
+ });
152
+
153
+ describe('parseStatus — real hardware captures', () => {
154
+ it('DK-22251 (62mm two-color continuous) resolves to id 251', () => {
155
+ const status = parseStatus(CAPTURE_DK_22251);
156
+ expect(status.mediaLoaded).toBe(true);
157
+ expect(status.detectedMedia?.id).toBe(251);
158
+ expect(status.twoColorRoll).toBe(true);
159
+ expect(status.errors).toEqual([]);
160
+ });
161
+
162
+ it('DK-22205 (62mm single-color continuous) resolves to id 259, NOT 251', () => {
163
+ const status = parseStatus(CAPTURE_DK_22205);
164
+ expect(status.mediaLoaded).toBe(true);
165
+ expect(status.detectedMedia?.id).toBe(259);
166
+ expect(status.twoColorRoll).toBe(false);
167
+ expect(status.errors).toEqual([]);
168
+ });
169
+
170
+ it('DK-11201 (29×90mm die-cut) resolves to id 271', () => {
171
+ const status = parseStatus(CAPTURE_DK_11201);
172
+ expect(status.mediaLoaded).toBe(true);
173
+ expect(status.detectedMedia?.id).toBe(271);
174
+ expect(status.twoColorRoll).toBe(false);
175
+ expect(status.errors).toEqual([]);
176
+ });
177
+
178
+ it('DK-22214 (12mm continuous) resolves to id 257', () => {
179
+ const status = parseStatus(CAPTURE_DK_22214);
180
+ expect(status.mediaLoaded).toBe(true);
181
+ expect(status.detectedMedia?.id).toBe(257);
182
+ expect(status.twoColorRoll).toBe(false);
183
+ expect(status.errors).toEqual([]);
184
+ });
98
185
  });
99
186
 
100
187
  describe('STATUS_REQUEST', () => {