tapirscan 1.2.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,10 +1,5 @@
1
1
  # Tapirscan for JavaScript and TypeScript
2
2
 
3
- This guide describes the 1.2.0 API revision. See [migration](../../docs/API_MIGRATION.md).
4
- Build/install this checkout using [the development guide](../../docs/DEVELOPMENT.md)
5
- to use these changes before publication; older registry packages use their own
6
- versioned API.
7
-
8
3
  Scan image pixels in a browser or Node with the same Rust/WASM core.
9
4
  [Try the live demo](https://tapirscan.netlify.app) · [Quick start](#quick-start) · [WASM loading](#wasm-loading) · [Functions](#functions) · [All options](#all-options) · [Results](#results)
10
5
 
@@ -29,7 +24,7 @@ const result = await scan(image);
29
24
  console.log(result.values); // e.g. ["4006381333931"]
30
25
  ```
31
26
 
32
- Defaults are Medium effort, EAN13, multiple results, and debug disabled.
27
+ Defaults are Medium effort, retail formats, multiple results, and debug disabled.
33
28
  `result.barcodes` also gives each read's text, format, polygon and rectangle.
34
29
  The helper creates and disposes a scanner automatically. Browser apps need to
35
30
  serve its [WASM assets](#wasm-loading); Node loads the packaged files automatically.
@@ -141,7 +136,7 @@ call and does not change the default formats. Previously returned results surviv
141
136
  | Option | Where | Default | Meaning |
142
137
  | ---------------- | ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
143
138
  | `mode` | Creation | `"medium"` | `"low"`, `"medium"`, `"high"`, `"very-high"`. |
144
- | `formats` | Creation / scan | `["EAN13"]` | A single identifier, `"retail"`, `"common1D"`, `"common"`, `"1D"`, `"2D"`, `"all"`, or a nonempty array. Per-call selections must be subsets of creation formats. |
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. |
145
140
  | `wasmBaseUrl` | Creation | Module-relative assets | Directory URL for packaged WASMs. Use this for normal browser hosting. |
146
141
  | `loadWasm` | Creation | Module-relative loader | `(url: URL) => Promise<ArrayBuffer>`. Uses HTTP fetch in browsers and filesystem reads for Node file URLs. |
147
142
  | `eanAddOnPolicy` | Creation / one-shot | `"Ignore"` | `"Ignore"`, `"Read"`, `"Require"`; optional EAN/UPC supplement policy. |
@@ -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.js CHANGED
@@ -125,10 +125,10 @@ function publicResult(raw, image, debug) {
125
125
  });
126
126
  }
127
127
  const modes = {
128
- low: "low-complete-release-20260916.wasm",
129
- medium: "medium-complete-release-20260916.wasm",
130
- high: "high-complete-release-20260916.wasm",
131
- "very-high": "very-high-complete-release-20260916.wasm",
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
132
  };
133
133
  /** Mode selects a compiled implementation. Create another instance to switch. */
134
134
  export class Scanner {
@@ -136,7 +136,7 @@ export class Scanner {
136
136
  mode;
137
137
  configuredFormats;
138
138
  addOnPolicy;
139
- constructor(host, mode, configuredFormats = ["EAN13"], addOnPolicy = "Ignore") {
139
+ constructor(host, mode, configuredFormats = resolveFormats(), addOnPolicy = "Ignore") {
140
140
  this.host = host;
141
141
  this.mode = mode;
142
142
  this.configuredFormats = configuredFormats;
@@ -176,11 +176,13 @@ export class Scanner {
176
176
  if (!base.pathname.endsWith("/"))
177
177
  base.pathname += "/";
178
178
  const load = options.loadWasm ?? loadDefault;
179
- 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")));
180
181
  const bytes = needsPrimary ? await load(new URL(modes[mode], base)) : undefined;
181
182
  const recovery = !needsPrimary || mode === "low" ? undefined : await load(new URL(modes.low, base));
182
183
  const multiformat = addonPolicy !== "Ignore" || formats.length !== 1 || formats[0] !== "EAN13";
183
- 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")))
184
186
  ? await load(new URL("multiformat.wasm", base))
185
187
  : undefined;
186
188
  if (multiformat)
@@ -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.2.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