@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/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
- }