@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.
- package/dist/__tests__/media.test.js +23 -3
- 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.js +1 -1
- package/dist/__tests__/preview.test.js.map +1 -1
- package/dist/__tests__/protocol.test.js +46 -1
- package/dist/__tests__/protocol.test.js.map +1 -1
- package/dist/devices.d.ts +1 -1
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.js +0 -1
- package/dist/devices.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +18 -6
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +47 -29
- 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 +6 -6
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +11 -12
- package/dist/preview.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +13 -7
- package/dist/protocol.js.map +1 -1
- package/dist/status.d.ts +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +0 -1
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +15 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -2
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/media.test.ts +25 -3
- package/src/__tests__/pack-bits.test.ts +92 -0
- package/src/__tests__/preview.test.ts +1 -1
- package/src/__tests__/protocol.test.ts +47 -1
- package/src/devices.ts +1 -1
- package/src/index.ts +16 -4
- package/src/media.ts +48 -29
- package/src/orientation.ts +11 -0
- package/src/pack-bits.ts +64 -0
- package/src/preview.ts +13 -12
- package/src/protocol.ts +14 -7
- package/src/status.ts +1 -2
- package/src/types.ts +21 -6
- package/dist/__tests__/colour.test.d.ts +0 -2
- package/dist/__tests__/colour.test.d.ts.map +0 -1
- package/dist/__tests__/colour.test.js +0 -106
- package/dist/__tests__/colour.test.js.map +0 -1
- package/dist/colour.d.ts +0 -26
- package/dist/colour.d.ts.map +0 -1
- package/dist/colour.js +0 -84
- package/dist/colour.js.map +0 -1
- package/src/__tests__/colour.test.ts +0 -126
- package/src/colour.ts +0 -101
package/src/preview.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
*
|
|
11
|
-
* image is split
|
|
12
|
-
*
|
|
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.
|
|
17
|
-
const
|
|
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 {
|
|
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
|
-
//
|
|
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.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
|
-
|
|
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,6 +1,5 @@
|
|
|
1
|
-
/* eslint-disable import-x/consistent-type-specifier-style */
|
|
2
1
|
import type { PrinterError } from '@thermal-label/contracts';
|
|
3
|
-
import {
|
|
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
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
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.
|
|
43
|
-
*
|
|
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 +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"}
|
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
|
package/dist/colour.d.ts.map
DELETED
|
@@ -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
|
package/dist/colour.js.map
DELETED
|
@@ -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
|
-
}
|