tapirscan 1.1.0 → 1.2.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/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Tapirscan for JavaScript and TypeScript
2
2
 
3
- This guide describes Tapirscan 1.1.0.
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.
4
7
 
5
8
  Scan image pixels in a browser or Node with the same Rust/WASM core.
6
9
  [Try the live demo](https://tapirscan.netlify.app) · [Quick start](#quick-start) · [WASM loading](#wasm-loading) · [Functions](#functions) · [All options](#all-options) · [Results](#results)
@@ -48,7 +51,7 @@ try {
48
51
  ```
49
52
 
50
53
  Here `image` is the `ImageData` above. `formats: "1D"` enables all supported linear
51
- formats; additional readers are experimental. Settings also work with the helper:
54
+ formats; readers outside the retail group remain experimental. Settings also work with the helper:
52
55
  `await scan(image, { mode: "high", formats: "1D" })`.
53
56
 
54
57
  `formats: "retail"` selects EAN13, UPCA,
@@ -135,15 +138,15 @@ call and does not change the default formats. Previously returned results surviv
135
138
 
136
139
  ## All options
137
140
 
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. |
141
+ | Option | Where | Default | Meaning |
142
+ | ---------------- | ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
143
+ | `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. |
145
+ | `wasmBaseUrl` | Creation | Module-relative assets | Directory URL for packaged WASMs. Use this for normal browser hosting. |
146
+ | `loadWasm` | Creation | Module-relative loader | `(url: URL) => Promise<ArrayBuffer>`. Uses HTTP fetch in browsers and filesystem reads for Node file URLs. |
147
+ | `eanAddOnPolicy` | Creation / one-shot | `"Ignore"` | `"Ignore"`, `"Read"`, `"Require"`; optional EAN/UPC supplement policy. |
148
+ | `extendedBudget` | Scan / one-shot | `false` | Allow extra reader work for any format. Exact budgets may evolve. |
149
+ | `debug` | Scan | `false` | Include search evidence under `result.debug`. Decoded polygons are always returned. |
147
150
 
148
151
  Format presets cover supported symbologies. Exports `commonFormats`, `commonLinearFormats`, `linearFormats`, `matrixFormats`
149
152
  and `retailFormats` let you compose custom selections; `formatBits` provides their
@@ -180,6 +183,7 @@ call. Convert DOM image elements or encoded images to pixels before scanning.
180
183
  | `result.mode` | `Mode` | Selected effort. |
181
184
  | `result.elapsedMs` | `number` | Host scan time in milliseconds; excludes file loading and scanner initialization. |
182
185
  | `result.unfinished` | `boolean` | Incomplete work; returned reads may still be useful. |
186
+ | `result.undecoded` | `readonly UndecodedRegion[]` | Localized proposals without accepted decodes; always available. |
183
187
  | `result.debug` | `Diagnostics \| undefined` | Requested diagnostic evidence; absent by default. |
184
188
  | `barcode.payloadBytes` | `readonly number[] \| undefined` | Original decoded matrix payload bytes when available; use `Uint8Array.from(...)` for an owned byte buffer. |
185
189
  | `barcode.text` | `string` | Decoded text. |
@@ -194,7 +198,8 @@ call. Convert DOM image elements or encoded images to pixels before scanning.
194
198
 
195
199
  Results, including nested geometry and requested diagnostics, are immutable at
196
200
  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
201
+ copy. `barcodes` and `values` are empty when nothing is decoded; `undecoded` may still
202
+ contain proposals. Use `result.best` for
198
203
  one read, or `undefined` when empty. All decoded instances remain available,
199
204
  including separate copies of the same value. Coordinates start at the
200
205
  top left, x rightward and y downward. Geometry is returned, not a cropped bitmap.
@@ -221,9 +226,9 @@ The policy is fixed for that scanner; its default is `"Ignore"`.
221
226
  supplement. Supplement geometry is not exposed separately.
222
227
 
223
228
  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.
229
+ the main payload. Reading supplements enables additional decoding
230
+ work independently of the effort mode. Retail reads rejected
231
+ by `"Require"` remain available in `result.undecoded`.
227
232
 
228
233
  ## Evidence and work limits
229
234
 
@@ -294,17 +299,22 @@ raise the exported `ScannerError` with a `.code` and `.message`. Loader/fetch
294
299
  errors propagate to the caller; creation and the one-shot helper reject their
295
300
  promises on failure. Always dispose reusable scanners with `finally`.
296
301
 
297
- ## Finishing candidate work
302
+ ## Extended work budget
298
303
 
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.
304
+ Use `scanner.scan(image, { extendedBudget: true })` to allow additional reader work. The default
305
+ is false. This option is valid for every format; the exact budgets and stages are
306
+ implementation details that may evolve. Effort mode remains a separate setting.
304
307
 
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.
308
+ Today this relaxes shared EAN-13/UPC-A retry and association limits. Other readers
309
+ currently retain their existing budgets. Per-candidate limits and intentional
310
+ deferrals remain; `unfinished` can still be true. This is not unlimited search,
311
+ an exhaustiveness guarantee or a wall-clock deadline. Custom primary-reader
312
+ engines must support the extended-work capability or report an error.
313
+
314
+ ## Undecoded regions
315
+
316
+ `result.undecoded` is always available, independently of `debug`. Each entry has
317
+ a source-image polygon and a format hint. It is a localized proposal without an
318
+ accepted decode, not proof of a real or permanently unreadable barcode. Entries
319
+ can overlap or describe false candidates. An empty collection does not prove
320
+ that every barcode was found. Raw candidate attempts remain in debug diagnostics.
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,15 +111,17 @@ 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 = {
@@ -196,23 +198,22 @@ export class Scanner {
196
198
  if (input === null || typeof input !== "object" || Array.isArray(input))
197
199
  throw new TypeError("Invalid scan options");
198
200
  for (const key of Object.keys(options))
199
- if (key !== "debug" && key !== "formats" && key !== "finishCandidates")
201
+ if (key !== "debug" && key !== "formats" && key !== "extendedBudget")
200
202
  throw new TypeError(`Unknown scan option: ${key}`);
201
203
  if (options.debug !== undefined && typeof options.debug !== "boolean")
202
204
  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");
205
+ if (options.extendedBudget !== undefined && typeof options.extendedBudget !== "boolean")
206
+ throw new TypeError("extendedBudget must be a boolean");
207
+ const finishCandidates = options.extendedBudget ?? false;
205
208
  const debug = options.debug ?? false;
206
- const scanPolicy = { ...policy, finishCandidates: options.finishCandidates ?? false };
209
+ const scanPolicy = { ...policy, finishCandidates };
207
210
  const formats = options.formats === undefined ? this.formats : resolveFormats(options.formats);
208
211
  if (formats.some((format) => !this.formats.includes(format)))
209
212
  throw new TypeError(`Scan formats must be a subset of configured formats. Requested: ${formats.join(", ")}; configured: ${this.formats.join(", ")}`);
210
213
  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
214
  const frame = this.host.scan(image, formats, {
214
215
  eanAddOnSymbol: this.eanAddOnPolicy,
215
- finishCandidates: options.finishCandidates,
216
+ finishCandidates,
216
217
  });
217
218
  const barcodes = frame.barcodes;
218
219
  const primary = frame.primary;
@@ -225,12 +226,12 @@ export class Scanner {
225
226
  // Preserve the aggregate limit flag when additional readers cannot separate causes.
226
227
  localizationLimited: frame.unfinished,
227
228
  scan: {
228
- ...(debug ? primary?.scan : {}),
229
+ ...primary?.scan,
229
230
  barcodes,
230
231
  unfinished: frame.unfinished,
231
- ...(debug ? { regions: frame.regions } : {}),
232
+ regions: frame.regions,
232
233
  },
233
- ...(debug && primary
234
+ ...(primary
234
235
  ? {
235
236
  localization,
236
237
  searchWindows: primary.searchWindows,
@@ -253,17 +254,13 @@ export class Scanner {
253
254
  multiple: true,
254
255
  elapsedMs: full.scanMs,
255
256
  localizationLimited: localization.workLimited || localization.omitted > 0,
256
- scan: debug ? { ...full.scan, barcodes } : { barcodes, unfinished: full.scan.unfinished },
257
- ...(debug
257
+ scan: { ...full.scan, barcodes },
258
+ localization,
259
+ searchWindows: full.searchWindows,
260
+ ...("recovery" in full && "detailRegions" in full
258
261
  ? {
259
- localization,
260
- searchWindows: full.searchWindows,
261
- ...("recovery" in full && "detailRegions" in full
262
- ? {
263
- recovery: full.recovery,
264
- detailRegions: full.detailRegions,
265
- }
266
- : {}),
262
+ recovery: full.recovery,
263
+ detailRegions: full.detailRegions,
267
264
  }
268
265
  : {}),
269
266
  }, image, debug);
@@ -277,12 +274,12 @@ export async function scan(image, options = {}) {
277
274
  const input = options;
278
275
  if (input === null || typeof input !== "object" || Array.isArray(input))
279
276
  throw new TypeError("Invalid scan options");
280
- const { debug, finishCandidates, ...creation } = options;
277
+ const { debug, extendedBudget, ...creation } = options;
281
278
  if (debug !== undefined && typeof debug !== "boolean")
282
279
  throw new TypeError("debug must be a boolean");
283
280
  const scanner = await Scanner.create(creation);
284
281
  try {
285
- return scanner.scan(image, { debug, finishCandidates });
282
+ return scanner.scan(image, { debug, extendedBudget });
286
283
  }
287
284
  finally {
288
285
  scanner.dispose();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tapirscan",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Orientation-aware barcode scanning with a Rust/WASM core and four effort modes",
6
6
  "exports": {