@thermal-label/brother-ql-web 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mannes Brak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @thermal-label/brother-ql-web
2
+
3
+ WebUSB browser driver for Brother QL label printers.
4
+
5
+ ## Browser Support
6
+
7
+ | Browser | Support |
8
+ | ------------- | ------- |
9
+ | Chrome / Edge | ✅ |
10
+ | Firefox | ❌ |
11
+ | Safari | ❌ |
12
+
13
+ Requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (`https://` or `localhost`).
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add @thermal-label/brother-ql-web
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```ts
24
+ import { requestPrinter, findMedia } from '@thermal-label/brother-ql-web';
25
+
26
+ const media = findMedia(259)!; // 62mm continuous
27
+ const printer = await requestPrinter();
28
+ await printer.printText('Hello WebUSB', media);
29
+ ```
30
+
31
+ ## Two-Color Printing
32
+
33
+ Requires a QL-800, QL-810W, or QL-820NWB with DK-22251 labels.
34
+
35
+ ```ts
36
+ const blackCanvas = document.getElementById('black-layer') as HTMLCanvasElement;
37
+ const redCanvas = document.getElementById('red-layer') as HTMLCanvasElement;
38
+ const blackCtx = blackCanvas.getContext('2d')!;
39
+ const redCtx = redCanvas.getContext('2d')!;
40
+
41
+ await printer.printTwoColor(
42
+ blackCtx.getImageData(0, 0, blackCanvas.width, blackCanvas.height),
43
+ redCtx.getImageData(0, 0, redCanvas.width, redCanvas.height),
44
+ media,
45
+ );
46
+ ```
47
+
48
+ ## API
49
+
50
+ ### `requestPrinter(options?)`
51
+
52
+ Opens the browser's USB device picker filtered to all known Brother QL PIDs.
53
+ Returns a `WebBrotherQLPrinter`.
54
+
55
+ ### `fromUSBDevice(device)`
56
+
57
+ Wraps an already-obtained `USBDevice`. The caller is responsible for opening and claiming the interface.
58
+
59
+ ### `WebBrotherQLPrinter`
60
+
61
+ | Method | Description |
62
+ | -------------------------------------------- | ---------------------------------------- |
63
+ | `print(pages, options?)` | Send a pre-encoded job |
64
+ | `printText(text, media, options?)` | Render and print a text label |
65
+ | `printImage(imageData, media, options?)` | Print from `ImageData` |
66
+ | `printImageURL(url, media, options?)` | Fetch URL and print |
67
+ | `printTwoColor(black, red, media, options?)` | Two-color label (QL-800 series) |
68
+ | `getStatus()` | Query printer status |
69
+ | `isConnected()` | Returns `true` if the USB device is open |
70
+ | `disconnect()` | Release interface and close device |
71
+
72
+ ## Requirements
73
+
74
+ - Chromium-based browser (Chrome 61+, Edge 79+)
75
+ - Secure context (`https://` or `localhost`)
76
+ - User gesture required to call `requestPrinter()`
77
+
78
+ ## License
79
+
80
+ MIT © Mannes Brak
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=printer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"printer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/printer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,156 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { WebBrotherQLPrinter, openWebDevice } from '../printer.js';
3
+ import { DEVICES, findMedia } from '@thermal-label/brother-ql-core';
4
+ import { createMockUSBDevice } from './webusb-mock.js';
5
+ const media62mm = findMedia(259);
6
+ function makeImageData(width = 10, height = 10) {
7
+ return { width, height, data: new Uint8ClampedArray(width * height * 4) };
8
+ }
9
+ describe('WebBrotherQLPrinter', () => {
10
+ let mock;
11
+ beforeEach(() => {
12
+ mock = createMockUSBDevice({ productId: 0x20a7 }); // QL-820NWB
13
+ });
14
+ afterEach(() => {
15
+ vi.unstubAllGlobals();
16
+ });
17
+ it('openWebDevice opens and claims interface', async () => {
18
+ const printer = await openWebDevice(mock.device);
19
+ expect(mock.spies.open).toHaveBeenCalledOnce();
20
+ expect(mock.spies.selectConfiguration).toHaveBeenCalledWith(1);
21
+ expect(mock.spies.claimInterface).toHaveBeenCalledWith(0);
22
+ expect(printer.descriptor.name).toBe('QL-820NWB');
23
+ });
24
+ it('openWebDevice throws for unknown device', async () => {
25
+ const { device } = createMockUSBDevice({ vendorId: 0x04f9, productId: 0x9999 });
26
+ await expect(openWebDevice(device)).rejects.toThrow('Unsupported device');
27
+ });
28
+ it('print writes encoded bytes via transferOut', async () => {
29
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
30
+ const bitmap = { widthPx: 720, heightPx: 10, data: new Uint8Array(90 * 10) };
31
+ await printer.print([{ bitmap, media: media62mm }]);
32
+ expect(mock.spies.transferOut).toHaveBeenCalled();
33
+ const bytes = mock.spies.transferOut.mock.calls[0]?.[1];
34
+ // Job starts with ESC i a 01 (raster mode) then 200-byte invalidate
35
+ expect(bytes[0]).toBe(0x1b);
36
+ expect(bytes[1]).toBe(0x69);
37
+ expect(bytes[2]).toBe(0x61);
38
+ // Ends with 0x1A (print last page)
39
+ expect(bytes.at(-1)).toBe(0x1a);
40
+ });
41
+ it('getStatus sends status request and parses response', async () => {
42
+ const statusBytes = new Uint8Array(32);
43
+ statusBytes[0] = 0x80;
44
+ statusBytes[10] = 62; // 62mm media
45
+ statusBytes[11] = 0x0a; // continuous
46
+ mock.spies.transferIn.mockResolvedValueOnce({
47
+ status: 'ok',
48
+ data: new DataView(statusBytes.buffer),
49
+ });
50
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
51
+ const status = await printer.getStatus();
52
+ const outBytes = mock.spies.transferOut.mock.calls[0]?.[1];
53
+ expect(outBytes).toEqual(new Uint8Array([0x1b, 0x69, 0x53]));
54
+ expect(status.mediaWidthMm).toBe(62);
55
+ expect(status.mediaType).toBe('continuous');
56
+ });
57
+ it('getStatus throws when no data is returned', async () => {
58
+ mock.spies.transferIn.mockResolvedValueOnce({ status: 'ok', data: null });
59
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
60
+ await expect(printer.getStatus()).rejects.toThrow('No status data received');
61
+ });
62
+ it('disconnect releases interface and closes device', async () => {
63
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
64
+ await printer.disconnect();
65
+ expect(mock.spies.releaseInterface).toHaveBeenCalledWith(0);
66
+ expect(mock.spies.close).toHaveBeenCalledOnce();
67
+ });
68
+ it('isConnected reflects device opened state', async () => {
69
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
70
+ expect(printer.isConnected()).toBe(false);
71
+ await mock.device.open();
72
+ expect(printer.isConnected()).toBe(true);
73
+ });
74
+ it('printText() renders text and sends to device', async () => {
75
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
76
+ await printer.printText('hello', media62mm);
77
+ expect(mock.spies.transferOut).toHaveBeenCalled();
78
+ });
79
+ it('printText() passes PageOptions through to print', async () => {
80
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
81
+ await printer.printText('hi', media62mm, { autoCut: true });
82
+ expect(mock.spies.transferOut).toHaveBeenCalled();
83
+ });
84
+ it('printText() forwards invert and scale options', async () => {
85
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
86
+ await printer.printText('test', media62mm, { invert: true, scaleX: 2, scaleY: 2 });
87
+ expect(mock.spies.transferOut).toHaveBeenCalled();
88
+ });
89
+ it('printImage() renders ImageData and sends to device', async () => {
90
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
91
+ await printer.printImage(makeImageData(), media62mm);
92
+ expect(mock.spies.transferOut).toHaveBeenCalled();
93
+ });
94
+ it('printImage() passes renderImage options', async () => {
95
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
96
+ await printer.printImage(makeImageData(), media62mm, {
97
+ threshold: 100,
98
+ dither: true,
99
+ invert: true,
100
+ });
101
+ expect(mock.spies.transferOut).toHaveBeenCalled();
102
+ });
103
+ it('printImage() applies explicit rotation when provided', async () => {
104
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
105
+ await printer.printImage(makeImageData(), media62mm, { rotate: 180 });
106
+ expect(mock.spies.transferOut).toHaveBeenCalled();
107
+ });
108
+ it('printImage() passes PageOptions through to print', async () => {
109
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
110
+ await printer.printImage(makeImageData(), media62mm, { autoCut: true });
111
+ expect(mock.spies.transferOut).toHaveBeenCalled();
112
+ });
113
+ it('printImageURL() fetches URL and prints via canvas', async () => {
114
+ const blobMock = new Blob();
115
+ const bmpMock = { width: 10, height: 10 };
116
+ const ctxMock = {
117
+ drawImage: vi.fn(),
118
+ getImageData: vi.fn().mockReturnValue(makeImageData()),
119
+ };
120
+ const canvasMock = { getContext: vi.fn().mockReturnValue(ctxMock) };
121
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ blob: vi.fn().mockResolvedValue(blobMock) }));
122
+ vi.stubGlobal('createImageBitmap', vi.fn().mockResolvedValue(bmpMock));
123
+ vi.stubGlobal('OffscreenCanvas', vi.fn().mockReturnValue(canvasMock));
124
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
125
+ await printer.printImageURL('https://example.com/label.png', media62mm);
126
+ expect(mock.spies.transferOut).toHaveBeenCalled();
127
+ });
128
+ it('printImageURL() throws when canvas context cannot be obtained', async () => {
129
+ const blobMock = new Blob();
130
+ const bmpMock = { width: 10, height: 10 };
131
+ const canvasMock = { getContext: vi.fn().mockReturnValue(null) };
132
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ blob: vi.fn().mockResolvedValue(blobMock) }));
133
+ vi.stubGlobal('createImageBitmap', vi.fn().mockResolvedValue(bmpMock));
134
+ vi.stubGlobal('OffscreenCanvas', vi.fn().mockReturnValue(canvasMock));
135
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
136
+ await expect(printer.printImageURL('https://example.com/label.png', media62mm)).rejects.toThrow('Could not get canvas context');
137
+ });
138
+ it('printTwoColor throws on non-two-color device', async () => {
139
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_700);
140
+ const imgData = makeImageData();
141
+ await expect(printer.printTwoColor(imgData, imgData, media62mm)).rejects.toThrow('does not support two-color');
142
+ });
143
+ it('printTwoColor() renders and prints both channels', async () => {
144
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
145
+ const imgData = makeImageData();
146
+ await printer.printTwoColor(imgData, imgData, media62mm);
147
+ expect(mock.spies.transferOut).toHaveBeenCalled();
148
+ });
149
+ it('printTwoColor() passes options through to print', async () => {
150
+ const printer = new WebBrotherQLPrinter(mock.device, DEVICES.QL_820NWB);
151
+ const imgData = makeImageData();
152
+ await printer.printTwoColor(imgData, imgData, media62mm, { autoCut: true });
153
+ expect(mock.spies.transferOut).toHaveBeenCalled();
154
+ });
155
+ });
156
+ //# sourceMappingURL=printer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"printer.test.js","sourceRoot":"","sources":["../../src/__tests__/printer.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=request.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/request.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { DEVICES } from '@thermal-label/brother-ql-core';
3
+ import { fromUSBDevice } from '../request.js';
4
+ import { createMockUSBDevice } from './webusb-mock.js';
5
+ const mockRequestDevice = vi.fn();
6
+ Object.defineProperty(globalThis, 'navigator', {
7
+ value: { usb: { requestDevice: mockRequestDevice } },
8
+ writable: true,
9
+ });
10
+ beforeEach(() => {
11
+ mockRequestDevice.mockClear();
12
+ });
13
+ describe('requestPrinter', () => {
14
+ it('passes all known printer-class PIDs to requestDevice', async () => {
15
+ const { device } = createMockUSBDevice({ productId: 0x20a7 });
16
+ mockRequestDevice.mockResolvedValueOnce(device);
17
+ const { requestPrinter } = await import('../request.js');
18
+ await requestPrinter();
19
+ expect(mockRequestDevice).toHaveBeenCalledOnce();
20
+ const [opts] = mockRequestDevice.mock.calls[0];
21
+ const pids = opts.filters.map(f => f.productId);
22
+ for (const d of Object.values(DEVICES)) {
23
+ expect(pids).toContain(d.pid);
24
+ }
25
+ });
26
+ it('does not include mass-storage PIDs in filters', async () => {
27
+ const { device } = createMockUSBDevice({ productId: 0x20a7 });
28
+ mockRequestDevice.mockResolvedValueOnce(device);
29
+ const { requestPrinter } = await import('../request.js');
30
+ await requestPrinter();
31
+ const [opts] = mockRequestDevice.mock.calls[0];
32
+ const pids = opts.filters.map(f => f.productId);
33
+ expect(pids).not.toContain(0x20aa);
34
+ expect(pids).not.toContain(0x20ab);
35
+ });
36
+ it('uses custom filters when provided', async () => {
37
+ const { device } = createMockUSBDevice({ productId: 0x20a7 });
38
+ mockRequestDevice.mockResolvedValueOnce(device);
39
+ const customFilters = [{ vendorId: 0x04f9, productId: 0x20a7 }];
40
+ const { requestPrinter } = await import('../request.js');
41
+ await requestPrinter({ filters: customFilters });
42
+ const [opts] = mockRequestDevice.mock.calls[0];
43
+ expect(opts.filters).toEqual(customFilters);
44
+ });
45
+ });
46
+ describe('fromUSBDevice', () => {
47
+ it('returns WebBrotherQLPrinter for known device', () => {
48
+ const { device } = createMockUSBDevice({ productId: 0x20a7 });
49
+ const printer = fromUSBDevice(device);
50
+ expect(printer.descriptor.name).toBe('QL-820NWB');
51
+ });
52
+ it('throws for unknown product ID', () => {
53
+ const { device } = createMockUSBDevice({ productId: 0x9999 });
54
+ expect(() => fromUSBDevice(device)).toThrow('Unsupported device');
55
+ });
56
+ });
57
+ //# sourceMappingURL=request.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.test.js","sourceRoot":"","sources":["../../src/__tests__/request.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { vi } from 'vitest';
2
+ export interface MockUSBDeviceSpies {
3
+ open: ReturnType<typeof vi.fn>;
4
+ close: ReturnType<typeof vi.fn>;
5
+ selectConfiguration: ReturnType<typeof vi.fn>;
6
+ claimInterface: ReturnType<typeof vi.fn>;
7
+ releaseInterface: ReturnType<typeof vi.fn>;
8
+ transferOut: ReturnType<typeof vi.fn>;
9
+ transferIn: ReturnType<typeof vi.fn>;
10
+ }
11
+ export interface MockUSBDevice {
12
+ device: USBDevice;
13
+ spies: MockUSBDeviceSpies;
14
+ }
15
+ export declare function createMockUSBDevice(overrides?: {
16
+ vendorId?: number;
17
+ productId?: number;
18
+ serialNumber?: string;
19
+ statusBytes?: Uint8Array;
20
+ }): MockUSBDevice;
21
+ //# sourceMappingURL=webusb-mock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webusb-mock.d.ts","sourceRoot":"","sources":["../../src/__tests__/webusb-mock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE5B,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/B,KAAK,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAChC,mBAAmB,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9C,cAAc,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACzC,gBAAgB,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC3C,WAAW,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACtC,UAAU,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,SAAS,CAAC,EAAE;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B,GAAG,aAAa,CA8ChB"}
@@ -0,0 +1,44 @@
1
+ import { vi } from 'vitest';
2
+ export function createMockUSBDevice(overrides) {
3
+ const spies = {
4
+ open: vi.fn().mockImplementation(() => Promise.resolve()),
5
+ close: vi.fn().mockImplementation(() => Promise.resolve()),
6
+ selectConfiguration: vi.fn().mockImplementation(() => Promise.resolve()),
7
+ claimInterface: vi.fn().mockImplementation(() => Promise.resolve()),
8
+ releaseInterface: vi.fn().mockImplementation(() => Promise.resolve()),
9
+ transferOut: vi
10
+ .fn()
11
+ .mockImplementation(() => Promise.resolve({ bytesWritten: 0, status: 'ok' })),
12
+ transferIn: vi.fn().mockImplementation(() => Promise.resolve({
13
+ status: 'ok',
14
+ data: new DataView((overrides?.statusBytes ?? new Uint8Array(32)).buffer),
15
+ })),
16
+ };
17
+ let opened = false;
18
+ spies.open.mockImplementation(() => {
19
+ opened = true;
20
+ return Promise.resolve();
21
+ });
22
+ spies.close.mockImplementation(() => {
23
+ opened = false;
24
+ return Promise.resolve();
25
+ });
26
+ const device = {
27
+ vendorId: overrides?.vendorId ?? 0x04f9,
28
+ productId: overrides?.productId ?? 0x20a7,
29
+ serialNumber: overrides?.serialNumber,
30
+ configuration: { interfaces: [{ interfaceNumber: 0, claimed: false }] },
31
+ get opened() {
32
+ return opened;
33
+ },
34
+ open: spies.open,
35
+ close: spies.close,
36
+ selectConfiguration: spies.selectConfiguration,
37
+ claimInterface: spies.claimInterface,
38
+ releaseInterface: spies.releaseInterface,
39
+ transferOut: spies.transferOut,
40
+ transferIn: spies.transferIn,
41
+ };
42
+ return { device, spies };
43
+ }
44
+ //# sourceMappingURL=webusb-mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webusb-mock.js","sourceRoot":"","sources":["../../src/__tests__/webusb-mock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAiB5B,MAAM,UAAU,mBAAmB,CAAC,SAKnC;IACC,MAAM,KAAK,GAAuB;QAChC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACzD,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAC1D,mBAAmB,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACxE,cAAc,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACnE,gBAAgB,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACrE,WAAW,EAAE,EAAE;aACZ,EAAE,EAAE;aACJ,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/E,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAC1C,OAAO,CAAC,OAAO,CAAC;YACd,MAAM,EAAE,IAAI;YACZ,IAAI,EAAE,IAAI,QAAQ,CAAC,CAAC,SAAS,EAAE,WAAW,IAAI,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;SAC1E,CAAC,CACH;KACF,CAAC;IAEF,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE;QACjC,MAAM,GAAG,IAAI,CAAC;QACd,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAAC,GAAG,EAAE;QAClC,MAAM,GAAG,KAAK,CAAC;QACf,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG;QACb,QAAQ,EAAE,SAAS,EAAE,QAAQ,IAAI,MAAM;QACvC,SAAS,EAAE,SAAS,EAAE,SAAS,IAAI,MAAM;QACzC,YAAY,EAAE,SAAS,EAAE,YAAY;QACrC,aAAa,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;QACvE,IAAI,MAAM;YACR,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,mBAAmB,EAAE,KAAK,CAAC,mBAAmB;QAC9C,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;QACxC,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;KACL,CAAC;IAE1B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { WebBrotherQLPrinter } from './printer.js';
2
+ export { requestPrinter, fromUSBDevice } from './request.js';
3
+ export type { RequestOptions } from './request.js';
4
+ export type { DeviceDescriptor, MediaDescriptor, PageData, PageOptions, JobOptions, PrinterStatus, TextPrintOptions, ImagePrintOptions, MediaType, } from '@thermal-label/brother-ql-core';
5
+ export { DEVICES, MEDIA, findDevice, findMedia, findMediaByWidth, renderText, renderImage, rotateBitmap, } from '@thermal-label/brother-ql-core';
6
+ export type { LabelBitmap, RawImageData } from '@thermal-label/brother-ql-core';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACR,WAAW,EACX,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,GACV,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACL,OAAO,EACP,KAAK,EACL,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,gCAAgC,CAAC;AACxC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { WebBrotherQLPrinter } from './printer.js';
2
+ export { requestPrinter, fromUSBDevice } from './request.js';
3
+ export { DEVICES, MEDIA, findDevice, findMedia, findMediaByWidth, renderText, renderImage, rotateBitmap, } from '@thermal-label/brother-ql-core';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAe7D,OAAO,EACL,OAAO,EACP,KAAK,EACL,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,gCAAgC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type DeviceDescriptor, type JobOptions, type MediaDescriptor, type PageData, type PageOptions, type PrinterStatus, type TextPrintOptions, type ImagePrintOptions } from '@thermal-label/brother-ql-core';
2
+ export declare class WebBrotherQLPrinter {
3
+ readonly device: USBDevice;
4
+ readonly descriptor: DeviceDescriptor;
5
+ constructor(device: USBDevice, descriptor: DeviceDescriptor);
6
+ isConnected(): boolean;
7
+ getStatus(): Promise<PrinterStatus>;
8
+ print(pages: PageData[], options?: JobOptions): Promise<void>;
9
+ printText(text: string, media: MediaDescriptor, options?: TextPrintOptions): Promise<void>;
10
+ printImage(imageData: ImageData, media: MediaDescriptor, options?: ImagePrintOptions): Promise<void>;
11
+ printImageURL(url: string, media: MediaDescriptor, options?: ImagePrintOptions): Promise<void>;
12
+ printTwoColor(blackImageData: ImageData, redImageData: ImageData, media: MediaDescriptor, options?: PageOptions): Promise<void>;
13
+ disconnect(): Promise<void>;
14
+ }
15
+ export declare function openWebDevice(device: USBDevice): Promise<WebBrotherQLPrinter>;
16
+ //# sourceMappingURL=printer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"printer.d.ts","sourceRoot":"","sources":["../src/printer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAQvB,MAAM,gCAAgC,CAAC;AASxC,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;gBAE1B,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB;IAK3D,WAAW,IAAI,OAAO;IAIhB,SAAS,IAAI,OAAO,CAAC,aAAa,CAAC;IAOnC,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7D,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB1F,UAAU,CACd,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC;IAuBV,aAAa,CACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC;IAYV,aAAa,CACjB,cAAc,EAAE,SAAS,EACzB,YAAY,EAAE,SAAS,EACvB,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC;IAuBV,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAIlC;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAWnF"}
@@ -0,0 +1,114 @@
1
+ import { encodeJob, findDevice, parseStatus, renderImage, renderText, rotateBitmap, STATUS_REQUEST, } from '@thermal-label/brother-ql-core';
2
+ const BROTHER_VID = 0x04f9;
3
+ const USB_INTERFACE = 0;
4
+ const BULK_OUT_ENDPOINT = 2;
5
+ const BULK_IN_ENDPOINT = 1;
6
+ const STATUS_BYTE_COUNT = 32;
7
+ const CONFIGURATION_VALUE = 1;
8
+ export class WebBrotherQLPrinter {
9
+ device;
10
+ descriptor;
11
+ constructor(device, descriptor) {
12
+ this.device = device;
13
+ this.descriptor = descriptor;
14
+ }
15
+ isConnected() {
16
+ return this.device.opened;
17
+ }
18
+ async getStatus() {
19
+ await this.device.transferOut(BULK_OUT_ENDPOINT, STATUS_REQUEST);
20
+ const result = await this.device.transferIn(BULK_IN_ENDPOINT, STATUS_BYTE_COUNT);
21
+ if (!result.data)
22
+ throw new Error('No status data received');
23
+ return parseStatus(new Uint8Array(result.data.buffer));
24
+ }
25
+ async print(pages, options) {
26
+ const bytes = encodeJob(pages, options);
27
+ await this.device.transferOut(BULK_OUT_ENDPOINT, bytes);
28
+ }
29
+ async printText(text, media, options) {
30
+ const { invert, scaleX, scaleY, ...pageOptions } = options ?? {};
31
+ const base = renderText(text, { scaleX: 1, scaleY: 1 });
32
+ const autoScale = Math.max(1, Math.floor(media.printAreaDots / Math.max(base.widthPx, base.heightPx)));
33
+ const rawBitmap = renderText(text, {
34
+ ...(invert !== undefined ? { invert } : {}),
35
+ scaleX: scaleX ?? autoScale,
36
+ scaleY: scaleY ?? autoScale,
37
+ });
38
+ const bitmap = rotateBitmap(rawBitmap, 270);
39
+ const page = {
40
+ bitmap,
41
+ media,
42
+ ...(Object.keys(pageOptions).length > 0 ? { options: pageOptions } : {}),
43
+ };
44
+ await this.print([page]);
45
+ }
46
+ async printImage(imageData, media, options) {
47
+ const { threshold, dither, invert, rotate, ...pageOptions } = options ?? {};
48
+ const rawImage = {
49
+ width: imageData.width,
50
+ height: imageData.height,
51
+ data: new Uint8Array(imageData.data.buffer),
52
+ };
53
+ const rawBitmap = renderImage(rawImage, {
54
+ ...(threshold !== undefined ? { threshold } : {}),
55
+ ...(dither ? { dither: true } : {}),
56
+ ...(invert ? { invert: true } : {}),
57
+ });
58
+ const rotationAngle = rotate ?? 0;
59
+ const bitmap = rotationAngle === 0 ? rotateBitmap(rawBitmap, 270) : rotateBitmap(rawBitmap, rotationAngle);
60
+ const page = {
61
+ bitmap,
62
+ media,
63
+ ...(Object.keys(pageOptions).length > 0 ? { options: pageOptions } : {}),
64
+ };
65
+ await this.print([page]);
66
+ }
67
+ async printImageURL(url, media, options) {
68
+ const response = await fetch(url);
69
+ const blob = await response.blob();
70
+ const bmp = await createImageBitmap(blob);
71
+ const canvas = new OffscreenCanvas(bmp.width, bmp.height);
72
+ const ctx = canvas.getContext('2d');
73
+ if (!ctx)
74
+ throw new Error('Could not get canvas context');
75
+ ctx.drawImage(bmp, 0, 0);
76
+ const imageData = ctx.getImageData(0, 0, bmp.width, bmp.height);
77
+ await this.printImage(imageData, media, options);
78
+ }
79
+ async printTwoColor(blackImageData, redImageData, media, options) {
80
+ if (!this.descriptor.twoColor) {
81
+ throw new Error(`Device ${this.descriptor.name} does not support two-color printing. ` +
82
+ 'Use a QL-800, QL-810W, or QL-820NWB.');
83
+ }
84
+ const toRaw = (img) => ({
85
+ width: img.width,
86
+ height: img.height,
87
+ data: new Uint8Array(img.data.buffer),
88
+ });
89
+ const blackBitmap = rotateBitmap(renderImage(toRaw(blackImageData)), 270);
90
+ const redBitmap = rotateBitmap(renderImage(toRaw(redImageData)), 270);
91
+ const page = {
92
+ bitmap: blackBitmap,
93
+ redBitmap,
94
+ media,
95
+ ...(options !== undefined ? { options } : {}),
96
+ };
97
+ await this.print([page]);
98
+ }
99
+ async disconnect() {
100
+ await this.device.releaseInterface(USB_INTERFACE);
101
+ await this.device.close();
102
+ }
103
+ }
104
+ export async function openWebDevice(device) {
105
+ const descriptor = findDevice(BROTHER_VID, device.productId);
106
+ if (!descriptor) {
107
+ throw new Error(`Unsupported device: VID=${BROTHER_VID.toString(16)} PID=${device.productId.toString(16)}`);
108
+ }
109
+ await device.open();
110
+ await device.selectConfiguration(CONFIGURATION_VALUE);
111
+ await device.claimInterface(USB_INTERFACE);
112
+ return new WebBrotherQLPrinter(device, descriptor);
113
+ }
114
+ //# sourceMappingURL=printer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"printer.js","sourceRoot":"","sources":["../src/printer.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,SAAS,EACT,UAAU,EACV,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,gCAAgC,CAAC;AAExC,MAAM,WAAW,GAAG,MAAM,CAAC;AAC3B,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,MAAM,OAAO,mBAAmB;IACrB,MAAM,CAAY;IAClB,UAAU,CAAmB;IAEtC,YAAY,MAAiB,EAAE,UAA4B;QACzD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAAC;QACjF,IAAI,CAAC,MAAM,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7D,OAAO,WAAW,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAiB,EAAE,OAAoB;QACjD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACxC,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,iBAAiB,EAAE,KAAK,CAAC,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,KAAsB,EAAE,OAA0B;QAC9E,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QACxD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CACxB,CAAC,EACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CACxE,CAAC;QACF,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,EAAE;YACjC,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,MAAM,EAAE,MAAM,IAAI,SAAS;YAC3B,MAAM,EAAE,MAAM,IAAI,SAAS;SAC5B,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAC5C,MAAM,IAAI,GAAa;YACrB,MAAM;YACN,KAAK;YACL,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACzE,CAAC;QACF,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,UAAU,CACd,SAAoB,EACpB,KAAsB,EACtB,OAA2B;QAE3B,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;QAC5E,MAAM,QAAQ,GAAG;YACf,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,MAAM,EAAE,SAAS,CAAC,MAAM;YACxB,IAAI,EAAE,IAAI,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;SAC5C,CAAC;QACF,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,EAAE;YACtC,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpC,CAAC,CAAC;QACH,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,CAAC;QAClC,MAAM,MAAM,GACV,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAC9F,MAAM,IAAI,GAAa;YACrB,MAAM;YACN,KAAK;YACL,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACzE,CAAC;QACF,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,GAAW,EACX,KAAsB,EACtB,OAA2B;QAE3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1D,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAC1D,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACzB,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAChE,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,cAAyB,EACzB,YAAuB,EACvB,KAAsB,EACtB,OAAqB;QAErB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,UAAU,IAAI,CAAC,UAAU,CAAC,IAAI,wCAAwC;gBACpE,sCAAsC,CACzC,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAG,CAAC,GAAc,EAAuD,EAAE,CAAC,CAAC;YACtF,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,IAAI,EAAE,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;SACtC,CAAC,CAAC;QACH,MAAM,WAAW,GAAG,YAAY,CAAC,WAAW,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAC1E,MAAM,SAAS,GAAG,YAAY,CAAC,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACtE,MAAM,IAAI,GAAa;YACrB,MAAM,EAAE,WAAW;YACnB,SAAS;YACT,KAAK;YACL,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9C,CAAC;QACF,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC;QAClD,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAiB;IACnD,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IAC7D,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,2BAA2B,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAC3F,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IACpB,MAAM,MAAM,CAAC,mBAAmB,CAAC,mBAAmB,CAAC,CAAC;IACtD,MAAM,MAAM,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAC3C,OAAO,IAAI,mBAAmB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { WebBrotherQLPrinter } from './printer.js';
2
+ export interface RequestOptions {
3
+ filters?: USBDeviceFilter[];
4
+ }
5
+ export declare function requestPrinter(options?: RequestOptions): Promise<WebBrotherQLPrinter>;
6
+ export declare function fromUSBDevice(device: USBDevice): WebBrotherQLPrinter;
7
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAiB,MAAM,cAAc,CAAC;AAElE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;CAC7B;AAMD,wBAAsB,cAAc,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAI3F;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,mBAAmB,CAMpE"}
@@ -0,0 +1,18 @@
1
+ import { DEVICES, findDevice } from '@thermal-label/brother-ql-core';
2
+ import { WebBrotherQLPrinter, openWebDevice } from './printer.js';
3
+ function defaultFilters() {
4
+ return Object.values(DEVICES).map(d => ({ vendorId: d.vid, productId: d.pid }));
5
+ }
6
+ export async function requestPrinter(options) {
7
+ const filters = options?.filters ?? defaultFilters();
8
+ const device = await navigator.usb.requestDevice({ filters });
9
+ return openWebDevice(device);
10
+ }
11
+ export function fromUSBDevice(device) {
12
+ const descriptor = findDevice(0x04f9, device.productId);
13
+ if (!descriptor) {
14
+ throw new Error(`Unsupported device: productId=0x${device.productId.toString(16)}`);
15
+ }
16
+ return new WebBrotherQLPrinter(device, descriptor);
17
+ }
18
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAMlE,SAAS,cAAc;IACrB,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAwB;IAC3D,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,CAAC;IACrD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9D,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAiB;IAC7C,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACxD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,mCAAmC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,OAAO,IAAI,mBAAmB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AACrD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@thermal-label/brother-ql-web",
3
+ "version": "0.0.1",
4
+ "description": "WebUSB browser driver for Brother QL label printers",
5
+ "keywords": [
6
+ "brother",
7
+ "brother-ql",
8
+ "label-printer",
9
+ "thermal-label",
10
+ "webusb",
11
+ "browser"
12
+ ],
13
+ "type": "module",
14
+ "author": "Mannes Brak",
15
+ "license": "MIT",
16
+ "homepage": "https://thermal-label.github.io/brother-ql/",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/thermal-label/brother-ql.git",
20
+ "directory": "packages/web"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/thermal-label/brother-ql/issues"
24
+ },
25
+ "funding": [
26
+ {
27
+ "type": "github",
28
+ "url": "https://github.com/sponsors/mannes"
29
+ },
30
+ {
31
+ "type": "ko-fi",
32
+ "url": "https://ko-fi.com/mannes"
33
+ }
34
+ ],
35
+ "files": [
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "engines": {
40
+ "node": ">=24.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "sideEffects": false,
46
+ "main": "./dist/index.js",
47
+ "types": "./src/index.ts",
48
+ "exports": {
49
+ ".": {
50
+ "import": "./dist/index.js",
51
+ "types": "./src/index.ts"
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "@thermal-label/brother-ql-core": "0.0.1"
56
+ },
57
+ "peerDependencies": {
58
+ "typescript": ">=5.0"
59
+ },
60
+ "devDependencies": {
61
+ "@mbtech-nl/tsconfig": "^1.0.0",
62
+ "@vitest/coverage-v8": "^2.1.9",
63
+ "jsdom": "^26.1.0",
64
+ "typescript": "~5.5.0",
65
+ "vitest": "^2.0.0"
66
+ },
67
+ "scripts": {
68
+ "build": "tsc -p tsconfig.json",
69
+ "typecheck": "tsc -p tsconfig.json --noEmit",
70
+ "test": "vitest run",
71
+ "test:coverage": "vitest run --coverage"
72
+ }
73
+ }