tapirscan 1.2.1 → 1.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 (84) hide show
  1. package/LICENSE +220 -0
  2. package/README.md +384 -226
  3. package/dist/browser-worker.d.ts +33 -0
  4. package/dist/browser-worker.js +83 -0
  5. package/dist/browser.d.ts +44 -0
  6. package/dist/browser.js +211 -0
  7. package/dist/freeze.d.ts +6 -0
  8. package/dist/freeze.js +7 -0
  9. package/dist/index.d.ts +53 -46
  10. package/dist/index.js +168 -209
  11. package/dist/layout.d.ts +19 -0
  12. package/dist/layout.js +27 -0
  13. package/dist/multiformat/format-registry.d.ts +26 -0
  14. package/dist/multiformat/format-registry.js +63 -0
  15. package/dist/multiformat/formats.d.ts +4 -24
  16. package/dist/multiformat/formats.js +15 -46
  17. package/dist/rust-session.d.ts +19 -0
  18. package/dist/rust-session.js +102 -0
  19. package/examples/camera.html +28 -29
  20. package/examples/vite/README.md +31 -0
  21. package/examples/vite/index.html +14 -0
  22. package/examples/vite/main.js +33 -0
  23. package/examples/vite/package.json +16 -0
  24. package/examples/vite/vite.config.js +6 -0
  25. package/package.json +28 -11
  26. package/wasm/build.json +295 -0
  27. package/wasm/experimental-turbo16.wasm +0 -0
  28. package/wasm/experimental-turbo2.wasm +0 -0
  29. package/wasm/experimental-turbo4.wasm +0 -0
  30. package/wasm/experimental-turbo8.wasm +0 -0
  31. package/wasm/high.wasm +0 -0
  32. package/wasm/low.wasm +0 -0
  33. package/wasm/medium.wasm +0 -0
  34. package/wasm/very-high.wasm +0 -0
  35. package/dist/completion-host.d.mts +0 -40
  36. package/dist/completion-host.mjs +0 -188
  37. package/dist/detail-20260914/continuity.d.mts +0 -2
  38. package/dist/detail-20260914/continuity.mjs +0 -62
  39. package/dist/detail-20260914/detail-proposals-rich.d.mts +0 -13
  40. package/dist/detail-20260914/detail-proposals-rich.mjs +0 -53
  41. package/dist/detail-20260914/direct-recovery.d.mts +0 -28
  42. package/dist/detail-20260914/direct-recovery.mjs +0 -139
  43. package/dist/detail-20260914/host.d.mts +0 -40
  44. package/dist/detail-20260914/host.mjs +0 -280
  45. package/dist/detail-20260914/scanner.d.mts +0 -12
  46. package/dist/detail-20260914/scanner.mjs +0 -64
  47. package/dist/detail-20260914/source-evidence.d.mts +0 -10
  48. package/dist/detail-20260914/source-evidence.mjs +0 -94
  49. package/dist/detail-canvas.d.ts +0 -35
  50. package/dist/detail-canvas.js +0 -73
  51. package/dist/detail-runtime/direct-recovery.d.mts +0 -28
  52. package/dist/detail-runtime/direct-recovery.mjs +0 -142
  53. package/dist/detail-runtime/host.d.mts +0 -40
  54. package/dist/detail-runtime/host.mjs +0 -284
  55. package/dist/detail-runtime/scanner.d.mts +0 -12
  56. package/dist/detail-runtime/scanner.mjs +0 -70
  57. package/dist/detail.d.ts +0 -20
  58. package/dist/detail.js +0 -84
  59. package/dist/host.d.ts +0 -112
  60. package/dist/host.js +0 -184
  61. package/dist/host64.d.ts +0 -112
  62. package/dist/host64.js +0 -184
  63. package/dist/multiformat/coverage.d.ts +0 -7
  64. package/dist/multiformat/coverage.js +0 -26
  65. package/dist/multiformat/geometry.d.ts +0 -37
  66. package/dist/multiformat/geometry.js +0 -132
  67. package/dist/multiformat/linear-duplicates.d.ts +0 -16
  68. package/dist/multiformat/linear-duplicates.js +0 -163
  69. package/dist/multiformat/pixels.d.ts +0 -3
  70. package/dist/multiformat/pixels.js +0 -36
  71. package/dist/multiformat/scanner.d.ts +0 -60
  72. package/dist/multiformat/scanner.js +0 -382
  73. package/dist/multiformat-host.d.ts +0 -121
  74. package/dist/multiformat-host.js +0 -207
  75. package/dist/policy.d.ts +0 -12
  76. package/dist/policy.js +0 -12
  77. package/examples/scan-worker.mjs +0 -20
  78. package/examples/worker-client.mjs +0 -63
  79. package/wasm/high-shared-retail-portable-20260919.wasm +0 -0
  80. package/wasm/low-shared-retail-portable-20260919.wasm +0 -0
  81. package/wasm/medium-shared-retail-portable-20260919.wasm +0 -0
  82. package/wasm/multiformat.json +0 -7
  83. package/wasm/multiformat.wasm +0 -0
  84. package/wasm/very-high-shared-retail-portable-20260919.wasm +0 -0
@@ -0,0 +1,33 @@
1
+ import { type EanAddOnPolicy, type ExperimentalTurbo, type FormatSelection, type Mode, type PixelImage, type ScanOptions } from "./index.js";
2
+ /** Scanner options that can cross the worker boundary. */
3
+ export interface WorkerScannerOptions {
4
+ mode?: Mode;
5
+ formats?: FormatSelection;
6
+ eanAddOnPolicy?: EanAddOnPolicy;
7
+ experimentalTurbo?: ExperimentalTurbo;
8
+ /** Absolute URL of a directory serving the packaged WASM files. */
9
+ wasmBaseUrl?: string;
10
+ }
11
+ /** Inputs that are cheap to send: everything else becomes an ImageBitmap first. */
12
+ export type WorkerSource = Blob | ImageBitmap | PixelImage;
13
+ export type WorkerRequest = {
14
+ id: number;
15
+ type: "create";
16
+ options: WorkerScannerOptions;
17
+ } | {
18
+ id: number;
19
+ type: "scan" | "inspect";
20
+ source: WorkerSource;
21
+ options: ScanOptions;
22
+ };
23
+ export type WorkerResponse = {
24
+ id: number;
25
+ value: unknown;
26
+ } | {
27
+ id: number;
28
+ error: {
29
+ name: string;
30
+ message: string;
31
+ code?: string;
32
+ };
33
+ };
@@ -0,0 +1,83 @@
1
+ // Worker side of tapirscan/browser: one core Scanner, fed with decoded pixels.
2
+ import { Scanner, } from "./index.js";
3
+ // Literal URLs let bundlers such as Vite and webpack emit these assets without
4
+ // configuration. Keep the file names in sync with index.ts (verify-package checks).
5
+ const wasm = {
6
+ low: new URL("../wasm/low.wasm", import.meta.url),
7
+ medium: new URL("../wasm/medium.wasm", import.meta.url),
8
+ high: new URL("../wasm/high.wasm", import.meta.url),
9
+ "very-high": new URL("../wasm/very-high.wasm", import.meta.url),
10
+ };
11
+ const turboWasm = {
12
+ 2: new URL("../wasm/experimental-turbo2.wasm", import.meta.url),
13
+ 4: new URL("../wasm/experimental-turbo4.wasm", import.meta.url),
14
+ 8: new URL("../wasm/experimental-turbo8.wasm", import.meta.url),
15
+ 16: new URL("../wasm/experimental-turbo16.wasm", import.meta.url),
16
+ };
17
+ // Typed locally: the DOM and WebWorker libraries cannot share one compilation.
18
+ const scope = globalThis;
19
+ let scanner;
20
+ async function load(url) {
21
+ const response = await fetch(url);
22
+ if (!response.ok)
23
+ throw new Error(`WASM load failed: ${String(response.status)} ${url.href}`);
24
+ return response.arrayBuffer();
25
+ }
26
+ /** Decode a Blob or ImageBitmap onto white RGBA pixels; pixel inputs pass through. */
27
+ async function pixels(source) {
28
+ if (!(source instanceof Blob) && !(source instanceof ImageBitmap))
29
+ return source;
30
+ const bitmap = source instanceof Blob ? await createImageBitmap(source) : source;
31
+ try {
32
+ const { width, height } = bitmap;
33
+ // Reject before allocating a canvas; the scanner enforces the same limits.
34
+ if (width < 3 || height < 3 || width * height > 32 * 1024 * 1024)
35
+ throw new TypeError(`Images must be 3×3 to 32 megapixels, got ${String(width)}×${String(height)}`);
36
+ const context = new OffscreenCanvas(width, height).getContext("2d", {
37
+ willReadFrequently: true,
38
+ });
39
+ if (!context)
40
+ throw new Error("2D canvas is unavailable in this browser's workers");
41
+ // Composite onto white, as images are displayed: the scanner ignores alpha, so
42
+ // transparent areas would otherwise read as black.
43
+ context.fillStyle = "#fff";
44
+ context.fillRect(0, 0, width, height);
45
+ context.drawImage(bitmap, 0, 0);
46
+ return context.getImageData(0, 0, width, height);
47
+ }
48
+ finally {
49
+ bitmap.close();
50
+ }
51
+ }
52
+ async function handle(request) {
53
+ if (request.type === "create") {
54
+ const { wasmBaseUrl, ...options } = request.options;
55
+ const { experimentalTurbo: turbo, mode = "medium" } = options;
56
+ // The core validates options before loading; a custom base URL uses its loader.
57
+ // Invalid modes or presets fail validation, so the lookup below is never used for them.
58
+ const asset = turbo === undefined ? wasm[mode] : turboWasm[turbo];
59
+ scanner = await Scanner.create(wasmBaseUrl === undefined
60
+ ? { ...options, loadWasm: () => load(asset) }
61
+ : { ...options, wasmBaseUrl });
62
+ return { mode: scanner.mode, formats: scanner.formats, eanAddOnPolicy: scanner.eanAddOnPolicy };
63
+ }
64
+ if (!scanner)
65
+ throw new Error("Scanner is not created");
66
+ const image = await pixels(request.source);
67
+ return request.type === "scan"
68
+ ? scanner.scan(image, request.options)
69
+ : scanner.inspect(image, request.options);
70
+ }
71
+ scope.onmessage = ({ data: request }) => {
72
+ handle(request).then((value) => {
73
+ scope.postMessage({ id: request.id, value });
74
+ }, (error) => {
75
+ const { name, message, code } = error instanceof Error
76
+ ? error
77
+ : { name: "Error", message: String(error), code: undefined };
78
+ scope.postMessage({
79
+ id: request.id,
80
+ error: { name, message, ...(typeof code === "string" ? { code } : {}) },
81
+ });
82
+ });
83
+ };
@@ -0,0 +1,44 @@
1
+ import type { EanAddOnPolicy, ExperimentalTurbo, FormatSelection, Mode, PixelImage, ScanOptions, InspectionResult, ScanResult } from "./index.js";
2
+ export { ScannerError } from "./rust-session.js";
3
+ export type { Barcode, DiagnosticBarcode, Diagnostics, EanAddOnPolicy, ExperimentalTurbo, Format, FormatSelection, Image, Mode, PixelImage, Quad, RegionEvidence, ScanOptions, InspectionResult, ScanResult, StructuredAppend, UndecodedRegion, } from "./index.js";
4
+ /**
5
+ * A file or blob (JPEG, PNG and other browser-decodable images), an `<img>`,
6
+ * `<video>` (its current frame), canvas, `ImageBitmap`, `VideoFrame`, `ImageData`
7
+ * or decoded pixels.
8
+ */
9
+ export type ImageSource = ImageBitmapSource | PixelImage;
10
+ export interface ScannerOptions {
11
+ /** Effort; defaults to "medium". */
12
+ mode?: Mode;
13
+ /** Formats to read; defaults to retail EAN/UPC. */
14
+ formats?: FormatSelection;
15
+ /** EAN/UPC supplement policy; defaults to "ignore". */
16
+ eanAddOnPolicy?: EanAddOnPolicy;
17
+ /** @experimental Faster 1D preset instead of a mode; may change in minor releases. */
18
+ experimentalTurbo?: ExperimentalTurbo;
19
+ /** Serve the packaged WASM files from another directory, such as a CDN. */
20
+ wasmBaseUrl?: string | URL;
21
+ }
22
+ /**
23
+ * A reusable scanner running in a dedicated worker, so scanning never blocks the
24
+ * page. Construction is synchronous and starts loading in the background; during
25
+ * server rendering it does nothing. Inputs and options are captured when a call
26
+ * starts, and calls reach the worker in call order. Results are deeply frozen and
27
+ * survive disposal. Call dispose() when done to stop the worker.
28
+ */
29
+ export declare class Scanner {
30
+ #private;
31
+ /** Resolves once the scanner is loaded; rejects with the loading error. Optional. */
32
+ readonly ready: Promise<void>;
33
+ constructor(options?: ScannerOptions);
34
+ /** Decode barcodes with source-image positions. No detection returns empty values and barcodes. */
35
+ scan(source: ImageSource, options?: ScanOptions): Promise<ScanResult>;
36
+ /** Scan with unread regions, timing and engine diagnostics. */
37
+ inspect(source: ImageSource, options?: ScanOptions): Promise<InspectionResult>;
38
+ /** Stop the worker and reject queued scans. Repeated disposal is safe. */
39
+ dispose(): void;
40
+ }
41
+ /** Scan one image with a temporary scanner. Reuse a Scanner for several images. */
42
+ export declare function scan(source: ImageSource, options?: ScannerOptions): Promise<ScanResult>;
43
+ /** Inspect one image with a temporary scanner. */
44
+ export declare function inspect(source: ImageSource, options?: ScannerOptions): Promise<InspectionResult>;
@@ -0,0 +1,211 @@
1
+ import { freeze } from "./freeze.js";
2
+ import { pixelLayout } from "./layout.js";
3
+ import { ScannerError } from "./rust-session.js";
4
+ export { ScannerError } from "./rust-session.js";
5
+ const scannerOptions = ["mode", "formats", "eanAddOnPolicy", "experimentalTurbo", "wasmBaseUrl"];
6
+ function revive({ name, message, code }) {
7
+ if (code !== undefined)
8
+ return new ScannerError(code, message);
9
+ return name === "TypeError" ? new TypeError(message) : new Error(message);
10
+ }
11
+ /** Snapshot mutable inputs before the first await; Blobs are immutable. */
12
+ async function prepare(source) {
13
+ const input = source;
14
+ if (input === null || typeof input !== "object")
15
+ throw new TypeError("Expected an image source");
16
+ if (source instanceof Blob)
17
+ return [source, []];
18
+ if ("data" in source) {
19
+ // Validate first, then copy only the addressed rows of a larger backing buffer.
20
+ const { data, width, height, channels, stride, addressed } = pixelLayout(source);
21
+ const copy = new Uint8Array(data.subarray(0, addressed));
22
+ return [{ data: copy, width, height, channels, stride }, [copy.buffer]];
23
+ }
24
+ // A copy, so a caller's ImageBitmap stays usable after it is transferred.
25
+ const bitmap = await createImageBitmap(source);
26
+ return [bitmap, [bitmap]];
27
+ }
28
+ /**
29
+ * A reusable scanner running in a dedicated worker, so scanning never blocks the
30
+ * page. Construction is synchronous and starts loading in the background; during
31
+ * server rendering it does nothing. Inputs and options are captured when a call
32
+ * starts, and calls reach the worker in call order. Results are deeply frozen and
33
+ * survive disposal. Call dispose() when done to stop the worker.
34
+ */
35
+ export class Scanner {
36
+ /** Resolves once the scanner is loaded; rejects with the loading error. Optional. */
37
+ ready;
38
+ #worker;
39
+ #pending = new Map();
40
+ #nextId = 0;
41
+ #closed;
42
+ /** Rejecters of calls still waiting; each removes itself when its wait ends. */
43
+ #waiting = new Set();
44
+ /** Settles once the previous call has been sent, keeping submissions in call order. */
45
+ #submitted = Promise.resolve();
46
+ constructor(options = {}) {
47
+ const input = options;
48
+ if (input === null || typeof input !== "object" || Array.isArray(input))
49
+ throw new TypeError("Invalid scanner options");
50
+ for (const key of Object.keys(options))
51
+ if (!scannerOptions.includes(key))
52
+ throw new TypeError(`Unknown scanner option: ${key}. Use the core "tapirscan" entry for loadWasm.`);
53
+ if (typeof Worker === "undefined") {
54
+ this.#closed = new Error("tapirscan/browser scans in browsers; this environment has no Worker");
55
+ this.ready = Promise.reject(this.#closed);
56
+ }
57
+ else {
58
+ const { wasmBaseUrl, ...rest } = options;
59
+ if (wasmBaseUrl !== undefined &&
60
+ typeof wasmBaseUrl !== "string" &&
61
+ !(wasmBaseUrl instanceof URL))
62
+ throw new TypeError("wasmBaseUrl must be a string or URL");
63
+ const base = typeof document === "undefined" ? location.href : document.baseURI;
64
+ const workerOptions = wasmBaseUrl === undefined
65
+ ? rest
66
+ : { ...rest, wasmBaseUrl: new URL(wasmBaseUrl, base).href };
67
+ const worker = new Worker(new URL("./browser-worker.js", import.meta.url), {
68
+ type: "module",
69
+ });
70
+ this.#worker = worker;
71
+ worker.onmessage = ({ data }) => {
72
+ const pending = this.#pending.get(data.id);
73
+ if (!pending)
74
+ return;
75
+ this.#pending.delete(data.id);
76
+ if ("error" in data)
77
+ pending.reject(revive(data.error));
78
+ else
79
+ pending.resolve(data.value);
80
+ };
81
+ worker.onerror = (event) => {
82
+ event.preventDefault();
83
+ // A worker script that cannot load gives no message; name the usual cause.
84
+ this.#close(new Error(event.message ||
85
+ 'Scanner worker failed to start. With the Vite dev server, add optimizeDeps: { exclude: ["tapirscan"] } to vite.config.'));
86
+ };
87
+ worker.onmessageerror = () => {
88
+ this.#close(new Error("Could not read a scanner worker message"));
89
+ };
90
+ this.ready = this.#request({ type: "create", options: workerOptions }, []).then(() => undefined, (error) => {
91
+ this.#close(error instanceof Error ? error : new Error(String(error)));
92
+ throw error;
93
+ });
94
+ }
95
+ // Loading errors also reject every scan; awaiting ready is optional.
96
+ this.ready.catch(() => undefined);
97
+ }
98
+ /** Decode barcodes with source-image positions. No detection returns empty values and barcodes. */
99
+ async scan(source, options = {}) {
100
+ return freeze((await this.#scan("scan", source, options)));
101
+ }
102
+ /** Scan with unread regions, timing and engine diagnostics. */
103
+ async inspect(source, options = {}) {
104
+ return freeze((await this.#scan("inspect", source, options)));
105
+ }
106
+ /** Stop the worker and reject queued scans. Repeated disposal is safe. */
107
+ dispose() {
108
+ // Same error as the core scanner, so one handler covers both entries.
109
+ this.#close(new ScannerError("disposed", "Scanner was disposed"));
110
+ }
111
+ async #scan(type, source, options) {
112
+ if (this.#closed)
113
+ throw this.#closed;
114
+ // Capture the options and the frame before any await, so later changes by the
115
+ // caller (a reused options object or buffer, the next video frame) cannot leak in.
116
+ // Plain copies (scan options hold at most a format selection); invalid values
117
+ // pass through unchanged for the core to reject.
118
+ const input = options;
119
+ const scanOptions = input !== null && typeof input === "object" && !Array.isArray(input)
120
+ ? {
121
+ ...options,
122
+ ...(typeof options.formats === "object" ? { formats: [...options.formats] } : {}),
123
+ }
124
+ : options;
125
+ const preparing = prepare(source);
126
+ const previous = this.#submitted;
127
+ let submitted = () => undefined;
128
+ this.#submitted = new Promise((resolve) => {
129
+ submitted = resolve;
130
+ });
131
+ try {
132
+ const [prepared, transfer] = await this.#untilClosed(preparing);
133
+ await this.#untilClosed(previous);
134
+ await this.#untilClosed(this.ready);
135
+ const result = this.#request({ type, source: prepared, options: scanOptions }, transfer);
136
+ submitted();
137
+ return await result;
138
+ }
139
+ finally {
140
+ // A failed call releases its successor only once its own predecessor was
141
+ // sent, so later calls never overtake earlier ones.
142
+ void previous.then(submitted);
143
+ // Release a bitmap snapshot that was not sent, including one still being prepared.
144
+ void preparing.then(([prepared]) => {
145
+ if (prepared instanceof ImageBitmap)
146
+ prepared.close();
147
+ }, () => undefined);
148
+ }
149
+ }
150
+ /** Wait for `promise`, but reject at once on dispose. Leaves nothing behind. */
151
+ #untilClosed(promise) {
152
+ if (this.#closed)
153
+ return Promise.reject(this.#closed);
154
+ return new Promise((resolve, reject) => {
155
+ this.#waiting.add(reject);
156
+ promise
157
+ .then(resolve, reject)
158
+ .finally(() => this.#waiting.delete(reject))
159
+ .catch(() => undefined);
160
+ });
161
+ }
162
+ #request(message, transfer) {
163
+ const worker = this.#worker;
164
+ // Without a worker, the constructor has already recorded why.
165
+ if (this.#closed || !worker)
166
+ return Promise.reject(this.#closed ?? new Error("No scanner worker"));
167
+ const id = this.#nextId++;
168
+ return new Promise((resolve, reject) => {
169
+ this.#pending.set(id, { resolve, reject });
170
+ try {
171
+ worker.postMessage({ ...message, id }, transfer);
172
+ }
173
+ catch (error) {
174
+ this.#pending.delete(id);
175
+ reject(error instanceof Error ? error : new Error(String(error)));
176
+ }
177
+ });
178
+ }
179
+ #close(reason) {
180
+ if (this.#closed)
181
+ return;
182
+ this.#closed = reason;
183
+ for (const reject of this.#waiting)
184
+ reject(reason);
185
+ this.#waiting.clear();
186
+ this.#worker?.terminate();
187
+ for (const pending of this.#pending.values())
188
+ pending.reject(reason);
189
+ this.#pending.clear();
190
+ }
191
+ }
192
+ /** Scan one image with a temporary scanner. Reuse a Scanner for several images. */
193
+ export async function scan(source, options = {}) {
194
+ const scanner = new Scanner(options);
195
+ try {
196
+ return await scanner.scan(source);
197
+ }
198
+ finally {
199
+ scanner.dispose();
200
+ }
201
+ }
202
+ /** Inspect one image with a temporary scanner. */
203
+ export async function inspect(source, options = {}) {
204
+ const scanner = new Scanner(options);
205
+ try {
206
+ return await scanner.inspect(source);
207
+ }
208
+ finally {
209
+ scanner.dispose();
210
+ }
211
+ }
@@ -0,0 +1,6 @@
1
+ /** Deeply readonly view of plain result data. */
2
+ export type ReadonlyDeep<T> = T extends object ? {
3
+ readonly [K in keyof T]: ReadonlyDeep<T[K]>;
4
+ } : T;
5
+ /** Freeze plain result data in place, so results stay immutable at runtime. */
6
+ export declare function freeze<T extends object>(value: T): ReadonlyDeep<T>;
package/dist/freeze.js ADDED
@@ -0,0 +1,7 @@
1
+ /** Freeze plain result data in place, so results stay immutable at runtime. */
2
+ export function freeze(value) {
3
+ for (const child of Object.values(value))
4
+ if (child !== null && typeof child === "object")
5
+ freeze(child);
6
+ return Object.freeze(value);
7
+ }
package/dist/index.d.ts CHANGED
@@ -1,17 +1,14 @@
1
- import type { Recovery, DetailRegion } from "./detail-20260914/scanner.mjs";
2
- import { type Barcode as FormatBarcode } from "./multiformat/scanner.js";
3
1
  import { type Format, type FormatSelection } from "./multiformat/formats.js";
4
- export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, } from "./multiformat/formats.js";
2
+ import { type ReadonlyDeep } from "./freeze.js";
3
+ export { commonFormats, commonLinearFormats, linearFormats, matrixFormats, retailFormats, } from "./multiformat/formats.js";
5
4
  export type { Format, FormatSelection } from "./multiformat/formats.js";
6
- import { type ScanFrame, type Quad as HostQuad } from "./completion-host.mjs";
7
- /** Source-image corners, in pixels. */
5
+ export { ScannerError } from "./rust-session.js";
8
6
  export type Quad = readonly [
9
7
  readonly [number, number],
10
8
  readonly [number, number],
11
9
  readonly [number, number],
12
10
  readonly [number, number]
13
11
  ];
14
- /** Decoded pixels; stride defaults to width * channels. Alpha is ignored. */
15
12
  export interface Image {
16
13
  readonly data: Uint8Array;
17
14
  readonly width: number;
@@ -19,58 +16,49 @@ export interface Image {
19
16
  readonly channels: 1 | 3 | 4;
20
17
  readonly stride?: number;
21
18
  }
22
- export { ScannerError } from "./completion-host.mjs";
23
- type RawDiagnosticBarcode = FormatBarcode | (ScanFrame["barcodes"][number] & {
24
- format: "EAN13";
25
- });
26
19
  export type Mode = "low" | "medium" | "high" | "very-high";
27
20
  export interface ScanOptions {
28
- /** Allow reader-specific extra work. Supported for every format; exact budgets may evolve. */
29
- extendedBudget?: boolean;
30
- /** Per-call subset of the formats configured at creation. */
21
+ /** Formats for this call only; defaults to the scanner's formats. */
31
22
  formats?: FormatSelection;
32
- debug?: boolean;
33
23
  }
34
24
  interface RawDiagnostics {
35
- schemaVersion: 2;
36
- mode: Mode;
37
- multiple: boolean;
38
- elapsedMs: number;
39
- localizationLimited: boolean;
40
25
  scan: {
41
26
  barcodes: RawDiagnosticBarcode[];
42
27
  unfinished: boolean;
43
- regions?: FormatBarcode[];
44
- } & Partial<Omit<ScanFrame, "barcodes" | "unfinished">>;
28
+ candidates?: unknown[];
29
+ };
45
30
  localization?: {
46
31
  proposals: {
47
- polygon: HostQuad;
48
- score: number;
49
- text: string;
32
+ polygon: Quad;
33
+ score?: number;
34
+ text?: string;
50
35
  }[];
51
- omitted: number;
52
- workLimited: boolean;
53
- trace?: Record<string, number>;
54
36
  };
55
- recovery?: Recovery;
56
- detailRegions?: DetailRegion[];
57
37
  searchWindows?: {
58
38
  kind: string;
59
39
  polygon: number[][];
60
40
  candidateIndex: number;
61
41
  }[];
42
+ [key: string]: unknown;
43
+ }
44
+ interface RawDiagnosticBarcode {
45
+ text: string;
46
+ format: Format | "Unknown";
47
+ polygon: Quad;
48
+ support: number;
49
+ bytes?: number[];
50
+ candidate_indices?: number[];
51
+ gs1?: boolean;
52
+ readerInitialization?: boolean;
53
+ structuredAppend?: StructuredAppend;
54
+ eanAddOn?: string;
55
+ [key: string]: unknown;
62
56
  }
63
- /** Deeply immutable scan evidence, independent of the scanner lifetime. */
64
- type ReadonlyDeep<T> = T extends object ? {
65
- readonly [K in keyof T]: ReadonlyDeep<T[K]>;
66
- } : T;
67
57
  export type DiagnosticBarcode = ReadonlyDeep<RawDiagnosticBarcode>;
68
- /** Source geometry with no accepted decode; format is a reader hint. */
69
58
  export interface UndecodedRegion {
70
59
  readonly format: Format | "Unknown";
71
60
  readonly polygon: Quad;
72
61
  }
73
- /** Stable region evidence. null means this reader did not expose that evidence. */
74
62
  export interface RegionEvidence {
75
63
  readonly proposals: ReadonlyDeep<NonNullable<RawDiagnostics["localization"]>["proposals"]> | null;
76
64
  readonly searchWindows: ReadonlyDeep<NonNullable<RawDiagnostics["searchWindows"]>> | null;
@@ -80,7 +68,6 @@ export type Diagnostics = ReadonlyDeep<RawDiagnostics> & {
80
68
  readonly regions: RegionEvidence;
81
69
  };
82
70
  export interface StructuredAppend {
83
- /** One-based symbol index; symbols are not automatically assembled. */
84
71
  readonly index: number;
85
72
  readonly count: number;
86
73
  readonly id?: string;
@@ -90,7 +77,8 @@ export interface Barcode {
90
77
  /** Payload bytes before character-set interpretation; absent when unavailable. */
91
78
  readonly payloadBytes?: readonly number[];
92
79
  readonly text: string;
93
- readonly format: Format | "Unknown";
80
+ /** Decoded barcodes always have a known format; only undecoded regions report "Unknown". */
81
+ readonly format: Format;
94
82
  /** Reader-specific ranking evidence, not a probability or cross-reader confidence. */
95
83
  readonly support: number;
96
84
  readonly gs1?: boolean;
@@ -105,26 +93,35 @@ export interface Barcode {
105
93
  readonly height: number;
106
94
  };
107
95
  }
96
+ /** Decoded values and their source-image locations. */
108
97
  export interface ScanResult {
109
98
  readonly barcodes: readonly Barcode[];
110
99
  readonly values: readonly string[];
111
100
  /** Largest reader-specific support; not a cross-format confidence comparison. */
112
101
  readonly best: Barcode | undefined;
102
+ }
103
+ export interface InspectionResult extends ScanResult {
113
104
  readonly image: {
114
105
  readonly width: number;
115
106
  readonly height: number;
116
107
  };
117
- readonly mode: Mode;
108
+ /** Effort mode used; absent for Turbo presets, which report `experimentalTurbo`. */
109
+ readonly mode?: Mode;
110
+ /** @experimental Selected Turbo preset, when requested. */
111
+ readonly experimentalTurbo?: ExperimentalTurbo;
112
+ /** Whole synchronous WASM call time measured by the JavaScript host. */
118
113
  readonly elapsedMs: number;
119
- readonly unfinished: boolean;
120
114
  readonly undecoded: readonly UndecodedRegion[];
121
- readonly debug?: Diagnostics;
115
+ readonly diagnostics: Diagnostics;
122
116
  }
123
- export type EanAddOnPolicy = "Ignore" | "Read" | "Require";
117
+ export type EanAddOnPolicy = "ignore" | "read" | "require";
118
+ export type ExperimentalTurbo = 2 | 4 | 8 | 16;
124
119
  export interface ScannerOptions {
120
+ mode?: Mode;
121
+ /** @experimental Targets faster 1D scanning, not 2D speedups. May change in minor releases. */
122
+ experimentalTurbo?: ExperimentalTurbo;
125
123
  /** Optional EAN/UPC supplement policy, fixed at creation. */
126
124
  eanAddOnPolicy?: EanAddOnPolicy;
127
- mode?: Mode;
128
125
  formats?: FormatSelection;
129
126
  /** Directory containing the packaged WASMs; relative to the page in browsers. */
130
127
  wasmBaseUrl?: string | URL;
@@ -132,19 +129,29 @@ export interface ScannerOptions {
132
129
  loadWasm?: (url: URL) => Promise<ArrayBuffer>;
133
130
  }
134
131
  export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
135
- /** Mode selects a compiled implementation. Create another instance to switch. */
132
+ /** A reusable scanner. Results own their data and remain valid after later scans or disposal. */
136
133
  export declare class Scanner {
137
134
  private readonly host;
138
- readonly mode: Mode;
135
+ /** Effort mode, fixed at creation; undefined for Turbo presets. */
136
+ readonly mode: Mode | undefined;
139
137
  private readonly configuredFormats;
140
138
  private readonly addOnPolicy;
139
+ /** @experimental Selected Turbo preset, fixed at creation. */
140
+ readonly experimentalTurbo?: ExperimentalTurbo | undefined;
141
141
  private constructor();
142
- /** Formats available for scanning, fixed at creation. */
142
+ /** Default formats, fixed at creation; individual scans may override them. */
143
143
  get formats(): readonly Format[];
144
144
  get eanAddOnPolicy(): EanAddOnPolicy;
145
145
  static create(options?: ScannerOptions): Promise<Scanner>;
146
+ /** Decode barcodes with positions. Use inspect() for diagnostic evidence. */
146
147
  scan(inputImage: PixelImage, options?: ScanOptions): ScanResult;
148
+ /** Inspect barcodes, unread regions, timing and engine diagnostics. */
149
+ inspect(inputImage: PixelImage, options?: ScanOptions): InspectionResult;
150
+ private run;
151
+ /** Release the WASM session. Repeated disposal is safe; scanning afterward fails. */
147
152
  dispose(): void;
148
153
  }
149
154
  /** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
150
- export declare function scan(image: PixelImage, options?: ScannerOptions & ScanOptions): Promise<ScanResult>;
155
+ export declare function scan(image: PixelImage, options?: ScannerOptions): Promise<ScanResult>;
156
+ /** Inspect one image with automatic cleanup. */
157
+ export declare function inspect(image: PixelImage, options?: ScannerOptions): Promise<InspectionResult>;