tapirscan 1.1.0 → 1.2.1
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/README.md +34 -29
- package/dist/detail-runtime/host.mjs +2 -0
- package/dist/detail.d.ts +3 -1
- package/dist/detail.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +29 -30
- package/dist/multiformat/formats.js +1 -1
- package/dist/multiformat/scanner.d.ts +1 -1
- package/dist/multiformat/scanner.js +27 -6
- package/package.json +6 -6
- package/wasm/low-shared-retail-portable-20260919.wasm +0 -0
- package/wasm/medium-shared-retail-portable-20260919.wasm +0 -0
- package/wasm/multiformat.json +2 -2
- package/wasm/multiformat.wasm +0 -0
- package/wasm/low-complete-release-20260916.wasm +0 -0
- package/wasm/medium-complete-release-20260916.wasm +0 -0
- /package/wasm/{high-complete-release-20260916.wasm → high-shared-retail-portable-20260919.wasm} +0 -0
- /package/wasm/{very-high-complete-release-20260916.wasm → very-high-shared-retail-portable-20260919.wasm} +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# Tapirscan for JavaScript and TypeScript
|
|
2
2
|
|
|
3
|
-
This guide describes Tapirscan 1.1.0.
|
|
4
|
-
|
|
5
3
|
Scan image pixels in a browser or Node with the same Rust/WASM core.
|
|
6
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)
|
|
7
5
|
|
|
@@ -26,7 +24,7 @@ const result = await scan(image);
|
|
|
26
24
|
console.log(result.values); // e.g. ["4006381333931"]
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
Defaults are Medium effort,
|
|
27
|
+
Defaults are Medium effort, retail formats, multiple results, and debug disabled.
|
|
30
28
|
`result.barcodes` also gives each read's text, format, polygon and rectangle.
|
|
31
29
|
The helper creates and disposes a scanner automatically. Browser apps need to
|
|
32
30
|
serve its [WASM assets](#wasm-loading); Node loads the packaged files automatically.
|
|
@@ -48,7 +46,7 @@ try {
|
|
|
48
46
|
```
|
|
49
47
|
|
|
50
48
|
Here `image` is the `ImageData` above. `formats: "1D"` enables all supported linear
|
|
51
|
-
formats;
|
|
49
|
+
formats; readers outside the retail group remain experimental. Settings also work with the helper:
|
|
52
50
|
`await scan(image, { mode: "high", formats: "1D" })`.
|
|
53
51
|
|
|
54
52
|
`formats: "retail"` selects EAN13, UPCA,
|
|
@@ -135,15 +133,15 @@ call and does not change the default formats. Previously returned results surviv
|
|
|
135
133
|
|
|
136
134
|
## All options
|
|
137
135
|
|
|
138
|
-
| Option
|
|
139
|
-
|
|
|
140
|
-
| `mode`
|
|
141
|
-
| `formats`
|
|
142
|
-
| `wasmBaseUrl`
|
|
143
|
-
| `loadWasm`
|
|
144
|
-
| `eanAddOnPolicy`
|
|
145
|
-
| `
|
|
146
|
-
| `debug`
|
|
136
|
+
| Option | Where | Default | Meaning |
|
|
137
|
+
| ---------------- | ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `mode` | Creation | `"medium"` | `"low"`, `"medium"`, `"high"`, `"very-high"`. |
|
|
139
|
+
| `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. |
|
|
140
|
+
| `wasmBaseUrl` | Creation | Module-relative assets | Directory URL for packaged WASMs. Use this for normal browser hosting. |
|
|
141
|
+
| `loadWasm` | Creation | Module-relative loader | `(url: URL) => Promise<ArrayBuffer>`. Uses HTTP fetch in browsers and filesystem reads for Node file URLs. |
|
|
142
|
+
| `eanAddOnPolicy` | Creation / one-shot | `"Ignore"` | `"Ignore"`, `"Read"`, `"Require"`; optional EAN/UPC supplement policy. |
|
|
143
|
+
| `extendedBudget` | Scan / one-shot | `false` | Allow extra reader work for any format. Exact budgets may evolve. |
|
|
144
|
+
| `debug` | Scan | `false` | Include search evidence under `result.debug`. Decoded polygons are always returned. |
|
|
147
145
|
|
|
148
146
|
Format presets cover supported symbologies. Exports `commonFormats`, `commonLinearFormats`, `linearFormats`, `matrixFormats`
|
|
149
147
|
and `retailFormats` let you compose custom selections; `formatBits` provides their
|
|
@@ -180,6 +178,7 @@ call. Convert DOM image elements or encoded images to pixels before scanning.
|
|
|
180
178
|
| `result.mode` | `Mode` | Selected effort. |
|
|
181
179
|
| `result.elapsedMs` | `number` | Host scan time in milliseconds; excludes file loading and scanner initialization. |
|
|
182
180
|
| `result.unfinished` | `boolean` | Incomplete work; returned reads may still be useful. |
|
|
181
|
+
| `result.undecoded` | `readonly UndecodedRegion[]` | Localized proposals without accepted decodes; always available. |
|
|
183
182
|
| `result.debug` | `Diagnostics \| undefined` | Requested diagnostic evidence; absent by default. |
|
|
184
183
|
| `barcode.payloadBytes` | `readonly number[] \| undefined` | Original decoded matrix payload bytes when available; use `Uint8Array.from(...)` for an owned byte buffer. |
|
|
185
184
|
| `barcode.text` | `string` | Decoded text. |
|
|
@@ -194,7 +193,8 @@ call. Convert DOM image elements or encoded images to pixels before scanning.
|
|
|
194
193
|
|
|
195
194
|
Results, including nested geometry and requested diagnostics, are immutable at
|
|
196
195
|
runtime and in TypeScript. Use `structuredClone(result)` if you need a mutable
|
|
197
|
-
copy.
|
|
196
|
+
copy. `barcodes` and `values` are empty when nothing is decoded; `undecoded` may still
|
|
197
|
+
contain proposals. Use `result.best` for
|
|
198
198
|
one read, or `undefined` when empty. All decoded instances remain available,
|
|
199
199
|
including separate copies of the same value. Coordinates start at the
|
|
200
200
|
top left, x rightward and y downward. Geometry is returned, not a cropped bitmap.
|
|
@@ -221,9 +221,9 @@ The policy is fixed for that scanner; its default is `"Ignore"`.
|
|
|
221
221
|
supplement. Supplement geometry is not exposed separately.
|
|
222
222
|
|
|
223
223
|
The supplement appears separately in `barcode.eanAddOn`; `barcode.text` remains
|
|
224
|
-
the main payload. Reading supplements enables additional
|
|
225
|
-
work independently of the effort mode.
|
|
226
|
-
by `"Require"` remain available
|
|
224
|
+
the main payload. Reading supplements enables additional decoding
|
|
225
|
+
work independently of the effort mode. Retail reads rejected
|
|
226
|
+
by `"Require"` remain available in `result.undecoded`.
|
|
227
227
|
|
|
228
228
|
## Evidence and work limits
|
|
229
229
|
|
|
@@ -294,17 +294,22 @@ raise the exported `ScannerError` with a `.code` and `.message`. Loader/fetch
|
|
|
294
294
|
errors propagate to the caller; creation and the one-shot helper reject their
|
|
295
295
|
promises on failure. Always dispose reusable scanners with `finally`.
|
|
296
296
|
|
|
297
|
-
##
|
|
297
|
+
## Extended work budget
|
|
298
|
+
|
|
299
|
+
Use `scanner.scan(image, { extendedBudget: true })` to allow additional reader work. The default
|
|
300
|
+
is false. This option is valid for every format; the exact budgets and stages are
|
|
301
|
+
implementation details that may evolve. Effort mode remains a separate setting.
|
|
302
|
+
|
|
303
|
+
Today this relaxes shared EAN-13/UPC-A retry and association limits. Other readers
|
|
304
|
+
currently retain their existing budgets. Per-candidate limits and intentional
|
|
305
|
+
deferrals remain; `unfinished` can still be true. This is not unlimited search,
|
|
306
|
+
an exhaustiveness guarantee or a wall-clock deadline. Custom primary-reader
|
|
307
|
+
engines must support the extended-work capability or report an error.
|
|
298
308
|
|
|
299
|
-
|
|
300
|
-
`await scan(image, { finishCandidates: true })` to let all selected EAN13/UPC-A
|
|
301
|
-
candidates use their effort budget, without the shared frame retry and association
|
|
302
|
-
budgets stopping later candidates. The default is `false`; at least one of
|
|
303
|
-
`EAN13` or `UPCA` must be selected. This is a per-scan option.
|
|
309
|
+
## Undecoded regions
|
|
304
310
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Custom WASM engines must advertise support; unsupported engines fail clearly.
|
|
311
|
+
`result.undecoded` is always available, independently of `debug`. Each entry has
|
|
312
|
+
a source-image polygon and a format hint. It is a localized proposal without an
|
|
313
|
+
accepted decode, not proof of a real or permanently unreadable barcode. Entries
|
|
314
|
+
can overlap or describe false candidates. An empty collection does not prove
|
|
315
|
+
that every barcode was found. Raw candidate attempts remain in debug diagnostics.
|
|
@@ -252,6 +252,8 @@ export class IndependentScanner {
|
|
|
252
252
|
coordinates[i * 8 + j * 2] = p[0];
|
|
253
253
|
coordinates[i * 8 + j * 2 + 1] = p[1];
|
|
254
254
|
}));
|
|
255
|
+
if (e.regions_retail)
|
|
256
|
+
status(e.regions_retail(id, policy.retailMask ?? 1, policy.retailMask ? 3 : 0));
|
|
255
257
|
const mask = policy.retryMask ?? [4294967295, 4294967295];
|
|
256
258
|
if (!Array.isArray(mask) || mask.length !== 2)
|
|
257
259
|
throw new ScannerError("invalid_input", "Invalid retry mask");
|
package/dist/detail.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare class ReleaseDetailScanner {
|
|
|
13
13
|
private disposed;
|
|
14
14
|
private constructor();
|
|
15
15
|
static create(primary: ArrayBuffer, low: ArrayBuffer, mode: Exclude<Mode, "low">): Promise<ReleaseDetailScanner>;
|
|
16
|
-
scanLocalized(image: Image, policy: Parameters<IndependentScanner["scanLocalized"]>[1]
|
|
16
|
+
scanLocalized(image: Image, policy: Parameters<IndependentScanner["scanLocalized"]>[1] & {
|
|
17
|
+
retailMask?: number;
|
|
18
|
+
}, fit: number, full: boolean, coverage?: readonly Quad[]): DetailResult;
|
|
17
19
|
dispose(): void;
|
|
18
20
|
}
|
package/dist/detail.js
CHANGED
|
@@ -52,7 +52,7 @@ export class ReleaseDetailScanner {
|
|
|
52
52
|
scanLocalized(image, policy, fit, full, coverage = []) {
|
|
53
53
|
if (this.disposed)
|
|
54
54
|
throw Error("Scanner is disposed");
|
|
55
|
-
const result = this.scanner.scanLocalized(packed(image), policy
|
|
55
|
+
const result = this.scanner.scanLocalized(packed(image), policy, fit, full, coverage);
|
|
56
56
|
const primaryCount = result.scan.barcodes.length;
|
|
57
57
|
const barcodes = result.recovery.barcodes.map((b, i) => i < primaryCount ? b : { ...b, candidate_indices: [] });
|
|
58
58
|
const localization = result.localization;
|
package/dist/index.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ type RawDiagnosticBarcode = FormatBarcode | (ScanFrame["barcodes"][number] & {
|
|
|
25
25
|
});
|
|
26
26
|
export type Mode = "low" | "medium" | "high" | "very-high";
|
|
27
27
|
export interface ScanOptions {
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/** Allow reader-specific extra work. Supported for every format; exact budgets may evolve. */
|
|
29
|
+
extendedBudget?: boolean;
|
|
30
30
|
/** Per-call subset of the formats configured at creation. */
|
|
31
31
|
formats?: FormatSelection;
|
|
32
32
|
debug?: boolean;
|
|
@@ -117,6 +117,7 @@ export interface ScanResult {
|
|
|
117
117
|
readonly mode: Mode;
|
|
118
118
|
readonly elapsedMs: number;
|
|
119
119
|
readonly unfinished: boolean;
|
|
120
|
+
readonly undecoded: readonly UndecodedRegion[];
|
|
120
121
|
readonly debug?: Diagnostics;
|
|
121
122
|
}
|
|
122
123
|
export type EanAddOnPolicy = "Ignore" | "Read" | "Require";
|
package/dist/index.js
CHANGED
|
@@ -111,22 +111,24 @@ function publicResult(raw, image, debug) {
|
|
|
111
111
|
for (let i = 0; i < barcodes.length; i++)
|
|
112
112
|
if (bestIndex < 0 || raw.scan.barcodes[i].support > raw.scan.barcodes[bestIndex].support)
|
|
113
113
|
bestIndex = i;
|
|
114
|
+
const regions = regionEvidence(raw);
|
|
114
115
|
return freeze({
|
|
115
116
|
barcodes,
|
|
116
117
|
values: barcodes.map((b) => b.text),
|
|
117
118
|
best: barcodes[bestIndex],
|
|
119
|
+
undecoded: regions.undecoded,
|
|
118
120
|
image: { width: image.width, height: image.height },
|
|
119
121
|
mode: raw.mode,
|
|
120
122
|
elapsedMs: raw.elapsedMs,
|
|
121
123
|
unfinished: raw.scan.unfinished || raw.localizationLimited || (raw.localization?.omitted ?? 0) > 0,
|
|
122
|
-
...(debug ? { debug: { ...raw, regions
|
|
124
|
+
...(debug ? { debug: { ...raw, regions } } : {}),
|
|
123
125
|
});
|
|
124
126
|
}
|
|
125
127
|
const modes = {
|
|
126
|
-
low: "low-
|
|
127
|
-
medium: "medium-
|
|
128
|
-
high: "high-
|
|
129
|
-
"very-high": "very-high-
|
|
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",
|
|
130
132
|
};
|
|
131
133
|
/** Mode selects a compiled implementation. Create another instance to switch. */
|
|
132
134
|
export class Scanner {
|
|
@@ -134,7 +136,7 @@ export class Scanner {
|
|
|
134
136
|
mode;
|
|
135
137
|
configuredFormats;
|
|
136
138
|
addOnPolicy;
|
|
137
|
-
constructor(host, mode, configuredFormats =
|
|
139
|
+
constructor(host, mode, configuredFormats = resolveFormats(), addOnPolicy = "Ignore") {
|
|
138
140
|
this.host = host;
|
|
139
141
|
this.mode = mode;
|
|
140
142
|
this.configuredFormats = configuredFormats;
|
|
@@ -174,11 +176,13 @@ export class Scanner {
|
|
|
174
176
|
if (!base.pathname.endsWith("/"))
|
|
175
177
|
base.pathname += "/";
|
|
176
178
|
const load = options.loadWasm ?? loadDefault;
|
|
177
|
-
const
|
|
179
|
+
const sharedRetail = mode === "medium" && addonPolicy === "Ignore";
|
|
180
|
+
const needsPrimary = formats.some((f) => f === "EAN13" || f === "UPCA" || (sharedRetail && (f === "EAN8" || f === "UPCE")));
|
|
178
181
|
const bytes = needsPrimary ? await load(new URL(modes[mode], base)) : undefined;
|
|
179
182
|
const recovery = !needsPrimary || mode === "low" ? undefined : await load(new URL(modes.low, base));
|
|
180
183
|
const multiformat = addonPolicy !== "Ignore" || formats.length !== 1 || formats[0] !== "EAN13";
|
|
181
|
-
const extra = addonPolicy !== "Ignore" ||
|
|
184
|
+
const extra = addonPolicy !== "Ignore" ||
|
|
185
|
+
formats.some((f) => f !== "EAN13" && f !== "UPCA" && (!sharedRetail || (f !== "EAN8" && f !== "UPCE")))
|
|
182
186
|
? await load(new URL("multiformat.wasm", base))
|
|
183
187
|
: undefined;
|
|
184
188
|
if (multiformat)
|
|
@@ -196,23 +200,22 @@ export class Scanner {
|
|
|
196
200
|
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
197
201
|
throw new TypeError("Invalid scan options");
|
|
198
202
|
for (const key of Object.keys(options))
|
|
199
|
-
if (key !== "debug" && key !== "formats" && key !== "
|
|
203
|
+
if (key !== "debug" && key !== "formats" && key !== "extendedBudget")
|
|
200
204
|
throw new TypeError(`Unknown scan option: ${key}`);
|
|
201
205
|
if (options.debug !== undefined && typeof options.debug !== "boolean")
|
|
202
206
|
throw new TypeError("debug must be a boolean");
|
|
203
|
-
if (options.
|
|
204
|
-
throw new TypeError("
|
|
207
|
+
if (options.extendedBudget !== undefined && typeof options.extendedBudget !== "boolean")
|
|
208
|
+
throw new TypeError("extendedBudget must be a boolean");
|
|
209
|
+
const finishCandidates = options.extendedBudget ?? false;
|
|
205
210
|
const debug = options.debug ?? false;
|
|
206
|
-
const scanPolicy = { ...policy, finishCandidates
|
|
211
|
+
const scanPolicy = { ...policy, finishCandidates };
|
|
207
212
|
const formats = options.formats === undefined ? this.formats : resolveFormats(options.formats);
|
|
208
213
|
if (formats.some((format) => !this.formats.includes(format)))
|
|
209
214
|
throw new TypeError(`Scan formats must be a subset of configured formats. Requested: ${formats.join(", ")}; configured: ${this.formats.join(", ")}`);
|
|
210
215
|
if (this.host instanceof MediumMultiformatScanner) {
|
|
211
|
-
if (options.finishCandidates && !formats.some((f) => f === "EAN13" || f === "UPCA"))
|
|
212
|
-
throw new TypeError("finishCandidates requires EAN13 or UPCA");
|
|
213
216
|
const frame = this.host.scan(image, formats, {
|
|
214
217
|
eanAddOnSymbol: this.eanAddOnPolicy,
|
|
215
|
-
finishCandidates
|
|
218
|
+
finishCandidates,
|
|
216
219
|
});
|
|
217
220
|
const barcodes = frame.barcodes;
|
|
218
221
|
const primary = frame.primary;
|
|
@@ -225,12 +228,12 @@ export class Scanner {
|
|
|
225
228
|
// Preserve the aggregate limit flag when additional readers cannot separate causes.
|
|
226
229
|
localizationLimited: frame.unfinished,
|
|
227
230
|
scan: {
|
|
228
|
-
...
|
|
231
|
+
...primary?.scan,
|
|
229
232
|
barcodes,
|
|
230
233
|
unfinished: frame.unfinished,
|
|
231
|
-
|
|
234
|
+
regions: frame.regions,
|
|
232
235
|
},
|
|
233
|
-
...(
|
|
236
|
+
...(primary
|
|
234
237
|
? {
|
|
235
238
|
localization,
|
|
236
239
|
searchWindows: primary.searchWindows,
|
|
@@ -253,17 +256,13 @@ export class Scanner {
|
|
|
253
256
|
multiple: true,
|
|
254
257
|
elapsedMs: full.scanMs,
|
|
255
258
|
localizationLimited: localization.workLimited || localization.omitted > 0,
|
|
256
|
-
scan:
|
|
257
|
-
|
|
259
|
+
scan: { ...full.scan, barcodes },
|
|
260
|
+
localization,
|
|
261
|
+
searchWindows: full.searchWindows,
|
|
262
|
+
...("recovery" in full && "detailRegions" in full
|
|
258
263
|
? {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
...("recovery" in full && "detailRegions" in full
|
|
262
|
-
? {
|
|
263
|
-
recovery: full.recovery,
|
|
264
|
-
detailRegions: full.detailRegions,
|
|
265
|
-
}
|
|
266
|
-
: {}),
|
|
264
|
+
recovery: full.recovery,
|
|
265
|
+
detailRegions: full.detailRegions,
|
|
267
266
|
}
|
|
268
267
|
: {}),
|
|
269
268
|
}, image, debug);
|
|
@@ -277,12 +276,12 @@ export async function scan(image, options = {}) {
|
|
|
277
276
|
const input = options;
|
|
278
277
|
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
279
278
|
throw new TypeError("Invalid scan options");
|
|
280
|
-
const { debug,
|
|
279
|
+
const { debug, extendedBudget, ...creation } = options;
|
|
281
280
|
if (debug !== undefined && typeof debug !== "boolean")
|
|
282
281
|
throw new TypeError("debug must be a boolean");
|
|
283
282
|
const scanner = await Scanner.create(creation);
|
|
284
283
|
try {
|
|
285
|
-
return scanner.scan(image, { debug,
|
|
284
|
+
return scanner.scan(image, { debug, extendedBudget });
|
|
286
285
|
}
|
|
287
286
|
finally {
|
|
288
287
|
scanner.dispose();
|
|
@@ -53,7 +53,7 @@ export function resolveFormats(input) {
|
|
|
53
53
|
if (input === "all")
|
|
54
54
|
return [...linearFormats, ...matrixFormats];
|
|
55
55
|
if (input === undefined)
|
|
56
|
-
return [
|
|
56
|
+
return [...retailFormats];
|
|
57
57
|
if (typeof input === "string" && Object.hasOwn(formatBits, input))
|
|
58
58
|
return [input];
|
|
59
59
|
if (!Array.isArray(input) || input.length === 0)
|
|
@@ -36,7 +36,7 @@ export interface Frame {
|
|
|
36
36
|
localizationMs: number;
|
|
37
37
|
linearStrategy: "scanlines" | "medium-localized";
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
39
|
+
/** Shared retail evidence over primary profiles, plus independent additional readers. */
|
|
40
40
|
export declare class MediumMultiformatScanner {
|
|
41
41
|
private medium?;
|
|
42
42
|
private mode;
|
|
@@ -10,7 +10,7 @@ function scanGray(image) {
|
|
|
10
10
|
? image.data.subarray(0, image.width * image.height)
|
|
11
11
|
: toGray(image);
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/** Shared retail evidence over primary profiles, plus independent additional readers. */
|
|
14
14
|
export class MediumMultiformatScanner {
|
|
15
15
|
medium;
|
|
16
16
|
mode = "medium";
|
|
@@ -104,7 +104,12 @@ export class MediumMultiformatScanner {
|
|
|
104
104
|
const barcodes = [], regions = [];
|
|
105
105
|
let primary;
|
|
106
106
|
let unfinished = false, mediumMs = 0, additionalMs = 0, preparationMs = 0, localizationMs = 0;
|
|
107
|
-
const
|
|
107
|
+
const sharedRetail = this.mode === "medium" &&
|
|
108
|
+
eanAddOnSymbol === "Ignore" &&
|
|
109
|
+
formats.some((f) => f === "EAN8" || f === "UPCE");
|
|
110
|
+
const retailPolicy = { ...scanPolicy, ...(sharedRetail ? { retailMask: 15 } : {}) };
|
|
111
|
+
const extraFormats = formats.filter((f) => (!sharedRetail || (f !== "EAN8" && f !== "UPCE")) &&
|
|
112
|
+
(eanAddOnSymbol !== "Ignore" || (f !== "EAN13" && f !== "UPCA")));
|
|
108
113
|
const conservative = medium instanceof ReleaseDetailScanner &&
|
|
109
114
|
!localized &&
|
|
110
115
|
eanAddOnSymbol === "Ignore" &&
|
|
@@ -131,12 +136,12 @@ export class MediumMultiformatScanner {
|
|
|
131
136
|
coverage.push(read.polygon);
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
|
-
if (formats.includes("EAN13") || formats.includes("UPCA")) {
|
|
139
|
+
if (sharedRetail || formats.includes("EAN13") || formats.includes("UPCA")) {
|
|
135
140
|
if (!medium)
|
|
136
141
|
throw Error("EAN13 engine has not been initialized.");
|
|
137
142
|
const begin = performance.now();
|
|
138
143
|
const found = medium instanceof ReleaseDetailScanner
|
|
139
|
-
? medium.scanLocalized(image,
|
|
144
|
+
? medium.scanLocalized(image, retailPolicy, fitLimits[this.mode], true, coverage)
|
|
140
145
|
: medium.scanLocalized(image, scanPolicy, fitLimits[this.mode], true);
|
|
141
146
|
primary = found;
|
|
142
147
|
const localization = found.localization;
|
|
@@ -153,6 +158,22 @@ export class MediumMultiformatScanner {
|
|
|
153
158
|
else if (formats.includes("EAN13"))
|
|
154
159
|
barcodes.push({ format: "EAN13", text: b.text, polygon: b.polygon, support: b.support });
|
|
155
160
|
}
|
|
161
|
+
if (sharedRetail) {
|
|
162
|
+
const evidence = found;
|
|
163
|
+
const shared = [...(evidence.scan.retail?.barcodes ?? [])];
|
|
164
|
+
unfinished ||= evidence.scan.retail?.unfinished ?? false;
|
|
165
|
+
for (const attempt of evidence.recovery?.attempts ?? []) {
|
|
166
|
+
unfinished ||= attempt.frame.retail?.unfinished ?? false;
|
|
167
|
+
for (const b of attempt.frame.retail?.barcodes ?? []) {
|
|
168
|
+
const polygon = b.polygon.map(([x, y]) => [
|
|
169
|
+
attempt.x + x / attempt.factor,
|
|
170
|
+
attempt.y + y / attempt.factor,
|
|
171
|
+
]);
|
|
172
|
+
shared.push({ ...b, polygon });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
barcodes.push(...shared.filter((b) => formats.includes(b.format)));
|
|
176
|
+
}
|
|
156
177
|
const decoded = new Set(found.scan.barcodes.flatMap((b) => b.candidate_indices));
|
|
157
178
|
localization.proposals.forEach((p, i) => {
|
|
158
179
|
if (!decoded.has(i))
|
|
@@ -168,13 +189,13 @@ export class MediumMultiformatScanner {
|
|
|
168
189
|
});
|
|
169
190
|
}
|
|
170
191
|
}
|
|
171
|
-
unfinished
|
|
192
|
+
unfinished ||=
|
|
172
193
|
found.scan.unfinished ||
|
|
173
194
|
Boolean(localization.workLimited) ||
|
|
174
195
|
(localization.omitted ?? 0) > 0;
|
|
175
196
|
mediumMs = performance.now() - begin;
|
|
176
197
|
}
|
|
177
|
-
if (localized && !formats.includes("EAN13") && !formats.includes("UPCA")) {
|
|
198
|
+
if (!sharedRetail && localized && !formats.includes("EAN13") && !formats.includes("UPCA")) {
|
|
178
199
|
if (!medium)
|
|
179
200
|
throw Error("EAN13 localization engine has not been initialized.");
|
|
180
201
|
const begin = performance.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tapirscan",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Orientation-aware barcode scanning with a Rust/WASM core and four effort modes",
|
|
6
6
|
"exports": {
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"examples",
|
|
15
15
|
"wasm/multiformat.json",
|
|
16
16
|
"THIRD_PARTY_NOTICES.md",
|
|
17
|
-
"wasm/low-
|
|
18
|
-
"wasm/medium-
|
|
19
|
-
"wasm/high-
|
|
20
|
-
"wasm/very-high-
|
|
17
|
+
"wasm/low-shared-retail-portable-20260919.wasm",
|
|
18
|
+
"wasm/medium-shared-retail-portable-20260919.wasm",
|
|
19
|
+
"wasm/high-shared-retail-portable-20260919.wasm",
|
|
20
|
+
"wasm/very-high-shared-retail-portable-20260919.wasm",
|
|
21
21
|
"wasm/multiformat.wasm"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc -p tsconfig.json",
|
|
25
|
-
"test": "node --test test/scanner.mjs test/linear-duplicates.mjs && tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext test/api-types.ts",
|
|
25
|
+
"test": "node --test test/scanner.mjs test/linear-duplicates.mjs test/shared-retail.mjs && tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext test/api-types.ts",
|
|
26
26
|
"prepack": "npm run build && node scripts/verify-package.mjs"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
Binary file
|
|
Binary file
|
package/wasm/multiformat.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "fb020f1c9ca597c1c4dd8ae8d45a70eae78e425eb08ca9225f85d8efc8652b29",
|
|
3
3
|
"sourceManifest": "provenance/import.json",
|
|
4
|
-
"sourceDigest": "
|
|
4
|
+
"sourceDigest": "e9b4ee4dacf9df2477b86dff44e2c74768c78f21d0e743158b09fff7386f6898",
|
|
5
5
|
"rustToolchain": "1.91.1",
|
|
6
6
|
"rustflags": "-C target-feature=+simd128 --remap-path-prefix=/home/runner/.rustup/toolchains/1.91.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library=/rustc/ed61e7d7e242494fb7057f2657300d9e77bb4fcb/library"
|
|
7
7
|
}
|
package/wasm/multiformat.wasm
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
/package/wasm/{high-complete-release-20260916.wasm → high-shared-retail-portable-20260919.wasm}
RENAMED
|
File without changes
|
|
File without changes
|