tapirscan 1.2.0 → 1.2.2

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 (62) hide show
  1. package/README.md +9 -10
  2. package/dist/index.d.ts +24 -29
  3. package/dist/index.js +64 -154
  4. package/dist/multiformat/format-registry.d.ts +26 -0
  5. package/dist/multiformat/format-registry.js +62 -0
  6. package/dist/multiformat/formats.d.ts +2 -24
  7. package/dist/multiformat/formats.js +3 -41
  8. package/dist/rust-session.d.ts +19 -0
  9. package/dist/rust-session.js +92 -0
  10. package/package.json +7 -9
  11. package/wasm/high-release-1.2.2-public-low-r2.wasm +0 -0
  12. package/wasm/low-release-1.2.2-public-low-r2.wasm +0 -0
  13. package/wasm/medium-release-1.2.2-public-low-r2.wasm +0 -0
  14. package/wasm/very-high-release-1.2.2-public-low-r2.wasm +0 -0
  15. package/dist/completion-host.d.mts +0 -40
  16. package/dist/completion-host.mjs +0 -188
  17. package/dist/detail-20260914/continuity.d.mts +0 -2
  18. package/dist/detail-20260914/continuity.mjs +0 -62
  19. package/dist/detail-20260914/detail-proposals-rich.d.mts +0 -13
  20. package/dist/detail-20260914/detail-proposals-rich.mjs +0 -53
  21. package/dist/detail-20260914/direct-recovery.d.mts +0 -28
  22. package/dist/detail-20260914/direct-recovery.mjs +0 -139
  23. package/dist/detail-20260914/host.d.mts +0 -40
  24. package/dist/detail-20260914/host.mjs +0 -280
  25. package/dist/detail-20260914/scanner.d.mts +0 -12
  26. package/dist/detail-20260914/scanner.mjs +0 -64
  27. package/dist/detail-20260914/source-evidence.d.mts +0 -10
  28. package/dist/detail-20260914/source-evidence.mjs +0 -94
  29. package/dist/detail-canvas.d.ts +0 -35
  30. package/dist/detail-canvas.js +0 -73
  31. package/dist/detail-runtime/direct-recovery.d.mts +0 -28
  32. package/dist/detail-runtime/direct-recovery.mjs +0 -142
  33. package/dist/detail-runtime/host.d.mts +0 -40
  34. package/dist/detail-runtime/host.mjs +0 -282
  35. package/dist/detail-runtime/scanner.d.mts +0 -12
  36. package/dist/detail-runtime/scanner.mjs +0 -70
  37. package/dist/detail.d.ts +0 -18
  38. package/dist/detail.js +0 -84
  39. package/dist/host.d.ts +0 -112
  40. package/dist/host.js +0 -184
  41. package/dist/host64.d.ts +0 -112
  42. package/dist/host64.js +0 -184
  43. package/dist/multiformat/coverage.d.ts +0 -7
  44. package/dist/multiformat/coverage.js +0 -26
  45. package/dist/multiformat/geometry.d.ts +0 -37
  46. package/dist/multiformat/geometry.js +0 -132
  47. package/dist/multiformat/linear-duplicates.d.ts +0 -16
  48. package/dist/multiformat/linear-duplicates.js +0 -163
  49. package/dist/multiformat/pixels.d.ts +0 -3
  50. package/dist/multiformat/pixels.js +0 -36
  51. package/dist/multiformat/scanner.d.ts +0 -60
  52. package/dist/multiformat/scanner.js +0 -361
  53. package/dist/multiformat-host.d.ts +0 -121
  54. package/dist/multiformat-host.js +0 -207
  55. package/dist/policy.d.ts +0 -12
  56. package/dist/policy.js +0 -12
  57. package/wasm/high-complete-release-20260916.wasm +0 -0
  58. package/wasm/low-complete-release-20260916.wasm +0 -0
  59. package/wasm/medium-complete-release-20260916.wasm +0 -0
  60. package/wasm/multiformat.json +0 -7
  61. package/wasm/multiformat.wasm +0 -0
  62. package/wasm/very-high-complete-release-20260916.wasm +0 -0
package/README.md CHANGED
@@ -1,10 +1,5 @@
1
1
  # Tapirscan for JavaScript and TypeScript
2
2
 
3
- This guide describes the 1.2.0 API revision. See [migration](../../docs/API_MIGRATION.md).
4
- Build/install this checkout using [the development guide](../../docs/DEVELOPMENT.md)
5
- to use these changes before publication; older registry packages use their own
6
- versioned API.
7
-
8
3
  Scan image pixels in a browser or Node with the same Rust/WASM core.
9
4
  [Try the live demo](https://tapirscan.netlify.app) · [Quick start](#quick-start) · [WASM loading](#wasm-loading) · [Functions](#functions) · [All options](#all-options) · [Results](#results)
10
5
 
@@ -29,7 +24,7 @@ const result = await scan(image);
29
24
  console.log(result.values); // e.g. ["4006381333931"]
30
25
  ```
31
26
 
32
- Defaults are Medium effort, EAN13, multiple results, and debug disabled.
27
+ Defaults are Medium effort, retail formats, multiple results, and debug disabled.
33
28
  `result.barcodes` also gives each read's text, format, polygon and rectangle.
34
29
  The helper creates and disposes a scanner automatically. Browser apps need to
35
30
  serve its [WASM assets](#wasm-loading); Node loads the packaged files automatically.
@@ -96,11 +91,12 @@ The one-shot helper accepts the same option:
96
91
  URLs resolve against the page/worker URL in browsers and the package module in
97
92
  Node; use an absolute URL for an unambiguous location. For authenticated requests
98
93
  or custom storage, use `loadWasm: async (url) => arrayBuffer`. The callback receives
99
- URLs resolved against `wasmBaseUrl` when both options are supplied. Only engines needed by the selected formats are loaded.
94
+ URLs resolved against `wasmBaseUrl` when both options are supplied. Creation loads
95
+ one complete Rust scanner for the selected effort mode.
100
96
 
101
97
  Use the deployed base path if your app is hosted below a subpath. Copy all current
102
- WASMs: EAN13/UPCA scanning in Medium/High/Very high also loads the Low recovery decoder. The demo and its
103
- comparison engines are not needed in your app.
98
+ WASMs so every effort mode remains available. The demo and its comparison engines
99
+ are not needed in your app.
104
100
 
105
101
  ## Camera and worker use
106
102
 
@@ -141,7 +137,7 @@ call and does not change the default formats. Previously returned results surviv
141
137
  | Option | Where | Default | Meaning |
142
138
  | ---------------- | ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
143
139
  | `mode` | Creation | `"medium"` | `"low"`, `"medium"`, `"high"`, `"very-high"`. |
144
- | `formats` | Creation / scan | `["EAN13"]` | A single identifier, `"retail"`, `"common1D"`, `"common"`, `"1D"`, `"2D"`, `"all"`, or a nonempty array. Per-call selections must be subsets of creation formats. |
140
+ | `formats` | Creation / scan | `"retail"` | A single identifier, `"retail"`, `"common1D"`, `"common"`, `"1D"`, `"2D"`, `"all"`, or a nonempty array. Per-call selections must be subsets of creation formats. |
145
141
  | `wasmBaseUrl` | Creation | Module-relative assets | Directory URL for packaged WASMs. Use this for normal browser hosting. |
146
142
  | `loadWasm` | Creation | Module-relative loader | `(url: URL) => Promise<ArrayBuffer>`. Uses HTTP fetch in browsers and filesystem reads for Node file URLs. |
147
143
  | `eanAddOnPolicy` | Creation / one-shot | `"Ignore"` | `"Ignore"`, `"Read"`, `"Require"`; optional EAN/UPC supplement policy. |
@@ -205,6 +201,9 @@ including separate copies of the same value. Coordinates start at the
205
201
  top left, x rightward and y downward. Geometry is returned, not a cropped bitmap.
206
202
  Map coordinates back yourself if you resize/rotate before scanning. Support is a
207
203
  ranking heuristic, not a probability.
204
+ Polygon coordinates are Rust `f32` values exposed as JavaScript numbers. Their
205
+ decimal string form may show the exact binary value instead of the shorter
206
+ decimal spelling used by older package builds.
208
207
 
209
208
  The package exports `EanAddOnPolicy`, `ScannerOptions`, `ScanOptions`, `ScanResult`, `Barcode`,
210
209
  `PixelImage`, `Image`, `Quad`, `Mode`, `Format`, `FormatSelection`, `Diagnostics`,
package/dist/index.d.ts CHANGED
@@ -1,17 +1,13 @@
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
2
  export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, } from "./multiformat/formats.js";
5
3
  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. */
4
+ export { ScannerError } from "./rust-session.js";
8
5
  export type Quad = readonly [
9
6
  readonly [number, number],
10
7
  readonly [number, number],
11
8
  readonly [number, number],
12
9
  readonly [number, number]
13
10
  ];
14
- /** Decoded pixels; stride defaults to width * channels. Alpha is ignored. */
15
11
  export interface Image {
16
12
  readonly data: Uint8Array;
17
13
  readonly width: number;
@@ -19,58 +15,56 @@ export interface Image {
19
15
  readonly channels: 1 | 3 | 4;
20
16
  readonly stride?: number;
21
17
  }
22
- export { ScannerError } from "./completion-host.mjs";
23
- type RawDiagnosticBarcode = FormatBarcode | (ScanFrame["barcodes"][number] & {
24
- format: "EAN13";
25
- });
26
18
  export type Mode = "low" | "medium" | "high" | "very-high";
27
19
  export interface ScanOptions {
28
20
  /** Allow reader-specific extra work. Supported for every format; exact budgets may evolve. */
29
21
  extendedBudget?: boolean;
30
22
  /** Per-call subset of the formats configured at creation. */
31
23
  formats?: FormatSelection;
24
+ /** Include raw engine diagnostics. Public barcodes and unread geometry are always included. */
32
25
  debug?: boolean;
33
26
  }
34
27
  interface RawDiagnostics {
35
- schemaVersion: 2;
36
- mode: Mode;
37
- multiple: boolean;
38
- elapsedMs: number;
39
- localizationLimited: boolean;
40
28
  scan: {
41
29
  barcodes: RawDiagnosticBarcode[];
42
30
  unfinished: boolean;
43
- regions?: FormatBarcode[];
44
- } & Partial<Omit<ScanFrame, "barcodes" | "unfinished">>;
31
+ candidates?: unknown[];
32
+ };
45
33
  localization?: {
46
34
  proposals: {
47
- polygon: HostQuad;
48
- score: number;
49
- text: string;
35
+ polygon: Quad;
36
+ score?: number;
37
+ text?: string;
50
38
  }[];
51
- omitted: number;
52
- workLimited: boolean;
53
- trace?: Record<string, number>;
54
39
  };
55
- recovery?: Recovery;
56
- detailRegions?: DetailRegion[];
57
40
  searchWindows?: {
58
41
  kind: string;
59
42
  polygon: number[][];
60
43
  candidateIndex: number;
61
44
  }[];
45
+ [key: string]: unknown;
62
46
  }
63
- /** Deeply immutable scan evidence, independent of the scanner lifetime. */
64
47
  type ReadonlyDeep<T> = T extends object ? {
65
48
  readonly [K in keyof T]: ReadonlyDeep<T[K]>;
66
49
  } : T;
50
+ interface RawDiagnosticBarcode {
51
+ text: string;
52
+ format: Format | "Unknown";
53
+ polygon: Quad;
54
+ support: number;
55
+ bytes?: number[];
56
+ candidate_indices?: number[];
57
+ gs1?: boolean;
58
+ readerInitialization?: boolean;
59
+ structuredAppend?: StructuredAppend;
60
+ eanAddOn?: string;
61
+ [key: string]: unknown;
62
+ }
67
63
  export type DiagnosticBarcode = ReadonlyDeep<RawDiagnosticBarcode>;
68
- /** Source geometry with no accepted decode; format is a reader hint. */
69
64
  export interface UndecodedRegion {
70
65
  readonly format: Format | "Unknown";
71
66
  readonly polygon: Quad;
72
67
  }
73
- /** Stable region evidence. null means this reader did not expose that evidence. */
74
68
  export interface RegionEvidence {
75
69
  readonly proposals: ReadonlyDeep<NonNullable<RawDiagnostics["localization"]>["proposals"]> | null;
76
70
  readonly searchWindows: ReadonlyDeep<NonNullable<RawDiagnostics["searchWindows"]>> | null;
@@ -80,7 +74,6 @@ export type Diagnostics = ReadonlyDeep<RawDiagnostics> & {
80
74
  readonly regions: RegionEvidence;
81
75
  };
82
76
  export interface StructuredAppend {
83
- /** One-based symbol index; symbols are not automatically assembled. */
84
77
  readonly index: number;
85
78
  readonly count: number;
86
79
  readonly id?: string;
@@ -115,6 +108,7 @@ export interface ScanResult {
115
108
  readonly height: number;
116
109
  };
117
110
  readonly mode: Mode;
111
+ /** Whole synchronous WASM call time measured by the JavaScript host. */
118
112
  readonly elapsedMs: number;
119
113
  readonly unfinished: boolean;
120
114
  readonly undecoded: readonly UndecodedRegion[];
@@ -132,7 +126,7 @@ export interface ScannerOptions {
132
126
  loadWasm?: (url: URL) => Promise<ArrayBuffer>;
133
127
  }
134
128
  export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
135
- /** Mode selects a compiled implementation. Create another instance to switch. */
129
+ /** A reusable scanner. Results own their data and remain valid after later scans or disposal. */
136
130
  export declare class Scanner {
137
131
  private readonly host;
138
132
  readonly mode: Mode;
@@ -144,6 +138,7 @@ export declare class Scanner {
144
138
  get eanAddOnPolicy(): EanAddOnPolicy;
145
139
  static create(options?: ScannerOptions): Promise<Scanner>;
146
140
  scan(inputImage: PixelImage, options?: ScanOptions): ScanResult;
141
+ /** Release the WASM session. Repeated disposal is safe; scanning afterward fails. */
147
142
  dispose(): void;
148
143
  }
149
144
  /** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
- import { mergeLinearDuplicates } from "./multiformat/linear-duplicates.js";
2
- import { ReleaseDetailScanner, fitLimits } from "./detail.js";
3
- import { policy } from "./policy.js";
4
- import { MediumMultiformatScanner } from "./multiformat/scanner.js";
5
- import { resolveFormats } from "./multiformat/formats.js";
1
+ import { maskFor, resolveFormats, } from "./multiformat/formats.js";
2
+ import { RustScannerSession, ScannerError } from "./rust-session.js";
6
3
  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";
4
+ export { ScannerError } from "./rust-session.js";
5
+ const modes = {
6
+ low: { id: 0, file: "low-release-1.2.2-public-low-r2.wasm" },
7
+ medium: { id: 1, file: "medium-release-1.2.2-public-low-r2.wasm" },
8
+ high: { id: 2, file: "high-release-1.2.2-public-low-r2.wasm" },
9
+ "very-high": { id: 3, file: "very-high-release-1.2.2-public-low-r2.wasm" },
10
+ };
11
+ const addOnPolicies = { Ignore: 0, Read: 1, Require: 2 };
9
12
  function pixels(image) {
10
13
  const input = image;
11
14
  if (input === null || typeof input !== "object")
@@ -36,7 +39,6 @@ function pixels(image) {
36
39
  stride,
37
40
  };
38
41
  }
39
- /** Freeze only plain result data, never caller-owned input buffers. */
40
42
  function freeze(value) {
41
43
  for (const child of Object.values(value))
42
44
  if (child !== null && typeof child === "object")
@@ -54,89 +56,58 @@ async function loadDefault(url) {
54
56
  throw new Error(`WASM load failed: ${String(response.status)}`);
55
57
  return response.arrayBuffer();
56
58
  }
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,
59
+ function wireResult(value) {
60
+ if (value === null ||
61
+ typeof value !== "object" ||
62
+ !("barcodes" in value) ||
63
+ !Array.isArray(value.barcodes) ||
64
+ !("bestIndex" in value) ||
65
+ (value.bestIndex !== null &&
66
+ (!Number.isInteger(value.bestIndex) ||
67
+ value.bestIndex < 0 ||
68
+ value.bestIndex >= value.barcodes.length)) ||
69
+ !("undecoded" in value) ||
70
+ !Array.isArray(value.undecoded) ||
71
+ !("image" in value) ||
72
+ value.image === null ||
73
+ typeof value.image !== "object" ||
74
+ !("mode" in value) ||
75
+ !Object.hasOwn(modes, String(value.mode)) ||
76
+ !("unfinished" in value) ||
77
+ typeof value.unfinished !== "boolean")
78
+ throw new ScannerError("invalid_output", "Scanner returned an invalid result");
79
+ return value;
80
+ }
81
+ function publicResult(raw, elapsedMs, debug) {
82
+ const barcodes = raw.barcodes;
83
+ const undecoded = raw.undecoded.map(({ format, polygon }) => ({
84
+ format: format ?? "Unknown",
85
+ polygon,
86
+ }));
87
+ const regions = {
88
+ proposals: raw.debug?.localization?.proposals ?? null,
89
+ searchWindows: raw.debug?.searchWindows ?? null,
82
90
  undecoded,
83
91
  };
84
- }
85
- function publicResult(raw, image, debug) {
86
- const barcodes = raw.scan.barcodes.map((read) => {
87
- const { text, format, polygon, support } = read;
88
- const left = Math.floor(Math.min(...polygon.map((p) => p[0])));
89
- const top = Math.floor(Math.min(...polygon.map((p) => p[1])));
90
- return {
91
- text,
92
- format,
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 } : {}),
102
- rect: {
103
- left,
104
- top,
105
- width: Math.ceil(Math.max(...polygon.map((p) => p[0]))) - left,
106
- height: Math.ceil(Math.max(...polygon.map((p) => p[1]))) - top,
107
- },
108
- };
109
- });
110
- let bestIndex = -1;
111
- for (let i = 0; i < barcodes.length; i++)
112
- if (bestIndex < 0 || raw.scan.barcodes[i].support > raw.scan.barcodes[bestIndex].support)
113
- bestIndex = i;
114
- const regions = regionEvidence(raw);
115
92
  return freeze({
116
93
  barcodes,
117
94
  values: barcodes.map((b) => b.text),
118
- best: barcodes[bestIndex],
119
- undecoded: regions.undecoded,
120
- image: { width: image.width, height: image.height },
95
+ best: raw.bestIndex === null ? undefined : barcodes[raw.bestIndex],
96
+ image: raw.image,
121
97
  mode: raw.mode,
122
- elapsedMs: raw.elapsedMs,
123
- unfinished: raw.scan.unfinished || raw.localizationLimited || (raw.localization?.omitted ?? 0) > 0,
124
- ...(debug ? { debug: { ...raw, regions } } : {}),
98
+ elapsedMs,
99
+ unfinished: raw.unfinished,
100
+ undecoded,
101
+ ...(debug && raw.debug ? { debug: { ...raw.debug, regions } } : {}),
125
102
  });
126
103
  }
127
- const modes = {
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",
132
- };
133
- /** Mode selects a compiled implementation. Create another instance to switch. */
104
+ /** A reusable scanner. Results own their data and remain valid after later scans or disposal. */
134
105
  export class Scanner {
135
106
  host;
136
107
  mode;
137
108
  configuredFormats;
138
109
  addOnPolicy;
139
- constructor(host, mode, configuredFormats = ["EAN13"], addOnPolicy = "Ignore") {
110
+ constructor(host, mode, configuredFormats, addOnPolicy) {
140
111
  this.host = host;
141
112
  this.mode = mode;
142
113
  this.configuredFormats = configuredFormats;
@@ -161,8 +132,8 @@ export class Scanner {
161
132
  if (!Object.hasOwn(modes, mode))
162
133
  throw new TypeError("Unknown scanner mode");
163
134
  const formats = resolveFormats(options.formats);
164
- const addonPolicy = options.eanAddOnPolicy === undefined ? "Ignore" : options.eanAddOnPolicy;
165
- if (!["Ignore", "Read", "Require"].includes(addonPolicy))
135
+ const addOnPolicy = options.eanAddOnPolicy === undefined ? "Ignore" : options.eanAddOnPolicy;
136
+ if (!Object.hasOwn(addOnPolicies, addOnPolicy))
166
137
  throw new TypeError("eanAddOnPolicy must be Ignore, Read or Require");
167
138
  if (options.loadWasm !== undefined && typeof options.loadWasm !== "function")
168
139
  throw new TypeError("loadWasm must be a function");
@@ -175,22 +146,10 @@ export class Scanner {
175
146
  : new URL(options.wasmBaseUrl, typeof location === "undefined" ? import.meta.url : location.href);
176
147
  if (!base.pathname.endsWith("/"))
177
148
  base.pathname += "/";
178
- const load = options.loadWasm ?? loadDefault;
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");
190
- const host = recovery
191
- ? await ReleaseDetailScanner.create(bytes, recovery, mode)
192
- : await IndependentScanner.create(bytes);
193
- return new Scanner(host, mode, formats);
149
+ const selected = modes[mode];
150
+ const bytes = await (options.loadWasm ?? loadDefault)(new URL(selected.file, base));
151
+ const host = await RustScannerSession.create(bytes, selected.id, maskFor(formats), addOnPolicies[addOnPolicy]);
152
+ return new Scanner(host, mode, formats, addOnPolicy);
194
153
  }
195
154
  scan(inputImage, options = {}) {
196
155
  const image = pixels(inputImage);
@@ -198,73 +157,24 @@ export class Scanner {
198
157
  if (input === null || typeof input !== "object" || Array.isArray(input))
199
158
  throw new TypeError("Invalid scan options");
200
159
  for (const key of Object.keys(options))
201
- if (key !== "debug" && key !== "formats" && key !== "extendedBudget")
160
+ if (!["debug", "formats", "extendedBudget"].includes(key))
202
161
  throw new TypeError(`Unknown scan option: ${key}`);
203
162
  if (options.debug !== undefined && typeof options.debug !== "boolean")
204
163
  throw new TypeError("debug must be a boolean");
205
164
  if (options.extendedBudget !== undefined && typeof options.extendedBudget !== "boolean")
206
165
  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
166
  const formats = options.formats === undefined ? this.formats : resolveFormats(options.formats);
211
167
  if (formats.some((format) => !this.formats.includes(format)))
212
168
  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;
221
- return publicResult({
222
- schemaVersion: 2,
223
- mode: this.mode,
224
- multiple: true,
225
- elapsedMs: frame.scanMs,
226
- // Preserve the aggregate limit flag when additional readers cannot separate causes.
227
- localizationLimited: frame.unfinished,
228
- scan: {
229
- ...primary?.scan,
230
- barcodes,
231
- unfinished: frame.unfinished,
232
- regions: frame.regions,
233
- },
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);
243
- }
244
- const full = this.host.scanLocalized(image, scanPolicy, fitLimits[this.mode], true);
245
- // Isolate the imported host's JSON result at the public ABI type boundary.
246
- const localization = full.localization;
247
- const barcodes = mergeLinearDuplicates(full.scan.barcodes.map((b) => ({
248
- ...b,
249
- format: "EAN13",
250
- })), image);
251
- return publicResult({
252
- schemaVersion: 2,
253
- mode: this.mode,
254
- multiple: true,
255
- elapsedMs: full.scanMs,
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
261
- ? {
262
- recovery: full.recovery,
263
- detailRegions: full.detailRegions,
264
- }
265
- : {}),
266
- }, image, debug);
169
+ const debug = options.debug ?? false;
170
+ const flags = (options.extendedBudget ? 1 : 0) | (debug ? 2 : 0);
171
+ const start = performance.now();
172
+ const raw = wireResult(this.host.scan(image, flags, options.formats === undefined ? 0 : maskFor(formats)));
173
+ if (debug && !raw.debug)
174
+ throw new ScannerError("invalid_output", "Scanner omitted requested diagnostics");
175
+ return publicResult(raw, performance.now() - start, debug);
267
176
  }
177
+ /** Release the WASM session. Repeated disposal is safe; scanning afterward fails. */
268
178
  dispose() {
269
179
  this.host.dispose();
270
180
  }
@@ -0,0 +1,26 @@
1
+ export declare const formatBits: {
2
+ readonly EAN13: 1;
3
+ readonly UPCA: 2;
4
+ readonly EAN8: 4;
5
+ readonly UPCE: 8;
6
+ readonly Code128: 16;
7
+ readonly Code39: 32;
8
+ readonly ITF: 64;
9
+ readonly Codabar: 128;
10
+ readonly Code93: 256;
11
+ readonly QRCode: 512;
12
+ readonly DataMatrix: 1024;
13
+ readonly PDF417: 2048;
14
+ readonly Aztec: 4096;
15
+ readonly DataBar: 8192;
16
+ readonly DataBarExpanded: 16384;
17
+ readonly MaxiCode: 131072;
18
+ };
19
+ export type Format = keyof typeof formatBits;
20
+ export declare const retailFormats: readonly Format[];
21
+ export declare const commonLinearFormats: readonly Format[];
22
+ export declare const commonFormats: readonly Format[];
23
+ export declare const linearFormats: readonly Format[];
24
+ export declare const matrixFormats: readonly Format[];
25
+ export declare const eanAddOnReadFlag = 32768;
26
+ export declare const eanAddOnRequireFlag = 65536;
@@ -0,0 +1,62 @@
1
+ // Generated by scripts/generate_formats.py from config/formats.json. Do not edit.
2
+ export const formatBits = {
3
+ EAN13: 1,
4
+ UPCA: 2,
5
+ EAN8: 4,
6
+ UPCE: 8,
7
+ Code128: 16,
8
+ Code39: 32,
9
+ ITF: 64,
10
+ Codabar: 128,
11
+ Code93: 256,
12
+ QRCode: 512,
13
+ DataMatrix: 1024,
14
+ PDF417: 2048,
15
+ Aztec: 4096,
16
+ DataBar: 8192,
17
+ DataBarExpanded: 16384,
18
+ MaxiCode: 131072,
19
+ };
20
+ export const retailFormats = ["EAN13", "UPCA", "EAN8", "UPCE"];
21
+ export const commonLinearFormats = [
22
+ "EAN13",
23
+ "UPCA",
24
+ "EAN8",
25
+ "UPCE",
26
+ "Code128",
27
+ "Code39",
28
+ "ITF",
29
+ ];
30
+ export const commonFormats = [
31
+ "EAN13",
32
+ "UPCA",
33
+ "EAN8",
34
+ "UPCE",
35
+ "Code128",
36
+ "Code39",
37
+ "ITF",
38
+ "QRCode",
39
+ "DataMatrix",
40
+ ];
41
+ export const linearFormats = [
42
+ "EAN13",
43
+ "UPCA",
44
+ "EAN8",
45
+ "UPCE",
46
+ "Code128",
47
+ "Code39",
48
+ "ITF",
49
+ "Codabar",
50
+ "Code93",
51
+ "DataBar",
52
+ "DataBarExpanded",
53
+ ];
54
+ export const matrixFormats = [
55
+ "QRCode",
56
+ "DataMatrix",
57
+ "PDF417",
58
+ "Aztec",
59
+ "MaxiCode",
60
+ ];
61
+ export const eanAddOnReadFlag = 32768;
62
+ export const eanAddOnRequireFlag = 65536;
@@ -1,28 +1,6 @@
1
1
  /** Explicit opt-in formats for the independent Medium extension. */
2
- export declare const formatBits: {
3
- readonly EAN13: 1;
4
- readonly UPCA: 2;
5
- readonly EAN8: 4;
6
- readonly UPCE: 8;
7
- readonly Code128: 16;
8
- readonly Code39: 32;
9
- readonly ITF: 64;
10
- readonly Codabar: 128;
11
- readonly Code93: 256;
12
- readonly QRCode: 512;
13
- readonly DataMatrix: 1024;
14
- readonly PDF417: 2048;
15
- readonly Aztec: 4096;
16
- readonly DataBar: 8192;
17
- readonly DataBarExpanded: 16384;
18
- readonly MaxiCode: 131072;
19
- };
20
- export type Format = keyof typeof formatBits;
21
- export declare const retailFormats: readonly Format[];
22
- export declare const commonLinearFormats: readonly Format[];
23
- export declare const commonFormats: readonly Format[];
24
- export declare const linearFormats: readonly Format[];
25
- export declare const matrixFormats: readonly Format[];
2
+ import { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, type Format } from "./format-registry.js";
3
+ export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, type Format, };
26
4
  export type FormatSelection = Format | readonly Format[] | "retail" | "common1D" | "common" | "1D" | "2D" | "all";
27
5
  export declare function resolveFormats(input?: readonly string[] | string): Format[];
28
6
  export declare function maskFor(formats: readonly Format[]): number;
@@ -1,44 +1,6 @@
1
1
  /** Explicit opt-in formats for the independent Medium extension. */
2
- export const formatBits = {
3
- EAN13: 1,
4
- UPCA: 2,
5
- EAN8: 4,
6
- UPCE: 8,
7
- Code128: 16,
8
- Code39: 32,
9
- ITF: 64,
10
- Codabar: 128,
11
- Code93: 256,
12
- QRCode: 512,
13
- DataMatrix: 1024,
14
- PDF417: 2048,
15
- Aztec: 4096,
16
- DataBar: 8192,
17
- DataBarExpanded: 16384,
18
- MaxiCode: 131072,
19
- };
20
- export const retailFormats = ["EAN13", "UPCA", "EAN8", "UPCE"];
21
- export const commonLinearFormats = [
22
- ...retailFormats,
23
- "Code128",
24
- "Code39",
25
- "ITF",
26
- ];
27
- export const commonFormats = [...commonLinearFormats, "QRCode", "DataMatrix"];
28
- export const linearFormats = [
29
- ...commonLinearFormats,
30
- "Codabar",
31
- "Code93",
32
- "DataBar",
33
- "DataBarExpanded",
34
- ];
35
- export const matrixFormats = [
36
- "QRCode",
37
- "DataMatrix",
38
- "PDF417",
39
- "Aztec",
40
- "MaxiCode",
41
- ];
2
+ import { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, } from "./format-registry.js";
3
+ export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, };
42
4
  export function resolveFormats(input) {
43
5
  if (input === "retail")
44
6
  return [...retailFormats];
@@ -53,7 +15,7 @@ export function resolveFormats(input) {
53
15
  if (input === "all")
54
16
  return [...linearFormats, ...matrixFormats];
55
17
  if (input === undefined)
56
- return ["EAN13"];
18
+ return [...retailFormats];
57
19
  if (typeof input === "string" && Object.hasOwn(formatBits, input))
58
20
  return [input];
59
21
  if (!Array.isArray(input) || input.length === 0)
@@ -0,0 +1,19 @@
1
+ export declare class ScannerError extends Error {
2
+ readonly code: string;
3
+ constructor(code: string, message: string);
4
+ }
5
+ export declare class RustScannerSession {
6
+ private readonly exports;
7
+ private handle;
8
+ private constructor();
9
+ static create(bytes: ArrayBuffer, mode: number, formats: number, addOnPolicy: number): Promise<RustScannerSession>;
10
+ scan(image: {
11
+ data: Uint8Array;
12
+ width: number;
13
+ height: number;
14
+ channels: number;
15
+ stride: number;
16
+ }, flags: number, formats: number): unknown;
17
+ dispose(): void;
18
+ private activeExports;
19
+ }