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 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, EAN13, multiple results, and debug disabled.
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; additional readers are experimental. Settings also work with the helper:
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 | Where | Default | Meaning |
139
- | ------------------ | ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
- | `mode` | Creation | `"medium"` | `"low"`, `"medium"`, `"high"`, `"very-high"`. |
141
- | `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. |
142
- | `wasmBaseUrl` | Creation | Module-relative assets | Directory URL for packaged WASMs. Use this for normal browser hosting. |
143
- | `loadWasm` | Creation | Module-relative loader | `(url: URL) => Promise<ArrayBuffer>`. Uses HTTP fetch in browsers and filesystem reads for Node file URLs. |
144
- | `eanAddOnPolicy` | Creation / one-shot | `"Ignore"` | `"Ignore"`, `"Read"`, `"Require"`; optional EAN/UPC supplement policy. |
145
- | `finishCandidates` | Scan / one-shot | `false` | Let selected EAN13/UPC-A candidates continue beyond shared frame budgets; other limits remain. See [finishing candidate work](#finishing-candidate-work). |
146
- | `debug` | Scan | `false` | Include search evidence under `result.debug`. Decoded polygons are always returned. |
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. Both result arrays are empty when nothing is decoded. Use `result.best` for
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 experimental decoding
225
- work independently of the effort mode. With debug enabled, retail reads rejected
226
- by `"Require"` remain available as undecoded-region evidence.
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
- ## Finishing candidate work
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
- Use `scanner.scan(image, { finishCandidates: true })` or
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
- Crowded or difficult images can take longer. Per-candidate effort, intentional
306
- weak-candidate deferral, localization, sampling and result limits still apply.
307
- Other formats keep their existing budgets. The synchronous scan has no library
308
- wall-clock deadline; use a Worker when responsiveness matters.
309
- `result.unfinished` can remain true, so this is not an exhaustiveness guarantee.
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], fit: number, full: boolean, coverage?: readonly Quad[]): DetailResult;
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 ?? {}, fit, full, coverage);
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
- /** Finish effort-selected EAN13/UPCA work beyond shared frame budgets. */
29
- finishCandidates?: boolean;
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: regionEvidence(raw) } } : {}),
124
+ ...(debug ? { debug: { ...raw, regions } } : {}),
123
125
  });
124
126
  }
125
127
  const modes = {
126
- low: "low-complete-release-20260916.wasm",
127
- medium: "medium-complete-release-20260916.wasm",
128
- high: "high-complete-release-20260916.wasm",
129
- "very-high": "very-high-complete-release-20260916.wasm",
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 = ["EAN13"], addOnPolicy = "Ignore") {
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 needsPrimary = formats.some((f) => f === "EAN13" || f === "UPCA");
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" || formats.some((f) => f !== "EAN13" && f !== "UPCA")
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 !== "finishCandidates")
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.finishCandidates !== undefined && typeof options.finishCandidates !== "boolean")
204
- throw new TypeError("finishCandidates 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;
205
210
  const debug = options.debug ?? false;
206
- const scanPolicy = { ...policy, finishCandidates: options.finishCandidates ?? false };
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: options.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
- ...(debug ? primary?.scan : {}),
231
+ ...primary?.scan,
229
232
  barcodes,
230
233
  unfinished: frame.unfinished,
231
- ...(debug ? { regions: frame.regions } : {}),
234
+ regions: frame.regions,
232
235
  },
233
- ...(debug && primary
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: debug ? { ...full.scan, barcodes } : { barcodes, unfinished: full.scan.unfinished },
257
- ...(debug
259
+ scan: { ...full.scan, barcodes },
260
+ localization,
261
+ searchWindows: full.searchWindows,
262
+ ...("recovery" in full && "detailRegions" in full
258
263
  ? {
259
- localization,
260
- searchWindows: full.searchWindows,
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, finishCandidates, ...creation } = options;
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, finishCandidates });
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 ["EAN13"];
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
- /** Frozen EAN13 Medium plus project-owned opt-in readers. No reference decoder. */
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
- /** Frozen EAN13 Medium plus project-owned opt-in readers. No reference decoder. */
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 extraFormats = formats.filter((f) => eanAddOnSymbol !== "Ignore" || (f !== "EAN13" && f !== "UPCA"));
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, scanPolicy, fitLimits[this.mode], true, coverage)
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.0",
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-complete-release-20260916.wasm",
18
- "wasm/medium-complete-release-20260916.wasm",
19
- "wasm/high-complete-release-20260916.wasm",
20
- "wasm/very-high-complete-release-20260916.wasm",
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": {
@@ -1,7 +1,7 @@
1
1
  {
2
- "sha256": "06598e5fc13957c21e2c206de51990decb415d4e7037e8a42160d6108008df55",
2
+ "sha256": "fb020f1c9ca597c1c4dd8ae8d45a70eae78e425eb08ca9225f85d8efc8652b29",
3
3
  "sourceManifest": "provenance/import.json",
4
- "sourceDigest": "bfbc48d3bba63fbbe16c8396d3cf4b5278095f2affd0bc4f15d380e1414a8111",
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
  }
Binary file