@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/protocol.ts CHANGED
@@ -1,8 +1,18 @@
1
1
  import { getRow, createBitmap } from '@mbtech-nl/bitmap';
2
- import { type BrotherQLMedia, type PageData, type JobOptions, type PageOptions } from './types.js';
2
+ import type { PrintEngine } from '@thermal-label/contracts';
3
+ import { packBits } from './pack-bits.js';
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';
3
13
 
4
- export function buildInvalidate(): Uint8Array {
5
- return new Uint8Array(200);
14
+ export function buildInvalidate(byteCount = 200): Uint8Array {
15
+ return new Uint8Array(byteCount);
6
16
  }
7
17
 
8
18
  export function buildStatusRequest(): Uint8Array {
@@ -57,11 +67,12 @@ export function buildExpandedMode(
57
67
  cutAtEnd: boolean,
58
68
  highRes: boolean,
59
69
  twoColor = false,
70
+ highResFlagBit = 0x10,
60
71
  ): Uint8Array {
61
72
  let flags = 0x00;
62
73
  if (twoColor) flags |= 0x01;
63
74
  if (cutAtEnd) flags |= 0x08;
64
- if (highRes) flags |= 0x10;
75
+ if (highRes) flags |= highResFlagBit;
65
76
  return new Uint8Array([0x1b, 0x69, 0x4b, flags]);
66
77
  }
67
78
 
@@ -134,14 +145,133 @@ function concat(...arrays: Uint8Array[]): Uint8Array {
134
145
  return out;
135
146
  }
136
147
 
137
- export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Array {
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;
138
260
  const copies = options.copies ?? 1;
139
261
  const chunks: Uint8Array[] = [];
140
262
 
141
- // Python brother_ql sequence: raster-mode first, then 200-byte invalidate, then init, then
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
142
272
  // raster-mode again (matches observed working sequence for QL-820NWB).
143
273
  chunks.push(buildRasterMode());
144
- chunks.push(buildInvalidate());
274
+ chunks.push(buildInvalidate(invalidateBytes));
145
275
  chunks.push(buildInitialize());
146
276
 
147
277
  const allPageInstances: PageData[] = [];
@@ -157,16 +287,29 @@ export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Arr
157
287
  const autoCut = opts.autoCut ?? true;
158
288
  const cutAtEnd = opts.cutAtEnd ?? true;
159
289
  const highRes = opts.highResolution ?? false;
160
- const marginDots = opts.marginDots ?? 35;
161
290
  const compress = opts.compress ?? false;
162
291
  const { bitmap, media } = page;
163
292
 
164
- // colorCapable media (e.g. DK-22251) requires two-color mode even for black-only jobs.
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
+
307
+ // Multi-ink media (e.g. DK-22251) requires two-color mode even for black-only jobs.
165
308
  // Auto-create an empty red plane when the tape demands it but caller didn't supply one.
166
- const twoColor = page.redBitmap !== undefined || media.colorCapable;
309
+ const multiInk = media.palette !== undefined;
310
+ const twoColor = page.redBitmap !== undefined || multiInk;
167
311
  const redBitmap =
168
- page.redBitmap ??
169
- (media.colorCapable ? createBitmap(bitmap.widthPx, bitmap.heightPx) : undefined);
312
+ page.redBitmap ?? (multiInk ? createBitmap(bitmap.widthPx, bitmap.heightPx) : undefined);
170
313
 
171
314
  if (twoColor && redBitmap !== undefined) {
172
315
  if (bitmap.widthPx !== redBitmap.widthPx || bitmap.heightPx !== redBitmap.heightPx) {
@@ -181,28 +324,41 @@ export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Arr
181
324
  chunks.push(buildPrintInfo(media, rowCount, i));
182
325
  chunks.push(buildVariousMode(autoCut));
183
326
  chunks.push(buildCutEach(1));
184
- chunks.push(buildExpandedMode(cutAtEnd, highRes, twoColor));
327
+ chunks.push(buildExpandedMode(cutAtEnd, highRes, twoColor, config.highResFlagBit));
185
328
  chunks.push(buildMargin(marginDots));
186
329
  if (compress) chunks.push(buildCompression(true));
187
330
 
188
331
  // Each raster row must cover the full print head width (derived from media geometry).
189
- // leftMarginPins + printAreaDots + rightMarginPins = head pin count (720 or 1296).
190
- const totalPins = media.leftMarginPins + media.printAreaDots + media.rightMarginPins;
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;
191
336
  const rowByteLen = Math.ceil(totalPins / 8);
192
337
 
193
338
  // Rows interleaved per raster line (matches Python brother_ql behaviour).
194
339
  // Two-color: black row then red row for each line. Single-color: black only.
340
+ // When `compress` is on, each row's bytes are PackBits-encoded and the
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;
195
345
  for (let r = 0; r < rowCount; r++) {
196
346
  const blackSrc = getRow(bitmap, r);
197
347
  const blackBytes = new Uint8Array(rowByteLen);
198
- placeBits(blackSrc, bitmap.widthPx, blackBytes, media.leftMarginPins);
199
- chunks.push(buildRasterRow(blackBytes, 'black', twoColor));
348
+ placeBits(blackSrc, bitmap.widthPx, blackBytes, leftMarginPins);
349
+ const blackPayload = compress ? packBits(blackBytes) : blackBytes;
350
+ const blackChunk = buildRasterRow(blackPayload, 'black', twoColor);
351
+ chunks.push(blackChunk);
352
+ if (duplicate) chunks.push(blackChunk);
200
353
 
201
354
  if (twoColor && redBitmap !== undefined) {
202
355
  const redSrc = getRow(redBitmap, r);
203
356
  const redBytes = new Uint8Array(rowByteLen);
204
- placeBits(redSrc, redBitmap.widthPx, redBytes, media.leftMarginPins);
205
- chunks.push(buildRasterRow(redBytes, 'red', twoColor));
357
+ placeBits(redSrc, redBitmap.widthPx, redBytes, leftMarginPins);
358
+ const redPayload = compress ? packBits(redBytes) : redBytes;
359
+ const redChunk = buildRasterRow(redPayload, 'red', twoColor);
360
+ chunks.push(redChunk);
361
+ if (duplicate) chunks.push(redChunk);
206
362
  }
207
363
  }
208
364
 
@@ -211,3 +367,32 @@ export function encodeJob(pages: PageData[], options: JobOptions = {}): Uint8Arr
211
367
 
212
368
  return concat(...chunks);
213
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
+ }
package/src/status.ts CHANGED
@@ -1,6 +1,5 @@
1
- /* eslint-disable import-x/consistent-type-specifier-style */
2
- import type { PrinterError } from '@thermal-label/contracts';
3
- import { type BrotherQLStatus } from './types.js';
1
+ import type { PrintEngine, PrinterError } from '@thermal-label/contracts';
2
+ import type { BrotherQLStatus } from './types.js';
4
3
  import { findMediaByDimensions } from './media.js';
5
4
 
6
5
  export const STATUS_REQUEST = new Uint8Array([0x1b, 0x69, 0x53]);
@@ -38,6 +37,8 @@ const ERROR_INFO_2: { bit: number; code: string; message: string }[] = [
38
37
  * byte 11 — media type (0x0A continuous, 0x0B die-cut)
39
38
  * byte 17 — media length (mm), 0 for continuous
40
39
  * byte 18 — status type (0x02 = error response)
40
+ * byte 25 — bit 7 set when the loaded roll is two-color (DK-22251);
41
+ * clear on single-color rolls. See scripts/STATUS-CAPTURE.md.
41
42
  *
42
43
  * `detectedMedia` is resolved against the media registry via
43
44
  * `findMediaByDimensions`. `editorLiteMode` is a driver-specific
@@ -46,7 +47,10 @@ const ERROR_INFO_2: { bit: number; code: string; message: string }[] = [
46
47
  * set it from other signals (e.g. mass-storage PID detected during
47
48
  * discovery) without changing the return type.
48
49
  */
49
- export function parseStatus(bytes: Uint8Array): BrotherQLStatus {
50
+ export function parseStatus(
51
+ bytes: Uint8Array,
52
+ engine?: Pick<PrintEngine, 'headDots' | 'mediaCompatibility'>,
53
+ ): BrotherQLStatus {
50
54
  if (bytes.length < 32) {
51
55
  throw new Error(`Status response too short: ${bytes.length.toString()} bytes`);
52
56
  }
@@ -58,6 +62,7 @@ export function parseStatus(bytes: Uint8Array): BrotherQLStatus {
58
62
  const mediaTypeByte = view.getUint8(11);
59
63
  const mediaLengthMm = view.getUint8(17);
60
64
  const statusType = view.getUint8(18);
65
+ const twoColorFlag = (view.getUint8(25) & 0x80) !== 0;
61
66
 
62
67
  const errors: PrinterError[] = [];
63
68
  for (const { bit, code, message } of ERROR_INFO_1) {
@@ -69,13 +74,14 @@ export function parseStatus(bytes: Uint8Array): BrotherQLStatus {
69
74
 
70
75
  const mediaLoaded = mediaWidthMm > 0 && mediaTypeByte !== 0;
71
76
  const detected = mediaLoaded
72
- ? findMediaByDimensions(mediaWidthMm, mediaLengthMm, false)
77
+ ? findMediaByDimensions(mediaWidthMm, mediaLengthMm, twoColorFlag, engine)
73
78
  : undefined;
74
79
 
75
80
  return {
76
81
  ready: errors.length === 0 && statusType !== 0x02,
77
82
  mediaLoaded,
78
83
  ...(detected === undefined ? {} : { detectedMedia: detected }),
84
+ ...(mediaLoaded ? { twoColorRoll: twoColorFlag } : {}),
79
85
  errors,
80
86
  editorLiteMode: false,
81
87
  rawBytes: bytes,
package/src/types.ts CHANGED
@@ -1,54 +1,112 @@
1
- /* eslint-disable import-x/consistent-type-specifier-style */
2
- import { type LabelBitmap } from '@mbtech-nl/bitmap';
3
- import type { DeviceDescriptor, MediaDescriptor, PrinterStatus } from '@thermal-label/contracts';
1
+ import type { LabelBitmap } from '@mbtech-nl/bitmap';
2
+ import type {
3
+ DeviceEntry,
4
+ MediaDescriptor,
5
+ PrintEngineCapabilities,
6
+ PrintOptions,
7
+ PrinterStatus,
8
+ } from '@thermal-label/contracts';
4
9
 
5
10
  export type MediaType = 'continuous' | 'die-cut';
6
11
  export type HeadWidth = 720 | 1296;
7
12
  export type ColorMode = 'single' | 'two-color';
8
- export type NetworkSupport = 'none' | 'wifi' | 'wired' | 'wifi+wired';
9
13
 
10
14
  /**
11
- * Brother QL device descriptor.
15
+ * Tape-system discriminator on `BrotherQLMedia`. DK is the QL series'
16
+ * paper-label system; TZe is the laminated-tape system used by the
17
+ * PT-P / PT-E line; HSe 2:1 and HSe 3:1 are heat-shrink tubing systems
18
+ * supported by most P900-series and PT-E550W. Lookup paths gate on
19
+ * this so a QL printer never resolves a TZe entry, and vice versa.
20
+ */
21
+ export type TapeSystem = 'dk' | 'tze' | 'hse-2to1' | 'hse-3to1';
22
+
23
+ /**
24
+ * Per-head-family geometry on `BrotherQLMedia`.
12
25
  *
13
- * Extends the contracts base with QL-specific fields: head geometry,
14
- * protocol feature flags, and the optional mass-storage PID for Editor
15
- * Lite mode.
26
+ * Brother's PT-P / PT-E line ships two head families with different
27
+ * per-tape pin layouts. The same TZe id maps to different
28
+ * `printAreaDots` / `leftMarginPins` / `rightMarginPins` values on a
29
+ * 128-pin head (PT-E550W, PT-P750W) versus a 560-pin head (PT-P900,
30
+ * P900W, P950NW, P910BT). DK media leaves these unset and resolves via
31
+ * the flat fields on `BrotherQLMedia` directly.
32
+ */
33
+ export interface TapeGeometry {
34
+ printAreaDots: number;
35
+ leftMarginPins: number;
36
+ rightMarginPins: number;
37
+ }
38
+
39
+ /**
40
+ * Brother-specific engine capabilities.
16
41
  *
17
- * **Bluetooth on the QL-820NWB / 820NWBc**: not exposed over GATT.
18
- * Classic Bluetooth (SPP) is paired at the OS level; the kernel/driver
19
- * exposes an RFCOMM serial port, reachable via the `'serial'` transport
20
- * in Node.js and the `'web-serial'` transport in Chrome/Edge. macOS has
21
- * dropped classic Bluetooth SPP no serial route there.
42
+ * Extends the contracts-defined `PrintEngineCapabilities` (which
43
+ * carries the multi-vendor named flags `autocut` and `mediaDetection`)
44
+ * with the driver-side `twoColor` flag Brother-only today, so it
45
+ * lands here via the contracts open index signature. Promote to a
46
+ * named contracts key when a second vendor implements the same
47
+ * capability with compatible semantics.
22
48
  */
23
- export interface BrotherQLDevice extends DeviceDescriptor {
24
- family: 'brother-ql';
25
- vid: number;
26
- pid: number;
27
- headPins: HeadWidth;
28
- bytesPerRow: number;
29
- twoColor: boolean;
30
- network: NetworkSupport;
31
- autocut: boolean;
32
- compression: boolean;
33
- editorLite: boolean;
34
- /** Alternate PID seen when the printer is in Editor Lite mass-storage mode. */
35
- massStoragePid?: number;
49
+ export interface BrotherEngineCapabilities extends PrintEngineCapabilities {
50
+ /** Two-colour ribbon path — black + red plane raster encoding. */
51
+ twoColor?: boolean;
52
+ /**
53
+ * Doubled-density mode along the feed axis (`ESC i K` bit 6).
54
+ * `360` on PT-E550W / PT-P750W (native 180); `720` on the PT-P900
55
+ * family (native 360). Undefined on QL and PT models that don't
56
+ * support high-res. The encoder branches on this when
57
+ * `BrotherQLPrintOptions.highRes` is set.
58
+ */
59
+ highResDpi?: 360 | 720;
36
60
  }
37
61
 
62
+ /**
63
+ * Brother QL device entry — alias for the contracts `DeviceEntry`
64
+ * shape. Re-exported under a driver-named type so consumers don't
65
+ * have to import contracts directly. Per-device chassis-level
66
+ * capabilities (`editorLite`, `massStoragePid`) ride on the open
67
+ * index signature of `DeviceEntry.capabilities`; engine-level flags
68
+ * (`autocut`, `mediaDetection`, `twoColor`) ride on
69
+ * `engines[].capabilities`.
70
+ *
71
+ * **Bluetooth on the QL-820NWB / 820NWBc**: not exposed over GATT.
72
+ * Classic Bluetooth SPP is paired at the OS level — declared as the
73
+ * `bluetooth-spp` transport. The runtime's serial implementation
74
+ * satisfies that transport key by opening the OS-paired RFCOMM
75
+ * device path. macOS dropped classic Bluetooth SPP — no SPP route
76
+ * there.
77
+ */
78
+ export type BrotherQLDevice = DeviceEntry;
79
+
38
80
  /**
39
81
  * Brother QL media descriptor.
40
82
  *
41
83
  * Extends `MediaDescriptor` with the dots-based geometry the raster
42
- * encoder needs. `colorCapable: true` flips the driver into
43
- * two-colour mode — only DK-22251 has this set in the registry.
84
+ * encoder needs. The base `palette` field flips the driver into
85
+ * multi-plane mode — only DK-22251 declares one in the registry.
44
86
  */
45
87
  export interface BrotherQLMedia extends MediaDescriptor {
46
88
  id: number;
47
89
  type: MediaType;
48
- colorCapable: boolean;
49
- printAreaDots: number;
50
- leftMarginPins: number;
51
- rightMarginPins: number;
90
+ /**
91
+ * Tape system this entry belongs to. Drives lookup gating in
92
+ * `findMediaByDimensions(width, height, engine)` so QL engines never
93
+ * resolve TZe / HSe entries and vice versa.
94
+ */
95
+ tapeSystem: TapeSystem;
96
+ /**
97
+ * Per-head-family geometry. `narrow` = 128-pin head (PT-E550W,
98
+ * PT-P750W); `wide` = 560-pin head (PT-P900 family). DK entries
99
+ * leave both unset and use the flat fields below; TZe / HSe entries
100
+ * leave the flat fields undefined and populate `narrow` and/or
101
+ * `wide` per the *Raster Command Reference* PDFs. `undefined` on a
102
+ * head family means "this tape doesn't fit this head" (e.g. 36 mm
103
+ * TZe and 31 mm HSe-3:1 have no `narrow` entry).
104
+ */
105
+ geometry?: { narrow?: TapeGeometry; wide?: TapeGeometry };
106
+ /** DK-only flat geometry. PT-* entries populate `geometry` instead. */
107
+ printAreaDots?: number;
108
+ leftMarginPins?: number;
109
+ rightMarginPins?: number;
52
110
  /** Die-cut masked area in dots (registration windows). */
53
111
  dieCutMaskedAreaDots?: number;
54
112
  }
@@ -60,6 +118,11 @@ export interface BrotherQLMedia extends MediaDescriptor {
60
118
  */
61
119
  export interface BrotherQLStatus extends PrinterStatus {
62
120
  editorLiteMode: boolean;
121
+ /**
122
+ * True when the loaded roll reports two-color capability via byte 25
123
+ * bit 7 of the status response. Undefined when no media is loaded.
124
+ */
125
+ twoColorRoll?: boolean;
63
126
  }
64
127
 
65
128
  export interface PageData {
@@ -80,3 +143,21 @@ export interface PageOptions {
80
143
  export interface JobOptions {
81
144
  copies?: number;
82
145
  }
146
+
147
+ /**
148
+ * Per-call print options for `BrotherQLPrinter.print()`.
149
+ *
150
+ * Extends the cross-driver `PrintOptions` with QL-specific knobs. The
151
+ * `rotate` override picks the rotation angle passed to
152
+ * `renderImage` / `renderMultiPlaneImage` — `'auto'` (the default)
153
+ * defers to the media's `defaultOrientation` heuristic.
154
+ */
155
+ export interface BrotherQLPrintOptions extends PrintOptions {
156
+ rotate?: 'auto' | 0 | 90 | 180 | 270;
157
+ /**
158
+ * Opt into high-resolution mode (doubles dpi along the feed axis).
159
+ * Requires the engine's `capabilities.highResDpi` to be set; throws
160
+ * at job-build time otherwise. PT-* only — QL ignores the option.
161
+ */
162
+ highRes?: boolean;
163
+ }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=colour.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"colour.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/colour.test.ts"],"names":[],"mappings":""}
@@ -1,106 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { getPixel } from '@mbtech-nl/bitmap';
3
- import { isRedish, splitTwoColor } from '../colour.js';
4
- function rgbaOf(width, height, [r, g, b, a]) {
5
- const data = new Uint8Array(width * height * 4);
6
- for (let i = 0; i < data.length; i += 4) {
7
- data[i] = r;
8
- data[i + 1] = g;
9
- data[i + 2] = b;
10
- data[i + 3] = a;
11
- }
12
- return { width, height, data };
13
- }
14
- function countInkPixels(bitmap) {
15
- let n = 0;
16
- for (let y = 0; y < bitmap.heightPx; y++) {
17
- for (let x = 0; x < bitmap.widthPx; x++) {
18
- if (getPixel(bitmap, x, y))
19
- n++;
20
- }
21
- }
22
- return n;
23
- }
24
- describe('isRedish', () => {
25
- it('treats a strong red as red', () => {
26
- expect(isRedish(255, 0, 0, 255)).toBe(true);
27
- });
28
- it('rejects red when green is too high (threshold g < 100)', () => {
29
- expect(isRedish(255, 100, 0, 255)).toBe(false);
30
- expect(isRedish(255, 99, 0, 255)).toBe(true);
31
- });
32
- it('rejects red when blue is too high (threshold b < 100)', () => {
33
- expect(isRedish(255, 0, 100, 255)).toBe(false);
34
- expect(isRedish(255, 0, 99, 255)).toBe(true);
35
- });
36
- it('rejects red when the red channel is too low (threshold r > 180)', () => {
37
- expect(isRedish(180, 0, 0, 255)).toBe(false);
38
- expect(isRedish(181, 0, 0, 255)).toBe(true);
39
- });
40
- it('rejects transparent pixels (alpha < 128)', () => {
41
- expect(isRedish(255, 0, 0, 127)).toBe(false);
42
- expect(isRedish(255, 0, 0, 128)).toBe(true);
43
- });
44
- it('rejects black (no red dominance)', () => {
45
- expect(isRedish(0, 0, 0, 255)).toBe(false);
46
- });
47
- it('rejects white', () => {
48
- expect(isRedish(255, 255, 255, 255)).toBe(false);
49
- });
50
- });
51
- describe('splitTwoColor', () => {
52
- it('routes a solid red image to the red plane, nothing to black', () => {
53
- const { black, red } = splitTwoColor(rgbaOf(8, 8, [255, 0, 0, 255]));
54
- expect(countInkPixels(red)).toBeGreaterThan(0);
55
- expect(countInkPixels(black)).toBe(0);
56
- });
57
- it('routes a solid black image to the black plane, nothing to red', () => {
58
- const { black, red } = splitTwoColor(rgbaOf(8, 8, [0, 0, 0, 255]));
59
- expect(countInkPixels(black)).toBeGreaterThan(0);
60
- expect(countInkPixels(red)).toBe(0);
61
- });
62
- it('produces bitmaps matching the source dimensions', () => {
63
- const { black, red } = splitTwoColor(rgbaOf(16, 12, [128, 128, 128, 255]));
64
- expect(black.widthPx).toBe(16);
65
- expect(black.heightPx).toBe(12);
66
- expect(red.widthPx).toBe(16);
67
- expect(red.heightPx).toBe(12);
68
- });
69
- it('resolves overlapping bits in favour of black (red bit cleared)', () => {
70
- // Construct a mixed image: one row red, one row black.
71
- const data = new Uint8Array(8 * 2 * 4);
72
- // Row 0: red
73
- for (let i = 0; i < 8; i++) {
74
- data[i * 4] = 255;
75
- data[i * 4 + 3] = 255;
76
- }
77
- // Row 1: black
78
- for (let i = 0; i < 8; i++) {
79
- const offset = (8 + i) * 4;
80
- data[offset] = 0;
81
- data[offset + 1] = 0;
82
- data[offset + 2] = 0;
83
- data[offset + 3] = 255;
84
- }
85
- const image = { width: 8, height: 2, data };
86
- const { black, red } = splitTwoColor(image);
87
- // Every non-transparent pixel should land on exactly one plane —
88
- // resolveOverlap guarantees no bit is set in both.
89
- for (let y = 0; y < 2; y++) {
90
- for (let x = 0; x < 8; x++) {
91
- const inBlack = getPixel(black, x, y);
92
- const inRed = getPixel(red, x, y);
93
- expect(inBlack && inRed).toBe(false);
94
- }
95
- }
96
- });
97
- it('accepts custom threshold + dither options', () => {
98
- const { black, red } = splitTwoColor(rgbaOf(4, 4, [0, 0, 0, 255]), {
99
- threshold: 64,
100
- dither: false,
101
- });
102
- expect(black.widthPx).toBe(4);
103
- expect(red.widthPx).toBe(4);
104
- });
105
- });
106
- //# sourceMappingURL=colour.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"colour.test.js","sourceRoot":"","sources":["../../src/__tests__/colour.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEvD,SAAS,MAAM,CACb,KAAa,EACb,MAAc,EACd,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAmC;IAM9C,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACZ,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACjC,CAAC;AAED,SAAS,cAAc,CAAC,MAA+D;IACrF,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;gBAAE,CAAC,EAAE,CAAC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7C,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7C,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,eAAe,EAAE,GAAG,EAAE;QACvB,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACrE,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACnE,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3E,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,uDAAuD;QACvD,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACvC,aAAa;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;YAClB,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;QACxB,CAAC;QACD,eAAe;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YACrB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YACrB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;QACzB,CAAC;QACD,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC;QAC5C,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5C,iEAAiE;QACjE,mDAAmD;QACnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC3B,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;gBACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;gBAClC,MAAM,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE;YACjE,SAAS,EAAE,EAAE;YACb,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}