@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.
- package/README.md +1 -1
- package/data/devices.json +823 -0
- package/data/media.json +823 -0
- package/dist/__tests__/devices.test.js +112 -31
- package/dist/__tests__/devices.test.js.map +1 -1
- package/dist/__tests__/media.test.js +251 -1
- package/dist/__tests__/media.test.js.map +1 -1
- package/dist/__tests__/protocol.test.js +168 -1
- package/dist/__tests__/protocol.test.js.map +1 -1
- package/dist/__tests__/status.test.js +71 -0
- package/dist/__tests__/status.test.js.map +1 -1
- package/dist/devices.d.ts +13 -270
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.generated.d.ts +696 -0
- package/dist/devices.generated.d.ts.map +1 -0
- package/dist/devices.generated.js +831 -0
- package/dist/devices.generated.js.map +1 -0
- package/dist/devices.js +28 -272
- package/dist/devices.js.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +37 -22
- package/dist/media.d.ts.map +1 -1
- package/dist/media.generated.d.ts +4 -0
- package/dist/media.generated.d.ts.map +1 -0
- package/dist/media.generated.js +1640 -0
- package/dist/media.generated.js.map +1 -0
- package/dist/media.js +74 -281
- package/dist/media.js.map +1 -1
- package/dist/protocol.d.ts +54 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +117 -18
- package/dist/protocol.js.map +1 -1
- package/dist/status.d.ts +4 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +6 -2
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +92 -27
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -9
- package/src/__tests__/devices.test.ts +122 -32
- package/src/__tests__/media.test.ts +287 -1
- package/src/__tests__/protocol.test.ts +209 -0
- package/src/__tests__/status.test.ts +87 -0
- package/src/devices.generated.ts +840 -0
- package/src/devices.ts +30 -272
- package/src/index.ts +28 -4
- package/src/media.generated.ts +1644 -0
- package/src/media.ts +86 -282
- package/src/protocol.ts +196 -18
- package/src/status.ts +10 -3
- package/src/types.ts +93 -27
package/src/media.ts
CHANGED
|
@@ -1,287 +1,49 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PrintEngine } from '@thermal-label/contracts';
|
|
2
|
+
import type { BrotherQLMedia, MediaType, TapeGeometry } from './types.js';
|
|
3
|
+
import { MEDIA } from './media.generated.js';
|
|
4
|
+
|
|
5
|
+
export { MEDIA };
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Entries are keyed by the firmware media id — the same number the
|
|
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).
|
|
8
|
+
* Resolve per-head-family geometry for a media entry against the
|
|
9
|
+
* engine that's about to print it.
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
11
|
+
* DK entries fall back to the flat `printAreaDots` / `leftMarginPins` /
|
|
12
|
+
* `rightMarginPins` fields on the entry — the same values every QL
|
|
13
|
+
* code path read before the head-family split landed. TZe and HSe
|
|
14
|
+
* entries dispatch on `engine.headDots`: 128 picks `geometry.narrow`,
|
|
15
|
+
* anything else picks `geometry.wide`. Throws when the requested head
|
|
16
|
+
* family has no entry (e.g. 36 mm TZe on a 128-dot head, or any HSe
|
|
17
|
+
* on PT-P910BT — those engines simply shouldn't reach this call site
|
|
18
|
+
* because `findMediaByDimensions` gates them upstream).
|
|
24
19
|
*/
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
rightMarginPins: 12,
|
|
53
|
-
},
|
|
54
|
-
262: {
|
|
55
|
-
id: 262,
|
|
56
|
-
name: '50mm continuous (DK-22246)',
|
|
57
|
-
type: 'continuous',
|
|
58
|
-
widthMm: 50,
|
|
59
|
-
printAreaDots: 554,
|
|
60
|
-
leftMarginPins: 154,
|
|
61
|
-
rightMarginPins: 12,
|
|
62
|
-
},
|
|
63
|
-
261: {
|
|
64
|
-
id: 261,
|
|
65
|
-
name: '54mm continuous (DK-22214)',
|
|
66
|
-
type: 'continuous',
|
|
67
|
-
widthMm: 54,
|
|
68
|
-
printAreaDots: 590,
|
|
69
|
-
leftMarginPins: 130,
|
|
70
|
-
rightMarginPins: 0,
|
|
71
|
-
},
|
|
72
|
-
259: {
|
|
73
|
-
id: 259,
|
|
74
|
-
name: '62mm continuous (DK-22205)',
|
|
75
|
-
type: 'continuous',
|
|
76
|
-
widthMm: 62,
|
|
77
|
-
printAreaDots: 696,
|
|
78
|
-
leftMarginPins: 12,
|
|
79
|
-
rightMarginPins: 12,
|
|
80
|
-
},
|
|
81
|
-
251: {
|
|
82
|
-
id: 251,
|
|
83
|
-
name: '62mm continuous two-color (DK-22251)',
|
|
84
|
-
type: 'continuous',
|
|
85
|
-
widthMm: 62,
|
|
86
|
-
palette: [
|
|
87
|
-
{ name: 'black', rgb: [0, 0, 0] },
|
|
88
|
-
{ name: 'red', rgb: [255, 0, 0] },
|
|
89
|
-
],
|
|
90
|
-
printAreaDots: 696,
|
|
91
|
-
leftMarginPins: 12,
|
|
92
|
-
rightMarginPins: 12,
|
|
93
|
-
},
|
|
94
|
-
260: {
|
|
95
|
-
id: 260,
|
|
96
|
-
name: '102mm continuous (DK-22243)',
|
|
97
|
-
type: 'continuous',
|
|
98
|
-
widthMm: 102,
|
|
99
|
-
printAreaDots: 1164,
|
|
100
|
-
leftMarginPins: 76,
|
|
101
|
-
rightMarginPins: 56,
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
// Die-cut labels
|
|
105
|
-
269: {
|
|
106
|
-
id: 269,
|
|
107
|
-
name: '17×54mm die-cut (DK-11204)',
|
|
108
|
-
type: 'die-cut',
|
|
109
|
-
widthMm: 17,
|
|
110
|
-
heightMm: 54,
|
|
111
|
-
defaultOrientation: 'horizontal',
|
|
112
|
-
cornerRadiusMm: 3,
|
|
113
|
-
printAreaDots: 165,
|
|
114
|
-
leftMarginPins: 0,
|
|
115
|
-
rightMarginPins: 0,
|
|
116
|
-
dieCutMaskedAreaDots: 566,
|
|
117
|
-
},
|
|
118
|
-
270: {
|
|
119
|
-
id: 270,
|
|
120
|
-
name: '17×87mm die-cut (DK-11203)',
|
|
121
|
-
type: 'die-cut',
|
|
122
|
-
widthMm: 17,
|
|
123
|
-
heightMm: 87,
|
|
124
|
-
defaultOrientation: 'horizontal',
|
|
125
|
-
cornerRadiusMm: 3,
|
|
126
|
-
printAreaDots: 165,
|
|
127
|
-
leftMarginPins: 0,
|
|
128
|
-
rightMarginPins: 0,
|
|
129
|
-
dieCutMaskedAreaDots: 956,
|
|
130
|
-
},
|
|
131
|
-
370: {
|
|
132
|
-
id: 370,
|
|
133
|
-
name: '23×23mm die-cut',
|
|
134
|
-
type: 'die-cut',
|
|
135
|
-
widthMm: 23,
|
|
136
|
-
heightMm: 23,
|
|
137
|
-
defaultOrientation: 'horizontal',
|
|
138
|
-
cornerRadiusMm: 3,
|
|
139
|
-
printAreaDots: 236,
|
|
140
|
-
leftMarginPins: 0,
|
|
141
|
-
rightMarginPins: 0,
|
|
142
|
-
dieCutMaskedAreaDots: 202,
|
|
143
|
-
},
|
|
144
|
-
271: {
|
|
145
|
-
id: 271,
|
|
146
|
-
name: '29×90mm die-cut (DK-11201)',
|
|
147
|
-
type: 'die-cut',
|
|
148
|
-
widthMm: 29,
|
|
149
|
-
heightMm: 90,
|
|
150
|
-
defaultOrientation: 'horizontal',
|
|
151
|
-
cornerRadiusMm: 3,
|
|
152
|
-
printAreaDots: 306,
|
|
153
|
-
leftMarginPins: 0,
|
|
154
|
-
rightMarginPins: 0,
|
|
155
|
-
dieCutMaskedAreaDots: 991,
|
|
156
|
-
},
|
|
157
|
-
272: {
|
|
158
|
-
id: 272,
|
|
159
|
-
name: '38×90mm die-cut (DK-11218)',
|
|
160
|
-
type: 'die-cut',
|
|
161
|
-
widthMm: 38,
|
|
162
|
-
heightMm: 90,
|
|
163
|
-
defaultOrientation: 'horizontal',
|
|
164
|
-
cornerRadiusMm: 3,
|
|
165
|
-
printAreaDots: 413,
|
|
166
|
-
leftMarginPins: 0,
|
|
167
|
-
rightMarginPins: 0,
|
|
168
|
-
dieCutMaskedAreaDots: 991,
|
|
169
|
-
},
|
|
170
|
-
367: {
|
|
171
|
-
id: 367,
|
|
172
|
-
name: '39×48mm die-cut (DK-11219)',
|
|
173
|
-
type: 'die-cut',
|
|
174
|
-
widthMm: 39,
|
|
175
|
-
heightMm: 48,
|
|
176
|
-
defaultOrientation: 'horizontal',
|
|
177
|
-
cornerRadiusMm: 3,
|
|
178
|
-
printAreaDots: 425,
|
|
179
|
-
leftMarginPins: 0,
|
|
180
|
-
rightMarginPins: 0,
|
|
181
|
-
dieCutMaskedAreaDots: 495,
|
|
182
|
-
},
|
|
183
|
-
374: {
|
|
184
|
-
id: 374,
|
|
185
|
-
name: '52×29mm die-cut',
|
|
186
|
-
type: 'die-cut',
|
|
187
|
-
widthMm: 52,
|
|
188
|
-
heightMm: 29,
|
|
189
|
-
defaultOrientation: 'horizontal',
|
|
190
|
-
cornerRadiusMm: 3,
|
|
191
|
-
printAreaDots: 578,
|
|
192
|
-
leftMarginPins: 0,
|
|
193
|
-
rightMarginPins: 0,
|
|
194
|
-
dieCutMaskedAreaDots: 271,
|
|
195
|
-
},
|
|
196
|
-
274: {
|
|
197
|
-
id: 274,
|
|
198
|
-
name: '62×29mm die-cut (DK-11209)',
|
|
199
|
-
type: 'die-cut',
|
|
200
|
-
widthMm: 62,
|
|
201
|
-
heightMm: 29,
|
|
202
|
-
defaultOrientation: 'horizontal',
|
|
203
|
-
cornerRadiusMm: 3,
|
|
204
|
-
printAreaDots: 696,
|
|
205
|
-
leftMarginPins: 0,
|
|
206
|
-
rightMarginPins: 0,
|
|
207
|
-
dieCutMaskedAreaDots: 271,
|
|
208
|
-
},
|
|
209
|
-
275: {
|
|
210
|
-
id: 275,
|
|
211
|
-
name: '62×100mm die-cut (DK-11202)',
|
|
212
|
-
type: 'die-cut',
|
|
213
|
-
widthMm: 62,
|
|
214
|
-
heightMm: 100,
|
|
215
|
-
defaultOrientation: 'horizontal',
|
|
216
|
-
cornerRadiusMm: 3,
|
|
217
|
-
printAreaDots: 696,
|
|
218
|
-
leftMarginPins: 0,
|
|
219
|
-
rightMarginPins: 0,
|
|
220
|
-
dieCutMaskedAreaDots: 1109,
|
|
221
|
-
},
|
|
222
|
-
365: {
|
|
223
|
-
id: 365,
|
|
224
|
-
name: '102×51mm die-cut (DK-11240)',
|
|
225
|
-
type: 'die-cut',
|
|
226
|
-
widthMm: 102,
|
|
227
|
-
heightMm: 51,
|
|
228
|
-
defaultOrientation: 'horizontal',
|
|
229
|
-
cornerRadiusMm: 3,
|
|
230
|
-
printAreaDots: 1164,
|
|
231
|
-
leftMarginPins: 0,
|
|
232
|
-
rightMarginPins: 0,
|
|
233
|
-
dieCutMaskedAreaDots: 526,
|
|
234
|
-
},
|
|
235
|
-
366: {
|
|
236
|
-
id: 366,
|
|
237
|
-
name: '102×152mm die-cut (DK-11241)',
|
|
238
|
-
type: 'die-cut',
|
|
239
|
-
widthMm: 102,
|
|
240
|
-
heightMm: 152,
|
|
241
|
-
defaultOrientation: 'horizontal',
|
|
242
|
-
cornerRadiusMm: 3,
|
|
243
|
-
printAreaDots: 1164,
|
|
244
|
-
leftMarginPins: 0,
|
|
245
|
-
rightMarginPins: 0,
|
|
246
|
-
dieCutMaskedAreaDots: 1660,
|
|
247
|
-
},
|
|
248
|
-
362: {
|
|
249
|
-
id: 362,
|
|
250
|
-
name: '12mm Ø die-cut',
|
|
251
|
-
type: 'die-cut',
|
|
252
|
-
widthMm: 12,
|
|
253
|
-
heightMm: 12,
|
|
254
|
-
cornerRadiusMm: 6,
|
|
255
|
-
printAreaDots: 94,
|
|
256
|
-
leftMarginPins: 0,
|
|
257
|
-
rightMarginPins: 0,
|
|
258
|
-
dieCutMaskedAreaDots: 94,
|
|
259
|
-
},
|
|
260
|
-
363: {
|
|
261
|
-
id: 363,
|
|
262
|
-
name: '24mm Ø die-cut (DK-11221)',
|
|
263
|
-
type: 'die-cut',
|
|
264
|
-
widthMm: 24,
|
|
265
|
-
heightMm: 24,
|
|
266
|
-
cornerRadiusMm: 12,
|
|
267
|
-
printAreaDots: 236,
|
|
268
|
-
leftMarginPins: 0,
|
|
269
|
-
rightMarginPins: 0,
|
|
270
|
-
dieCutMaskedAreaDots: 236,
|
|
271
|
-
},
|
|
272
|
-
273: {
|
|
273
|
-
id: 273,
|
|
274
|
-
name: '58mm Ø die-cut (DK-11207)',
|
|
275
|
-
type: 'die-cut',
|
|
276
|
-
widthMm: 58,
|
|
277
|
-
heightMm: 58,
|
|
278
|
-
cornerRadiusMm: 29,
|
|
279
|
-
printAreaDots: 618,
|
|
280
|
-
leftMarginPins: 0,
|
|
281
|
-
rightMarginPins: 0,
|
|
282
|
-
dieCutMaskedAreaDots: 618,
|
|
283
|
-
},
|
|
284
|
-
};
|
|
20
|
+
export function resolveTapeGeometry(
|
|
21
|
+
media: BrotherQLMedia,
|
|
22
|
+
engine: Pick<PrintEngine, 'headDots'>,
|
|
23
|
+
): TapeGeometry {
|
|
24
|
+
if (media.tapeSystem === 'dk') {
|
|
25
|
+
if (
|
|
26
|
+
typeof media.printAreaDots !== 'number' ||
|
|
27
|
+
typeof media.leftMarginPins !== 'number' ||
|
|
28
|
+
typeof media.rightMarginPins !== 'number'
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`DK media ${media.id.toString()} missing flat geometry fields`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
printAreaDots: media.printAreaDots,
|
|
34
|
+
leftMarginPins: media.leftMarginPins,
|
|
35
|
+
rightMarginPins: media.rightMarginPins,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const family = engine.headDots === 128 ? 'narrow' : 'wide';
|
|
39
|
+
const geometry = media.geometry?.[family];
|
|
40
|
+
if (!geometry) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`${media.name} not supported on a ${engine.headDots.toString()}-dot head (no \`geometry.${family}\` entry)`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return geometry;
|
|
46
|
+
}
|
|
285
47
|
|
|
286
48
|
/**
|
|
287
49
|
* Default media when `createPreview()` is called without media and
|
|
@@ -291,6 +53,22 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
|
|
|
291
53
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DK-22205 is a hard-coded registry key
|
|
292
54
|
export const DEFAULT_MEDIA: BrotherQLMedia = MEDIA[259]!;
|
|
293
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Default media keyed by engine protocol. QL falls back to 62 mm DK
|
|
58
|
+
* continuous (DK-22205, the common starter roll); PT falls back to
|
|
59
|
+
* 12 mm TZe (id 404, the most common PT starter tape).
|
|
60
|
+
*/
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 12 mm TZe is a hard-coded registry key
|
|
62
|
+
export const DEFAULT_PT_MEDIA: BrotherQLMedia = MEDIA[404]!;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Pick a default media entry for an engine. Used by `createPreview()`
|
|
66
|
+
* when neither user-supplied media nor a detected roll is available.
|
|
67
|
+
*/
|
|
68
|
+
export function defaultMediaForEngine(engine: Pick<PrintEngine, 'protocol'>): BrotherQLMedia {
|
|
69
|
+
return engine.protocol === 'pt-raster' ? DEFAULT_PT_MEDIA : DEFAULT_MEDIA;
|
|
70
|
+
}
|
|
71
|
+
|
|
294
72
|
export function findMedia(id: number): BrotherQLMedia | undefined {
|
|
295
73
|
return MEDIA[id];
|
|
296
74
|
}
|
|
@@ -308,21 +86,47 @@ export function findMediaByWidth(widthMm: number, type: MediaType): BrotherQLMed
|
|
|
308
86
|
* printer is configured for two-colour media. When
|
|
309
87
|
* both DK-22205 (259) and DK-22251 (251) match the
|
|
310
88
|
* dimensions, the flag picks the right one.
|
|
89
|
+
* @param engine optional engine descriptor used to gate the
|
|
90
|
+
* lookup by tape-system and head-family. When
|
|
91
|
+
* omitted, the search is restricted to DK media —
|
|
92
|
+
* legacy QL behaviour. PT-* callers pass the
|
|
93
|
+
* primary engine so the search returns TZe / HSe
|
|
94
|
+
* entries with the right `narrow` / `wide`
|
|
95
|
+
* geometry, and so a 128-dot head never resolves
|
|
96
|
+
* a 36 mm TZe / 31 mm HSe-3:1 width and
|
|
97
|
+
* PT-P910BT (TZe-only) never resolves an HSe
|
|
98
|
+
* entry.
|
|
311
99
|
*/
|
|
312
100
|
export function findMediaByDimensions(
|
|
313
101
|
widthMm: number,
|
|
314
102
|
heightMm: number,
|
|
315
103
|
twoColorMode = false,
|
|
104
|
+
engine?: Pick<PrintEngine, 'headDots' | 'mediaCompatibility'>,
|
|
316
105
|
): BrotherQLMedia | undefined {
|
|
106
|
+
// When no engine is supplied we preserve the original DK-only
|
|
107
|
+
// behaviour so legacy callers don't suddenly resolve TZe or HSe
|
|
108
|
+
// entries on width-only ambiguity (e.g. 12 mm DK-22214 vs 12 mm TZe).
|
|
109
|
+
const tapeSystems: readonly string[] | undefined =
|
|
110
|
+
engine?.mediaCompatibility ?? (engine ? undefined : ['dk']);
|
|
111
|
+
const allowed = (m: BrotherQLMedia): boolean => {
|
|
112
|
+
if (tapeSystems && !tapeSystems.includes(m.tapeSystem)) return false;
|
|
113
|
+
if (engine && m.tapeSystem !== 'dk') {
|
|
114
|
+
// TZe / HSe — require the head-family geometry to exist.
|
|
115
|
+
const family = engine.headDots === 128 ? 'narrow' : 'wide';
|
|
116
|
+
if (!m.geometry?.[family]) return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
};
|
|
120
|
+
|
|
317
121
|
if (heightMm === 0) {
|
|
318
122
|
const continuousMatches = Object.values(MEDIA).filter(
|
|
319
|
-
m => m.type === 'continuous' && m.widthMm === widthMm,
|
|
123
|
+
m => m.type === 'continuous' && m.widthMm === widthMm && allowed(m),
|
|
320
124
|
);
|
|
321
125
|
if (continuousMatches.length === 0) return undefined;
|
|
322
126
|
const preferred = continuousMatches.find(m => (m.palette !== undefined) === twoColorMode);
|
|
323
127
|
return preferred ?? continuousMatches[0];
|
|
324
128
|
}
|
|
325
129
|
return Object.values(MEDIA).find(
|
|
326
|
-
m => m.type === 'die-cut' && m.widthMm === widthMm && m.heightMm === heightMm,
|
|
130
|
+
m => m.type === 'die-cut' && m.widthMm === widthMm && m.heightMm === heightMm && allowed(m),
|
|
327
131
|
);
|
|
328
132
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { getRow, createBitmap } from '@mbtech-nl/bitmap';
|
|
2
|
+
import type { PrintEngine } from '@thermal-label/contracts';
|
|
2
3
|
import { packBits } from './pack-bits.js';
|
|
3
|
-
import
|
|
4
|
+
import { resolveTapeGeometry } from './media.js';
|
|
5
|
+
import type {
|
|
6
|
+
BrotherEngineCapabilities,
|
|
7
|
+
BrotherQLMedia,
|
|
8
|
+
PageData,
|
|
9
|
+
JobOptions,
|
|
10
|
+
PageOptions,
|
|
11
|
+
TapeGeometry,
|
|
12
|
+
} from './types.js';
|
|
4
13
|
|
|
5
|
-
export function buildInvalidate(): Uint8Array {
|
|
6
|
-
return new Uint8Array(
|
|
14
|
+
export function buildInvalidate(byteCount = 200): Uint8Array {
|
|
15
|
+
return new Uint8Array(byteCount);
|
|
7
16
|
}
|
|
8
17
|
|
|
9
18
|
export function buildStatusRequest(): Uint8Array {
|
|
@@ -58,11 +67,12 @@ export function buildExpandedMode(
|
|
|
58
67
|
cutAtEnd: boolean,
|
|
59
68
|
highRes: boolean,
|
|
60
69
|
twoColor = false,
|
|
70
|
+
highResFlagBit = 0x10,
|
|
61
71
|
): Uint8Array {
|
|
62
72
|
let flags = 0x00;
|
|
63
73
|
if (twoColor) flags |= 0x01;
|
|
64
74
|
if (cutAtEnd) flags |= 0x08;
|
|
65
|
-
if (highRes) flags |=
|
|
75
|
+
if (highRes) flags |= highResFlagBit;
|
|
66
76
|
return new Uint8Array([0x1b, 0x69, 0x4b, flags]);
|
|
67
77
|
}
|
|
68
78
|
|
|
@@ -135,14 +145,133 @@ function concat(...arrays: Uint8Array[]): Uint8Array {
|
|
|
135
145
|
return out;
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Per-protocol wire-format constants.
|
|
150
|
+
*
|
|
151
|
+
* QL and PT raster differ in three numeric constants and one rule —
|
|
152
|
+
* everything else (status request, raster opcode, PackBits, two-colour
|
|
153
|
+
* plane encoding) is shared and lives in `encodeRasterJob`. Per the
|
|
154
|
+
* plan §4.2 / §7, these are protocol-internal and do not leak onto
|
|
155
|
+
* the device registry.
|
|
156
|
+
*
|
|
157
|
+
* - `feedMarginDots` — leading/trailing blank tape (`ESC i d`). QL = 35,
|
|
158
|
+
* PT = 14. Per `brother_label/devices.py` and Brother's PT raster
|
|
159
|
+
* manual; verify against print output during phase 4.
|
|
160
|
+
* - `invalidateBytes` — leading invalidate sequence. QL is 200 by
|
|
161
|
+
* default but the encoder bumps it to 400 when the engine carries
|
|
162
|
+
* `capabilities.twoColor`. PT is always 200 (no two-colour PT model
|
|
163
|
+
* exists today).
|
|
164
|
+
* - `highResFlagBit` — bit set in `ESC i K` flags when `highRes`
|
|
165
|
+
* is requested. QL uses bit 4 (0x10) for 300x600; PT uses bit 6
|
|
166
|
+
* (0x40) for 180x360 / 360x720 (per nbuchwitz/ptouch).
|
|
167
|
+
* - `duplicateRasterLines` — when `highRes` is on, PT requires each
|
|
168
|
+
* raster line to be sent twice. QL's high-res mode does not.
|
|
169
|
+
*/
|
|
170
|
+
export interface RasterProtocolConfig {
|
|
171
|
+
feedMarginDots: number;
|
|
172
|
+
invalidateBytes: number;
|
|
173
|
+
highResFlagBit: number;
|
|
174
|
+
duplicateRasterLines: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const QL_PROTOCOL_CONFIG: RasterProtocolConfig = {
|
|
178
|
+
feedMarginDots: 35,
|
|
179
|
+
invalidateBytes: 200,
|
|
180
|
+
highResFlagBit: 0x10,
|
|
181
|
+
duplicateRasterLines: false,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const PT_PROTOCOL_CONFIG: RasterProtocolConfig = {
|
|
185
|
+
feedMarginDots: 14,
|
|
186
|
+
invalidateBytes: 200,
|
|
187
|
+
highResFlagBit: 0x40,
|
|
188
|
+
duplicateRasterLines: true,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** Engine shape consumed by the encoder — narrow `Pick` so unit tests can synthesise minimal stubs. */
|
|
192
|
+
export type EncoderEngine = Pick<PrintEngine, 'protocol' | 'headDots'> & {
|
|
193
|
+
capabilities?: BrotherEngineCapabilities;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Cutter compression-required quirk.
|
|
198
|
+
*
|
|
199
|
+
* PT-E550W silently fails to cut when compression is disabled —
|
|
200
|
+
* documented in nbuchwitz/ptouch:PTE550W ("E550W requires compression
|
|
201
|
+
* ON for cutting to work"). Encoded as a per-name guard rather than a
|
|
202
|
+
* registry capability so we don't promote a one-model bug into the
|
|
203
|
+
* data shape.
|
|
204
|
+
*/
|
|
205
|
+
const COMPRESSION_REQUIRED_FOR_CUTTER = new Set(['PT-E550W']);
|
|
206
|
+
|
|
207
|
+
function maybeCheckCutterCompressionQuirk(
|
|
208
|
+
deviceName: string | undefined,
|
|
209
|
+
autoCut: boolean,
|
|
210
|
+
compress: boolean,
|
|
211
|
+
): void {
|
|
212
|
+
if (autoCut && !compress && deviceName && COMPRESSION_REQUIRED_FOR_CUTTER.has(deviceName)) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`${deviceName} requires compression to be enabled when autocut is on (per nbuchwitz/ptouch)`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resolve per-page geometry for the encoder.
|
|
221
|
+
*
|
|
222
|
+
* QL paths take the flat fields off the media row directly; PT paths
|
|
223
|
+
* delegate to `resolveTapeGeometry` so the head-family dispatch lives
|
|
224
|
+
* in one place. `engine` is required for PT and ignored for QL.
|
|
225
|
+
*/
|
|
226
|
+
function resolveEncoderGeometry(
|
|
227
|
+
media: BrotherQLMedia,
|
|
228
|
+
engine: EncoderEngine | undefined,
|
|
229
|
+
): TapeGeometry {
|
|
230
|
+
if (media.tapeSystem === 'dk') {
|
|
231
|
+
if (
|
|
232
|
+
typeof media.printAreaDots !== 'number' ||
|
|
233
|
+
typeof media.leftMarginPins !== 'number' ||
|
|
234
|
+
typeof media.rightMarginPins !== 'number'
|
|
235
|
+
) {
|
|
236
|
+
throw new Error(`DK media ${media.id.toString()} missing flat geometry fields`);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
printAreaDots: media.printAreaDots,
|
|
240
|
+
leftMarginPins: media.leftMarginPins,
|
|
241
|
+
rightMarginPins: media.rightMarginPins,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (!engine) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`tape system "${media.tapeSystem}" requires an engine to resolve head-family geometry`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return resolveTapeGeometry(media, engine);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface EncodeContext {
|
|
253
|
+
config: RasterProtocolConfig;
|
|
254
|
+
engine?: EncoderEngine | undefined;
|
|
255
|
+
deviceName?: string | undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function encodeRasterJob(pages: PageData[], options: JobOptions, ctx: EncodeContext): Uint8Array {
|
|
259
|
+
const { config, engine, deviceName } = ctx;
|
|
139
260
|
const copies = options.copies ?? 1;
|
|
140
261
|
const chunks: Uint8Array[] = [];
|
|
141
262
|
|
|
142
|
-
//
|
|
263
|
+
// Two-colour invalidate-byte derivation (§7.1): QL bumps to 400 when
|
|
264
|
+
// the engine carries `twoColor`. PT has no two-colour models today;
|
|
265
|
+
// its config keeps invalidateBytes at 200 regardless.
|
|
266
|
+
const baseInvalidate = config.invalidateBytes;
|
|
267
|
+
const twoColorInvalidateBoost =
|
|
268
|
+
engine?.protocol === 'ql-raster' && engine.capabilities?.twoColor === true;
|
|
269
|
+
const invalidateBytes = twoColorInvalidateBoost ? baseInvalidate * 2 : baseInvalidate;
|
|
270
|
+
|
|
271
|
+
// Python brother_ql sequence: raster-mode first, then invalidate, then init, then
|
|
143
272
|
// raster-mode again (matches observed working sequence for QL-820NWB).
|
|
144
273
|
chunks.push(buildRasterMode());
|
|
145
|
-
chunks.push(buildInvalidate());
|
|
274
|
+
chunks.push(buildInvalidate(invalidateBytes));
|
|
146
275
|
chunks.push(buildInitialize());
|
|
147
276
|
|
|
148
277
|
const allPageInstances: PageData[] = [];
|
|
@@ -158,10 +287,23 @@ export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Arr
|
|
|
158
287
|
const autoCut = opts.autoCut ?? true;
|
|
159
288
|
const cutAtEnd = opts.cutAtEnd ?? true;
|
|
160
289
|
const highRes = opts.highResolution ?? false;
|
|
161
|
-
const marginDots = opts.marginDots ?? 35;
|
|
162
290
|
const compress = opts.compress ?? false;
|
|
163
291
|
const { bitmap, media } = page;
|
|
164
292
|
|
|
293
|
+
// High-res mode requires the engine to declare `capabilities.highResDpi`.
|
|
294
|
+
if (highRes && engine && engine.capabilities?.highResDpi === undefined) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`${deviceName ?? 'device'} does not support high-res mode (engine.capabilities.highResDpi is not set)`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
maybeCheckCutterCompressionQuirk(deviceName, autoCut, compress);
|
|
301
|
+
|
|
302
|
+
// Per §7.3: PT high-res doubles the feed margin and duplicates
|
|
303
|
+
// each raster line. QL high-res leaves both untouched.
|
|
304
|
+
const baseMargin = opts.marginDots ?? config.feedMarginDots;
|
|
305
|
+
const marginDots = config.duplicateRasterLines && highRes ? baseMargin * 2 : baseMargin;
|
|
306
|
+
|
|
165
307
|
// Multi-ink media (e.g. DK-22251) requires two-color mode even for black-only jobs.
|
|
166
308
|
// Auto-create an empty red plane when the tape demands it but caller didn't supply one.
|
|
167
309
|
const multiInk = media.palette !== undefined;
|
|
@@ -182,34 +324,41 @@ export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Arr
|
|
|
182
324
|
chunks.push(buildPrintInfo(media, rowCount, i));
|
|
183
325
|
chunks.push(buildVariousMode(autoCut));
|
|
184
326
|
chunks.push(buildCutEach(1));
|
|
185
|
-
chunks.push(buildExpandedMode(cutAtEnd, highRes, twoColor));
|
|
327
|
+
chunks.push(buildExpandedMode(cutAtEnd, highRes, twoColor, config.highResFlagBit));
|
|
186
328
|
chunks.push(buildMargin(marginDots));
|
|
187
329
|
if (compress) chunks.push(buildCompression(true));
|
|
188
330
|
|
|
189
331
|
// Each raster row must cover the full print head width (derived from media geometry).
|
|
190
|
-
// leftMarginPins + printAreaDots + rightMarginPins = head pin count (720
|
|
191
|
-
const
|
|
332
|
+
// leftMarginPins + printAreaDots + rightMarginPins = head pin count (720 / 1296 / 128 / 560).
|
|
333
|
+
const geometry = resolveEncoderGeometry(media, engine);
|
|
334
|
+
const leftMarginPins = geometry.leftMarginPins;
|
|
335
|
+
const totalPins = leftMarginPins + geometry.printAreaDots + geometry.rightMarginPins;
|
|
192
336
|
const rowByteLen = Math.ceil(totalPins / 8);
|
|
193
337
|
|
|
194
338
|
// Rows interleaved per raster line (matches Python brother_ql behaviour).
|
|
195
339
|
// Two-color: black row then red row for each line. Single-color: black only.
|
|
196
340
|
// When `compress` is on, each row's bytes are PackBits-encoded and the
|
|
197
|
-
// raster-row LEN byte carries the compressed length.
|
|
198
|
-
//
|
|
199
|
-
//
|
|
341
|
+
// raster-row LEN byte carries the compressed length.
|
|
342
|
+
//
|
|
343
|
+
// Per §7.3: PT high-res duplicates each raster line. QL doesn't.
|
|
344
|
+
const duplicate = config.duplicateRasterLines && highRes;
|
|
200
345
|
for (let r = 0; r < rowCount; r++) {
|
|
201
346
|
const blackSrc = getRow(bitmap, r);
|
|
202
347
|
const blackBytes = new Uint8Array(rowByteLen);
|
|
203
|
-
placeBits(blackSrc, bitmap.widthPx, blackBytes,
|
|
348
|
+
placeBits(blackSrc, bitmap.widthPx, blackBytes, leftMarginPins);
|
|
204
349
|
const blackPayload = compress ? packBits(blackBytes) : blackBytes;
|
|
205
|
-
|
|
350
|
+
const blackChunk = buildRasterRow(blackPayload, 'black', twoColor);
|
|
351
|
+
chunks.push(blackChunk);
|
|
352
|
+
if (duplicate) chunks.push(blackChunk);
|
|
206
353
|
|
|
207
354
|
if (twoColor && redBitmap !== undefined) {
|
|
208
355
|
const redSrc = getRow(redBitmap, r);
|
|
209
356
|
const redBytes = new Uint8Array(rowByteLen);
|
|
210
|
-
placeBits(redSrc, redBitmap.widthPx, redBytes,
|
|
357
|
+
placeBits(redSrc, redBitmap.widthPx, redBytes, leftMarginPins);
|
|
211
358
|
const redPayload = compress ? packBits(redBytes) : redBytes;
|
|
212
|
-
|
|
359
|
+
const redChunk = buildRasterRow(redPayload, 'red', twoColor);
|
|
360
|
+
chunks.push(redChunk);
|
|
361
|
+
if (duplicate) chunks.push(redChunk);
|
|
213
362
|
}
|
|
214
363
|
}
|
|
215
364
|
|
|
@@ -218,3 +367,32 @@ export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Arr
|
|
|
218
367
|
|
|
219
368
|
return concat(...chunks);
|
|
220
369
|
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Encode a QL job. Public legacy entry point — DK media only, no
|
|
373
|
+
* engine awareness, two-colour invalidate-byte boost not applied.
|
|
374
|
+
* Use `encodeJobForEngine` for PT or for QL with two-colour invalidate
|
|
375
|
+
* derivation from `engine.capabilities.twoColor`.
|
|
376
|
+
*/
|
|
377
|
+
export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Array {
|
|
378
|
+
return encodeRasterJob(pages, options, { config: QL_PROTOCOL_CONFIG });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Encode a job for a specific engine. Dispatches on `engine.protocol`:
|
|
383
|
+
* `'ql-raster'` picks the QL config, `'pt-raster'` picks the PT config
|
|
384
|
+
* and threads `engine.headDots` through to head-family geometry
|
|
385
|
+
* resolution for TZe / HSe media.
|
|
386
|
+
*
|
|
387
|
+
* `deviceName` is optional; when supplied, it enables the per-name
|
|
388
|
+
* cutter-compression guard for PT-E550W (§7.2 / §12.12).
|
|
389
|
+
*/
|
|
390
|
+
export function encodeJobForEngine(
|
|
391
|
+
pages: PageData[],
|
|
392
|
+
options: JobOptions,
|
|
393
|
+
engine: EncoderEngine,
|
|
394
|
+
deviceName?: string,
|
|
395
|
+
): Uint8Array {
|
|
396
|
+
const config = engine.protocol === 'pt-raster' ? PT_PROTOCOL_CONFIG : QL_PROTOCOL_CONFIG;
|
|
397
|
+
return encodeRasterJob(pages, options, { config, engine, deviceName });
|
|
398
|
+
}
|