@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.
- package/dist/__tests__/media.test.js +23 -3
- package/dist/__tests__/media.test.js.map +1 -1
- package/dist/__tests__/pack-bits.test.d.ts +2 -0
- package/dist/__tests__/pack-bits.test.d.ts.map +1 -0
- package/dist/__tests__/pack-bits.test.js +90 -0
- package/dist/__tests__/pack-bits.test.js.map +1 -0
- package/dist/__tests__/preview.test.js +1 -1
- package/dist/__tests__/preview.test.js.map +1 -1
- package/dist/__tests__/protocol.test.js +46 -1
- package/dist/__tests__/protocol.test.js.map +1 -1
- package/dist/devices.d.ts +1 -1
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.js +0 -1
- package/dist/devices.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +18 -6
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +47 -29
- package/dist/media.js.map +1 -1
- package/dist/orientation.d.ts +11 -0
- package/dist/orientation.d.ts.map +1 -0
- package/dist/orientation.js +10 -0
- package/dist/orientation.js.map +1 -0
- package/dist/pack-bits.d.ts +20 -0
- package/dist/pack-bits.d.ts.map +1 -0
- package/dist/pack-bits.js +61 -0
- package/dist/pack-bits.js.map +1 -0
- package/dist/preview.d.ts +6 -6
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +11 -12
- package/dist/preview.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +13 -7
- package/dist/protocol.js.map +1 -1
- package/dist/status.d.ts +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -1
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +15 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -2
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/media.test.ts +25 -3
- package/src/__tests__/pack-bits.test.ts +92 -0
- package/src/__tests__/preview.test.ts +1 -1
- package/src/__tests__/protocol.test.ts +47 -1
- package/src/devices.ts +1 -1
- package/src/index.ts +16 -4
- package/src/media.ts +48 -29
- package/src/orientation.ts +11 -0
- package/src/pack-bits.ts +64 -0
- package/src/preview.ts +13 -12
- package/src/protocol.ts +14 -7
- package/src/status.ts +1 -2
- package/src/types.ts +21 -6
- package/dist/__tests__/colour.test.d.ts +0 -2
- package/dist/__tests__/colour.test.d.ts.map +0 -1
- package/dist/__tests__/colour.test.js +0 -106
- package/dist/__tests__/colour.test.js.map +0 -1
- package/dist/colour.d.ts +0 -26
- package/dist/colour.d.ts.map +0 -1
- package/dist/colour.js +0 -84
- package/dist/colour.js.map +0 -1
- package/src/__tests__/colour.test.ts +0 -126
- package/src/colour.ts +0 -101
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
39
|
-
*
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
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
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
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.
|
|
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": "^
|
|
57
|
-
"@thermal-label/contracts": "^0.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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 {
|
|
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
package/src/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
export type { LabelBitmap, RawImageData } from '@mbtech-nl/bitmap';
|
|
2
|
-
export {
|
|
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 {
|
|
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 {
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|
package/src/pack-bits.ts
ADDED
|
@@ -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
|
+
}
|