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.
- package/LICENSE +220 -0
- package/README.md +384 -226
- package/dist/browser-worker.d.ts +33 -0
- package/dist/browser-worker.js +83 -0
- package/dist/browser.d.ts +44 -0
- package/dist/browser.js +211 -0
- package/dist/freeze.d.ts +6 -0
- package/dist/freeze.js +7 -0
- package/dist/index.d.ts +53 -46
- package/dist/index.js +168 -209
- package/dist/layout.d.ts +19 -0
- package/dist/layout.js +27 -0
- package/dist/multiformat/format-registry.d.ts +26 -0
- package/dist/multiformat/format-registry.js +63 -0
- package/dist/multiformat/formats.d.ts +4 -24
- package/dist/multiformat/formats.js +15 -46
- package/dist/rust-session.d.ts +19 -0
- package/dist/rust-session.js +102 -0
- package/examples/camera.html +28 -29
- package/examples/vite/README.md +31 -0
- package/examples/vite/index.html +14 -0
- package/examples/vite/main.js +33 -0
- package/examples/vite/package.json +16 -0
- package/examples/vite/vite.config.js +6 -0
- package/package.json +28 -11
- package/wasm/build.json +295 -0
- package/wasm/experimental-turbo16.wasm +0 -0
- package/wasm/experimental-turbo2.wasm +0 -0
- package/wasm/experimental-turbo4.wasm +0 -0
- package/wasm/experimental-turbo8.wasm +0 -0
- package/wasm/high.wasm +0 -0
- package/wasm/low.wasm +0 -0
- package/wasm/medium.wasm +0 -0
- package/wasm/very-high.wasm +0 -0
- package/dist/completion-host.d.mts +0 -40
- package/dist/completion-host.mjs +0 -188
- package/dist/detail-20260914/continuity.d.mts +0 -2
- package/dist/detail-20260914/continuity.mjs +0 -62
- package/dist/detail-20260914/detail-proposals-rich.d.mts +0 -13
- package/dist/detail-20260914/detail-proposals-rich.mjs +0 -53
- package/dist/detail-20260914/direct-recovery.d.mts +0 -28
- package/dist/detail-20260914/direct-recovery.mjs +0 -139
- package/dist/detail-20260914/host.d.mts +0 -40
- package/dist/detail-20260914/host.mjs +0 -280
- package/dist/detail-20260914/scanner.d.mts +0 -12
- package/dist/detail-20260914/scanner.mjs +0 -64
- package/dist/detail-20260914/source-evidence.d.mts +0 -10
- package/dist/detail-20260914/source-evidence.mjs +0 -94
- package/dist/detail-canvas.d.ts +0 -35
- package/dist/detail-canvas.js +0 -73
- package/dist/detail-runtime/direct-recovery.d.mts +0 -28
- package/dist/detail-runtime/direct-recovery.mjs +0 -142
- package/dist/detail-runtime/host.d.mts +0 -40
- package/dist/detail-runtime/host.mjs +0 -284
- package/dist/detail-runtime/scanner.d.mts +0 -12
- package/dist/detail-runtime/scanner.mjs +0 -70
- package/dist/detail.d.ts +0 -20
- package/dist/detail.js +0 -84
- package/dist/host.d.ts +0 -112
- package/dist/host.js +0 -184
- package/dist/host64.d.ts +0 -112
- package/dist/host64.js +0 -184
- package/dist/multiformat/coverage.d.ts +0 -7
- package/dist/multiformat/coverage.js +0 -26
- package/dist/multiformat/geometry.d.ts +0 -37
- package/dist/multiformat/geometry.js +0 -132
- package/dist/multiformat/linear-duplicates.d.ts +0 -16
- package/dist/multiformat/linear-duplicates.js +0 -163
- package/dist/multiformat/pixels.d.ts +0 -3
- package/dist/multiformat/pixels.js +0 -36
- package/dist/multiformat/scanner.d.ts +0 -60
- package/dist/multiformat/scanner.js +0 -382
- package/dist/multiformat-host.d.ts +0 -121
- package/dist/multiformat-host.js +0 -207
- package/dist/policy.d.ts +0 -12
- package/dist/policy.js +0 -12
- package/examples/scan-worker.mjs +0 -20
- package/examples/worker-client.mjs +0 -63
- package/wasm/high-shared-retail-portable-20260919.wasm +0 -0
- package/wasm/low-shared-retail-portable-20260919.wasm +0 -0
- package/wasm/medium-shared-retail-portable-20260919.wasm +0 -0
- package/wasm/multiformat.json +0 -7
- package/wasm/multiformat.wasm +0 -0
- package/wasm/very-high-shared-retail-portable-20260919.wasm +0 -0
package/dist/index.js
CHANGED
|
@@ -1,49 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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(
|
|
33
|
-
width
|
|
34
|
-
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 (![
|
|
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
|
|
161
|
-
if (
|
|
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
|
|
165
|
-
if (
|
|
166
|
-
throw new TypeError(
|
|
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
|
-
?
|
|
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
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
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 !== "
|
|
211
|
+
if (key !== "formats")
|
|
204
212
|
throw new TypeError(`Unknown scan option: ${key}`);
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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.
|
|
243
|
+
return scanner.inspect(image);
|
|
285
244
|
}
|
|
286
245
|
finally {
|
|
287
246
|
scanner.dispose();
|
package/dist/layout.d.ts
ADDED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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;
|