tapirscan 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +220 -0
  2. package/README.md +384 -226
  3. package/dist/browser-worker.d.ts +33 -0
  4. package/dist/browser-worker.js +83 -0
  5. package/dist/browser.d.ts +44 -0
  6. package/dist/browser.js +211 -0
  7. package/dist/freeze.d.ts +6 -0
  8. package/dist/freeze.js +7 -0
  9. package/dist/index.d.ts +53 -46
  10. package/dist/index.js +168 -209
  11. package/dist/layout.d.ts +19 -0
  12. package/dist/layout.js +27 -0
  13. package/dist/multiformat/format-registry.d.ts +26 -0
  14. package/dist/multiformat/format-registry.js +63 -0
  15. package/dist/multiformat/formats.d.ts +4 -24
  16. package/dist/multiformat/formats.js +15 -46
  17. package/dist/rust-session.d.ts +19 -0
  18. package/dist/rust-session.js +102 -0
  19. package/examples/camera.html +28 -29
  20. package/examples/vite/README.md +31 -0
  21. package/examples/vite/index.html +14 -0
  22. package/examples/vite/main.js +33 -0
  23. package/examples/vite/package.json +16 -0
  24. package/examples/vite/vite.config.js +6 -0
  25. package/package.json +28 -11
  26. package/wasm/build.json +295 -0
  27. package/wasm/experimental-turbo16.wasm +0 -0
  28. package/wasm/experimental-turbo2.wasm +0 -0
  29. package/wasm/experimental-turbo4.wasm +0 -0
  30. package/wasm/experimental-turbo8.wasm +0 -0
  31. package/wasm/high.wasm +0 -0
  32. package/wasm/low.wasm +0 -0
  33. package/wasm/medium.wasm +0 -0
  34. package/wasm/very-high.wasm +0 -0
  35. package/dist/completion-host.d.mts +0 -40
  36. package/dist/completion-host.mjs +0 -188
  37. package/dist/detail-20260914/continuity.d.mts +0 -2
  38. package/dist/detail-20260914/continuity.mjs +0 -62
  39. package/dist/detail-20260914/detail-proposals-rich.d.mts +0 -13
  40. package/dist/detail-20260914/detail-proposals-rich.mjs +0 -53
  41. package/dist/detail-20260914/direct-recovery.d.mts +0 -28
  42. package/dist/detail-20260914/direct-recovery.mjs +0 -139
  43. package/dist/detail-20260914/host.d.mts +0 -40
  44. package/dist/detail-20260914/host.mjs +0 -280
  45. package/dist/detail-20260914/scanner.d.mts +0 -12
  46. package/dist/detail-20260914/scanner.mjs +0 -64
  47. package/dist/detail-20260914/source-evidence.d.mts +0 -10
  48. package/dist/detail-20260914/source-evidence.mjs +0 -94
  49. package/dist/detail-canvas.d.ts +0 -35
  50. package/dist/detail-canvas.js +0 -73
  51. package/dist/detail-runtime/direct-recovery.d.mts +0 -28
  52. package/dist/detail-runtime/direct-recovery.mjs +0 -142
  53. package/dist/detail-runtime/host.d.mts +0 -40
  54. package/dist/detail-runtime/host.mjs +0 -284
  55. package/dist/detail-runtime/scanner.d.mts +0 -12
  56. package/dist/detail-runtime/scanner.mjs +0 -70
  57. package/dist/detail.d.ts +0 -20
  58. package/dist/detail.js +0 -84
  59. package/dist/host.d.ts +0 -112
  60. package/dist/host.js +0 -184
  61. package/dist/host64.d.ts +0 -112
  62. package/dist/host64.js +0 -184
  63. package/dist/multiformat/coverage.d.ts +0 -7
  64. package/dist/multiformat/coverage.js +0 -26
  65. package/dist/multiformat/geometry.d.ts +0 -37
  66. package/dist/multiformat/geometry.js +0 -132
  67. package/dist/multiformat/linear-duplicates.d.ts +0 -16
  68. package/dist/multiformat/linear-duplicates.js +0 -163
  69. package/dist/multiformat/pixels.d.ts +0 -3
  70. package/dist/multiformat/pixels.js +0 -36
  71. package/dist/multiformat/scanner.d.ts +0 -60
  72. package/dist/multiformat/scanner.js +0 -382
  73. package/dist/multiformat-host.d.ts +0 -121
  74. package/dist/multiformat-host.js +0 -207
  75. package/dist/policy.d.ts +0 -12
  76. package/dist/policy.js +0 -12
  77. package/examples/scan-worker.mjs +0 -20
  78. package/examples/worker-client.mjs +0 -63
  79. package/wasm/high-shared-retail-portable-20260919.wasm +0 -0
  80. package/wasm/low-shared-retail-portable-20260919.wasm +0 -0
  81. package/wasm/medium-shared-retail-portable-20260919.wasm +0 -0
  82. package/wasm/multiformat.json +0 -7
  83. package/wasm/multiformat.wasm +0 -0
  84. package/wasm/very-high-shared-retail-portable-20260919.wasm +0 -0
package/dist/index.js CHANGED
@@ -1,49 +1,61 @@
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";
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";
1
+ import { hasKey, maskFor, resolveFormats, } from "./multiformat/formats.js";
2
+ import { freeze } from "./freeze.js";
3
+ import { pixelLayout } from "./layout.js";
4
+ import { RustScannerSession, ScannerError } from "./rust-session.js";
5
+ export { commonFormats, commonLinearFormats, linearFormats, matrixFormats, retailFormats, } from "./multiformat/formats.js";
6
+ export { ScannerError } from "./rust-session.js";
7
+ const modes = {
8
+ low: { id: 0, file: "low.wasm" },
9
+ medium: { id: 1, file: "medium.wasm" },
10
+ high: { id: 2, file: "high.wasm" },
11
+ "very-high": { id: 3, file: "very-high.wasm" },
12
+ };
13
+ const turboFiles = {
14
+ 2: "experimental-turbo2.wasm",
15
+ 4: "experimental-turbo4.wasm",
16
+ 8: "experimental-turbo8.wasm",
17
+ 16: "experimental-turbo16.wasm",
18
+ };
19
+ const addOnPolicies = { ignore: 0, read: 1, require: 2 };
9
20
  function pixels(image) {
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)");
21
+ const { data, width, height, channels, stride } = pixelLayout(image);
31
22
  return {
32
- data: new Uint8Array(image.data.buffer, image.data.byteOffset, image.data.byteLength),
33
- width: image.width,
34
- height: image.height,
23
+ data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
24
+ width,
25
+ height,
35
26
  channels,
36
27
  stride,
37
28
  };
38
29
  }
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);
30
+ // The package's wasm/ directory. Bundlers (Turbopack, webpack, Vite) resolve
31
+ // `new URL("literal", import.meta.url)` as a file and fail on a directory; browser
32
+ // bundles load WASM through tapirscan/browser's static per-file URLs instead.
33
+ function packagedWasm() {
34
+ const url = new URL(import.meta.url);
35
+ url.pathname = url.pathname.replace(/[^/]*\/[^/]*$/, "wasm/");
36
+ return url;
37
+ }
38
+ // Bound retained WASM bytes across stable modes, experimental presets and custom URLs.
39
+ const wasmLoads = new Map();
40
+ function loadDefault(url) {
41
+ const key = url.href;
42
+ const cached = wasmLoads.get(key);
43
+ if (cached)
44
+ return cached;
45
+ const pending = readWasm(url).catch((error) => {
46
+ if (wasmLoads.get(key) === pending)
47
+ wasmLoads.delete(key);
48
+ throw error;
49
+ });
50
+ wasmLoads.set(key, pending);
51
+ if (wasmLoads.size > 4) {
52
+ const oldest = wasmLoads.keys().next().value;
53
+ if (oldest !== undefined)
54
+ wasmLoads.delete(oldest);
55
+ }
56
+ return pending;
45
57
  }
46
- async function loadDefault(url) {
58
+ async function readWasm(url) {
47
59
  if (url.protocol === "file:") {
48
60
  const nodeFs = "node:fs/promises";
49
61
  const fs = (await import(/* @vite-ignore */ nodeFs));
@@ -54,96 +66,66 @@ async function loadDefault(url) {
54
66
  throw new Error(`WASM load failed: ${String(response.status)}`);
55
67
  return response.arrayBuffer();
56
68
  }
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,
69
+ function wireResult(value) {
70
+ if (value === null ||
71
+ typeof value !== "object" ||
72
+ !("barcodes" in value) ||
73
+ !Array.isArray(value.barcodes) ||
74
+ !("undecoded" in value) ||
75
+ !Array.isArray(value.undecoded) ||
76
+ !("image" in value) ||
77
+ value.image === null ||
78
+ typeof value.image !== "object" ||
79
+ !("mode" in value) ||
80
+ typeof value.mode !== "string" ||
81
+ !hasKey(modes, value.mode))
82
+ throw new ScannerError("invalid_output", "Scanner returned an invalid result");
83
+ return value;
84
+ }
85
+ function publicResult(raw, elapsedMs, experimentalTurbo) {
86
+ if (!raw.debug)
87
+ throw new ScannerError("invalid_output", "Scanner omitted requested diagnostics");
88
+ const barcodes = raw.barcodes;
89
+ const undecoded = raw.undecoded.map(({ format, polygon }) => ({
90
+ format: format ?? "Unknown",
91
+ polygon,
92
+ }));
93
+ const regions = {
94
+ proposals: raw.debug.localization?.proposals ?? null,
95
+ searchWindows: raw.debug.searchWindows ?? null,
82
96
  undecoded,
83
97
  };
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
98
  return freeze({
116
99
  barcodes,
117
100
  values: barcodes.map((b) => b.text),
118
- best: barcodes[bestIndex],
119
- undecoded: regions.undecoded,
120
- image: { width: image.width, height: image.height },
121
- mode: raw.mode,
122
- elapsedMs: raw.elapsedMs,
123
- unfinished: raw.scan.unfinished || raw.localizationLimited || (raw.localization?.omitted ?? 0) > 0,
124
- ...(debug ? { debug: { ...raw, regions } } : {}),
101
+ best: bestOf(barcodes),
102
+ image: raw.image,
103
+ ...(experimentalTurbo === undefined ? { mode: raw.mode } : { experimentalTurbo }),
104
+ elapsedMs,
105
+ undecoded,
106
+ diagnostics: { ...raw.debug, regions },
125
107
  });
126
108
  }
127
- const modes = {
128
- low: "low-shared-retail-portable-20260919.wasm",
129
- medium: "medium-shared-retail-portable-20260919.wasm",
130
- high: "high-shared-retail-portable-20260919.wasm",
131
- "very-high": "very-high-shared-retail-portable-20260919.wasm",
132
- };
133
- /** Mode selects a compiled implementation. Create another instance to switch. */
109
+ /** A reusable scanner. Results own their data and remain valid after later scans or disposal. */
134
110
  export class Scanner {
135
111
  host;
136
112
  mode;
137
113
  configuredFormats;
138
114
  addOnPolicy;
139
- constructor(host, mode, configuredFormats = resolveFormats(), addOnPolicy = "Ignore") {
115
+ experimentalTurbo;
116
+ constructor(host,
117
+ /** Effort mode, fixed at creation; undefined for Turbo presets. */
118
+ mode, configuredFormats, addOnPolicy,
119
+ /** @experimental Selected Turbo preset, fixed at creation. */
120
+ experimentalTurbo) {
140
121
  this.host = host;
141
122
  this.mode = mode;
142
123
  this.configuredFormats = configuredFormats;
143
124
  this.addOnPolicy = addOnPolicy;
125
+ this.experimentalTurbo = experimentalTurbo;
144
126
  Object.freeze(configuredFormats);
145
127
  }
146
- /** Formats available for scanning, fixed at creation. */
128
+ /** Default formats, fixed at creation; individual scans may override them. */
147
129
  get formats() {
148
130
  return this.configuredFormats;
149
131
  }
@@ -155,15 +137,33 @@ export class Scanner {
155
137
  if (input === null || typeof input !== "object" || Array.isArray(input))
156
138
  throw new TypeError("Invalid scanner options");
157
139
  for (const key of Object.keys(options))
158
- if (!["mode", "formats", "wasmBaseUrl", "loadWasm", "eanAddOnPolicy"].includes(key))
140
+ if (![
141
+ "mode",
142
+ "experimentalTurbo",
143
+ "formats",
144
+ "wasmBaseUrl",
145
+ "loadWasm",
146
+ "eanAddOnPolicy",
147
+ ].includes(key))
159
148
  throw new TypeError(`Unknown scanner option: ${key}`);
160
- const mode = options.mode ?? "medium";
161
- if (!Object.hasOwn(modes, mode))
149
+ const turbo = options.experimentalTurbo;
150
+ if (turbo !== undefined) {
151
+ if (typeof turbo !== "number" || !hasKey(turboFiles, turbo))
152
+ throw new TypeError("experimentalTurbo must be 2, 4, 8 or 16");
153
+ if (options.mode !== undefined)
154
+ throw new TypeError("Choose mode or experimentalTurbo, not both");
155
+ if (options.eanAddOnPolicy !== undefined && options.eanAddOnPolicy !== "ignore")
156
+ throw new TypeError('experimentalTurbo requires eanAddOnPolicy: "ignore"');
157
+ }
158
+ // Only undefined or a supported string: null or objects that stringify to a mode are errors.
159
+ if (options.mode !== undefined &&
160
+ (typeof options.mode !== "string" || !hasKey(modes, options.mode)))
162
161
  throw new TypeError("Unknown scanner mode");
162
+ const mode = turbo === undefined ? (options.mode ?? "medium") : "low";
163
163
  const formats = resolveFormats(options.formats);
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");
164
+ const addOnPolicy = options.eanAddOnPolicy === undefined ? "ignore" : options.eanAddOnPolicy;
165
+ if (typeof addOnPolicy !== "string" || !hasKey(addOnPolicies, addOnPolicy))
166
+ throw new TypeError('eanAddOnPolicy must be "ignore", "read" or "require"');
167
167
  if (options.loadWasm !== undefined && typeof options.loadWasm !== "function")
168
168
  throw new TypeError("loadWasm must be a function");
169
169
  if (options.wasmBaseUrl !== undefined &&
@@ -171,117 +171,76 @@ export class Scanner {
171
171
  !(options.wasmBaseUrl instanceof URL))
172
172
  throw new TypeError("wasmBaseUrl must be a string or URL");
173
173
  const base = options.wasmBaseUrl === undefined
174
- ? new URL(/* @vite-ignore */ "../wasm/", import.meta.url)
174
+ ? packagedWasm()
175
175
  : new URL(options.wasmBaseUrl, typeof location === "undefined" ? import.meta.url : location.href);
176
176
  if (!base.pathname.endsWith("/"))
177
177
  base.pathname += "/";
178
- const load = options.loadWasm ?? loadDefault;
179
- const sharedRetail = mode === "medium" && addonPolicy === "Ignore";
180
- const needsPrimary = formats.some((f) => f === "EAN13" || f === "UPCA" || (sharedRetail && (f === "EAN8" || f === "UPCE")));
181
- const bytes = needsPrimary ? await load(new URL(modes[mode], base)) : undefined;
182
- const recovery = !needsPrimary || mode === "low" ? undefined : await load(new URL(modes.low, base));
183
- const multiformat = addonPolicy !== "Ignore" || formats.length !== 1 || formats[0] !== "EAN13";
184
- const extra = addonPolicy !== "Ignore" ||
185
- formats.some((f) => f !== "EAN13" && f !== "UPCA" && (!sharedRetail || (f !== "EAN8" && f !== "UPCE")))
186
- ? await load(new URL("multiformat.wasm", base))
187
- : undefined;
188
- if (multiformat)
189
- return new Scanner(await MediumMultiformatScanner.create(bytes, extra, mode, recovery), mode, formats, addonPolicy);
190
- if (!bytes)
191
- throw new Error("EAN13 engine was not loaded");
192
- const host = recovery
193
- ? await ReleaseDetailScanner.create(bytes, recovery, mode)
194
- : await IndependentScanner.create(bytes);
195
- return new Scanner(host, mode, formats);
178
+ const selected = modes[mode];
179
+ const bytes = await (options.loadWasm ?? loadDefault)(new URL(turbo === undefined ? selected.file : turboFiles[turbo], base));
180
+ const host = await RustScannerSession.create(bytes, selected.id, maskFor(formats), addOnPolicies[addOnPolicy], turbo);
181
+ // Turbo presets run in the Low engine slot but have no effort mode of their own.
182
+ return new Scanner(host, turbo === undefined ? mode : undefined, formats, addOnPolicy, turbo);
196
183
  }
184
+ /** Decode barcodes with positions. Use inspect() for diagnostic evidence. */
197
185
  scan(inputImage, options = {}) {
186
+ const raw = this.run(inputImage, options, false);
187
+ if (raw === null ||
188
+ typeof raw !== "object" ||
189
+ !("barcodes" in raw) ||
190
+ !Array.isArray(raw.barcodes))
191
+ throw new ScannerError("invalid_output", "Scanner returned an invalid barcode list");
192
+ const barcodes = raw.barcodes;
193
+ return freeze({
194
+ barcodes,
195
+ values: barcodes.map((barcode) => barcode.text),
196
+ best: bestOf(barcodes),
197
+ });
198
+ }
199
+ /** Inspect barcodes, unread regions, timing and engine diagnostics. */
200
+ inspect(inputImage, options = {}) {
201
+ const start = performance.now();
202
+ const raw = wireResult(this.run(inputImage, options, true));
203
+ return publicResult(raw, performance.now() - start, this.experimentalTurbo);
204
+ }
205
+ run(inputImage, options, inspect) {
198
206
  const image = pixels(inputImage);
199
207
  const input = options;
200
208
  if (input === null || typeof input !== "object" || Array.isArray(input))
201
209
  throw new TypeError("Invalid scan options");
202
210
  for (const key of Object.keys(options))
203
- if (key !== "debug" && key !== "formats" && key !== "extendedBudget")
211
+ if (key !== "formats")
204
212
  throw new TypeError(`Unknown scan option: ${key}`);
205
- if (options.debug !== undefined && typeof options.debug !== "boolean")
206
- throw new TypeError("debug must be a boolean");
207
- if (options.extendedBudget !== undefined && typeof options.extendedBudget !== "boolean")
208
- throw new TypeError("extendedBudget must be a boolean");
209
- const finishCandidates = options.extendedBudget ?? false;
210
- const debug = options.debug ?? false;
211
- const scanPolicy = { ...policy, finishCandidates };
212
- const formats = options.formats === undefined ? this.formats : resolveFormats(options.formats);
213
- if (formats.some((format) => !this.formats.includes(format)))
214
- throw new TypeError(`Scan formats must be a subset of configured formats. Requested: ${formats.join(", ")}; configured: ${this.formats.join(", ")}`);
215
- if (this.host instanceof MediumMultiformatScanner) {
216
- const frame = this.host.scan(image, formats, {
217
- eanAddOnSymbol: this.eanAddOnPolicy,
218
- finishCandidates,
219
- });
220
- const barcodes = frame.barcodes;
221
- const primary = frame.primary;
222
- const localization = primary?.localization;
223
- return publicResult({
224
- schemaVersion: 2,
225
- mode: this.mode,
226
- multiple: true,
227
- elapsedMs: frame.scanMs,
228
- // Preserve the aggregate limit flag when additional readers cannot separate causes.
229
- localizationLimited: frame.unfinished,
230
- scan: {
231
- ...primary?.scan,
232
- barcodes,
233
- unfinished: frame.unfinished,
234
- regions: frame.regions,
235
- },
236
- ...(primary
237
- ? {
238
- localization,
239
- searchWindows: primary.searchWindows,
240
- ...("recovery" in primary ? { recovery: primary.recovery } : {}),
241
- ...("detailRegions" in primary ? { detailRegions: primary.detailRegions } : {}),
242
- }
243
- : {}),
244
- }, image, debug);
245
- }
246
- const full = this.host.scanLocalized(image, scanPolicy, fitLimits[this.mode], true);
247
- // Isolate the imported host's JSON result at the public ABI type boundary.
248
- const localization = full.localization;
249
- const barcodes = mergeLinearDuplicates(full.scan.barcodes.map((b) => ({
250
- ...b,
251
- format: "EAN13",
252
- })), image);
253
- return publicResult({
254
- schemaVersion: 2,
255
- mode: this.mode,
256
- multiple: true,
257
- elapsedMs: full.scanMs,
258
- localizationLimited: localization.workLimited || localization.omitted > 0,
259
- scan: { ...full.scan, barcodes },
260
- localization,
261
- searchWindows: full.searchWindows,
262
- ...("recovery" in full && "detailRegions" in full
263
- ? {
264
- recovery: full.recovery,
265
- detailRegions: full.detailRegions,
266
- }
267
- : {}),
268
- }, image, debug);
213
+ const mask = options.formats === undefined ? 0 : maskFor(resolveFormats(options.formats));
214
+ return this.host.scan(image, inspect ? 2 : 0, mask);
269
215
  }
216
+ /** Release the WASM session. Repeated disposal is safe; scanning afterward fails. */
270
217
  dispose() {
271
218
  this.host.dispose();
272
219
  }
273
220
  }
221
+ /** Highest support, keeping the first read on ties; undefined when empty. */
222
+ function bestOf(barcodes) {
223
+ let winner;
224
+ for (const barcode of barcodes)
225
+ if (!winner || barcode.support > winner.support)
226
+ winner = barcode;
227
+ return winner;
228
+ }
274
229
  /** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
275
230
  export async function scan(image, options = {}) {
276
- const input = options;
277
- if (input === null || typeof input !== "object" || Array.isArray(input))
278
- throw new TypeError("Invalid scan options");
279
- const { debug, extendedBudget, ...creation } = options;
280
- if (debug !== undefined && typeof debug !== "boolean")
281
- throw new TypeError("debug must be a boolean");
282
- const scanner = await Scanner.create(creation);
231
+ const scanner = await Scanner.create(options);
232
+ try {
233
+ return scanner.scan(image);
234
+ }
235
+ finally {
236
+ scanner.dispose();
237
+ }
238
+ }
239
+ /** Inspect one image with automatic cleanup. */
240
+ export async function inspect(image, options = {}) {
241
+ const scanner = await Scanner.create(options);
283
242
  try {
284
- return scanner.scan(image, { debug, extendedBudget });
243
+ return scanner.inspect(image);
285
244
  }
286
245
  finally {
287
246
  scanner.dispose();
@@ -0,0 +1,19 @@
1
+ /** Decoded pixels: `ImageData` or an explicit gray, RGB or RGBA buffer. */
2
+ export interface PixelInput {
3
+ readonly data: Uint8Array | Uint8ClampedArray;
4
+ readonly width: number;
5
+ readonly height: number;
6
+ readonly channels?: 1 | 3 | 4;
7
+ readonly stride?: number;
8
+ }
9
+ /** A validated layout; `addressed` bytes from the start of `data` hold the image. */
10
+ export interface PixelLayout {
11
+ readonly data: Uint8Array | Uint8ClampedArray;
12
+ readonly width: number;
13
+ readonly height: number;
14
+ readonly channels: 1 | 3 | 4;
15
+ readonly stride: number;
16
+ readonly addressed: number;
17
+ }
18
+ /** Validate decoded pixels against the scanner limits without copying them. */
19
+ export declare function pixelLayout(image: unknown): PixelLayout;
package/dist/layout.js ADDED
@@ -0,0 +1,27 @@
1
+ // Pixel layout rules shared by the core and the browser entry. No WASM imports, so
2
+ // the browser entry can validate on the page without loading the core.
3
+ /** Validate decoded pixels against the scanner limits without copying them. */
4
+ export function pixelLayout(image) {
5
+ if (image === null || typeof image !== "object" || !("data" in image))
6
+ throw new TypeError("Expected ImageData or decoded pixels");
7
+ const { data, width, height } = image;
8
+ const explicit = "channels" in image;
9
+ if (!explicit && !(data instanceof Uint8ClampedArray))
10
+ throw new TypeError("Use ImageData or an explicit buffer with channels");
11
+ const channels = explicit ? image.channels : 4;
12
+ const stride = (explicit ? image.stride : undefined) ?? width * (channels ?? 0);
13
+ const addressed = (height - 1) * stride + width * (channels ?? 0);
14
+ if (!Number.isSafeInteger(width) ||
15
+ !Number.isSafeInteger(height) ||
16
+ width < 3 ||
17
+ height < 3 ||
18
+ width * height > 32 * 1024 * 1024 ||
19
+ (channels !== 1 && channels !== 3 && channels !== 4) ||
20
+ !Number.isSafeInteger(stride) ||
21
+ stride < width * channels ||
22
+ addressed > 128 * 1024 * 1024 ||
23
+ !(data instanceof Uint8Array || data instanceof Uint8ClampedArray) ||
24
+ data.byteLength < addressed)
25
+ throw new TypeError("Invalid image dimensions, channels, stride or buffer (maximum 128 MiB)");
26
+ return { data, width, height, channels, stride, addressed };
27
+ }
@@ -0,0 +1,26 @@
1
+ export declare const formatBits: Readonly<{
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,63 @@
1
+ // Generated by scripts/generate_formats.py from config/formats.json. Do not edit.
2
+ // Frozen: these shared constants also back the scanner defaults.
3
+ export const formatBits = Object.freeze({
4
+ EAN13: 1,
5
+ UPCA: 2,
6
+ EAN8: 4,
7
+ UPCE: 8,
8
+ Code128: 16,
9
+ Code39: 32,
10
+ ITF: 64,
11
+ Codabar: 128,
12
+ Code93: 256,
13
+ QRCode: 512,
14
+ DataMatrix: 1024,
15
+ PDF417: 2048,
16
+ Aztec: 4096,
17
+ DataBar: 8192,
18
+ DataBarExpanded: 16384,
19
+ MaxiCode: 131072,
20
+ });
21
+ export const retailFormats = Object.freeze(["EAN13", "UPCA", "EAN8", "UPCE"]);
22
+ export const commonLinearFormats = Object.freeze([
23
+ "EAN13",
24
+ "UPCA",
25
+ "EAN8",
26
+ "UPCE",
27
+ "Code128",
28
+ "Code39",
29
+ "ITF",
30
+ ]);
31
+ export const commonFormats = Object.freeze([
32
+ "EAN13",
33
+ "UPCA",
34
+ "EAN8",
35
+ "UPCE",
36
+ "Code128",
37
+ "Code39",
38
+ "ITF",
39
+ "QRCode",
40
+ "DataMatrix",
41
+ ]);
42
+ export const linearFormats = Object.freeze([
43
+ "EAN13",
44
+ "UPCA",
45
+ "EAN8",
46
+ "UPCE",
47
+ "Code128",
48
+ "Code39",
49
+ "ITF",
50
+ "Codabar",
51
+ "Code93",
52
+ "DataBar",
53
+ "DataBarExpanded",
54
+ ]);
55
+ export const matrixFormats = Object.freeze([
56
+ "QRCode",
57
+ "DataMatrix",
58
+ "PDF417",
59
+ "Aztec",
60
+ "MaxiCode",
61
+ ]);
62
+ export const eanAddOnReadFlag = 32768;
63
+ export const eanAddOnRequireFlag = 65536;
@@ -1,28 +1,8 @@
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, };
4
+ /** Own-property check that works in Chrome 91 (`Object.hasOwn` needs Chrome 93). */
5
+ export declare function hasKey(object: object, key: PropertyKey): boolean;
26
6
  export type FormatSelection = Format | readonly Format[] | "retail" | "common1D" | "common" | "1D" | "2D" | "all";
27
7
  export declare function resolveFormats(input?: readonly string[] | string): Format[];
28
8
  export declare function maskFor(formats: readonly Format[]): number;