@thermal-label/brother-ql-core 0.2.1 → 0.4.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 (89) hide show
  1. package/README.md +1 -1
  2. package/data/devices.json +823 -0
  3. package/data/media.json +823 -0
  4. package/dist/__tests__/devices.test.js +112 -31
  5. package/dist/__tests__/devices.test.js.map +1 -1
  6. package/dist/__tests__/media.test.js +274 -4
  7. package/dist/__tests__/media.test.js.map +1 -1
  8. package/dist/__tests__/pack-bits.test.d.ts +2 -0
  9. package/dist/__tests__/pack-bits.test.d.ts.map +1 -0
  10. package/dist/__tests__/pack-bits.test.js +90 -0
  11. package/dist/__tests__/pack-bits.test.js.map +1 -0
  12. package/dist/__tests__/preview.test.js +1 -1
  13. package/dist/__tests__/preview.test.js.map +1 -1
  14. package/dist/__tests__/protocol.test.js +214 -2
  15. package/dist/__tests__/protocol.test.js.map +1 -1
  16. package/dist/__tests__/status.test.js +71 -0
  17. package/dist/__tests__/status.test.js.map +1 -1
  18. package/dist/devices.d.ts +14 -271
  19. package/dist/devices.d.ts.map +1 -1
  20. package/dist/devices.generated.d.ts +696 -0
  21. package/dist/devices.generated.d.ts.map +1 -0
  22. package/dist/devices.generated.js +831 -0
  23. package/dist/devices.generated.js.map +1 -0
  24. package/dist/devices.js +28 -273
  25. package/dist/devices.js.map +1 -1
  26. package/dist/index.d.ts +10 -9
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +6 -6
  29. package/dist/index.js.map +1 -1
  30. package/dist/media.d.ts +37 -10
  31. package/dist/media.d.ts.map +1 -1
  32. package/dist/media.generated.d.ts +4 -0
  33. package/dist/media.generated.d.ts.map +1 -0
  34. package/dist/media.generated.js +1640 -0
  35. package/dist/media.generated.js.map +1 -0
  36. package/dist/media.js +75 -264
  37. package/dist/media.js.map +1 -1
  38. package/dist/orientation.d.ts +11 -0
  39. package/dist/orientation.d.ts.map +1 -0
  40. package/dist/orientation.js +10 -0
  41. package/dist/orientation.js.map +1 -0
  42. package/dist/pack-bits.d.ts +20 -0
  43. package/dist/pack-bits.d.ts.map +1 -0
  44. package/dist/pack-bits.js +61 -0
  45. package/dist/pack-bits.js.map +1 -0
  46. package/dist/preview.d.ts +6 -6
  47. package/dist/preview.d.ts.map +1 -1
  48. package/dist/preview.js +11 -12
  49. package/dist/preview.js.map +1 -1
  50. package/dist/protocol.d.ts +54 -3
  51. package/dist/protocol.d.ts.map +1 -1
  52. package/dist/protocol.js +125 -20
  53. package/dist/protocol.js.map +1 -1
  54. package/dist/status.d.ts +5 -2
  55. package/dist/status.d.ts.map +1 -1
  56. package/dist/status.js +6 -3
  57. package/dist/status.js.map +1 -1
  58. package/dist/types.d.ts +106 -31
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/types.js +1 -2
  61. package/dist/types.js.map +1 -1
  62. package/package.json +13 -9
  63. package/src/__tests__/devices.test.ts +122 -32
  64. package/src/__tests__/media.test.ts +312 -4
  65. package/src/__tests__/pack-bits.test.ts +92 -0
  66. package/src/__tests__/preview.test.ts +1 -1
  67. package/src/__tests__/protocol.test.ts +256 -1
  68. package/src/__tests__/status.test.ts +87 -0
  69. package/src/devices.generated.ts +840 -0
  70. package/src/devices.ts +31 -273
  71. package/src/index.ts +36 -8
  72. package/src/media.generated.ts +1644 -0
  73. package/src/media.ts +87 -264
  74. package/src/orientation.ts +11 -0
  75. package/src/pack-bits.ts +64 -0
  76. package/src/preview.ts +13 -12
  77. package/src/protocol.ts +204 -19
  78. package/src/status.ts +11 -5
  79. package/src/types.ts +113 -32
  80. package/dist/__tests__/colour.test.d.ts +0 -2
  81. package/dist/__tests__/colour.test.d.ts.map +0 -1
  82. package/dist/__tests__/colour.test.js +0 -106
  83. package/dist/__tests__/colour.test.js.map +0 -1
  84. package/dist/colour.d.ts +0 -26
  85. package/dist/colour.d.ts.map +0 -1
  86. package/dist/colour.js +0 -84
  87. package/dist/colour.js.map +0 -1
  88. package/src/__tests__/colour.test.ts +0 -126
  89. package/src/colour.ts +0 -101
package/src/media.ts CHANGED
@@ -1,268 +1,49 @@
1
- import { type BrotherQLMedia, type MediaType } from './types.js';
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
- * Registry of supported Brother QL consumables.
8
+ * Resolve per-head-family geometry for a media entry against the
9
+ * engine that's about to print it.
5
10
  *
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.
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).
12
19
  */
13
- export const MEDIA: Record<number, BrotherQLMedia> = {
14
- // Continuous length tape
15
- 257: {
16
- id: 257,
17
- name: '12mm continuous',
18
- type: 'continuous',
19
- widthMm: 12,
20
- colorCapable: false,
21
- printAreaDots: 106,
22
- leftMarginPins: 585,
23
- rightMarginPins: 29,
24
- },
25
- 258: {
26
- id: 258,
27
- name: '29mm continuous (DK-22210)',
28
- type: 'continuous',
29
- widthMm: 29,
30
- colorCapable: false,
31
- printAreaDots: 306,
32
- leftMarginPins: 408,
33
- rightMarginPins: 6,
34
- },
35
- 264: {
36
- id: 264,
37
- name: '38mm continuous (DK-22225)',
38
- type: 'continuous',
39
- widthMm: 38,
40
- colorCapable: false,
41
- printAreaDots: 413,
42
- leftMarginPins: 295,
43
- rightMarginPins: 12,
44
- },
45
- 262: {
46
- id: 262,
47
- name: '50mm continuous (DK-22246)',
48
- type: 'continuous',
49
- widthMm: 50,
50
- colorCapable: false,
51
- printAreaDots: 554,
52
- leftMarginPins: 154,
53
- rightMarginPins: 12,
54
- },
55
- 261: {
56
- id: 261,
57
- name: '54mm continuous (DK-22214)',
58
- type: 'continuous',
59
- widthMm: 54,
60
- colorCapable: false,
61
- printAreaDots: 590,
62
- leftMarginPins: 130,
63
- rightMarginPins: 0,
64
- },
65
- 259: {
66
- id: 259,
67
- name: '62mm continuous (DK-22205)',
68
- type: 'continuous',
69
- widthMm: 62,
70
- colorCapable: false,
71
- printAreaDots: 696,
72
- leftMarginPins: 12,
73
- rightMarginPins: 12,
74
- },
75
- 251: {
76
- id: 251,
77
- name: '62mm continuous two-color (DK-22251)',
78
- type: 'continuous',
79
- widthMm: 62,
80
- colorCapable: true,
81
- printAreaDots: 696,
82
- leftMarginPins: 12,
83
- rightMarginPins: 12,
84
- },
85
- 260: {
86
- id: 260,
87
- name: '102mm continuous (DK-22243)',
88
- type: 'continuous',
89
- widthMm: 102,
90
- colorCapable: false,
91
- printAreaDots: 1164,
92
- leftMarginPins: 76,
93
- rightMarginPins: 56,
94
- },
95
-
96
- // Die-cut labels
97
- 269: {
98
- id: 269,
99
- name: '17×54mm die-cut (DK-11204)',
100
- type: 'die-cut',
101
- widthMm: 17,
102
- heightMm: 54,
103
- colorCapable: false,
104
- printAreaDots: 165,
105
- leftMarginPins: 0,
106
- rightMarginPins: 0,
107
- dieCutMaskedAreaDots: 566,
108
- },
109
- 270: {
110
- id: 270,
111
- name: '17×87mm die-cut (DK-11203)',
112
- type: 'die-cut',
113
- widthMm: 17,
114
- heightMm: 87,
115
- colorCapable: false,
116
- printAreaDots: 165,
117
- leftMarginPins: 0,
118
- rightMarginPins: 0,
119
- dieCutMaskedAreaDots: 956,
120
- },
121
- 370: {
122
- id: 370,
123
- name: '23×23mm die-cut',
124
- type: 'die-cut',
125
- widthMm: 23,
126
- heightMm: 23,
127
- colorCapable: false,
128
- printAreaDots: 236,
129
- leftMarginPins: 0,
130
- rightMarginPins: 0,
131
- dieCutMaskedAreaDots: 202,
132
- },
133
- 271: {
134
- id: 271,
135
- name: '29×90mm die-cut (DK-11201)',
136
- type: 'die-cut',
137
- widthMm: 29,
138
- heightMm: 90,
139
- colorCapable: false,
140
- printAreaDots: 306,
141
- leftMarginPins: 0,
142
- rightMarginPins: 0,
143
- dieCutMaskedAreaDots: 991,
144
- },
145
- 272: {
146
- id: 272,
147
- name: '38×90mm die-cut (DK-11218)',
148
- type: 'die-cut',
149
- widthMm: 38,
150
- heightMm: 90,
151
- colorCapable: false,
152
- printAreaDots: 413,
153
- leftMarginPins: 0,
154
- rightMarginPins: 0,
155
- dieCutMaskedAreaDots: 991,
156
- },
157
- 367: {
158
- id: 367,
159
- name: '39×48mm die-cut (DK-11219)',
160
- type: 'die-cut',
161
- widthMm: 39,
162
- heightMm: 48,
163
- colorCapable: false,
164
- printAreaDots: 425,
165
- leftMarginPins: 0,
166
- rightMarginPins: 0,
167
- dieCutMaskedAreaDots: 495,
168
- },
169
- 374: {
170
- id: 374,
171
- name: '52×29mm die-cut',
172
- type: 'die-cut',
173
- widthMm: 52,
174
- heightMm: 29,
175
- colorCapable: false,
176
- printAreaDots: 578,
177
- leftMarginPins: 0,
178
- rightMarginPins: 0,
179
- dieCutMaskedAreaDots: 271,
180
- },
181
- 274: {
182
- id: 274,
183
- name: '62×29mm die-cut (DK-11209)',
184
- type: 'die-cut',
185
- widthMm: 62,
186
- heightMm: 29,
187
- colorCapable: false,
188
- printAreaDots: 696,
189
- leftMarginPins: 0,
190
- rightMarginPins: 0,
191
- dieCutMaskedAreaDots: 271,
192
- },
193
- 275: {
194
- id: 275,
195
- name: '62×100mm die-cut (DK-11202)',
196
- type: 'die-cut',
197
- widthMm: 62,
198
- heightMm: 100,
199
- colorCapable: false,
200
- printAreaDots: 696,
201
- leftMarginPins: 0,
202
- rightMarginPins: 0,
203
- dieCutMaskedAreaDots: 1109,
204
- },
205
- 365: {
206
- id: 365,
207
- name: '102×51mm die-cut (DK-11240)',
208
- type: 'die-cut',
209
- widthMm: 102,
210
- heightMm: 51,
211
- colorCapable: false,
212
- printAreaDots: 1164,
213
- leftMarginPins: 0,
214
- rightMarginPins: 0,
215
- dieCutMaskedAreaDots: 526,
216
- },
217
- 366: {
218
- id: 366,
219
- name: '102×152mm die-cut (DK-11241)',
220
- type: 'die-cut',
221
- widthMm: 102,
222
- heightMm: 152,
223
- colorCapable: false,
224
- printAreaDots: 1164,
225
- leftMarginPins: 0,
226
- rightMarginPins: 0,
227
- dieCutMaskedAreaDots: 1660,
228
- },
229
- 362: {
230
- id: 362,
231
- name: '12mm Ø die-cut',
232
- type: 'die-cut',
233
- widthMm: 12,
234
- heightMm: 12,
235
- colorCapable: false,
236
- printAreaDots: 94,
237
- leftMarginPins: 0,
238
- rightMarginPins: 0,
239
- dieCutMaskedAreaDots: 94,
240
- },
241
- 363: {
242
- id: 363,
243
- name: '24mm Ø die-cut (DK-11221)',
244
- type: 'die-cut',
245
- widthMm: 24,
246
- heightMm: 24,
247
- colorCapable: false,
248
- printAreaDots: 236,
249
- leftMarginPins: 0,
250
- rightMarginPins: 0,
251
- dieCutMaskedAreaDots: 236,
252
- },
253
- 273: {
254
- id: 273,
255
- name: '58mm Ø die-cut (DK-11207)',
256
- type: 'die-cut',
257
- widthMm: 58,
258
- heightMm: 58,
259
- colorCapable: false,
260
- printAreaDots: 618,
261
- leftMarginPins: 0,
262
- rightMarginPins: 0,
263
- dieCutMaskedAreaDots: 618,
264
- },
265
- };
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
+ }
266
47
 
267
48
  /**
268
49
  * Default media when `createPreview()` is called without media and
@@ -272,6 +53,22 @@ export const MEDIA: Record<number, BrotherQLMedia> = {
272
53
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DK-22205 is a hard-coded registry key
273
54
  export const DEFAULT_MEDIA: BrotherQLMedia = MEDIA[259]!;
274
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
+
275
72
  export function findMedia(id: number): BrotherQLMedia | undefined {
276
73
  return MEDIA[id];
277
74
  }
@@ -289,21 +86,47 @@ export function findMediaByWidth(widthMm: number, type: MediaType): BrotherQLMed
289
86
  * printer is configured for two-colour media. When
290
87
  * both DK-22205 (259) and DK-22251 (251) match the
291
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.
292
99
  */
293
100
  export function findMediaByDimensions(
294
101
  widthMm: number,
295
102
  heightMm: number,
296
103
  twoColorMode = false,
104
+ engine?: Pick<PrintEngine, 'headDots' | 'mediaCompatibility'>,
297
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
+
298
121
  if (heightMm === 0) {
299
122
  const continuousMatches = Object.values(MEDIA).filter(
300
- m => m.type === 'continuous' && m.widthMm === widthMm,
123
+ m => m.type === 'continuous' && m.widthMm === widthMm && allowed(m),
301
124
  );
302
125
  if (continuousMatches.length === 0) return undefined;
303
- const preferred = continuousMatches.find(m => m.colorCapable === twoColorMode);
126
+ const preferred = continuousMatches.find(m => (m.palette !== undefined) === twoColorMode);
304
127
  return preferred ?? continuousMatches[0];
305
128
  }
306
129
  return Object.values(MEDIA).find(
307
- 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),
308
131
  );
309
132
  }
@@ -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
+ }
package/src/preview.ts CHANGED
@@ -1,24 +1,25 @@
1
- /* eslint-disable import-x/consistent-type-specifier-style */
2
- import { renderImage, type RawImageData } from '@mbtech-nl/bitmap';
1
+ import { renderImage, renderMultiPlaneImage } from '@mbtech-nl/bitmap';
2
+ import type { LabelBitmap, RawImageData } from '@mbtech-nl/bitmap';
3
3
  import type { PreviewResult } from '@thermal-label/contracts';
4
- import { splitTwoColor } from './colour.js';
5
- import { type BrotherQLMedia } from './types.js';
4
+ import type { BrotherQLMedia } from './types.js';
6
5
 
7
6
  /**
8
7
  * Offline preview without a live printer connection.
9
8
  *
10
- * Two-colour aware: when `media.colorCapable` is true (DK-22251), the
11
- * image is split into a black and a red plane via `splitTwoColor()` —
12
- * exactly what `print()` does for that media. Single-colour media
13
- * returns one black plane.
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`.
14
13
  */
15
14
  export function createPreviewOffline(image: RawImageData, media: BrotherQLMedia): PreviewResult {
16
- if (media.colorCapable) {
17
- const { black, red } = splitTwoColor(image);
15
+ if (media.palette) {
16
+ const planes = renderMultiPlaneImage(image, {
17
+ palette: media.palette,
18
+ }) as Record<'black' | 'red', LabelBitmap>;
18
19
  return {
19
20
  planes: [
20
- { name: 'black', bitmap: black, displayColor: '#000000' },
21
- { name: 'red', bitmap: red, displayColor: '#ff0000' },
21
+ { name: 'black', bitmap: planes.black, displayColor: '#000000' },
22
+ { name: 'red', bitmap: planes.red, displayColor: '#ff0000' },
22
23
  ],
23
24
  media,
24
25
  assumed: false,