@thermal-label/brother-ql-core 0.0.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__/devices.test.js +16 -2
- package/dist/__tests__/devices.test.js.map +1 -1
- package/dist/__tests__/media.test.js +37 -6
- 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.d.ts +2 -0
- package/dist/__tests__/preview.test.d.ts.map +1 -0
- package/dist/__tests__/preview.test.js +41 -0
- package/dist/__tests__/preview.test.js.map +1 -0
- package/dist/__tests__/protocol.test.js +46 -1
- package/dist/__tests__/protocol.test.js.map +1 -1
- package/dist/__tests__/status.test.js +28 -22
- package/dist/__tests__/status.test.js.map +1 -1
- package/dist/devices.d.ts +40 -21
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.js +38 -20
- package/dist/devices.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +43 -4
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +92 -24
- 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 +13 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +31 -0
- package/dist/preview.js.map +1 -0
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +14 -8
- package/dist/protocol.js.map +1 -1
- package/dist/status.d.ts +20 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +59 -45
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +46 -30
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/devices.test.ts +18 -2
- package/src/__tests__/media.test.ts +39 -6
- package/src/__tests__/pack-bits.test.ts +92 -0
- package/src/__tests__/preview.test.ts +52 -0
- package/src/__tests__/protocol.test.ts +47 -1
- package/src/__tests__/status.test.ts +31 -22
- package/src/devices.ts +41 -22
- package/src/index.ts +45 -11
- package/src/media.ts +105 -27
- package/src/orientation.ts +11 -0
- package/src/pack-bits.ts +64 -0
- package/src/preview.ts +35 -0
- package/src/protocol.ts +16 -9
- package/src/status.ts +62 -47
- package/src/types.ts +52 -32
package/src/media.ts
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BrotherQLMedia, MediaType } from './types.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Registry of supported Brother QL consumables.
|
|
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).
|
|
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.
|
|
24
|
+
*/
|
|
25
|
+
export const MEDIA: Record<number, BrotherQLMedia> = {
|
|
4
26
|
// Continuous length tape
|
|
5
27
|
257: {
|
|
6
28
|
id: 257,
|
|
7
29
|
name: '12mm continuous',
|
|
8
30
|
type: 'continuous',
|
|
9
31
|
widthMm: 12,
|
|
10
|
-
lengthMm: 0,
|
|
11
32
|
printAreaDots: 106,
|
|
12
33
|
leftMarginPins: 585,
|
|
13
34
|
rightMarginPins: 29,
|
|
@@ -17,7 +38,6 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
17
38
|
name: '29mm continuous (DK-22210)',
|
|
18
39
|
type: 'continuous',
|
|
19
40
|
widthMm: 29,
|
|
20
|
-
lengthMm: 0,
|
|
21
41
|
printAreaDots: 306,
|
|
22
42
|
leftMarginPins: 408,
|
|
23
43
|
rightMarginPins: 6,
|
|
@@ -27,7 +47,6 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
27
47
|
name: '38mm continuous (DK-22225)',
|
|
28
48
|
type: 'continuous',
|
|
29
49
|
widthMm: 38,
|
|
30
|
-
lengthMm: 0,
|
|
31
50
|
printAreaDots: 413,
|
|
32
51
|
leftMarginPins: 295,
|
|
33
52
|
rightMarginPins: 12,
|
|
@@ -37,7 +56,6 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
37
56
|
name: '50mm continuous (DK-22246)',
|
|
38
57
|
type: 'continuous',
|
|
39
58
|
widthMm: 50,
|
|
40
|
-
lengthMm: 0,
|
|
41
59
|
printAreaDots: 554,
|
|
42
60
|
leftMarginPins: 154,
|
|
43
61
|
rightMarginPins: 12,
|
|
@@ -47,7 +65,6 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
47
65
|
name: '54mm continuous (DK-22214)',
|
|
48
66
|
type: 'continuous',
|
|
49
67
|
widthMm: 54,
|
|
50
|
-
lengthMm: 0,
|
|
51
68
|
printAreaDots: 590,
|
|
52
69
|
leftMarginPins: 130,
|
|
53
70
|
rightMarginPins: 0,
|
|
@@ -57,7 +74,6 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
57
74
|
name: '62mm continuous (DK-22205)',
|
|
58
75
|
type: 'continuous',
|
|
59
76
|
widthMm: 62,
|
|
60
|
-
lengthMm: 0,
|
|
61
77
|
printAreaDots: 696,
|
|
62
78
|
leftMarginPins: 12,
|
|
63
79
|
rightMarginPins: 12,
|
|
@@ -67,18 +83,19 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
67
83
|
name: '62mm continuous two-color (DK-22251)',
|
|
68
84
|
type: 'continuous',
|
|
69
85
|
widthMm: 62,
|
|
70
|
-
|
|
86
|
+
palette: [
|
|
87
|
+
{ name: 'black', rgb: [0, 0, 0] },
|
|
88
|
+
{ name: 'red', rgb: [255, 0, 0] },
|
|
89
|
+
],
|
|
71
90
|
printAreaDots: 696,
|
|
72
91
|
leftMarginPins: 12,
|
|
73
92
|
rightMarginPins: 12,
|
|
74
|
-
twoColorTape: true,
|
|
75
93
|
},
|
|
76
94
|
260: {
|
|
77
95
|
id: 260,
|
|
78
96
|
name: '102mm continuous (DK-22243)',
|
|
79
97
|
type: 'continuous',
|
|
80
98
|
widthMm: 102,
|
|
81
|
-
lengthMm: 0,
|
|
82
99
|
printAreaDots: 1164,
|
|
83
100
|
leftMarginPins: 76,
|
|
84
101
|
rightMarginPins: 56,
|
|
@@ -90,7 +107,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
90
107
|
name: '17×54mm die-cut (DK-11204)',
|
|
91
108
|
type: 'die-cut',
|
|
92
109
|
widthMm: 17,
|
|
93
|
-
|
|
110
|
+
heightMm: 54,
|
|
111
|
+
defaultOrientation: 'horizontal',
|
|
112
|
+
cornerRadiusMm: 3,
|
|
94
113
|
printAreaDots: 165,
|
|
95
114
|
leftMarginPins: 0,
|
|
96
115
|
rightMarginPins: 0,
|
|
@@ -101,7 +120,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
101
120
|
name: '17×87mm die-cut (DK-11203)',
|
|
102
121
|
type: 'die-cut',
|
|
103
122
|
widthMm: 17,
|
|
104
|
-
|
|
123
|
+
heightMm: 87,
|
|
124
|
+
defaultOrientation: 'horizontal',
|
|
125
|
+
cornerRadiusMm: 3,
|
|
105
126
|
printAreaDots: 165,
|
|
106
127
|
leftMarginPins: 0,
|
|
107
128
|
rightMarginPins: 0,
|
|
@@ -112,7 +133,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
112
133
|
name: '23×23mm die-cut',
|
|
113
134
|
type: 'die-cut',
|
|
114
135
|
widthMm: 23,
|
|
115
|
-
|
|
136
|
+
heightMm: 23,
|
|
137
|
+
defaultOrientation: 'horizontal',
|
|
138
|
+
cornerRadiusMm: 3,
|
|
116
139
|
printAreaDots: 236,
|
|
117
140
|
leftMarginPins: 0,
|
|
118
141
|
rightMarginPins: 0,
|
|
@@ -123,7 +146,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
123
146
|
name: '29×90mm die-cut (DK-11201)',
|
|
124
147
|
type: 'die-cut',
|
|
125
148
|
widthMm: 29,
|
|
126
|
-
|
|
149
|
+
heightMm: 90,
|
|
150
|
+
defaultOrientation: 'horizontal',
|
|
151
|
+
cornerRadiusMm: 3,
|
|
127
152
|
printAreaDots: 306,
|
|
128
153
|
leftMarginPins: 0,
|
|
129
154
|
rightMarginPins: 0,
|
|
@@ -134,7 +159,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
134
159
|
name: '38×90mm die-cut (DK-11218)',
|
|
135
160
|
type: 'die-cut',
|
|
136
161
|
widthMm: 38,
|
|
137
|
-
|
|
162
|
+
heightMm: 90,
|
|
163
|
+
defaultOrientation: 'horizontal',
|
|
164
|
+
cornerRadiusMm: 3,
|
|
138
165
|
printAreaDots: 413,
|
|
139
166
|
leftMarginPins: 0,
|
|
140
167
|
rightMarginPins: 0,
|
|
@@ -145,7 +172,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
145
172
|
name: '39×48mm die-cut (DK-11219)',
|
|
146
173
|
type: 'die-cut',
|
|
147
174
|
widthMm: 39,
|
|
148
|
-
|
|
175
|
+
heightMm: 48,
|
|
176
|
+
defaultOrientation: 'horizontal',
|
|
177
|
+
cornerRadiusMm: 3,
|
|
149
178
|
printAreaDots: 425,
|
|
150
179
|
leftMarginPins: 0,
|
|
151
180
|
rightMarginPins: 0,
|
|
@@ -156,7 +185,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
156
185
|
name: '52×29mm die-cut',
|
|
157
186
|
type: 'die-cut',
|
|
158
187
|
widthMm: 52,
|
|
159
|
-
|
|
188
|
+
heightMm: 29,
|
|
189
|
+
defaultOrientation: 'horizontal',
|
|
190
|
+
cornerRadiusMm: 3,
|
|
160
191
|
printAreaDots: 578,
|
|
161
192
|
leftMarginPins: 0,
|
|
162
193
|
rightMarginPins: 0,
|
|
@@ -167,7 +198,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
167
198
|
name: '62×29mm die-cut (DK-11209)',
|
|
168
199
|
type: 'die-cut',
|
|
169
200
|
widthMm: 62,
|
|
170
|
-
|
|
201
|
+
heightMm: 29,
|
|
202
|
+
defaultOrientation: 'horizontal',
|
|
203
|
+
cornerRadiusMm: 3,
|
|
171
204
|
printAreaDots: 696,
|
|
172
205
|
leftMarginPins: 0,
|
|
173
206
|
rightMarginPins: 0,
|
|
@@ -178,7 +211,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
178
211
|
name: '62×100mm die-cut (DK-11202)',
|
|
179
212
|
type: 'die-cut',
|
|
180
213
|
widthMm: 62,
|
|
181
|
-
|
|
214
|
+
heightMm: 100,
|
|
215
|
+
defaultOrientation: 'horizontal',
|
|
216
|
+
cornerRadiusMm: 3,
|
|
182
217
|
printAreaDots: 696,
|
|
183
218
|
leftMarginPins: 0,
|
|
184
219
|
rightMarginPins: 0,
|
|
@@ -189,7 +224,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
189
224
|
name: '102×51mm die-cut (DK-11240)',
|
|
190
225
|
type: 'die-cut',
|
|
191
226
|
widthMm: 102,
|
|
192
|
-
|
|
227
|
+
heightMm: 51,
|
|
228
|
+
defaultOrientation: 'horizontal',
|
|
229
|
+
cornerRadiusMm: 3,
|
|
193
230
|
printAreaDots: 1164,
|
|
194
231
|
leftMarginPins: 0,
|
|
195
232
|
rightMarginPins: 0,
|
|
@@ -200,7 +237,9 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
200
237
|
name: '102×152mm die-cut (DK-11241)',
|
|
201
238
|
type: 'die-cut',
|
|
202
239
|
widthMm: 102,
|
|
203
|
-
|
|
240
|
+
heightMm: 152,
|
|
241
|
+
defaultOrientation: 'horizontal',
|
|
242
|
+
cornerRadiusMm: 3,
|
|
204
243
|
printAreaDots: 1164,
|
|
205
244
|
leftMarginPins: 0,
|
|
206
245
|
rightMarginPins: 0,
|
|
@@ -211,7 +250,8 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
211
250
|
name: '12mm Ø die-cut',
|
|
212
251
|
type: 'die-cut',
|
|
213
252
|
widthMm: 12,
|
|
214
|
-
|
|
253
|
+
heightMm: 12,
|
|
254
|
+
cornerRadiusMm: 6,
|
|
215
255
|
printAreaDots: 94,
|
|
216
256
|
leftMarginPins: 0,
|
|
217
257
|
rightMarginPins: 0,
|
|
@@ -222,7 +262,8 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
222
262
|
name: '24mm Ø die-cut (DK-11221)',
|
|
223
263
|
type: 'die-cut',
|
|
224
264
|
widthMm: 24,
|
|
225
|
-
|
|
265
|
+
heightMm: 24,
|
|
266
|
+
cornerRadiusMm: 12,
|
|
226
267
|
printAreaDots: 236,
|
|
227
268
|
leftMarginPins: 0,
|
|
228
269
|
rightMarginPins: 0,
|
|
@@ -233,7 +274,8 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
233
274
|
name: '58mm Ø die-cut (DK-11207)',
|
|
234
275
|
type: 'die-cut',
|
|
235
276
|
widthMm: 58,
|
|
236
|
-
|
|
277
|
+
heightMm: 58,
|
|
278
|
+
cornerRadiusMm: 29,
|
|
237
279
|
printAreaDots: 618,
|
|
238
280
|
leftMarginPins: 0,
|
|
239
281
|
rightMarginPins: 0,
|
|
@@ -241,10 +283,46 @@ export const MEDIA: Record<number, MediaDescriptor> = {
|
|
|
241
283
|
},
|
|
242
284
|
};
|
|
243
285
|
|
|
244
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Default media when `createPreview()` is called without media and
|
|
288
|
+
* without a detected roll. 62mm continuous (DK-22205) is the common
|
|
289
|
+
* single-colour shipping roll.
|
|
290
|
+
*/
|
|
291
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DK-22205 is a hard-coded registry key
|
|
292
|
+
export const DEFAULT_MEDIA: BrotherQLMedia = MEDIA[259]!;
|
|
293
|
+
|
|
294
|
+
export function findMedia(id: number): BrotherQLMedia | undefined {
|
|
245
295
|
return MEDIA[id];
|
|
246
296
|
}
|
|
247
297
|
|
|
248
|
-
export function findMediaByWidth(widthMm: number, type: MediaType):
|
|
298
|
+
export function findMediaByWidth(widthMm: number, type: MediaType): BrotherQLMedia[] {
|
|
249
299
|
return Object.values(MEDIA).filter(m => m.widthMm === widthMm && m.type === type);
|
|
250
300
|
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Match status-response dimensions to a media registry entry.
|
|
304
|
+
*
|
|
305
|
+
* @param widthMm media width in mm (status byte 10)
|
|
306
|
+
* @param heightMm media length in mm (status byte 17) — 0 = continuous
|
|
307
|
+
* @param twoColorMode true when the status response indicates the
|
|
308
|
+
* printer is configured for two-colour media. When
|
|
309
|
+
* both DK-22205 (259) and DK-22251 (251) match the
|
|
310
|
+
* dimensions, the flag picks the right one.
|
|
311
|
+
*/
|
|
312
|
+
export function findMediaByDimensions(
|
|
313
|
+
widthMm: number,
|
|
314
|
+
heightMm: number,
|
|
315
|
+
twoColorMode = false,
|
|
316
|
+
): BrotherQLMedia | undefined {
|
|
317
|
+
if (heightMm === 0) {
|
|
318
|
+
const continuousMatches = Object.values(MEDIA).filter(
|
|
319
|
+
m => m.type === 'continuous' && m.widthMm === widthMm,
|
|
320
|
+
);
|
|
321
|
+
if (continuousMatches.length === 0) return undefined;
|
|
322
|
+
const preferred = continuousMatches.find(m => (m.palette !== undefined) === twoColorMode);
|
|
323
|
+
return preferred ?? continuousMatches[0];
|
|
324
|
+
}
|
|
325
|
+
return Object.values(MEDIA).find(
|
|
326
|
+
m => m.type === 'die-cut' && m.widthMm === widthMm && m.heightMm === heightMm,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
@@ -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
|
+
}
|
package/src/preview.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { renderImage, renderMultiPlaneImage } from '@mbtech-nl/bitmap';
|
|
2
|
+
import type { LabelBitmap, RawImageData } from '@mbtech-nl/bitmap';
|
|
3
|
+
import type { PreviewResult } from '@thermal-label/contracts';
|
|
4
|
+
import type { BrotherQLMedia } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Offline preview without a live printer connection.
|
|
8
|
+
*
|
|
9
|
+
* Multi-ink aware: when `media.palette` is defined (DK-22251 today),
|
|
10
|
+
* the image is split per-plane via `renderMultiPlaneImage()` — the
|
|
11
|
+
* same code path `print()` takes for that media. Single-ink media
|
|
12
|
+
* returns one black plane via dithered `renderImage`.
|
|
13
|
+
*/
|
|
14
|
+
export function createPreviewOffline(image: RawImageData, media: BrotherQLMedia): PreviewResult {
|
|
15
|
+
if (media.palette) {
|
|
16
|
+
const planes = renderMultiPlaneImage(image, {
|
|
17
|
+
palette: media.palette,
|
|
18
|
+
}) as Record<'black' | 'red', LabelBitmap>;
|
|
19
|
+
return {
|
|
20
|
+
planes: [
|
|
21
|
+
{ name: 'black', bitmap: planes.black, displayColor: '#000000' },
|
|
22
|
+
{ name: 'red', bitmap: planes.red, displayColor: '#ff0000' },
|
|
23
|
+
],
|
|
24
|
+
media,
|
|
25
|
+
assumed: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const bitmap = renderImage(image, { dither: true });
|
|
30
|
+
return {
|
|
31
|
+
planes: [{ name: 'black', bitmap, displayColor: '#000000' }],
|
|
32
|
+
media,
|
|
33
|
+
assumed: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getRow, createBitmap } from '@mbtech-nl/bitmap';
|
|
2
|
-
import {
|
|
2
|
+
import { packBits } from './pack-bits.js';
|
|
3
|
+
import type { BrotherQLMedia, PageData, JobOptions, PageOptions } from './types.js';
|
|
3
4
|
|
|
4
5
|
export function buildInvalidate(): Uint8Array {
|
|
5
6
|
return new Uint8Array(200);
|
|
@@ -22,7 +23,7 @@ export function buildStatusNotification(enabled: boolean): Uint8Array {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function buildPrintInfo(
|
|
25
|
-
media:
|
|
26
|
+
media: BrotherQLMedia,
|
|
26
27
|
rowCount: number,
|
|
27
28
|
pageIndex: number,
|
|
28
29
|
): Uint8Array {
|
|
@@ -38,7 +39,7 @@ export function buildPrintInfo(
|
|
|
38
39
|
buf[3] = validFlags;
|
|
39
40
|
buf[4] = mediaType;
|
|
40
41
|
buf[5] = media.widthMm;
|
|
41
|
-
buf[6] = media.
|
|
42
|
+
buf[6] = media.heightMm ?? 0;
|
|
42
43
|
// rowCount little-endian at bytes 7-8 (offsets 4-5 in param block)
|
|
43
44
|
buf[7] = rowCount & 0xff;
|
|
44
45
|
buf[8] = (rowCount >> 8) & 0xff;
|
|
@@ -161,12 +162,12 @@ export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Arr
|
|
|
161
162
|
const compress = opts.compress ?? false;
|
|
162
163
|
const { bitmap, media } = page;
|
|
163
164
|
|
|
164
|
-
//
|
|
165
|
+
// Multi-ink media (e.g. DK-22251) requires two-color mode even for black-only jobs.
|
|
165
166
|
// Auto-create an empty red plane when the tape demands it but caller didn't supply one.
|
|
166
|
-
const
|
|
167
|
+
const multiInk = media.palette !== undefined;
|
|
168
|
+
const twoColor = page.redBitmap !== undefined || multiInk;
|
|
167
169
|
const redBitmap =
|
|
168
|
-
page.redBitmap ??
|
|
169
|
-
(media.twoColorTape ? createBitmap(bitmap.widthPx, bitmap.heightPx) : undefined);
|
|
170
|
+
page.redBitmap ?? (multiInk ? createBitmap(bitmap.widthPx, bitmap.heightPx) : undefined);
|
|
170
171
|
|
|
171
172
|
if (twoColor && redBitmap !== undefined) {
|
|
172
173
|
if (bitmap.widthPx !== redBitmap.widthPx || bitmap.heightPx !== redBitmap.heightPx) {
|
|
@@ -192,17 +193,23 @@ export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Arr
|
|
|
192
193
|
|
|
193
194
|
// Rows interleaved per raster line (matches Python brother_ql behaviour).
|
|
194
195
|
// Two-color: black row then red row for each line. Single-color: black only.
|
|
196
|
+
// When `compress` is on, each row's bytes are PackBits-encoded and the
|
|
197
|
+
// raster-row LEN byte carries the compressed length. The printer was
|
|
198
|
+
// already switched into compression mode by `buildCompression(true)`
|
|
199
|
+
// above, so it expects every subsequent row to be PackBits.
|
|
195
200
|
for (let r = 0; r < rowCount; r++) {
|
|
196
201
|
const blackSrc = getRow(bitmap, r);
|
|
197
202
|
const blackBytes = new Uint8Array(rowByteLen);
|
|
198
203
|
placeBits(blackSrc, bitmap.widthPx, blackBytes, media.leftMarginPins);
|
|
199
|
-
|
|
204
|
+
const blackPayload = compress ? packBits(blackBytes) : blackBytes;
|
|
205
|
+
chunks.push(buildRasterRow(blackPayload, 'black', twoColor));
|
|
200
206
|
|
|
201
207
|
if (twoColor && redBitmap !== undefined) {
|
|
202
208
|
const redSrc = getRow(redBitmap, r);
|
|
203
209
|
const redBytes = new Uint8Array(rowByteLen);
|
|
204
210
|
placeBits(redSrc, redBitmap.widthPx, redBytes, media.leftMarginPins);
|
|
205
|
-
|
|
211
|
+
const redPayload = compress ? packBits(redBytes) : redBytes;
|
|
212
|
+
chunks.push(buildRasterRow(redPayload, 'red', twoColor));
|
|
206
213
|
}
|
|
207
214
|
}
|
|
208
215
|
|
package/src/status.ts
CHANGED
|
@@ -1,67 +1,82 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { PrinterError } from '@thermal-label/contracts';
|
|
2
|
+
import type { BrotherQLStatus } from './types.js';
|
|
3
|
+
import { findMediaByDimensions } from './media.js';
|
|
2
4
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
5
|
+
export const STATUS_REQUEST = new Uint8Array([0x1b, 0x69, 0x53]);
|
|
6
|
+
|
|
7
|
+
/** Error-info 1 bit → structured PrinterError code + human message. */
|
|
8
|
+
const ERROR_INFO_1: { bit: number; code: string; message: string }[] = [
|
|
9
|
+
{ bit: 0, code: 'no_media', message: 'No media' },
|
|
10
|
+
{ bit: 1, code: 'media_end', message: 'End of media' },
|
|
11
|
+
{ bit: 2, code: 'cutter_jam', message: 'Cutter jam' },
|
|
12
|
+
{ bit: 3, code: 'system_error', message: 'Weak battery' },
|
|
13
|
+
{ bit: 4, code: 'not_ready', message: 'Printer in use' },
|
|
14
|
+
{ bit: 6, code: 'system_error', message: 'High voltage adapter' },
|
|
15
|
+
{ bit: 7, code: 'system_error', message: 'Fan motor error' },
|
|
16
|
+
];
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
18
|
+
/** Error-info 2 bit → structured PrinterError code + human message. */
|
|
19
|
+
const ERROR_INFO_2: { bit: number; code: string; message: string }[] = [
|
|
20
|
+
{ bit: 0, code: 'wrong_media', message: 'Replace media' },
|
|
21
|
+
{ bit: 1, code: 'system_error', message: 'Expansion buffer full' },
|
|
22
|
+
{ bit: 2, code: 'system_error', message: 'Transmission error' },
|
|
23
|
+
{ bit: 3, code: 'system_error', message: 'Communication buffer full' },
|
|
24
|
+
{ bit: 4, code: 'cover_open', message: 'Cover open' },
|
|
25
|
+
{ bit: 5, code: 'not_ready', message: 'Cancel key pressed' },
|
|
26
|
+
{ bit: 6, code: 'media_end', message: 'Media cannot be fed' },
|
|
27
|
+
{ bit: 7, code: 'system_error', message: 'System error' },
|
|
28
|
+
];
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Parse a Brother QL 32-byte status response.
|
|
32
|
+
*
|
|
33
|
+
* Fields:
|
|
34
|
+
* byte 8 — error info 1 (bit mask, see ERROR_INFO_1)
|
|
35
|
+
* byte 9 — error info 2 (bit mask, see ERROR_INFO_2)
|
|
36
|
+
* byte 10 — media width (mm)
|
|
37
|
+
* byte 11 — media type (0x0A continuous, 0x0B die-cut)
|
|
38
|
+
* byte 17 — media length (mm), 0 for continuous
|
|
39
|
+
* byte 18 — status type (0x02 = error response)
|
|
40
|
+
*
|
|
41
|
+
* `detectedMedia` is resolved against the media registry via
|
|
42
|
+
* `findMediaByDimensions`. `editorLiteMode` is a driver-specific
|
|
43
|
+
* extension on `BrotherQLStatus` — the status-type byte doesn't
|
|
44
|
+
* actually report it, but keeping the field here means callers can
|
|
45
|
+
* set it from other signals (e.g. mass-storage PID detected during
|
|
46
|
+
* discovery) without changing the return type.
|
|
47
|
+
*/
|
|
48
|
+
export function parseStatus(bytes: Uint8Array): BrotherQLStatus {
|
|
49
|
+
if (bytes.length < 32) {
|
|
26
50
|
throw new Error(`Status response too short: ${bytes.length.toString()} bytes`);
|
|
51
|
+
}
|
|
27
52
|
|
|
28
|
-
// noUncheckedIndexedAccess forces `?? 0` fallbacks that are unreachable after
|
|
29
|
-
// the length check above. DataView avoids the issue: getUint8 returns number.
|
|
30
53
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
31
|
-
|
|
32
|
-
const errors: string[] = [];
|
|
33
54
|
const errInfo1 = view.getUint8(8);
|
|
34
55
|
const errInfo2 = view.getUint8(9);
|
|
35
|
-
|
|
36
|
-
for (const [bitStr, msg] of Object.entries(ERROR_INFO_1)) {
|
|
37
|
-
if (errInfo1 & (1 << Number(bitStr))) errors.push(msg);
|
|
38
|
-
}
|
|
39
|
-
for (const [bitStr, msg] of Object.entries(ERROR_INFO_2)) {
|
|
40
|
-
if (errInfo2 & (1 << Number(bitStr))) errors.push(msg);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
56
|
const mediaWidthMm = view.getUint8(10);
|
|
44
57
|
const mediaTypeByte = view.getUint8(11);
|
|
45
58
|
const mediaLengthMm = view.getUint8(17);
|
|
59
|
+
const statusType = view.getUint8(18);
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
const errors: PrinterError[] = [];
|
|
62
|
+
for (const { bit, code, message } of ERROR_INFO_1) {
|
|
63
|
+
if (errInfo1 & (1 << bit)) errors.push({ code, message });
|
|
64
|
+
}
|
|
65
|
+
for (const { bit, code, message } of ERROR_INFO_2) {
|
|
66
|
+
if (errInfo2 & (1 << bit)) errors.push({ code, message });
|
|
67
|
+
}
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
const mediaLoaded = mediaWidthMm > 0 && mediaTypeByte !== 0;
|
|
70
|
+
const detected = mediaLoaded
|
|
71
|
+
? findMediaByDimensions(mediaWidthMm, mediaLengthMm, false)
|
|
72
|
+
: undefined;
|
|
55
73
|
|
|
56
74
|
return {
|
|
57
|
-
ready,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
mediaType,
|
|
75
|
+
ready: errors.length === 0 && statusType !== 0x02,
|
|
76
|
+
mediaLoaded,
|
|
77
|
+
...(detected === undefined ? {} : { detectedMedia: detected }),
|
|
61
78
|
errors,
|
|
62
79
|
editorLiteMode: false,
|
|
63
80
|
rawBytes: bytes,
|
|
64
81
|
};
|
|
65
82
|
}
|
|
66
|
-
|
|
67
|
-
export const STATUS_REQUEST = new Uint8Array([0x1b, 0x69, 0x53]);
|