@thermal-label/brother-ql-core 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/__tests__/media.test.js +23 -3
  2. package/dist/__tests__/media.test.js.map +1 -1
  3. package/dist/__tests__/pack-bits.test.d.ts +2 -0
  4. package/dist/__tests__/pack-bits.test.d.ts.map +1 -0
  5. package/dist/__tests__/pack-bits.test.js +90 -0
  6. package/dist/__tests__/pack-bits.test.js.map +1 -0
  7. package/dist/__tests__/preview.test.js +1 -1
  8. package/dist/__tests__/preview.test.js.map +1 -1
  9. package/dist/__tests__/protocol.test.js +46 -1
  10. package/dist/__tests__/protocol.test.js.map +1 -1
  11. package/dist/devices.d.ts +1 -1
  12. package/dist/devices.d.ts.map +1 -1
  13. package/dist/devices.js +0 -1
  14. package/dist/devices.js.map +1 -1
  15. package/dist/index.d.ts +6 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/media.d.ts +18 -6
  20. package/dist/media.d.ts.map +1 -1
  21. package/dist/media.js +47 -29
  22. package/dist/media.js.map +1 -1
  23. package/dist/orientation.d.ts +11 -0
  24. package/dist/orientation.d.ts.map +1 -0
  25. package/dist/orientation.js +10 -0
  26. package/dist/orientation.js.map +1 -0
  27. package/dist/pack-bits.d.ts +20 -0
  28. package/dist/pack-bits.d.ts.map +1 -0
  29. package/dist/pack-bits.js +61 -0
  30. package/dist/pack-bits.js.map +1 -0
  31. package/dist/preview.d.ts +6 -6
  32. package/dist/preview.d.ts.map +1 -1
  33. package/dist/preview.js +11 -12
  34. package/dist/preview.js.map +1 -1
  35. package/dist/protocol.d.ts +1 -1
  36. package/dist/protocol.d.ts.map +1 -1
  37. package/dist/protocol.js +13 -7
  38. package/dist/protocol.js.map +1 -1
  39. package/dist/status.d.ts +1 -1
  40. package/dist/status.d.ts.map +1 -1
  41. package/dist/status.js +0 -1
  42. package/dist/status.js.map +1 -1
  43. package/dist/types.d.ts +15 -5
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/types.js +1 -2
  46. package/dist/types.js.map +1 -1
  47. package/package.json +3 -3
  48. package/src/__tests__/media.test.ts +25 -3
  49. package/src/__tests__/pack-bits.test.ts +92 -0
  50. package/src/__tests__/preview.test.ts +1 -1
  51. package/src/__tests__/protocol.test.ts +47 -1
  52. package/src/devices.ts +1 -1
  53. package/src/index.ts +16 -4
  54. package/src/media.ts +48 -29
  55. package/src/orientation.ts +11 -0
  56. package/src/pack-bits.ts +64 -0
  57. package/src/preview.ts +13 -12
  58. package/src/protocol.ts +14 -7
  59. package/src/status.ts +1 -2
  60. package/src/types.ts +21 -6
  61. package/dist/__tests__/colour.test.d.ts +0 -2
  62. package/dist/__tests__/colour.test.d.ts.map +0 -1
  63. package/dist/__tests__/colour.test.js +0 -106
  64. package/dist/__tests__/colour.test.js.map +0 -1
  65. package/dist/colour.d.ts +0 -26
  66. package/dist/colour.d.ts.map +0 -1
  67. package/dist/colour.js +0 -84
  68. package/dist/colour.js.map +0 -1
  69. package/src/__tests__/colour.test.ts +0 -126
  70. package/src/colour.ts +0 -101
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,
package/src/protocol.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getRow, createBitmap } from '@mbtech-nl/bitmap';
2
- import { type BrotherQLMedia, type PageData, type JobOptions, type PageOptions } from './types.js';
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);
@@ -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
- // colorCapable media (e.g. DK-22251) requires two-color mode even for black-only jobs.
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 twoColor = page.redBitmap !== undefined || media.colorCapable;
167
+ const multiInk = media.palette !== undefined;
168
+ const twoColor = page.redBitmap !== undefined || multiInk;
167
169
  const redBitmap =
168
- page.redBitmap ??
169
- (media.colorCapable ? 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
- chunks.push(buildRasterRow(blackBytes, 'black', twoColor));
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
- chunks.push(buildRasterRow(redBytes, 'red', twoColor));
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,6 +1,5 @@
1
- /* eslint-disable import-x/consistent-type-specifier-style */
2
1
  import type { PrinterError } from '@thermal-label/contracts';
3
- import { type BrotherQLStatus } from './types.js';
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]);
package/src/types.ts CHANGED
@@ -1,6 +1,10 @@
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
+ DeviceDescriptor,
4
+ MediaDescriptor,
5
+ PrintOptions,
6
+ PrinterStatus,
7
+ } from '@thermal-label/contracts';
4
8
 
5
9
  export type MediaType = 'continuous' | 'die-cut';
6
10
  export type HeadWidth = 720 | 1296;
@@ -39,13 +43,12 @@ export interface BrotherQLDevice extends DeviceDescriptor {
39
43
  * Brother QL media descriptor.
40
44
  *
41
45
  * 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.
46
+ * encoder needs. The base `palette` field flips the driver into
47
+ * multi-plane mode — only DK-22251 declares one in the registry.
44
48
  */
45
49
  export interface BrotherQLMedia extends MediaDescriptor {
46
50
  id: number;
47
51
  type: MediaType;
48
- colorCapable: boolean;
49
52
  printAreaDots: number;
50
53
  leftMarginPins: number;
51
54
  rightMarginPins: number;
@@ -80,3 +83,15 @@ export interface PageOptions {
80
83
  export interface JobOptions {
81
84
  copies?: number;
82
85
  }
86
+
87
+ /**
88
+ * Per-call print options for `BrotherQLPrinter.print()`.
89
+ *
90
+ * Extends the cross-driver `PrintOptions` with QL-specific knobs. The
91
+ * `rotate` override picks the rotation angle passed to
92
+ * `renderImage` / `renderMultiPlaneImage` — `'auto'` (the default)
93
+ * defers to the media's `defaultOrientation` heuristic.
94
+ */
95
+ export interface BrotherQLPrintOptions extends PrintOptions {
96
+ rotate?: 'auto' | 0 | 90 | 180 | 270;
97
+ }
@@ -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":"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"}
package/dist/colour.d.ts DELETED
@@ -1,26 +0,0 @@
1
- import { type LabelBitmap, type RawImageData } from '@mbtech-nl/bitmap';
2
- export interface TwoColorResult {
3
- black: LabelBitmap;
4
- red: LabelBitmap;
5
- }
6
- export interface TwoColorOptions {
7
- threshold?: number;
8
- dither?: boolean;
9
- }
10
- /**
11
- * A pixel is "red-ish" when the red channel clearly dominates and
12
- * alpha is opaque enough to matter. The thresholds come from testing
13
- * against DK-22251 output — wider tolerances let non-red warm tones
14
- * bleed into the red plane.
15
- */
16
- export declare function isRedish(r: number, g: number, b: number, a: number): boolean;
17
- /**
18
- * Split an RGBA image into black and red 1bpp planes for Brother QL
19
- * two-colour media (DK-22251).
20
- *
21
- * The rendering path matches `renderImage(image, { dither: true })`
22
- * used by `print()` for single-colour media, so overall print density
23
- * stays consistent regardless of media.
24
- */
25
- export declare function splitTwoColor(image: RawImageData, options?: TwoColorOptions): TwoColorResult;
26
- //# sourceMappingURL=colour.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"colour.d.ts","sourceRoot":"","sources":["../src/colour.ts"],"names":[],"mappings":"AAQA,OAAO,EAAe,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAErF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,GAAG,EAAE,WAAW,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAG5E;AAmDD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,cAAc,CAY5F"}
package/dist/colour.js DELETED
@@ -1,84 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion --
2
- * Uint8Array indexed reads are typed `number | undefined` under
3
- * `noUncheckedIndexedAccess`, but every index in this file is bounded
4
- * by `src.length` (a multiple of 4) or `Math.min(a.length, b.length)`.
5
- * The `!` assertions collapse the unreachable `undefined` branches so
6
- * branch coverage doesn't count them. See also `non-nullable-type-
7
- * assertion-style` which rules out the alternative `as number` form.
8
- */
9
- import { renderImage } from '@mbtech-nl/bitmap';
10
- /**
11
- * A pixel is "red-ish" when the red channel clearly dominates and
12
- * alpha is opaque enough to matter. The thresholds come from testing
13
- * against DK-22251 output — wider tolerances let non-red warm tones
14
- * bleed into the red plane.
15
- */
16
- export function isRedish(r, g, b, a) {
17
- if (a < 128)
18
- return false;
19
- return r > 180 && g < 100 && b < 100;
20
- }
21
- function extractRedPixels(image) {
22
- const src = image.data;
23
- const dst = new Uint8Array(src.length);
24
- for (let i = 0; i < src.length; i += 4) {
25
- const r = src[i];
26
- const g = src[i + 1];
27
- const b = src[i + 2];
28
- const a = src[i + 3];
29
- if (isRedish(r, g, b, a)) {
30
- dst[i] = r;
31
- dst[i + 1] = g;
32
- dst[i + 2] = b;
33
- dst[i + 3] = a;
34
- }
35
- }
36
- return { data: dst, width: image.width, height: image.height };
37
- }
38
- function extractNonRedPixels(image) {
39
- const src = image.data;
40
- const dst = new Uint8Array(src.length);
41
- for (let i = 0; i < src.length; i += 4) {
42
- const r = src[i];
43
- const g = src[i + 1];
44
- const b = src[i + 2];
45
- const a = src[i + 3];
46
- if (!isRedish(r, g, b, a)) {
47
- dst[i] = r;
48
- dst[i + 1] = g;
49
- dst[i + 2] = b;
50
- dst[i + 3] = a;
51
- }
52
- }
53
- return { data: dst, width: image.width, height: image.height };
54
- }
55
- /**
56
- * Where both planes have a set bit at the same position, black wins.
57
- * Done by masking the red bits with the inverse of the black bits.
58
- */
59
- function resolveOverlap(black, red) {
60
- const blackData = black.data;
61
- const redData = red.data;
62
- const len = Math.min(blackData.length, redData.length);
63
- for (let i = 0; i < len; i++) {
64
- redData[i] = redData[i] & ~blackData[i];
65
- }
66
- }
67
- /**
68
- * Split an RGBA image into black and red 1bpp planes for Brother QL
69
- * two-colour media (DK-22251).
70
- *
71
- * The rendering path matches `renderImage(image, { dither: true })`
72
- * used by `print()` for single-colour media, so overall print density
73
- * stays consistent regardless of media.
74
- */
75
- export function splitTwoColor(image, options) {
76
- const { threshold = 128, dither = true } = options ?? {};
77
- const blackImage = extractNonRedPixels(image);
78
- const redImage = extractRedPixels(image);
79
- const black = renderImage(blackImage, { threshold, dither });
80
- const red = renderImage(redImage, { threshold, dither });
81
- resolveOverlap(black, red);
82
- return { black, red };
83
- }
84
- //# sourceMappingURL=colour.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"colour.js","sourceRoot":"","sources":["../src/colour.ts"],"names":[],"mappings":"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"}
@@ -1,126 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { getPixel } from '@mbtech-nl/bitmap';
3
- import { isRedish, splitTwoColor } from '../colour.js';
4
-
5
- function rgbaOf(
6
- width: number,
7
- height: number,
8
- [r, g, b, a]: [number, number, number, number],
9
- ): {
10
- width: number;
11
- height: number;
12
- data: Uint8Array;
13
- } {
14
- const data = new Uint8Array(width * height * 4);
15
- for (let i = 0; i < data.length; i += 4) {
16
- data[i] = r;
17
- data[i + 1] = g;
18
- data[i + 2] = b;
19
- data[i + 3] = a;
20
- }
21
- return { width, height, data };
22
- }
23
-
24
- function countInkPixels(bitmap: { widthPx: number; heightPx: number; data: Uint8Array }): number {
25
- let n = 0;
26
- for (let y = 0; y < bitmap.heightPx; y++) {
27
- for (let x = 0; x < bitmap.widthPx; x++) {
28
- if (getPixel(bitmap, x, y)) n++;
29
- }
30
- }
31
- return n;
32
- }
33
-
34
- describe('isRedish', () => {
35
- it('treats a strong red as red', () => {
36
- expect(isRedish(255, 0, 0, 255)).toBe(true);
37
- });
38
-
39
- it('rejects red when green is too high (threshold g < 100)', () => {
40
- expect(isRedish(255, 100, 0, 255)).toBe(false);
41
- expect(isRedish(255, 99, 0, 255)).toBe(true);
42
- });
43
-
44
- it('rejects red when blue is too high (threshold b < 100)', () => {
45
- expect(isRedish(255, 0, 100, 255)).toBe(false);
46
- expect(isRedish(255, 0, 99, 255)).toBe(true);
47
- });
48
-
49
- it('rejects red when the red channel is too low (threshold r > 180)', () => {
50
- expect(isRedish(180, 0, 0, 255)).toBe(false);
51
- expect(isRedish(181, 0, 0, 255)).toBe(true);
52
- });
53
-
54
- it('rejects transparent pixels (alpha < 128)', () => {
55
- expect(isRedish(255, 0, 0, 127)).toBe(false);
56
- expect(isRedish(255, 0, 0, 128)).toBe(true);
57
- });
58
-
59
- it('rejects black (no red dominance)', () => {
60
- expect(isRedish(0, 0, 0, 255)).toBe(false);
61
- });
62
-
63
- it('rejects white', () => {
64
- expect(isRedish(255, 255, 255, 255)).toBe(false);
65
- });
66
- });
67
-
68
- describe('splitTwoColor', () => {
69
- it('routes a solid red image to the red plane, nothing to black', () => {
70
- const { black, red } = splitTwoColor(rgbaOf(8, 8, [255, 0, 0, 255]));
71
- expect(countInkPixels(red)).toBeGreaterThan(0);
72
- expect(countInkPixels(black)).toBe(0);
73
- });
74
-
75
- it('routes a solid black image to the black plane, nothing to red', () => {
76
- const { black, red } = splitTwoColor(rgbaOf(8, 8, [0, 0, 0, 255]));
77
- expect(countInkPixels(black)).toBeGreaterThan(0);
78
- expect(countInkPixels(red)).toBe(0);
79
- });
80
-
81
- it('produces bitmaps matching the source dimensions', () => {
82
- const { black, red } = splitTwoColor(rgbaOf(16, 12, [128, 128, 128, 255]));
83
- expect(black.widthPx).toBe(16);
84
- expect(black.heightPx).toBe(12);
85
- expect(red.widthPx).toBe(16);
86
- expect(red.heightPx).toBe(12);
87
- });
88
-
89
- it('resolves overlapping bits in favour of black (red bit cleared)', () => {
90
- // Construct a mixed image: one row red, one row black.
91
- const data = new Uint8Array(8 * 2 * 4);
92
- // Row 0: red
93
- for (let i = 0; i < 8; i++) {
94
- data[i * 4] = 255;
95
- data[i * 4 + 3] = 255;
96
- }
97
- // Row 1: black
98
- for (let i = 0; i < 8; i++) {
99
- const offset = (8 + i) * 4;
100
- data[offset] = 0;
101
- data[offset + 1] = 0;
102
- data[offset + 2] = 0;
103
- data[offset + 3] = 255;
104
- }
105
- const image = { width: 8, height: 2, data };
106
- const { black, red } = splitTwoColor(image);
107
- // Every non-transparent pixel should land on exactly one plane —
108
- // resolveOverlap guarantees no bit is set in both.
109
- for (let y = 0; y < 2; y++) {
110
- for (let x = 0; x < 8; x++) {
111
- const inBlack = getPixel(black, x, y);
112
- const inRed = getPixel(red, x, y);
113
- expect(inBlack && inRed).toBe(false);
114
- }
115
- }
116
- });
117
-
118
- it('accepts custom threshold + dither options', () => {
119
- const { black, red } = splitTwoColor(rgbaOf(4, 4, [0, 0, 0, 255]), {
120
- threshold: 64,
121
- dither: false,
122
- });
123
- expect(black.widthPx).toBe(4);
124
- expect(red.widthPx).toBe(4);
125
- });
126
- });
package/src/colour.ts DELETED
@@ -1,101 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion --
2
- * Uint8Array indexed reads are typed `number | undefined` under
3
- * `noUncheckedIndexedAccess`, but every index in this file is bounded
4
- * by `src.length` (a multiple of 4) or `Math.min(a.length, b.length)`.
5
- * The `!` assertions collapse the unreachable `undefined` branches so
6
- * branch coverage doesn't count them. See also `non-nullable-type-
7
- * assertion-style` which rules out the alternative `as number` form.
8
- */
9
- import { renderImage, type LabelBitmap, type RawImageData } from '@mbtech-nl/bitmap';
10
-
11
- export interface TwoColorResult {
12
- black: LabelBitmap;
13
- red: LabelBitmap;
14
- }
15
-
16
- export interface TwoColorOptions {
17
- threshold?: number;
18
- dither?: boolean;
19
- }
20
-
21
- /**
22
- * A pixel is "red-ish" when the red channel clearly dominates and
23
- * alpha is opaque enough to matter. The thresholds come from testing
24
- * against DK-22251 output — wider tolerances let non-red warm tones
25
- * bleed into the red plane.
26
- */
27
- export function isRedish(r: number, g: number, b: number, a: number): boolean {
28
- if (a < 128) return false;
29
- return r > 180 && g < 100 && b < 100;
30
- }
31
-
32
- function extractRedPixels(image: RawImageData): RawImageData {
33
- const src = image.data;
34
- const dst = new Uint8Array(src.length);
35
- for (let i = 0; i < src.length; i += 4) {
36
- const r = src[i]!;
37
- const g = src[i + 1]!;
38
- const b = src[i + 2]!;
39
- const a = src[i + 3]!;
40
- if (isRedish(r, g, b, a)) {
41
- dst[i] = r;
42
- dst[i + 1] = g;
43
- dst[i + 2] = b;
44
- dst[i + 3] = a;
45
- }
46
- }
47
- return { data: dst, width: image.width, height: image.height };
48
- }
49
-
50
- function extractNonRedPixels(image: RawImageData): RawImageData {
51
- const src = image.data;
52
- const dst = new Uint8Array(src.length);
53
- for (let i = 0; i < src.length; i += 4) {
54
- const r = src[i]!;
55
- const g = src[i + 1]!;
56
- const b = src[i + 2]!;
57
- const a = src[i + 3]!;
58
- if (!isRedish(r, g, b, a)) {
59
- dst[i] = r;
60
- dst[i + 1] = g;
61
- dst[i + 2] = b;
62
- dst[i + 3] = a;
63
- }
64
- }
65
- return { data: dst, width: image.width, height: image.height };
66
- }
67
-
68
- /**
69
- * Where both planes have a set bit at the same position, black wins.
70
- * Done by masking the red bits with the inverse of the black bits.
71
- */
72
- function resolveOverlap(black: LabelBitmap, red: LabelBitmap): void {
73
- const blackData = black.data;
74
- const redData = red.data;
75
- const len = Math.min(blackData.length, redData.length);
76
- for (let i = 0; i < len; i++) {
77
- redData[i] = redData[i]! & ~blackData[i]!;
78
- }
79
- }
80
-
81
- /**
82
- * Split an RGBA image into black and red 1bpp planes for Brother QL
83
- * two-colour media (DK-22251).
84
- *
85
- * The rendering path matches `renderImage(image, { dither: true })`
86
- * used by `print()` for single-colour media, so overall print density
87
- * stays consistent regardless of media.
88
- */
89
- export function splitTwoColor(image: RawImageData, options?: TwoColorOptions): TwoColorResult {
90
- const { threshold = 128, dither = true } = options ?? {};
91
-
92
- const blackImage = extractNonRedPixels(image);
93
- const redImage = extractRedPixels(image);
94
-
95
- const black = renderImage(blackImage, { threshold, dither });
96
- const red = renderImage(redImage, { threshold, dither });
97
-
98
- resolveOverlap(black, red);
99
-
100
- return { black, red };
101
- }