tapirscan 1.0.0 → 1.1.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 (33) hide show
  1. package/README.md +173 -104
  2. package/dist/completion-host.d.mts +40 -0
  3. package/dist/completion-host.mjs +188 -0
  4. package/dist/detail-runtime/direct-recovery.d.mts +28 -0
  5. package/dist/detail-runtime/direct-recovery.mjs +142 -0
  6. package/dist/detail-runtime/host.d.mts +40 -0
  7. package/dist/detail-runtime/host.mjs +282 -0
  8. package/dist/detail-runtime/scanner.d.mts +12 -0
  9. package/dist/detail-runtime/scanner.mjs +70 -0
  10. package/dist/detail.d.ts +3 -4
  11. package/dist/detail.js +3 -6
  12. package/dist/index.d.ts +90 -33
  13. package/dist/index.js +169 -78
  14. package/dist/multiformat/coverage.d.ts +7 -0
  15. package/dist/multiformat/coverage.js +26 -0
  16. package/dist/multiformat/formats.d.ts +4 -2
  17. package/dist/multiformat/formats.js +13 -1
  18. package/dist/multiformat/geometry.d.ts +5 -0
  19. package/dist/multiformat/geometry.js +13 -3
  20. package/dist/multiformat/linear-duplicates.d.ts +16 -0
  21. package/dist/multiformat/linear-duplicates.js +163 -0
  22. package/dist/multiformat/scanner.d.ts +11 -2
  23. package/dist/multiformat/scanner.js +127 -24
  24. package/examples/camera.html +63 -0
  25. package/examples/scan-worker.mjs +20 -0
  26. package/examples/worker-client.mjs +63 -0
  27. package/package.json +7 -6
  28. package/wasm/{high-release-20260915.wasm → high-complete-release-20260916.wasm} +0 -0
  29. package/wasm/{low-release-20260915.wasm → low-complete-release-20260916.wasm} +0 -0
  30. package/wasm/{medium-release-20260915.wasm → medium-complete-release-20260916.wasm} +0 -0
  31. package/wasm/multiformat.json +2 -1
  32. package/wasm/multiformat.wasm +0 -0
  33. package/wasm/{very-high-release-20260915.wasm → very-high-complete-release-20260916.wasm} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,35 +1,50 @@
1
1
  import type { Recovery, DetailRegion } from "./detail-20260914/scanner.mjs";
2
2
  import { type Barcode as FormatBarcode } from "./multiformat/scanner.js";
3
3
  import { type Format, type FormatSelection } from "./multiformat/formats.js";
4
- export { formatBits, linearFormats, matrixFormats, retailFormats } from "./multiformat/formats.js";
4
+ export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, } from "./multiformat/formats.js";
5
5
  export type { Format, FormatSelection } from "./multiformat/formats.js";
6
- import { type Image, type ScanFrame, type Quad } from "./host.js";
7
- export type { Image, Quad } from "./host.js";
8
- export { ScannerError } from "./host.js";
9
- export type DiagnosticBarcode = FormatBarcode | (ScanFrame["barcodes"][number] & {
6
+ import { type ScanFrame, type Quad as HostQuad } from "./completion-host.mjs";
7
+ /** Source-image corners, in pixels. */
8
+ export type Quad = readonly [
9
+ readonly [number, number],
10
+ readonly [number, number],
11
+ readonly [number, number],
12
+ readonly [number, number]
13
+ ];
14
+ /** Decoded pixels; stride defaults to width * channels. Alpha is ignored. */
15
+ export interface Image {
16
+ readonly data: Uint8Array;
17
+ readonly width: number;
18
+ readonly height: number;
19
+ readonly channels: 1 | 3 | 4;
20
+ readonly stride?: number;
21
+ }
22
+ export { ScannerError } from "./completion-host.mjs";
23
+ type RawDiagnosticBarcode = FormatBarcode | (ScanFrame["barcodes"][number] & {
10
24
  format: "EAN13";
11
25
  });
12
26
  export type Mode = "low" | "medium" | "high" | "very-high";
13
27
  export interface ScanOptions {
14
- multiple?: boolean;
28
+ /** Finish effort-selected EAN13/UPCA work beyond shared frame budgets. */
29
+ finishCandidates?: boolean;
30
+ /** Per-call subset of the formats configured at creation. */
31
+ formats?: FormatSelection;
15
32
  debug?: boolean;
16
- /** Compatibility alias; use debug. Decoded polygons are always returned. */
17
- includeRegions?: boolean;
18
33
  }
19
- export interface Diagnostics {
34
+ interface RawDiagnostics {
20
35
  schemaVersion: 2;
21
36
  mode: Mode;
22
37
  multiple: boolean;
23
38
  elapsedMs: number;
24
39
  localizationLimited: boolean;
25
40
  scan: {
26
- barcodes: DiagnosticBarcode[];
41
+ barcodes: RawDiagnosticBarcode[];
27
42
  unfinished: boolean;
28
43
  regions?: FormatBarcode[];
29
44
  } & Partial<Omit<ScanFrame, "barcodes" | "unfinished">>;
30
45
  localization?: {
31
46
  proposals: {
32
- polygon: Quad;
47
+ polygon: HostQuad;
33
48
  score: number;
34
49
  text: string;
35
50
  }[];
@@ -45,33 +60,74 @@ export interface Diagnostics {
45
60
  candidateIndex: number;
46
61
  }[];
47
62
  }
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
+ export type DiagnosticBarcode = ReadonlyDeep<RawDiagnosticBarcode>;
68
+ /** Source geometry with no accepted decode; format is a reader hint. */
69
+ export interface UndecodedRegion {
70
+ readonly format: Format | "Unknown";
71
+ readonly polygon: Quad;
72
+ }
73
+ /** Stable region evidence. null means this reader did not expose that evidence. */
74
+ export interface RegionEvidence {
75
+ readonly proposals: ReadonlyDeep<NonNullable<RawDiagnostics["localization"]>["proposals"]> | null;
76
+ readonly searchWindows: ReadonlyDeep<NonNullable<RawDiagnostics["searchWindows"]>> | null;
77
+ readonly undecoded: readonly UndecodedRegion[];
78
+ }
79
+ export type Diagnostics = ReadonlyDeep<RawDiagnostics> & {
80
+ readonly regions: RegionEvidence;
81
+ };
82
+ export interface StructuredAppend {
83
+ /** One-based symbol index; symbols are not automatically assembled. */
84
+ readonly index: number;
85
+ readonly count: number;
86
+ readonly id?: string;
87
+ readonly parity?: number;
88
+ }
48
89
  export interface Barcode {
49
- text: string;
50
- format: Format | "Unknown";
51
- polygon: Quad;
52
- rect: {
53
- left: number;
54
- top: number;
55
- width: number;
56
- height: number;
90
+ /** Payload bytes before character-set interpretation; absent when unavailable. */
91
+ readonly payloadBytes?: readonly number[];
92
+ readonly text: string;
93
+ readonly format: Format | "Unknown";
94
+ /** Reader-specific ranking evidence, not a probability or cross-reader confidence. */
95
+ readonly support: number;
96
+ readonly gs1?: boolean;
97
+ readonly readerInitialization?: boolean;
98
+ readonly structuredAppend?: StructuredAppend;
99
+ readonly eanAddOn?: string;
100
+ readonly polygon: Quad;
101
+ readonly rect: {
102
+ readonly left: number;
103
+ readonly top: number;
104
+ readonly width: number;
105
+ readonly height: number;
57
106
  };
58
107
  }
59
108
  export interface ScanResult {
60
- barcodes: Barcode[];
61
- values: string[];
62
- best: Barcode | undefined;
63
- image: {
64
- width: number;
65
- height: number;
109
+ readonly barcodes: readonly Barcode[];
110
+ readonly values: readonly string[];
111
+ /** Largest reader-specific support; not a cross-format confidence comparison. */
112
+ readonly best: Barcode | undefined;
113
+ readonly image: {
114
+ readonly width: number;
115
+ readonly height: number;
66
116
  };
67
- mode: Mode;
68
- elapsedMs: number;
69
- unfinished: boolean;
70
- debug?: Diagnostics;
117
+ readonly mode: Mode;
118
+ readonly elapsedMs: number;
119
+ readonly unfinished: boolean;
120
+ readonly debug?: Diagnostics;
71
121
  }
122
+ export type EanAddOnPolicy = "Ignore" | "Read" | "Require";
72
123
  export interface ScannerOptions {
124
+ /** Optional EAN/UPC supplement policy, fixed at creation. */
125
+ eanAddOnPolicy?: EanAddOnPolicy;
73
126
  mode?: Mode;
74
127
  formats?: FormatSelection;
128
+ /** Directory containing the packaged WASMs; relative to the page in browsers. */
129
+ wasmBaseUrl?: string | URL;
130
+ /** Advanced loader, receiving URLs resolved against wasmBaseUrl. */
75
131
  loadWasm?: (url: URL) => Promise<ArrayBuffer>;
76
132
  }
77
133
  export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
@@ -79,13 +135,14 @@ export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
79
135
  export declare class Scanner {
80
136
  private readonly host;
81
137
  readonly mode: Mode;
82
- private readonly additional?;
83
- private readonly formats;
138
+ private readonly configuredFormats;
139
+ private readonly addOnPolicy;
84
140
  private constructor();
141
+ /** Formats available for scanning, fixed at creation. */
142
+ get formats(): readonly Format[];
143
+ get eanAddOnPolicy(): EanAddOnPolicy;
85
144
  static create(options?: ScannerOptions): Promise<Scanner>;
86
145
  scan(inputImage: PixelImage, options?: ScanOptions): ScanResult;
87
- /** Convenience alias for result.best. */
88
- best(result: ScanResult): Barcode | undefined;
89
146
  dispose(): void;
90
147
  }
91
148
  /** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
package/dist/index.js CHANGED
@@ -1,23 +1,48 @@
1
+ import { mergeLinearDuplicates } from "./multiformat/linear-duplicates.js";
1
2
  import { ReleaseDetailScanner, fitLimits } from "./detail.js";
2
3
  import { policy } from "./policy.js";
3
4
  import { MediumMultiformatScanner } from "./multiformat/scanner.js";
4
5
  import { resolveFormats } from "./multiformat/formats.js";
5
- export { formatBits, linearFormats, matrixFormats, retailFormats } from "./multiformat/formats.js";
6
- import { IndependentScanner } from "./host.js";
7
- export { ScannerError } from "./host.js";
6
+ export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, } from "./multiformat/formats.js";
7
+ import { IndependentScanner, } from "./completion-host.mjs";
8
+ export { ScannerError } from "./completion-host.mjs";
8
9
  function pixels(image) {
9
- if ("channels" in image)
10
- return image;
11
- if (!(image.data instanceof Uint8ClampedArray))
12
- throw new TypeError("Use ImageData or an explicit buffer with channels and stride");
10
+ const input = image;
11
+ if (input === null || typeof input !== "object")
12
+ throw new TypeError("Expected ImageData or decoded pixels");
13
+ const explicit = "channels" in image;
14
+ if (!explicit && !(image.data instanceof Uint8ClampedArray))
15
+ throw new TypeError("Use ImageData or an explicit buffer with channels");
16
+ const channels = explicit ? image.channels : 4;
17
+ const stride = explicit ? (image.stride ?? image.width * channels) : image.width * 4;
18
+ const required = (image.height - 1) * stride + image.width * channels;
19
+ if (!Number.isSafeInteger(image.width) ||
20
+ !Number.isSafeInteger(image.height) ||
21
+ image.width < 3 ||
22
+ image.height < 3 ||
23
+ image.width * image.height > 32 * 1024 * 1024 ||
24
+ ![1, 3, 4].includes(channels) ||
25
+ !Number.isSafeInteger(stride) ||
26
+ stride < image.width * channels ||
27
+ required > 128 * 1024 * 1024 ||
28
+ !(image.data instanceof Uint8Array || image.data instanceof Uint8ClampedArray) ||
29
+ image.data.byteLength < required)
30
+ throw new TypeError("Invalid image dimensions, channels, stride or buffer (maximum 128 MiB)");
13
31
  return {
14
32
  data: new Uint8Array(image.data.buffer, image.data.byteOffset, image.data.byteLength),
15
33
  width: image.width,
16
34
  height: image.height,
17
- channels: 4,
18
- stride: image.width * 4,
35
+ channels,
36
+ stride,
19
37
  };
20
38
  }
39
+ /** Freeze only plain result data, never caller-owned input buffers. */
40
+ function freeze(value) {
41
+ for (const child of Object.values(value))
42
+ if (child !== null && typeof child === "object")
43
+ freeze(child);
44
+ return Object.freeze(value);
45
+ }
21
46
  async function loadDefault(url) {
22
47
  if (url.protocol === "file:") {
23
48
  const nodeFs = "node:fs/promises";
@@ -29,14 +54,51 @@ async function loadDefault(url) {
29
54
  throw new Error(`WASM load failed: ${String(response.status)}`);
30
55
  return response.arrayBuffer();
31
56
  }
57
+ function regionEvidence(raw) {
58
+ const undecoded = [];
59
+ if (raw.scan.regions) {
60
+ for (const region of raw.scan.regions) {
61
+ if (!raw.scan.barcodes.includes(region))
62
+ undecoded.push({ format: region.format, polygon: region.polygon });
63
+ }
64
+ }
65
+ else {
66
+ const decoded = new Set(raw.scan.barcodes.flatMap((b) => ("candidate_indices" in b ? b.candidate_indices : [])));
67
+ raw.localization?.proposals.forEach((p, i) => {
68
+ if (!decoded.has(i))
69
+ undecoded.push({ format: "Unknown", polygon: p.polygon });
70
+ });
71
+ for (const attempt of raw.recovery?.attempts ?? []) {
72
+ const decoded = new Set(attempt.reads.flatMap((b) => b.candidate_indices));
73
+ attempt.proposals.forEach((p, i) => {
74
+ if (!decoded.has(i))
75
+ undecoded.push({ format: "Unknown", polygon: p.polygon });
76
+ });
77
+ }
78
+ }
79
+ return {
80
+ proposals: raw.localization?.proposals ?? null,
81
+ searchWindows: raw.searchWindows ?? null,
82
+ undecoded,
83
+ };
84
+ }
32
85
  function publicResult(raw, image, debug) {
33
- const barcodes = raw.scan.barcodes.map(({ text, format, polygon }) => {
86
+ const barcodes = raw.scan.barcodes.map((read) => {
87
+ const { text, format, polygon, support } = read;
34
88
  const left = Math.floor(Math.min(...polygon.map((p) => p[0])));
35
89
  const top = Math.floor(Math.min(...polygon.map((p) => p[1])));
36
90
  return {
37
91
  text,
38
92
  format,
39
93
  polygon,
94
+ support,
95
+ ...("bytes" in read ? { payloadBytes: read.bytes } : {}),
96
+ ...("gs1" in read ? { gs1: read.gs1 } : {}),
97
+ ...("readerInitialization" in read
98
+ ? { readerInitialization: read.readerInitialization }
99
+ : {}),
100
+ ...("structuredAppend" in read ? { structuredAppend: read.structuredAppend } : {}),
101
+ ...("eanAddOn" in read ? { eanAddOn: read.eanAddOn } : {}),
40
102
  rect: {
41
103
  left,
42
104
  top,
@@ -49,121 +111,150 @@ function publicResult(raw, image, debug) {
49
111
  for (let i = 0; i < barcodes.length; i++)
50
112
  if (bestIndex < 0 || raw.scan.barcodes[i].support > raw.scan.barcodes[bestIndex].support)
51
113
  bestIndex = i;
52
- return {
114
+ return freeze({
53
115
  barcodes,
54
116
  values: barcodes.map((b) => b.text),
55
117
  best: barcodes[bestIndex],
56
118
  image: { width: image.width, height: image.height },
57
119
  mode: raw.mode,
58
120
  elapsedMs: raw.elapsedMs,
59
- unfinished: raw.scan.unfinished,
60
- ...(debug ? { debug: raw } : {}),
61
- };
121
+ unfinished: raw.scan.unfinished || raw.localizationLimited || (raw.localization?.omitted ?? 0) > 0,
122
+ ...(debug ? { debug: { ...raw, regions: regionEvidence(raw) } } : {}),
123
+ });
62
124
  }
63
125
  const modes = {
64
- low: "low-release-20260915.wasm",
65
- medium: "medium-release-20260915.wasm",
66
- high: "high-release-20260915.wasm",
67
- "very-high": "very-high-release-20260915.wasm",
126
+ low: "low-complete-release-20260916.wasm",
127
+ medium: "medium-complete-release-20260916.wasm",
128
+ high: "high-complete-release-20260916.wasm",
129
+ "very-high": "very-high-complete-release-20260916.wasm",
68
130
  };
69
131
  /** Mode selects a compiled implementation. Create another instance to switch. */
70
132
  export class Scanner {
71
133
  host;
72
134
  mode;
73
- additional;
74
- formats;
75
- constructor(host, mode, additional, formats = ["EAN13"]) {
135
+ configuredFormats;
136
+ addOnPolicy;
137
+ constructor(host, mode, configuredFormats = ["EAN13"], addOnPolicy = "Ignore") {
76
138
  this.host = host;
77
139
  this.mode = mode;
78
- this.additional = additional;
79
- this.formats = formats;
140
+ this.configuredFormats = configuredFormats;
141
+ this.addOnPolicy = addOnPolicy;
142
+ Object.freeze(configuredFormats);
143
+ }
144
+ /** Formats available for scanning, fixed at creation. */
145
+ get formats() {
146
+ return this.configuredFormats;
147
+ }
148
+ get eanAddOnPolicy() {
149
+ return this.addOnPolicy;
80
150
  }
81
151
  static async create(options = {}) {
82
152
  const input = options;
83
153
  if (input === null || typeof input !== "object" || Array.isArray(input))
84
154
  throw new TypeError("Invalid scanner options");
85
155
  for (const key of Object.keys(options))
86
- if (!["mode", "formats", "loadWasm"].includes(key))
156
+ if (!["mode", "formats", "wasmBaseUrl", "loadWasm", "eanAddOnPolicy"].includes(key))
87
157
  throw new TypeError(`Unknown scanner option: ${key}`);
88
158
  const mode = options.mode ?? "medium";
89
159
  if (!Object.hasOwn(modes, mode))
90
160
  throw new TypeError("Unknown scanner mode");
91
- const extraAsset = "../wasm/multiformat.wasm";
92
161
  const formats = resolveFormats(options.formats);
93
- const url = new URL("../wasm/" + modes[mode], import.meta.url);
162
+ const addonPolicy = options.eanAddOnPolicy === undefined ? "Ignore" : options.eanAddOnPolicy;
163
+ if (!["Ignore", "Read", "Require"].includes(addonPolicy))
164
+ throw new TypeError("eanAddOnPolicy must be Ignore, Read or Require");
165
+ if (options.loadWasm !== undefined && typeof options.loadWasm !== "function")
166
+ throw new TypeError("loadWasm must be a function");
167
+ if (options.wasmBaseUrl !== undefined &&
168
+ typeof options.wasmBaseUrl !== "string" &&
169
+ !(options.wasmBaseUrl instanceof URL))
170
+ throw new TypeError("wasmBaseUrl must be a string or URL");
171
+ const base = options.wasmBaseUrl === undefined
172
+ ? new URL(/* @vite-ignore */ "../wasm/", import.meta.url)
173
+ : new URL(options.wasmBaseUrl, typeof location === "undefined" ? import.meta.url : location.href);
174
+ if (!base.pathname.endsWith("/"))
175
+ base.pathname += "/";
94
176
  const load = options.loadWasm ?? loadDefault;
95
- const bytes = await load(url);
96
- const recovery = mode === "low" ? undefined : await load(new URL("../wasm/" + modes.low, import.meta.url));
177
+ const needsPrimary = formats.some((f) => f === "EAN13" || f === "UPCA");
178
+ const bytes = needsPrimary ? await load(new URL(modes[mode], base)) : undefined;
179
+ const recovery = !needsPrimary || mode === "low" ? undefined : await load(new URL(modes.low, base));
180
+ const multiformat = addonPolicy !== "Ignore" || formats.length !== 1 || formats[0] !== "EAN13";
181
+ const extra = addonPolicy !== "Ignore" || formats.some((f) => f !== "EAN13" && f !== "UPCA")
182
+ ? await load(new URL("multiformat.wasm", base))
183
+ : undefined;
184
+ if (multiformat)
185
+ return new Scanner(await MediumMultiformatScanner.create(bytes, extra, mode, recovery), mode, formats, addonPolicy);
186
+ if (!bytes)
187
+ throw new Error("EAN13 engine was not loaded");
97
188
  const host = recovery
98
189
  ? await ReleaseDetailScanner.create(bytes, recovery, mode)
99
190
  : await IndependentScanner.create(bytes);
100
- try {
101
- const extra = formats.some((f) => f !== "EAN13" && f !== "UPCA")
102
- ? await load(new URL(extraAsset, import.meta.url))
103
- : undefined;
104
- const additional = formats.length !== 1 || formats[0] !== "EAN13"
105
- ? await MediumMultiformatScanner.create(bytes, extra, mode, recovery)
106
- : undefined;
107
- return new Scanner(host, mode, additional, formats);
108
- }
109
- catch (error) {
110
- host.dispose();
111
- throw error;
112
- }
191
+ return new Scanner(host, mode, formats);
113
192
  }
114
193
  scan(inputImage, options = {}) {
115
194
  const image = pixels(inputImage);
116
195
  const input = options;
117
196
  if (input === null || typeof input !== "object" || Array.isArray(input))
118
197
  throw new TypeError("Invalid scan options");
119
- for (const key of Object.keys(options)) {
120
- if (key !== "multiple" && key !== "includeRegions" && key !== "debug")
198
+ for (const key of Object.keys(options))
199
+ if (key !== "debug" && key !== "formats" && key !== "finishCandidates")
121
200
  throw new TypeError(`Unknown scan option: ${key}`);
122
- }
123
- for (const key of ["multiple", "includeRegions", "debug"]) {
124
- if (options[key] !== undefined && typeof options[key] !== "boolean")
125
- throw new TypeError(`Invalid ${key}`);
126
- }
127
- const multiple = options.multiple ?? true;
128
- if (options.debug !== undefined &&
129
- options.includeRegions !== undefined &&
130
- options.debug !== options.includeRegions)
131
- throw new TypeError("debug and includeRegions disagree");
132
- const includeRegions = options.debug ?? options.includeRegions ?? false;
133
- if (this.additional) {
134
- const frame = this.additional.scan(image, this.formats);
135
- const barcodes = multiple ? frame.barcodes : frame.barcodes.slice(0, 1);
201
+ if (options.debug !== undefined && typeof options.debug !== "boolean")
202
+ throw new TypeError("debug must be a boolean");
203
+ if (options.finishCandidates !== undefined && typeof options.finishCandidates !== "boolean")
204
+ throw new TypeError("finishCandidates must be a boolean");
205
+ const debug = options.debug ?? false;
206
+ const scanPolicy = { ...policy, finishCandidates: options.finishCandidates ?? false };
207
+ const formats = options.formats === undefined ? this.formats : resolveFormats(options.formats);
208
+ if (formats.some((format) => !this.formats.includes(format)))
209
+ throw new TypeError(`Scan formats must be a subset of configured formats. Requested: ${formats.join(", ")}; configured: ${this.formats.join(", ")}`);
210
+ if (this.host instanceof MediumMultiformatScanner) {
211
+ if (options.finishCandidates && !formats.some((f) => f === "EAN13" || f === "UPCA"))
212
+ throw new TypeError("finishCandidates requires EAN13 or UPCA");
213
+ const frame = this.host.scan(image, formats, {
214
+ eanAddOnSymbol: this.eanAddOnPolicy,
215
+ finishCandidates: options.finishCandidates,
216
+ });
217
+ const barcodes = frame.barcodes;
218
+ const primary = frame.primary;
219
+ const localization = primary?.localization;
136
220
  return publicResult({
137
221
  schemaVersion: 2,
138
222
  mode: this.mode,
139
- multiple,
223
+ multiple: true,
140
224
  elapsedMs: frame.scanMs,
225
+ // Preserve the aggregate limit flag when additional readers cannot separate causes.
141
226
  localizationLimited: frame.unfinished,
142
227
  scan: {
228
+ ...(debug ? primary?.scan : {}),
143
229
  barcodes,
144
230
  unfinished: frame.unfinished,
145
- ...(includeRegions ? { regions: frame.regions } : {}),
231
+ ...(debug ? { regions: frame.regions } : {}),
146
232
  },
147
- }, image, includeRegions);
233
+ ...(debug && primary
234
+ ? {
235
+ localization,
236
+ searchWindows: primary.searchWindows,
237
+ ...("recovery" in primary ? { recovery: primary.recovery } : {}),
238
+ ...("detailRegions" in primary ? { detailRegions: primary.detailRegions } : {}),
239
+ }
240
+ : {}),
241
+ }, image, debug);
148
242
  }
149
- const full = this.host.scanLocalized(image, policy, fitLimits[this.mode], true);
243
+ const full = this.host.scanLocalized(image, scanPolicy, fitLimits[this.mode], true);
150
244
  // Isolate the imported host's JSON result at the public ABI type boundary.
151
245
  const localization = full.localization;
152
- const best = multiple ? undefined : this.host.best(full.scan);
153
- const barcodes = (multiple ? full.scan.barcodes : best ? [best] : []).map((b) => ({
246
+ const barcodes = mergeLinearDuplicates(full.scan.barcodes.map((b) => ({
154
247
  ...b,
155
248
  format: "EAN13",
156
- }));
249
+ })), image);
157
250
  return publicResult({
158
251
  schemaVersion: 2,
159
252
  mode: this.mode,
160
- multiple,
253
+ multiple: true,
161
254
  elapsedMs: full.scanMs,
162
- localizationLimited: localization.workLimited,
163
- scan: includeRegions
164
- ? { ...full.scan, barcodes }
165
- : { barcodes, unfinished: full.scan.unfinished },
166
- ...(includeRegions
255
+ localizationLimited: localization.workLimited || localization.omitted > 0,
256
+ scan: debug ? { ...full.scan, barcodes } : { barcodes, unfinished: full.scan.unfinished },
257
+ ...(debug
167
258
  ? {
168
259
  localization,
169
260
  searchWindows: full.searchWindows,
@@ -175,23 +266,23 @@ export class Scanner {
175
266
  : {}),
176
267
  }
177
268
  : {}),
178
- }, image, includeRegions);
179
- }
180
- /** Convenience alias for result.best. */
181
- best(result) {
182
- return result.best;
269
+ }, image, debug);
183
270
  }
184
271
  dispose() {
185
- this.additional?.dispose();
186
272
  this.host.dispose();
187
273
  }
188
274
  }
189
275
  /** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
190
276
  export async function scan(image, options = {}) {
191
- const { multiple, debug, includeRegions, ...creation } = options;
277
+ const input = options;
278
+ if (input === null || typeof input !== "object" || Array.isArray(input))
279
+ throw new TypeError("Invalid scan options");
280
+ const { debug, finishCandidates, ...creation } = options;
281
+ if (debug !== undefined && typeof debug !== "boolean")
282
+ throw new TypeError("debug must be a boolean");
192
283
  const scanner = await Scanner.create(creation);
193
284
  try {
194
- return scanner.scan(image, { multiple, debug, includeRegions });
285
+ return scanner.scan(image, { debug, finishCandidates });
195
286
  }
196
287
  finally {
197
288
  scanner.dispose();
@@ -0,0 +1,7 @@
1
+ import type { Quad } from "../host.js";
2
+ /** Convex containment, including the boundary; reject degenerate coverage. */
3
+ export declare function containsPoint(point: readonly number[], quad: Quad): boolean;
4
+ /** Only deep retries are deferred. Normal proposals and the full-frame bit remain. */
5
+ export declare function uncoveredRetryMask(proposals: readonly {
6
+ polygon: Quad;
7
+ }[], coverage: readonly Quad[], initial?: readonly number[]): [number, number];
@@ -0,0 +1,26 @@
1
+ /** Convex containment, including the boundary; reject degenerate coverage. */
2
+ export function containsPoint(point, quad) {
3
+ if (!point.every(Number.isFinite) || quad.some((p) => !p.every(Number.isFinite)))
4
+ return false;
5
+ let positive = false;
6
+ let negative = false;
7
+ let area = 0;
8
+ for (let i = 0; i < 4; i++) {
9
+ const a = quad[i], b = quad[(i + 1) % 4];
10
+ const cross = (b[0] - a[0]) * (point[1] - a[1]) - (b[1] - a[1]) * (point[0] - a[0]);
11
+ positive ||= cross > 1e-6;
12
+ negative ||= cross < -1e-6;
13
+ area += a[0] * b[1] - b[0] * a[1];
14
+ }
15
+ return Math.abs(area) > 1e-6 && !(positive && negative);
16
+ }
17
+ /** Only deep retries are deferred. Normal proposals and the full-frame bit remain. */
18
+ export function uncoveredRetryMask(proposals, coverage, initial = [0xffffffff, 0xffffffff]) {
19
+ const mask = [initial[0] >>> 0, initial[1] >>> 0];
20
+ proposals.forEach((proposal, index) => {
21
+ if (coverage.some((quad) => proposal.polygon.every((point) => containsPoint(point, quad)))) {
22
+ mask[index >>> 5] = (mask[index >>> 5] & ~(1 << (index & 31))) >>> 0;
23
+ }
24
+ });
25
+ return mask;
26
+ }
@@ -19,8 +19,10 @@ export declare const formatBits: {
19
19
  };
20
20
  export type Format = keyof typeof formatBits;
21
21
  export declare const retailFormats: readonly Format[];
22
+ export declare const commonLinearFormats: readonly Format[];
23
+ export declare const commonFormats: readonly Format[];
22
24
  export declare const linearFormats: readonly Format[];
23
25
  export declare const matrixFormats: readonly Format[];
24
- export type FormatSelection = readonly Format[] | "1D" | "2D" | "all";
25
- export declare function resolveFormats(input?: readonly string[] | "1D" | "2D" | "all"): Format[];
26
+ export type FormatSelection = Format | readonly Format[] | "retail" | "common1D" | "common" | "1D" | "2D" | "all";
27
+ export declare function resolveFormats(input?: readonly string[] | string): Format[];
26
28
  export declare function maskFor(formats: readonly Format[]): number;
@@ -18,11 +18,15 @@ export const formatBits = {
18
18
  MaxiCode: 131072,
19
19
  };
20
20
  export const retailFormats = ["EAN13", "UPCA", "EAN8", "UPCE"];
21
- export const linearFormats = [
21
+ export const commonLinearFormats = [
22
22
  ...retailFormats,
23
23
  "Code128",
24
24
  "Code39",
25
25
  "ITF",
26
+ ];
27
+ export const commonFormats = [...commonLinearFormats, "QRCode", "DataMatrix"];
28
+ export const linearFormats = [
29
+ ...commonLinearFormats,
26
30
  "Codabar",
27
31
  "Code93",
28
32
  "DataBar",
@@ -36,6 +40,12 @@ export const matrixFormats = [
36
40
  "MaxiCode",
37
41
  ];
38
42
  export function resolveFormats(input) {
43
+ if (input === "retail")
44
+ return [...retailFormats];
45
+ if (input === "common1D")
46
+ return [...commonLinearFormats];
47
+ if (input === "common")
48
+ return [...commonFormats];
39
49
  if (input === "1D")
40
50
  return [...linearFormats];
41
51
  if (input === "2D")
@@ -44,6 +54,8 @@ export function resolveFormats(input) {
44
54
  return [...linearFormats, ...matrixFormats];
45
55
  if (input === undefined)
46
56
  return ["EAN13"];
57
+ if (typeof input === "string" && Object.hasOwn(formatBits, input))
58
+ return [input];
47
59
  if (!Array.isArray(input) || input.length === 0)
48
60
  throw Error("Choose at least one barcode format.");
49
61
  for (const value of input) {
@@ -2,6 +2,11 @@ import type { Quad } from "../host.js";
2
2
  export type Transform = readonly number[];
3
3
  export declare function project(t: Transform, x: number, y: number): [number, number];
4
4
  export declare function transformFor(quad: Quad, width: number, height: number): Transform;
5
+ export declare function rectificationPlan(quad: Quad): {
6
+ width: number;
7
+ height: number;
8
+ transform: number[];
9
+ };
5
10
  export declare function rectify(gray: Uint8Array, width: number, height: number, quad: Quad): {
6
11
  data: Uint8Array<ArrayBuffer>;
7
12
  width: number;