tapirscan 1.0.0 → 1.2.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 +184 -105
  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 +91 -33
  13. package/dist/index.js +174 -86
  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
+ /** 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. */
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,75 @@ 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 undecoded: readonly UndecodedRegion[];
121
+ readonly debug?: Diagnostics;
71
122
  }
123
+ export type EanAddOnPolicy = "Ignore" | "Read" | "Require";
72
124
  export interface ScannerOptions {
125
+ /** Optional EAN/UPC supplement policy, fixed at creation. */
126
+ eanAddOnPolicy?: EanAddOnPolicy;
73
127
  mode?: Mode;
74
128
  formats?: FormatSelection;
129
+ /** Directory containing the packaged WASMs; relative to the page in browsers. */
130
+ wasmBaseUrl?: string | URL;
131
+ /** Advanced loader, receiving URLs resolved against wasmBaseUrl. */
75
132
  loadWasm?: (url: URL) => Promise<ArrayBuffer>;
76
133
  }
77
134
  export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
@@ -79,13 +136,14 @@ export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
79
136
  export declare class Scanner {
80
137
  private readonly host;
81
138
  readonly mode: Mode;
82
- private readonly additional?;
83
- private readonly formats;
139
+ private readonly configuredFormats;
140
+ private readonly addOnPolicy;
84
141
  private constructor();
142
+ /** Formats available for scanning, fixed at creation. */
143
+ get formats(): readonly Format[];
144
+ get eanAddOnPolicy(): EanAddOnPolicy;
85
145
  static create(options?: ScannerOptions): Promise<Scanner>;
86
146
  scan(inputImage: PixelImage, options?: ScanOptions): ScanResult;
87
- /** Convenience alias for result.best. */
88
- best(result: ScanResult): Barcode | undefined;
89
147
  dispose(): void;
90
148
  }
91
149
  /** 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,149 +111,175 @@ 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
+ const regions = regionEvidence(raw);
115
+ return freeze({
53
116
  barcodes,
54
117
  values: barcodes.map((b) => b.text),
55
118
  best: barcodes[bestIndex],
119
+ undecoded: regions.undecoded,
56
120
  image: { width: image.width, height: image.height },
57
121
  mode: raw.mode,
58
122
  elapsedMs: raw.elapsedMs,
59
- unfinished: raw.scan.unfinished,
60
- ...(debug ? { debug: raw } : {}),
61
- };
123
+ unfinished: raw.scan.unfinished || raw.localizationLimited || (raw.localization?.omitted ?? 0) > 0,
124
+ ...(debug ? { debug: { ...raw, regions } } : {}),
125
+ });
62
126
  }
63
127
  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",
128
+ low: "low-complete-release-20260916.wasm",
129
+ medium: "medium-complete-release-20260916.wasm",
130
+ high: "high-complete-release-20260916.wasm",
131
+ "very-high": "very-high-complete-release-20260916.wasm",
68
132
  };
69
133
  /** Mode selects a compiled implementation. Create another instance to switch. */
70
134
  export class Scanner {
71
135
  host;
72
136
  mode;
73
- additional;
74
- formats;
75
- constructor(host, mode, additional, formats = ["EAN13"]) {
137
+ configuredFormats;
138
+ addOnPolicy;
139
+ constructor(host, mode, configuredFormats = ["EAN13"], addOnPolicy = "Ignore") {
76
140
  this.host = host;
77
141
  this.mode = mode;
78
- this.additional = additional;
79
- this.formats = formats;
142
+ this.configuredFormats = configuredFormats;
143
+ this.addOnPolicy = addOnPolicy;
144
+ Object.freeze(configuredFormats);
145
+ }
146
+ /** Formats available for scanning, fixed at creation. */
147
+ get formats() {
148
+ return this.configuredFormats;
149
+ }
150
+ get eanAddOnPolicy() {
151
+ return this.addOnPolicy;
80
152
  }
81
153
  static async create(options = {}) {
82
154
  const input = options;
83
155
  if (input === null || typeof input !== "object" || Array.isArray(input))
84
156
  throw new TypeError("Invalid scanner options");
85
157
  for (const key of Object.keys(options))
86
- if (!["mode", "formats", "loadWasm"].includes(key))
158
+ if (!["mode", "formats", "wasmBaseUrl", "loadWasm", "eanAddOnPolicy"].includes(key))
87
159
  throw new TypeError(`Unknown scanner option: ${key}`);
88
160
  const mode = options.mode ?? "medium";
89
161
  if (!Object.hasOwn(modes, mode))
90
162
  throw new TypeError("Unknown scanner mode");
91
- const extraAsset = "../wasm/multiformat.wasm";
92
163
  const formats = resolveFormats(options.formats);
93
- const url = new URL("../wasm/" + modes[mode], import.meta.url);
164
+ const addonPolicy = options.eanAddOnPolicy === undefined ? "Ignore" : options.eanAddOnPolicy;
165
+ if (!["Ignore", "Read", "Require"].includes(addonPolicy))
166
+ throw new TypeError("eanAddOnPolicy must be Ignore, Read or Require");
167
+ if (options.loadWasm !== undefined && typeof options.loadWasm !== "function")
168
+ throw new TypeError("loadWasm must be a function");
169
+ if (options.wasmBaseUrl !== undefined &&
170
+ typeof options.wasmBaseUrl !== "string" &&
171
+ !(options.wasmBaseUrl instanceof URL))
172
+ throw new TypeError("wasmBaseUrl must be a string or URL");
173
+ const base = options.wasmBaseUrl === undefined
174
+ ? new URL(/* @vite-ignore */ "../wasm/", import.meta.url)
175
+ : new URL(options.wasmBaseUrl, typeof location === "undefined" ? import.meta.url : location.href);
176
+ if (!base.pathname.endsWith("/"))
177
+ base.pathname += "/";
94
178
  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));
179
+ const needsPrimary = formats.some((f) => f === "EAN13" || f === "UPCA");
180
+ const bytes = needsPrimary ? await load(new URL(modes[mode], base)) : undefined;
181
+ const recovery = !needsPrimary || mode === "low" ? undefined : await load(new URL(modes.low, base));
182
+ const multiformat = addonPolicy !== "Ignore" || formats.length !== 1 || formats[0] !== "EAN13";
183
+ const extra = addonPolicy !== "Ignore" || formats.some((f) => f !== "EAN13" && f !== "UPCA")
184
+ ? await load(new URL("multiformat.wasm", base))
185
+ : undefined;
186
+ if (multiformat)
187
+ return new Scanner(await MediumMultiformatScanner.create(bytes, extra, mode, recovery), mode, formats, addonPolicy);
188
+ if (!bytes)
189
+ throw new Error("EAN13 engine was not loaded");
97
190
  const host = recovery
98
191
  ? await ReleaseDetailScanner.create(bytes, recovery, mode)
99
192
  : 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
- }
193
+ return new Scanner(host, mode, formats);
113
194
  }
114
195
  scan(inputImage, options = {}) {
115
196
  const image = pixels(inputImage);
116
197
  const input = options;
117
198
  if (input === null || typeof input !== "object" || Array.isArray(input))
118
199
  throw new TypeError("Invalid scan options");
119
- for (const key of Object.keys(options)) {
120
- if (key !== "multiple" && key !== "includeRegions" && key !== "debug")
200
+ for (const key of Object.keys(options))
201
+ if (key !== "debug" && key !== "formats" && key !== "extendedBudget")
121
202
  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);
203
+ if (options.debug !== undefined && typeof options.debug !== "boolean")
204
+ throw new TypeError("debug must be a boolean");
205
+ if (options.extendedBudget !== undefined && typeof options.extendedBudget !== "boolean")
206
+ throw new TypeError("extendedBudget must be a boolean");
207
+ const finishCandidates = options.extendedBudget ?? false;
208
+ const debug = options.debug ?? false;
209
+ const scanPolicy = { ...policy, finishCandidates };
210
+ const formats = options.formats === undefined ? this.formats : resolveFormats(options.formats);
211
+ if (formats.some((format) => !this.formats.includes(format)))
212
+ throw new TypeError(`Scan formats must be a subset of configured formats. Requested: ${formats.join(", ")}; configured: ${this.formats.join(", ")}`);
213
+ if (this.host instanceof MediumMultiformatScanner) {
214
+ const frame = this.host.scan(image, formats, {
215
+ eanAddOnSymbol: this.eanAddOnPolicy,
216
+ finishCandidates,
217
+ });
218
+ const barcodes = frame.barcodes;
219
+ const primary = frame.primary;
220
+ const localization = primary?.localization;
136
221
  return publicResult({
137
222
  schemaVersion: 2,
138
223
  mode: this.mode,
139
- multiple,
224
+ multiple: true,
140
225
  elapsedMs: frame.scanMs,
226
+ // Preserve the aggregate limit flag when additional readers cannot separate causes.
141
227
  localizationLimited: frame.unfinished,
142
228
  scan: {
229
+ ...primary?.scan,
143
230
  barcodes,
144
231
  unfinished: frame.unfinished,
145
- ...(includeRegions ? { regions: frame.regions } : {}),
232
+ regions: frame.regions,
146
233
  },
147
- }, image, includeRegions);
234
+ ...(primary
235
+ ? {
236
+ localization,
237
+ searchWindows: primary.searchWindows,
238
+ ...("recovery" in primary ? { recovery: primary.recovery } : {}),
239
+ ...("detailRegions" in primary ? { detailRegions: primary.detailRegions } : {}),
240
+ }
241
+ : {}),
242
+ }, image, debug);
148
243
  }
149
- const full = this.host.scanLocalized(image, policy, fitLimits[this.mode], true);
244
+ const full = this.host.scanLocalized(image, scanPolicy, fitLimits[this.mode], true);
150
245
  // Isolate the imported host's JSON result at the public ABI type boundary.
151
246
  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) => ({
247
+ const barcodes = mergeLinearDuplicates(full.scan.barcodes.map((b) => ({
154
248
  ...b,
155
249
  format: "EAN13",
156
- }));
250
+ })), image);
157
251
  return publicResult({
158
252
  schemaVersion: 2,
159
253
  mode: this.mode,
160
- multiple,
254
+ multiple: true,
161
255
  elapsedMs: full.scanMs,
162
- localizationLimited: localization.workLimited,
163
- scan: includeRegions
164
- ? { ...full.scan, barcodes }
165
- : { barcodes, unfinished: full.scan.unfinished },
166
- ...(includeRegions
256
+ localizationLimited: localization.workLimited || localization.omitted > 0,
257
+ scan: { ...full.scan, barcodes },
258
+ localization,
259
+ searchWindows: full.searchWindows,
260
+ ...("recovery" in full && "detailRegions" in full
167
261
  ? {
168
- localization,
169
- searchWindows: full.searchWindows,
170
- ...("recovery" in full && "detailRegions" in full
171
- ? {
172
- recovery: full.recovery,
173
- detailRegions: full.detailRegions,
174
- }
175
- : {}),
262
+ recovery: full.recovery,
263
+ detailRegions: full.detailRegions,
176
264
  }
177
265
  : {}),
178
- }, image, includeRegions);
179
- }
180
- /** Convenience alias for result.best. */
181
- best(result) {
182
- return result.best;
266
+ }, image, debug);
183
267
  }
184
268
  dispose() {
185
- this.additional?.dispose();
186
269
  this.host.dispose();
187
270
  }
188
271
  }
189
272
  /** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
190
273
  export async function scan(image, options = {}) {
191
- const { multiple, debug, includeRegions, ...creation } = options;
274
+ const input = options;
275
+ if (input === null || typeof input !== "object" || Array.isArray(input))
276
+ throw new TypeError("Invalid scan options");
277
+ const { debug, extendedBudget, ...creation } = options;
278
+ if (debug !== undefined && typeof debug !== "boolean")
279
+ throw new TypeError("debug must be a boolean");
192
280
  const scanner = await Scanner.create(creation);
193
281
  try {
194
- return scanner.scan(image, { multiple, debug, includeRegions });
282
+ return scanner.scan(image, { debug, extendedBudget });
195
283
  }
196
284
  finally {
197
285
  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;