@thermal-label/brother-ql-core 0.2.1 → 0.3.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 (70) hide show
  1. package/dist/__tests__/media.test.js +23 -3
  2. package/dist/__tests__/media.test.js.map +1 -1
  3. package/dist/__tests__/pack-bits.test.d.ts +2 -0
  4. package/dist/__tests__/pack-bits.test.d.ts.map +1 -0
  5. package/dist/__tests__/pack-bits.test.js +90 -0
  6. package/dist/__tests__/pack-bits.test.js.map +1 -0
  7. package/dist/__tests__/preview.test.js +1 -1
  8. package/dist/__tests__/preview.test.js.map +1 -1
  9. package/dist/__tests__/protocol.test.js +46 -1
  10. package/dist/__tests__/protocol.test.js.map +1 -1
  11. package/dist/devices.d.ts +1 -1
  12. package/dist/devices.d.ts.map +1 -1
  13. package/dist/devices.js +0 -1
  14. package/dist/devices.js.map +1 -1
  15. package/dist/index.d.ts +6 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/media.d.ts +18 -6
  20. package/dist/media.d.ts.map +1 -1
  21. package/dist/media.js +47 -29
  22. package/dist/media.js.map +1 -1
  23. package/dist/orientation.d.ts +11 -0
  24. package/dist/orientation.d.ts.map +1 -0
  25. package/dist/orientation.js +10 -0
  26. package/dist/orientation.js.map +1 -0
  27. package/dist/pack-bits.d.ts +20 -0
  28. package/dist/pack-bits.d.ts.map +1 -0
  29. package/dist/pack-bits.js +61 -0
  30. package/dist/pack-bits.js.map +1 -0
  31. package/dist/preview.d.ts +6 -6
  32. package/dist/preview.d.ts.map +1 -1
  33. package/dist/preview.js +11 -12
  34. package/dist/preview.js.map +1 -1
  35. package/dist/protocol.d.ts +1 -1
  36. package/dist/protocol.d.ts.map +1 -1
  37. package/dist/protocol.js +13 -7
  38. package/dist/protocol.js.map +1 -1
  39. package/dist/status.d.ts +1 -1
  40. package/dist/status.d.ts.map +1 -1
  41. package/dist/status.js +0 -1
  42. package/dist/status.js.map +1 -1
  43. package/dist/types.d.ts +15 -5
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/types.js +1 -2
  46. package/dist/types.js.map +1 -1
  47. package/package.json +3 -3
  48. package/src/__tests__/media.test.ts +25 -3
  49. package/src/__tests__/pack-bits.test.ts +92 -0
  50. package/src/__tests__/preview.test.ts +1 -1
  51. package/src/__tests__/protocol.test.ts +47 -1
  52. package/src/devices.ts +1 -1
  53. package/src/index.ts +16 -4
  54. package/src/media.ts +48 -29
  55. package/src/orientation.ts +11 -0
  56. package/src/pack-bits.ts +64 -0
  57. package/src/preview.ts +13 -12
  58. package/src/protocol.ts +14 -7
  59. package/src/status.ts +1 -2
  60. package/src/types.ts +21 -6
  61. package/dist/__tests__/colour.test.d.ts +0 -2
  62. package/dist/__tests__/colour.test.d.ts.map +0 -1
  63. package/dist/__tests__/colour.test.js +0 -106
  64. package/dist/__tests__/colour.test.js.map +0 -1
  65. package/dist/colour.d.ts +0 -26
  66. package/dist/colour.d.ts.map +0 -1
  67. package/dist/colour.js +0 -84
  68. package/dist/colour.js.map +0 -1
  69. package/src/__tests__/colour.test.ts +0 -126
  70. package/src/colour.ts +0 -101
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type LabelBitmap } from '@mbtech-nl/bitmap';
2
- import type { DeviceDescriptor, MediaDescriptor, PrinterStatus } from '@thermal-label/contracts';
1
+ import type { LabelBitmap } from '@mbtech-nl/bitmap';
2
+ import type { DeviceDescriptor, MediaDescriptor, PrintOptions, PrinterStatus } from '@thermal-label/contracts';
3
3
  export type MediaType = 'continuous' | 'die-cut';
4
4
  export type HeadWidth = 720 | 1296;
5
5
  export type ColorMode = 'single' | 'two-color';
@@ -35,13 +35,12 @@ export interface BrotherQLDevice extends DeviceDescriptor {
35
35
  * Brother QL media descriptor.
36
36
  *
37
37
  * Extends `MediaDescriptor` with the dots-based geometry the raster
38
- * encoder needs. `colorCapable: true` flips the driver into
39
- * two-colour mode — only DK-22251 has this set in the registry.
38
+ * encoder needs. The base `palette` field flips the driver into
39
+ * multi-plane mode — only DK-22251 declares one in the registry.
40
40
  */
41
41
  export interface BrotherQLMedia extends MediaDescriptor {
42
42
  id: number;
43
43
  type: MediaType;
44
- colorCapable: boolean;
45
44
  printAreaDots: number;
46
45
  leftMarginPins: number;
47
46
  rightMarginPins: number;
@@ -72,4 +71,15 @@ export interface PageOptions {
72
71
  export interface JobOptions {
73
72
  copies?: number;
74
73
  }
74
+ /**
75
+ * Per-call print options for `BrotherQLPrinter.print()`.
76
+ *
77
+ * Extends the cross-driver `PrintOptions` with QL-specific knobs. The
78
+ * `rotate` override picks the rotation angle passed to
79
+ * `renderImage` / `renderMultiPlaneImage` — `'auto'` (the default)
80
+ * defers to the media's `defaultOrientation` heuristic.
81
+ */
82
+ export interface BrotherQLPrintOptions extends PrintOptions {
83
+ rotate?: 'auto' | 0 | 90 | 180 | 270;
84
+ }
75
85
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEjG,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AACjD,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,CAAC;AACnC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;AAC/C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,YAAY,CAAC;AAEtE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,MAAM,EAAE,YAAY,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,EAAE,cAAc,CAAC;IACtB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,aAAa,EACd,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AACjD,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,CAAC;AACnC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;AAC/C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,YAAY,CAAC;AAEtE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,MAAM,EAAE,YAAY,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,EAAE,cAAc,CAAC;IACtB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,YAAY;IACzD,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC;CACtC"}
package/dist/types.js CHANGED
@@ -1,3 +1,2 @@
1
- /* eslint-disable import-x/consistent-type-specifier-style */
2
- import {} from '@mbtech-nl/bitmap';
1
+ export {};
3
2
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,OAAO,EAAoB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thermal-label/brother-ql-core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Protocol encoding, device registry, and media registry for Brother QL label printers",
5
5
  "keywords": [
6
6
  "brother",
@@ -53,8 +53,8 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "@mbtech-nl/bitmap": "^0.1.0",
57
- "@thermal-label/contracts": "^0.1.2"
56
+ "@mbtech-nl/bitmap": "^1.2.1",
57
+ "@thermal-label/contracts": "^0.2.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@mbtech-nl/tsconfig": "^1.0.0",
@@ -63,16 +63,38 @@ describe('Media registry invariants', () => {
63
63
  }
64
64
  });
65
65
 
66
- it('only DK-22251 is colorCapable', () => {
66
+ it('only DK-22251 carries a multi-ink palette', () => {
67
67
  for (const m of Object.values(MEDIA)) {
68
68
  if (m.id === 251) {
69
- expect(m.colorCapable).toBe(true);
69
+ expect(m.palette).toBeDefined();
70
+ const palette = m.palette!;
71
+ expect(palette).toHaveLength(2);
72
+ expect(palette[0]!.name).toBe('black');
73
+ expect(palette[1]!.name).toBe('red');
70
74
  } else {
71
- expect(m.colorCapable).toBe(false);
75
+ expect(m.palette).toBeUndefined();
72
76
  }
73
77
  }
74
78
  });
75
79
 
80
+ it('rectangular die-cut entries declare defaultOrientation: horizontal', () => {
81
+ const rectangularDieCutIds = [269, 270, 370, 271, 272, 367, 374, 274, 275, 365, 366];
82
+ for (const id of rectangularDieCutIds) {
83
+ const m = MEDIA[id]!;
84
+ expect(m, `entry ${id.toString()}`).toBeDefined();
85
+ expect(m.defaultOrientation, `entry ${id.toString()}`).toBe('horizontal');
86
+ }
87
+ });
88
+
89
+ it('round die-cut entries set cornerRadiusMm to widthMm / 2', () => {
90
+ const roundDieCutIds = [362, 363, 273];
91
+ for (const id of roundDieCutIds) {
92
+ const m = MEDIA[id]!;
93
+ expect(m, `entry ${id.toString()}`).toBeDefined();
94
+ expect(m.cornerRadiusMm, `entry ${id.toString()}`).toBe(m.widthMm / 2);
95
+ }
96
+ });
97
+
76
98
  it('all IDs are unique', () => {
77
99
  const ids = Object.values(MEDIA).map(m => m.id);
78
100
  const unique = new Set(ids);
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { packBits } from '../pack-bits.js';
3
+
4
+ // Reference TIFF PackBits decoder. Used to round-trip arbitrary inputs and
5
+ // confirm the encoder emits a stream the canonical algorithm can decode.
6
+ function unpackBits(input: Uint8Array): Uint8Array {
7
+ const out: number[] = [];
8
+ let i = 0;
9
+ while (i < input.length) {
10
+ const header = input[i]!;
11
+ const signed = header > 127 ? header - 256 : header;
12
+ if (signed >= 0) {
13
+ const n = signed + 1;
14
+ for (let j = 0; j < n; j++) out.push(input[i + 1 + j]!);
15
+ i += 1 + n;
16
+ } else if (signed >= -127) {
17
+ const n = 1 - signed;
18
+ for (let j = 0; j < n; j++) out.push(input[i + 1]!);
19
+ i += 2;
20
+ } else {
21
+ i++; // signed === -128: no-op
22
+ }
23
+ }
24
+ return new Uint8Array(out);
25
+ }
26
+
27
+ describe('packBits', () => {
28
+ it('emits empty output for empty input', () => {
29
+ expect(packBits(new Uint8Array(0)).length).toBe(0);
30
+ });
31
+
32
+ it('encodes a single byte as a 1-byte literal', () => {
33
+ expect(Array.from(packBits(new Uint8Array([0x42])))).toEqual([0x00, 0x42]);
34
+ });
35
+
36
+ it('encodes a 2-byte repeat as 2 wire bytes', () => {
37
+ // -(2-1) = -1 → 0xFF
38
+ expect(Array.from(packBits(new Uint8Array([0x42, 0x42])))).toEqual([0xff, 0x42]);
39
+ });
40
+
41
+ it('compresses an all-zero 90-byte row to 2 bytes (the common case)', () => {
42
+ const result = packBits(new Uint8Array(90));
43
+ // -(90-1) = -89 = 0xA7
44
+ expect(Array.from(result)).toEqual([0xa7, 0x00]);
45
+ });
46
+
47
+ it('caps repeat runs at 128 bytes — 130 zeros split into 128 + 2', () => {
48
+ const result = packBits(new Uint8Array(130));
49
+ // -(128-1) = -127 = 0x81; -(2-1) = -1 = 0xFF
50
+ expect(Array.from(result)).toEqual([0x81, 0x00, 0xff, 0x00]);
51
+ });
52
+
53
+ it('caps literal runs at 128 bytes', () => {
54
+ // 130 distinct bytes split into 128 + 2 literals.
55
+ const input = new Uint8Array(130);
56
+ for (let i = 0; i < 130; i++) input[i] = i & 0xff;
57
+ const result = packBits(input);
58
+ expect(result[0]).toBe(0x7f); // header for 128 literals
59
+ expect(result[129]).toBe(0x01); // header for 2 literals
60
+ expect(result.length).toBe(132);
61
+ });
62
+
63
+ it('mixes literal and repeat runs', () => {
64
+ // A A B C D D D E
65
+ const input = new Uint8Array([0x10, 0x10, 0x20, 0x30, 0x40, 0x40, 0x40, 0x50]);
66
+ expect(Array.from(packBits(input))).toEqual([
67
+ 0xff,
68
+ 0x10, // -1: 2 repeats of 0x10
69
+ 0x01,
70
+ 0x20,
71
+ 0x30, // 1: 2 literals
72
+ 0xfe,
73
+ 0x40, // -2: 3 repeats of 0x40
74
+ 0x00,
75
+ 0x50, // 0: 1 literal
76
+ ]);
77
+ });
78
+
79
+ it('round-trips a label-like row (margins + bar + scattered) through the spec decoder', () => {
80
+ // Mimic a typical raster row: 12 zero-byte margin, 50 bytes of 0xFF
81
+ // (solid bar), 12 mixed bytes (text), 16 zero-byte trailing margin.
82
+ const original = new Uint8Array(90);
83
+ for (let i = 12; i < 62; i++) original[i] = 0xff;
84
+ for (let i = 62; i < 74; i++) original[i] = (i * 7) & 0xff;
85
+
86
+ const compressed = packBits(original);
87
+ const decoded = unpackBits(compressed);
88
+ expect(Array.from(decoded)).toEqual(Array.from(original));
89
+ // And it actually saves bytes — the whole point.
90
+ expect(compressed.length).toBeLessThan(original.length);
91
+ });
92
+ });
@@ -32,7 +32,7 @@ describe('createPreviewOffline', () => {
32
32
  expect(preview.assumed).toBe(false);
33
33
  });
34
34
 
35
- it('returns black + red planes for colorCapable media (DK-22251)', () => {
35
+ it('returns black + red planes for multi-ink media (DK-22251)', () => {
36
36
  const image = solidRgba(8, 8, [255, 0, 0, 255]);
37
37
  const preview = createPreviewOffline(image, MEDIA[251]!);
38
38
  expect(preview.planes.map(p => p.name)).toEqual(['black', 'red']);
@@ -14,7 +14,7 @@ import {
14
14
  buildExpandedMode,
15
15
  encodeJob,
16
16
  } from '../protocol.js';
17
- import { type PageData } from '../types.js';
17
+ import type { PageData } from '../types.js';
18
18
  import { MEDIA } from '../media.js';
19
19
 
20
20
  describe('buildInvalidate', () => {
@@ -170,6 +170,52 @@ describe('encodeJob', () => {
170
170
  }
171
171
  expect(found).toBe(true);
172
172
  });
173
+
174
+ it('compress option PackBits-encodes each raster row (LEN < uncompressed size)', () => {
175
+ // All-zero bitmap → every row is 90 zero bytes. PackBits collapses each
176
+ // row to 2 wire bytes (0xA7 0x00), so the LEN byte in the row header
177
+ // should be 2 instead of 90.
178
+ const page: PageData = { ...makePage(696, 3), options: { compress: true } };
179
+ const buf = encodeJob([page]);
180
+ // Find single-color row headers (0x67 0x00 LEN). With compression on,
181
+ // LEN should be 2 for each row, and the payload byte should be 0x00.
182
+ const rowHeaders: number[] = [];
183
+ for (let i = 0; i < buf.length - 4; i++) {
184
+ if (buf[i] === 0x67 && buf[i + 1] === 0x00) {
185
+ // Skip the byte sequence inside the 200-byte invalidate block
186
+ // (which is also all zeros, so 0x67 won't appear there). Heuristic:
187
+ // verify the next byte after LEN is the PackBits header 0xA7.
188
+ if (buf[i + 2] === 2 && buf[i + 3] === 0xa7 && buf[i + 4] === 0x00) {
189
+ rowHeaders.push(i);
190
+ }
191
+ }
192
+ }
193
+ expect(rowHeaders.length).toBe(3);
194
+ });
195
+
196
+ it('compress option two-color: both planes PackBits-encoded per row', () => {
197
+ const bitmap = createBitmap(696, 2);
198
+ const redBitmap = createBitmap(696, 2);
199
+ const page: PageData = {
200
+ bitmap,
201
+ redBitmap,
202
+ media: media62,
203
+ options: { compress: true },
204
+ };
205
+ const buf = encodeJob([page]);
206
+ const blackRows: number[] = [];
207
+ const redRows: number[] = [];
208
+ for (let i = 0; i < buf.length - 4; i++) {
209
+ // Each compressed row is [0x77 0x01|0x02 0x02 0xA7 0x00] for an empty
210
+ // 696-pin row (90 zero bytes → 2 PackBits bytes).
211
+ if (buf[i] === 0x77 && buf[i + 2] === 2 && buf[i + 3] === 0xa7 && buf[i + 4] === 0x00) {
212
+ if (buf[i + 1] === 0x01) blackRows.push(i);
213
+ if (buf[i + 1] === 0x02) redRows.push(i);
214
+ }
215
+ }
216
+ expect(blackRows.length).toBe(2);
217
+ expect(redRows.length).toBe(2);
218
+ });
173
219
  });
174
220
 
175
221
  describe('buildRasterMode', () => {
package/src/devices.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type BrotherQLDevice } from './types.js';
1
+ import type { BrotherQLDevice } from './types.js';
2
2
 
3
3
  const MASS_STORAGE_PIDS = new Set([0x20aa, 0x20ab]);
4
4
 
package/src/index.ts CHANGED
@@ -1,5 +1,11 @@
1
- export type { LabelBitmap, RawImageData } from '@mbtech-nl/bitmap';
2
- export { renderText, renderImage, rotateBitmap, flipHorizontal } from '@mbtech-nl/bitmap';
1
+ export type { LabelBitmap, PaletteEntry, RawImageData } from '@mbtech-nl/bitmap';
2
+ export {
3
+ flipHorizontal,
4
+ renderImage,
5
+ renderMultiPlaneImage,
6
+ renderText,
7
+ rotateBitmap,
8
+ } from '@mbtech-nl/bitmap';
3
9
 
4
10
  export type {
5
11
  DeviceDescriptor,
@@ -11,11 +17,16 @@ export type {
11
17
  PrinterAdapter,
12
18
  PrinterError,
13
19
  PrinterStatus,
20
+ RotateDirection,
14
21
  Transport,
15
22
  TransportType,
16
23
  } from '@thermal-label/contracts';
17
24
 
18
- export { MediaNotSpecifiedError, UnsupportedOperationError } from '@thermal-label/contracts';
25
+ export {
26
+ MediaNotSpecifiedError,
27
+ pickRotation,
28
+ UnsupportedOperationError,
29
+ } from '@thermal-label/contracts';
19
30
 
20
31
  export { DEVICES, findDevice, isMassStorageMode } from './devices.js';
21
32
  export {
@@ -25,14 +36,15 @@ export {
25
36
  findMediaByDimensions,
26
37
  findMediaByWidth,
27
38
  } from './media.js';
39
+ export { ROTATE_DIRECTION } from './orientation.js';
28
40
  export { encodeJob } from './protocol.js';
29
41
  export { parseStatus, STATUS_REQUEST } from './status.js';
30
- export { isRedish, splitTwoColor, type TwoColorOptions, type TwoColorResult } from './colour.js';
31
42
  export { createPreviewOffline } from './preview.js';
32
43
 
33
44
  export type {
34
45
  BrotherQLDevice,
35
46
  BrotherQLMedia,
47
+ BrotherQLPrintOptions,
36
48
  BrotherQLStatus,
37
49
  ColorMode,
38
50
  HeadWidth,
package/src/media.ts CHANGED
@@ -1,14 +1,26 @@
1
- import { type BrotherQLMedia, type MediaType } from './types.js';
1
+ import type { BrotherQLMedia, MediaType } from './types.js';
2
2
 
3
3
  /**
4
4
  * Registry of supported Brother QL consumables.
5
5
  *
6
6
  * Entries are keyed by the firmware media id — the same number the
7
- * printer reports in the 32-byte status response. `heightMm` is
8
- * omitted for continuous media (variable length) and set for die-cut
9
- * labels (fixed length). Only DK-22251 has `colorCapable: true` — the
10
- * driver uses that to run two-colour plane separation before sending
11
- * the raster.
7
+ * printer reports in the 32-byte status response. `heightMm` is omitted
8
+ * for continuous media (variable length) and set for die-cut labels
9
+ * (fixed length).
10
+ *
11
+ * `palette` is set on multi-ink media (DK-22251, today's only entry) —
12
+ * the driver routes those through `renderMultiPlaneImage` and emits the
13
+ * second plane in the raster job. Single-ink rolls leave it undefined
14
+ * and route through `renderImage` (dithered single-plane).
15
+ *
16
+ * Rectangular die-cut entries declare `defaultOrientation: 'horizontal'`
17
+ * so landscape input auto-rotates to read along the tape feed direction.
18
+ * Continuous wide tape leaves the hint undefined — users may go either
19
+ * way.
20
+ *
21
+ * `cornerRadiusMm` is informational; previews use it to render the
22
+ * actual paper outline. Round die-cut labels set the radius to
23
+ * `widthMm / 2` so the rounded rectangle degenerates to a circle.
12
24
  */
13
25
  export const MEDIA: Record<number, BrotherQLMedia> = {
14
26
  // Continuous length tape
@@ -17,7 +29,6 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
17
29
  name: '12mm continuous',
18
30
  type: 'continuous',
19
31
  widthMm: 12,
20
- colorCapable: false,
21
32
  printAreaDots: 106,
22
33
  leftMarginPins: 585,
23
34
  rightMarginPins: 29,
@@ -27,7 +38,6 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
27
38
  name: '29mm continuous (DK-22210)',
28
39
  type: 'continuous',
29
40
  widthMm: 29,
30
- colorCapable: false,
31
41
  printAreaDots: 306,
32
42
  leftMarginPins: 408,
33
43
  rightMarginPins: 6,
@@ -37,7 +47,6 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
37
47
  name: '38mm continuous (DK-22225)',
38
48
  type: 'continuous',
39
49
  widthMm: 38,
40
- colorCapable: false,
41
50
  printAreaDots: 413,
42
51
  leftMarginPins: 295,
43
52
  rightMarginPins: 12,
@@ -47,7 +56,6 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
47
56
  name: '50mm continuous (DK-22246)',
48
57
  type: 'continuous',
49
58
  widthMm: 50,
50
- colorCapable: false,
51
59
  printAreaDots: 554,
52
60
  leftMarginPins: 154,
53
61
  rightMarginPins: 12,
@@ -57,7 +65,6 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
57
65
  name: '54mm continuous (DK-22214)',
58
66
  type: 'continuous',
59
67
  widthMm: 54,
60
- colorCapable: false,
61
68
  printAreaDots: 590,
62
69
  leftMarginPins: 130,
63
70
  rightMarginPins: 0,
@@ -67,7 +74,6 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
67
74
  name: '62mm continuous (DK-22205)',
68
75
  type: 'continuous',
69
76
  widthMm: 62,
70
- colorCapable: false,
71
77
  printAreaDots: 696,
72
78
  leftMarginPins: 12,
73
79
  rightMarginPins: 12,
@@ -77,7 +83,10 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
77
83
  name: '62mm continuous two-color (DK-22251)',
78
84
  type: 'continuous',
79
85
  widthMm: 62,
80
- colorCapable: true,
86
+ palette: [
87
+ { name: 'black', rgb: [0, 0, 0] },
88
+ { name: 'red', rgb: [255, 0, 0] },
89
+ ],
81
90
  printAreaDots: 696,
82
91
  leftMarginPins: 12,
83
92
  rightMarginPins: 12,
@@ -87,7 +96,6 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
87
96
  name: '102mm continuous (DK-22243)',
88
97
  type: 'continuous',
89
98
  widthMm: 102,
90
- colorCapable: false,
91
99
  printAreaDots: 1164,
92
100
  leftMarginPins: 76,
93
101
  rightMarginPins: 56,
@@ -100,7 +108,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
100
108
  type: 'die-cut',
101
109
  widthMm: 17,
102
110
  heightMm: 54,
103
- colorCapable: false,
111
+ defaultOrientation: 'horizontal',
112
+ cornerRadiusMm: 3,
104
113
  printAreaDots: 165,
105
114
  leftMarginPins: 0,
106
115
  rightMarginPins: 0,
@@ -112,7 +121,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
112
121
  type: 'die-cut',
113
122
  widthMm: 17,
114
123
  heightMm: 87,
115
- colorCapable: false,
124
+ defaultOrientation: 'horizontal',
125
+ cornerRadiusMm: 3,
116
126
  printAreaDots: 165,
117
127
  leftMarginPins: 0,
118
128
  rightMarginPins: 0,
@@ -124,7 +134,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
124
134
  type: 'die-cut',
125
135
  widthMm: 23,
126
136
  heightMm: 23,
127
- colorCapable: false,
137
+ defaultOrientation: 'horizontal',
138
+ cornerRadiusMm: 3,
128
139
  printAreaDots: 236,
129
140
  leftMarginPins: 0,
130
141
  rightMarginPins: 0,
@@ -136,7 +147,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
136
147
  type: 'die-cut',
137
148
  widthMm: 29,
138
149
  heightMm: 90,
139
- colorCapable: false,
150
+ defaultOrientation: 'horizontal',
151
+ cornerRadiusMm: 3,
140
152
  printAreaDots: 306,
141
153
  leftMarginPins: 0,
142
154
  rightMarginPins: 0,
@@ -148,7 +160,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
148
160
  type: 'die-cut',
149
161
  widthMm: 38,
150
162
  heightMm: 90,
151
- colorCapable: false,
163
+ defaultOrientation: 'horizontal',
164
+ cornerRadiusMm: 3,
152
165
  printAreaDots: 413,
153
166
  leftMarginPins: 0,
154
167
  rightMarginPins: 0,
@@ -160,7 +173,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
160
173
  type: 'die-cut',
161
174
  widthMm: 39,
162
175
  heightMm: 48,
163
- colorCapable: false,
176
+ defaultOrientation: 'horizontal',
177
+ cornerRadiusMm: 3,
164
178
  printAreaDots: 425,
165
179
  leftMarginPins: 0,
166
180
  rightMarginPins: 0,
@@ -172,7 +186,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
172
186
  type: 'die-cut',
173
187
  widthMm: 52,
174
188
  heightMm: 29,
175
- colorCapable: false,
189
+ defaultOrientation: 'horizontal',
190
+ cornerRadiusMm: 3,
176
191
  printAreaDots: 578,
177
192
  leftMarginPins: 0,
178
193
  rightMarginPins: 0,
@@ -184,7 +199,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
184
199
  type: 'die-cut',
185
200
  widthMm: 62,
186
201
  heightMm: 29,
187
- colorCapable: false,
202
+ defaultOrientation: 'horizontal',
203
+ cornerRadiusMm: 3,
188
204
  printAreaDots: 696,
189
205
  leftMarginPins: 0,
190
206
  rightMarginPins: 0,
@@ -196,7 +212,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
196
212
  type: 'die-cut',
197
213
  widthMm: 62,
198
214
  heightMm: 100,
199
- colorCapable: false,
215
+ defaultOrientation: 'horizontal',
216
+ cornerRadiusMm: 3,
200
217
  printAreaDots: 696,
201
218
  leftMarginPins: 0,
202
219
  rightMarginPins: 0,
@@ -208,7 +225,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
208
225
  type: 'die-cut',
209
226
  widthMm: 102,
210
227
  heightMm: 51,
211
- colorCapable: false,
228
+ defaultOrientation: 'horizontal',
229
+ cornerRadiusMm: 3,
212
230
  printAreaDots: 1164,
213
231
  leftMarginPins: 0,
214
232
  rightMarginPins: 0,
@@ -220,7 +238,8 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
220
238
  type: 'die-cut',
221
239
  widthMm: 102,
222
240
  heightMm: 152,
223
- colorCapable: false,
241
+ defaultOrientation: 'horizontal',
242
+ cornerRadiusMm: 3,
224
243
  printAreaDots: 1164,
225
244
  leftMarginPins: 0,
226
245
  rightMarginPins: 0,
@@ -232,7 +251,7 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
232
251
  type: 'die-cut',
233
252
  widthMm: 12,
234
253
  heightMm: 12,
235
- colorCapable: false,
254
+ cornerRadiusMm: 6,
236
255
  printAreaDots: 94,
237
256
  leftMarginPins: 0,
238
257
  rightMarginPins: 0,
@@ -244,7 +263,7 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
244
263
  type: 'die-cut',
245
264
  widthMm: 24,
246
265
  heightMm: 24,
247
- colorCapable: false,
266
+ cornerRadiusMm: 12,
248
267
  printAreaDots: 236,
249
268
  leftMarginPins: 0,
250
269
  rightMarginPins: 0,
@@ -256,7 +275,7 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
256
275
  type: 'die-cut',
257
276
  widthMm: 58,
258
277
  heightMm: 58,
259
- colorCapable: false,
278
+ cornerRadiusMm: 29,
260
279
  printAreaDots: 618,
261
280
  leftMarginPins: 0,
262
281
  rightMarginPins: 0,
@@ -300,7 +319,7 @@ export function findMediaByDimensions(
300
319
  m => m.type === 'continuous' && m.widthMm === widthMm,
301
320
  );
302
321
  if (continuousMatches.length === 0) return undefined;
303
- const preferred = continuousMatches.find(m => m.colorCapable === twoColorMode);
322
+ const preferred = continuousMatches.find(m => (m.palette !== undefined) === twoColorMode);
304
323
  return preferred ?? continuousMatches[0];
305
324
  }
306
325
  return Object.values(MEDIA).find(
@@ -0,0 +1,11 @@
1
+ import type { RotateDirection } from '@thermal-label/contracts';
2
+
3
+ /**
4
+ * Direction the Brother QL print head rotates landscape input.
5
+ *
6
+ * `90` = clockwise. Verified once on hardware with a die-cut "F"
7
+ * landscape print (see plan §6 step 1). Identical across every QL
8
+ * model — this is a print-head/leading-edge mechanical fact, not a
9
+ * per-media setting.
10
+ */
11
+ export const ROTATE_DIRECTION: RotateDirection = 90;
@@ -0,0 +1,64 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion --
2
+ * `noUncheckedIndexedAccess` types Uint8Array reads as `number | undefined`
3
+ * but every index in this file is bounded by `i < input.length` or
4
+ * `runEnd < input.length` checks. The `!` assertions collapse the
5
+ * unreachable `undefined` branches so branch coverage doesn't count them.
6
+ */
7
+
8
+ /**
9
+ * TIFF-style PackBits row encoder used by Brother QL when compression
10
+ * mode (`M 02`, emitted by `buildCompression(true)`) is enabled for a job.
11
+ *
12
+ * Header byte `n` (interpreted as a signed int8):
13
+ * n in [0, 127]: literal run — the next `n + 1` bytes follow verbatim.
14
+ * n in [-127, -1]: repeat run — the next byte is repeated `1 - n` times.
15
+ * n = -128: no-op. Unused by this encoder.
16
+ *
17
+ * The encoder switches to repeat mode for runs of two or more identical
18
+ * bytes (a 2-byte repeat costs 2 wire bytes; the equivalent 2-byte literal
19
+ * would cost 3). Both run kinds are capped at 128 bytes to fit the header.
20
+ *
21
+ * For a typical Brother QL raster row (90 bytes, mostly zeros in margins
22
+ * and long runs of `0x00` / `0xff` in print area), this compresses to 5–15
23
+ * bytes — a 6–18× reduction. Worst-case for highly random input is one
24
+ * extra byte per 128 (the literal-mode header), i.e. < 1 % expansion.
25
+ */
26
+ export function packBits(input: Uint8Array): Uint8Array {
27
+ if (input.length === 0) return new Uint8Array(0);
28
+
29
+ const out: number[] = [];
30
+ let i = 0;
31
+
32
+ while (i < input.length) {
33
+ // How many identical bytes start at position i (cap at 128).
34
+ let runEnd = i + 1;
35
+ while (runEnd < input.length && runEnd - i < 128 && input[runEnd] === input[i]) {
36
+ runEnd++;
37
+ }
38
+ const runLen = runEnd - i;
39
+
40
+ if (runLen >= 2) {
41
+ // Repeat run: header is signed -(runLen - 1).
42
+ out.push((1 - runLen) & 0xff);
43
+ out.push(input[i]!);
44
+ i = runEnd;
45
+ continue;
46
+ }
47
+
48
+ // Otherwise emit a literal run. Stop at 128 bytes, or right before a
49
+ // 2+ repeat run starts (cheaper to encode that separately).
50
+ let litEnd = i + 1;
51
+ while (litEnd < input.length && litEnd - i < 128) {
52
+ if (litEnd + 1 < input.length && input[litEnd] === input[litEnd + 1]) {
53
+ break;
54
+ }
55
+ litEnd++;
56
+ }
57
+ const litLen = litEnd - i;
58
+ out.push(litLen - 1);
59
+ for (let x = i; x < litEnd; x++) out.push(input[x]!);
60
+ i = litEnd;
61
+ }
62
+
63
+ return new Uint8Array(out);
64
+ }