tapirscan 1.2.2 → 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.
Files changed (40) hide show
  1. package/LICENSE +220 -0
  2. package/README.md +384 -230
  3. package/dist/browser-worker.d.ts +33 -0
  4. package/dist/browser-worker.js +83 -0
  5. package/dist/browser.d.ts +44 -0
  6. package/dist/browser.js +211 -0
  7. package/dist/freeze.d.ts +6 -0
  8. package/dist/freeze.js +7 -0
  9. package/dist/index.d.ts +30 -18
  10. package/dist/index.js +138 -87
  11. package/dist/layout.d.ts +19 -0
  12. package/dist/layout.js +27 -0
  13. package/dist/multiformat/format-registry.d.ts +2 -2
  14. package/dist/multiformat/format-registry.js +12 -11
  15. package/dist/multiformat/formats.d.ts +2 -0
  16. package/dist/multiformat/formats.js +13 -6
  17. package/dist/rust-session.d.ts +2 -2
  18. package/dist/rust-session.js +16 -6
  19. package/examples/camera.html +28 -29
  20. package/examples/vite/README.md +31 -0
  21. package/examples/vite/index.html +14 -0
  22. package/examples/vite/main.js +33 -0
  23. package/examples/vite/package.json +16 -0
  24. package/examples/vite/vite.config.js +6 -0
  25. package/package.json +27 -8
  26. package/wasm/build.json +295 -0
  27. package/wasm/experimental-turbo16.wasm +0 -0
  28. package/wasm/experimental-turbo2.wasm +0 -0
  29. package/wasm/experimental-turbo4.wasm +0 -0
  30. package/wasm/experimental-turbo8.wasm +0 -0
  31. package/wasm/high.wasm +0 -0
  32. package/wasm/low.wasm +0 -0
  33. package/wasm/medium.wasm +0 -0
  34. package/wasm/very-high.wasm +0 -0
  35. package/examples/scan-worker.mjs +0 -20
  36. package/examples/worker-client.mjs +0 -63
  37. package/wasm/high-release-1.2.2-public-low-r2.wasm +0 -0
  38. package/wasm/low-release-1.2.2-public-low-r2.wasm +0 -0
  39. package/wasm/medium-release-1.2.2-public-low-r2.wasm +0 -0
  40. package/wasm/very-high-release-1.2.2-public-low-r2.wasm +0 -0
package/README.md CHANGED
@@ -1,35 +1,210 @@
1
1
  # Tapirscan for JavaScript and TypeScript
2
2
 
3
- Scan image pixels in a browser or Node with the same Rust/WASM core.
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)
3
+ Scan barcodes in the browser or Node with a Rust/WASM core. Each scan returns
4
+ every decoded barcode with its `text`, `format` and source-image `polygon` / `rect`.
5
5
 
6
- ## Quick start
6
+ - **`tapirscan/browser`** for web apps: scan files, `<img>`, `<video>`, canvases
7
+ or bitmaps in a bundled worker, with no WASM setup.
8
+ - **`tapirscan`** (the core) for Node and decoded pixels, scanning synchronously
9
+ on the calling thread.
10
+
11
+ [Try the live demo](https://tapirscan.f-kleinicke.de) · [Browser apps](#browser-apps-react-and-svelte) · [Core API](#core-api) · [WASM loading](#wasm-loading) · [All options](#all-options) · [Results](#results)
7
12
 
8
13
  ```sh
9
14
  npm install tapirscan
10
15
  ```
11
16
 
12
- TypeScript declarations and WASM binaries are included in the
13
- [npm package](https://www.npmjs.com/package/tapirscan). Your runtime must support
14
- WebAssembly SIMD.
17
+ TypeScript declarations and WASM binaries are included. Your runtime must support
18
+ WebAssembly SIMD; Node 20 or later is required.
19
+
20
+ ## Browser apps, React and Svelte
21
+
22
+ ```js
23
+ import { scan } from "tapirscan/browser";
24
+
25
+ const result = await scan(file); // a File from <input type="file">
26
+ console.log(result.values); // e.g. ["4006381333931"]
27
+ ```
28
+
29
+ Reuse a scanner for several images or camera frames. Construction is synchronous:
30
+ the worker and WASM load in the background, and the first scan waits for them.
31
+
32
+ ```js
33
+ import { Scanner } from "tapirscan/browser";
34
+
35
+ const scanner = new Scanner({ formats: ["EAN13", "QRCode"] });
36
+ const result = await scanner.scan(image); // a file, <img>, canvas, video frame, ...
37
+ scanner.dispose(); // stops the worker
38
+ ```
39
+
40
+ **Camera:** scan a playing `<video>` in a loop. Waiting for the next frame scans
41
+ each frame at most once, and awaiting each scan means slow devices skip frames
42
+ instead of falling behind:
43
+
44
+ ```js
45
+ while (running) {
46
+ await new Promise((resolve) => video.requestVideoFrameCallback(resolve));
47
+ if (!running) break;
48
+ const result = await scanner.scan(video);
49
+ // ...show result.values and result.barcodes
50
+ }
51
+ ```
52
+
53
+ `dispose()` rejects the scan in progress with "Scanner was disposed". In React
54
+ (add `"use client";` at the top in Next.js):
55
+
56
+ ```jsx
57
+ import { useEffect, useRef, useState } from "react";
58
+ import { Scanner } from "tapirscan/browser";
59
+
60
+ export default function BarcodeScanner() {
61
+ const video = useRef(null);
62
+ const [barcodes, setBarcodes] = useState([]);
63
+ const [error, setError] = useState(null);
64
+
65
+ useEffect(() => {
66
+ const element = video.current;
67
+ const scanner = new Scanner();
68
+ const camera = navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
69
+ let running = true; // React StrictMode mounts twice in development
70
+ // One cleanup for unmounting and for failures: stop scanning and the camera.
71
+ const stop = () => {
72
+ running = false;
73
+ scanner.dispose();
74
+ camera.then(
75
+ (stream) => stream.getTracks().forEach((track) => track.stop()),
76
+ () => {}, // No stream to stop.
77
+ );
78
+ };
79
+ camera
80
+ .then(async (stream) => {
81
+ if (!running) return;
82
+ element.srcObject = stream;
83
+ while (running) {
84
+ await new Promise((resolve) => element.requestVideoFrameCallback(resolve));
85
+ if (!running) break;
86
+ const found = await scanner.scan(element);
87
+ if (running) setBarcodes(found.barcodes);
88
+ }
89
+ })
90
+ .catch((cause) => {
91
+ if (running) setError(cause.message); // camera denied, loading or scan failure
92
+ stop();
93
+ });
94
+ return stop;
95
+ }, []);
96
+
97
+ return (
98
+ <>
99
+ <video ref={video} autoPlay muted playsInline />
100
+ {error && <p role="alert">{error}</p>}
101
+ {barcodes.map((barcode, i) => (
102
+ <p key={i}>
103
+ {barcode.format}: {barcode.text}
104
+ </p>
105
+ ))}
106
+ </>
107
+ );
108
+ }
109
+ ```
110
+
111
+ In Svelte 5 and SvelteKit:
112
+
113
+ ```svelte
114
+ <script>
115
+ import { Scanner } from "tapirscan/browser";
116
+ import { onDestroy } from "svelte";
117
+
118
+ const scanner = new Scanner();
119
+ onDestroy(() => scanner.dispose());
120
+
121
+ let barcodes = $state.raw([]);
122
+ let error = $state(null);
123
+ let latest = 0; // scans can finish out of order; keep only the newest
124
+ async function onchange(event) {
125
+ const file = event.currentTarget.files?.[0];
126
+ if (!file) return;
127
+ const request = ++latest;
128
+ try {
129
+ const found = await scanner.scan(file);
130
+ if (request === latest) [barcodes, error] = [found.barcodes, null];
131
+ } catch (cause) {
132
+ if (request === latest) [barcodes, error] = [[], cause.message];
133
+ }
134
+ }
135
+ </script>
136
+
137
+ <input type="file" accept="image/*" {onchange} />
138
+ {#if error}<p role="alert">{error}</p>{/if}
139
+ {#each barcodes as barcode}<p>{barcode.format}: {barcode.text}</p>{/each}
140
+ ```
141
+
142
+ **Bundlers:** Next.js (Turbopack or webpack), Vite 8 and production builds bundle
143
+ the worker and WASM files with no configuration. The Vite 6 and 7 development
144
+ servers (including SvelteKit on them) need one line, or the scanner reports that
145
+ its worker failed to start:
146
+
147
+ ```js
148
+ // vite.config.js
149
+ export default defineConfig({
150
+ // ...your plugins
151
+ optimizeDeps: { exclude: ["tapirscan"] },
152
+ });
153
+ ```
15
154
 
16
- Pass a canvas's `ImageData` directly:
155
+ CI builds the [Vite example](examples/vite/) from the packed package and scans an
156
+ uploaded photo with the production build in Chrome. The Next.js and Vite 7–8 setups
157
+ were verified by hand.
158
+
159
+ - **Sources:** a `File` or `Blob` (any image the browser decodes), `<img>`,
160
+ `<video>` (its current frame), `<canvas>`, `OffscreenCanvas`, `ImageBitmap`,
161
+ `VideoFrame`, `ImageData`, or decoded pixels as in the core API. Inputs are not
162
+ modified or transferred. Images and canvases are composited onto white, so
163
+ transparent areas count as background; `ImageData` and pixel buffers are scanned
164
+ as given, with alpha ignored.
165
+ - **Methods:** `scan` and `inspect` work as in the core API but return promises.
166
+ Each call captures its image and options when it starts, so buffers and option
167
+ objects can be reused immediately. Calls reach the worker in call order.
168
+ - **Options:** `mode`, `formats`, `eanAddOnPolicy` and
169
+ [`experimentalTurbo`](#experimental-turbo-presets) work as in the core, and
170
+ `scan(image, { formats })` overrides the formats for one call. `wasmBaseUrl`
171
+ serves the WASM files from another directory. `loadWasm` needs the core.
172
+ - **Lifecycle:** `scanner.ready` resolves once loaded; awaiting it is optional,
173
+ because loading errors also reject every scan. During server rendering the
174
+ constructor does nothing and scans reject. `dispose()` stops the worker and
175
+ immediately rejects every unfinished scan; returned results stay valid.
176
+ - **Errors:** unknown option names and invalid `wasmBaseUrl` values throw in the constructor; invalid option values
177
+ and scan arguments reject with `TypeError`; engine failures reject with
178
+ `ScannerError`. Browser image decoding and loading can also fail.
179
+ - **Requirements:** module workers, `OffscreenCanvas` and WebAssembly SIMD:
180
+ Chrome 91, Firefox 114, Safari 16.4 or later. The camera loop also needs
181
+ `requestVideoFrameCallback`. Images may have at most 32 megapixels.
182
+ - **Deployment size:** bundlers emit all eight WASM files (four modes and four
183
+ Turbo presets, 1.8–2.5 MB each, about 16 MB together), so any can be selected
184
+ without configuration. A page downloads only the one its scanner uses.
185
+
186
+ The [camera example](examples/camera.html) is a complete page: from this
187
+ directory (or the installed package), run `python3 -m http.server` and open
188
+ `/examples/camera.html` on localhost. Camera capture requires HTTPS; localhost
189
+ works for development.
190
+
191
+ ## Core API
192
+
193
+ The core scans decoded pixels, for example a canvas's `ImageData`:
17
194
 
18
195
  ```js
19
196
  import { scan } from "tapirscan";
20
197
 
21
- // Using an existing canvas and its 2D context:
22
198
  const image = context.getImageData(0, 0, canvas.width, canvas.height);
23
199
  const result = await scan(image);
24
200
  console.log(result.values); // e.g. ["4006381333931"]
25
201
  ```
26
202
 
27
- Defaults are Medium effort, retail formats, multiple results, and debug disabled.
28
- `result.barcodes` also gives each read's text, format, polygon and rectangle.
29
- The helper creates and disposes a scanner automatically. Browser apps need to
30
- serve its [WASM assets](#wasm-loading); Node loads the packaged files automatically.
203
+ The one-shot helper creates and disposes a scanner. Defaults are Medium effort
204
+ and retail formats. In browsers the core needs its [WASM assets](#wasm-loading)
205
+ served; Node loads the packaged files automatically.
31
206
 
32
- For more control or repeated images, reuse a scanner:
207
+ Reuse a scanner for repeated images. Its scans are synchronous:
33
208
 
34
209
  ```js
35
210
  import { Scanner } from "tapirscan";
@@ -45,14 +220,59 @@ try {
45
220
  }
46
221
  ```
47
222
 
48
- Here `image` is the `ImageData` above. `formats: "1D"` enables all supported linear
49
- formats; readers outside the retail group remain experimental. Settings also work with the helper:
50
- `await scan(image, { mode: "high", formats: "1D" })`.
223
+ Settings also work with the helper: `await scan(image, { mode: "high", formats: "1D" })`.
224
+ For unread regions, timing and diagnostics, call `inspect` instead of `scan`.
225
+
226
+ The core scans synchronously, so a long scan on the main thread blocks the page.
227
+ Use `tapirscan/browser`, or create one core scanner inside your own worker and
228
+ transfer frame buffers to it; configure `loadWasm` inside that worker.
229
+
230
+ | Function | Returns | Behavior |
231
+ | ------------------------------------- | --------------------------- | ---------------------------------------------------------------------------- |
232
+ | `scan(image, options = {})` | `Promise<ScanResult>` | One image; creates and disposes a scanner. |
233
+ | `inspect(image, options = {})` | `Promise<InspectionResult>` | Like `scan`, with timing, unread regions and diagnostics. |
234
+ | `Scanner.create(options = {})` | `Promise<Scanner>` | A reusable scanner with fixed mode, default formats and supplement policy. |
235
+ | `scanner.scan(image, { formats })` | `ScanResult` | Synchronous scan; `formats` optionally overrides the defaults for this call. |
236
+ | `scanner.inspect(image, { formats })` | `InspectionResult` | Synchronous inspection. |
237
+ | `scanner.dispose()` | `void` | Releases WASM memory. Repeated disposal is safe; do not scan after disposal. |
238
+
239
+ Create another scanner to change effort or the supplement policy. Per-call
240
+ formats such as `scanner.scan(image, { formats: "QRCode" })` apply only to that
241
+ call; `scanner.formats` exposes the defaults. Results stay valid after disposal.
242
+
243
+ ## Experimental Turbo presets
244
+
245
+ For faster **1D barcode scanning**, opt into a Turbo preset instead of a mode.
246
+ Both entries accept it; in the browser it suits camera loops:
247
+
248
+ ```js
249
+ import { Scanner } from "tapirscan/browser";
250
+
251
+ const scanner = new Scanner({ experimentalTurbo: 2, formats: "retail" });
252
+ const result = await scanner.scan(video);
253
+ ```
254
+
255
+ Accepted values are **2, 4, 8 and 16**. Each roughly indicates the speedup over Low
256
+ it targets; the actual speedup varies with images and devices and is not guaranteed.
257
+ Higher presets do less work and can miss more barcodes, including clean symbols
258
+ placed close together. Start with 2 and check detection on your own inputs.
259
+ The presets speed up EAN-13, UPC-A, EAN-8, UPC-E, Code 128, Code 39 and ITF.
260
+ Other formats stay readable when selected, but do not get faster, and mixed-format
261
+ scans still pay for the enabled 2D readers. See
262
+ [Turbo behavior and limitations](https://github.com/kleinicke/tapirscan/blob/main/docs/EXPERIMENTAL_TURBO.md).
263
+
264
+ `experimentalTurbo` and `mode` are mutually exclusive, and Turbo requires
265
+ `eanAddOnPolicy: "ignore"`. A Turbo scanner has no effort mode: `inspect` results
266
+ report `experimentalTurbo` and omit `mode`. The core scanner also exposes
267
+ `scanner.experimentalTurbo` and leaves `scanner.mode` `undefined`; the browser
268
+ `Scanner` has no configuration properties.
51
269
 
52
- `formats: "retail"` selects EAN13, UPCA,
53
- EAN8 and UPCE. `"common1D"` adds Code128, Code39 and ITF; `"common"` adds
54
- QRCode and DataMatrix to `"common1D"`. See
55
- [format presets and runtime behavior](../../docs/FORMATS.md).
270
+ **Stability:** this option, its presets and their asset imports may change or be
271
+ removed in a minor release. Pin the exact package version if you rely on them.
272
+
273
+ `tapirscan/browser` loads the preset's WASM by itself. With the core in Vite or
274
+ SvelteKit, load the matching asset as in [WASM loading](#vite-and-sveltekit):
275
+ `tapirscan/wasm/experimental-turbo2.wasm`, `-turbo4`, `-turbo8` or `-turbo16`.
56
276
 
57
277
  ## WASM loading
58
278
 
@@ -60,14 +280,41 @@ In Node, the default loader reads assets from the installed package. Decode your
60
280
  image with an image library first, then pass grayscale, RGB or RGBA bytes:
61
281
 
62
282
  ```js
283
+ import { scan } from "tapirscan";
284
+
63
285
  const result = await scan({ data: pixels, width, height, channels: 1, stride: width });
64
286
  ```
65
287
 
66
- Here `pixels` is a Uint8Array of decoded grayscale pixels. Image codecs are not
67
- included; filenames, URLs and encoded JPEG/PNG bytes are not scan inputs.
288
+ Here `pixels` is a `Uint8Array` of decoded grayscale pixels. Filenames, URLs and
289
+ encoded JPEG/PNG bytes are not scan inputs.
290
+
291
+ ### Vite and SvelteKit
292
+
293
+ [`tapirscan/browser`](#browser-apps-react-and-svelte) needs no WASM setup. To use the
294
+ core directly, import the WASM asset URL so Vite includes it in development and
295
+ production builds, including apps deployed under a base path:
296
+
297
+ ```js
298
+ import { Scanner } from "tapirscan";
299
+ import mediumWasmUrl from "tapirscan/wasm/medium.wasm?url";
300
+
301
+ // Load once; reuse these bytes if you create more than one scanner.
302
+ const response = await fetch(mediumWasmUrl);
303
+ if (!response.ok) throw new Error(`WASM load failed: ${response.status}`);
304
+ const bytes = await response.arrayBuffer();
305
+ const scanner = await Scanner.create({ loadWasm: async () => bytes });
306
+ ```
307
+
308
+ The imports are `tapirscan/wasm/low.wasm`, `medium.wasm`, `high.wasm` and
309
+ `very-high.wasm`; match the asset to `mode` (the example uses the default
310
+ Medium). A mismatched asset is an error. `?url` is Vite syntax. In SvelteKit,
311
+ create core scanners in browser code such as `onMount`, not during server rendering.
68
312
 
69
- In a browser, the default loader fetches assets relative to the module. If your
70
- bundler relocates modules, copy the WASMs into your public directory:
313
+ ### Other browser setups
314
+
315
+ The default loader fetches assets relative to the module. If your bundler
316
+ relocates modules, copy the WASM files to your public directory as part of your
317
+ build, so package upgrades cannot leave stale assets:
71
318
 
72
319
  ```sh
73
320
  mkdir -p public/tapirscan
@@ -78,242 +325,149 @@ Then point the scanner at that directory:
78
325
 
79
326
  ```js
80
327
  const scanner = await Scanner.create({ wasmBaseUrl: "/tapirscan/" });
81
- try {
82
- console.log(scanner.scan(image).values);
83
- } finally {
84
- scanner.dispose();
85
- }
86
328
  ```
87
329
 
88
- The one-shot helper accepts the same option:
89
- `await scan(image, { wasmBaseUrl: "/tapirscan/" })`.
90
330
  `wasmBaseUrl` accepts a string or URL, with or without a trailing slash. Relative
91
- URLs resolve against the page/worker URL in browsers and the package module in
92
- Node; use an absolute URL for an unambiguous location. For authenticated requests
93
- or custom storage, use `loadWasm: async (url) => arrayBuffer`. The callback receives
94
- URLs resolved against `wasmBaseUrl` when both options are supplied. Creation loads
95
- one complete Rust scanner for the selected effort mode.
96
-
97
- Use the deployed base path if your app is hosted below a subpath. Copy all current
98
- WASMs so every effort mode remains available. The demo and its comparison engines
99
- are not needed in your app.
100
-
101
- ## Camera and worker use
102
-
103
- A standalone [worker client](examples/worker-client.mjs), [worker](examples/scan-worker.mjs)
104
- and [camera page](examples/camera.html) are included in the package. From this
105
- binding directory (or the installed package directory), run `python3 -m http.server`
106
- and open `/examples/camera.html` on localhost. The example handles initialization,
107
- frame ownership transfer, one frame in flight, errors and shutdown. It transfers
108
- pixel buffers; callers must not reuse the transferred buffer. Worker messages
109
- produce independent mutable result copies through structured cloning. Custom
110
- `loadWasm` functions must be configured inside the worker; functions cannot be sent
111
- in a message. Adjust the worker import and WASM asset path for your bundler.
112
-
113
- Initialization is asynchronous; scanning is synchronous. For a responsive browser
114
- UI, initialize one scanner inside a Web Worker and transfer an owned frame buffer.
115
- Capture the next frame after the previous result arrives. Avoid racing scanner
116
- initialization or modifying pixels during scanning. The [demo worker](../../demo/src/lib/scan.worker.ts)
117
- shows a complete integration. Camera capture belongs to your app and requires
118
- HTTPS (localhost works for development).
119
-
120
- ## Functions
121
-
122
- | Function | Return type | Behavior |
123
- | ----------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
124
- | `scan(image, options = {})` | `Promise<ScanResult>` | One image with automatic scanner creation and disposal, including on failure. Accepts creation and scan options together. |
125
- | `Scanner.create(options = {})` | `Promise<Scanner>` | Initialize a reusable scanner. Mode is fixed; formats define defaults and allowed per-call subsets. |
126
- | `scanner.scan(image, options = {})` | `ScanResult` | Synchronously scan pixels. Accepts scan options only. |
127
- | `scanner.dispose()` | `void` | Release WASM sessions. Repeated disposal is safe; do not scan after disposal. |
128
-
129
- `image` is required for either scan function. All options are optional. Reuse a
130
- scanner for successive frames to avoid repeated initialization; create another
131
- to change effort or enable formats outside its configured selection. A per-call
132
- subset such as `scanner.scan(image, { formats: "EAN13" })` applies only to that
133
- call and does not change the default formats. Previously returned results survive disposal. `scanner.formats` exposes the frozen creation selection.
331
+ URLs resolve against the page or worker in browsers and the package module in
332
+ Node. For authenticated requests or custom storage, use
333
+ `loadWasm: async (url) => arrayBuffer`; it receives URLs resolved against
334
+ `wasmBaseUrl`. The default loader caches loaded assets within a module instance;
335
+ custom loaders manage their own caching. Each scanner owns its own WASM instance.
134
336
 
135
337
  ## All options
136
338
 
137
- | Option | Where | Default | Meaning |
138
- | ---------------- | ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
139
- | `mode` | Creation | `"medium"` | `"low"`, `"medium"`, `"high"`, `"very-high"`. |
140
- | `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. |
141
- | `wasmBaseUrl` | Creation | Module-relative assets | Directory URL for packaged WASMs. Use this for normal browser hosting. |
142
- | `loadWasm` | Creation | Module-relative loader | `(url: URL) => Promise<ArrayBuffer>`. Uses HTTP fetch in browsers and filesystem reads for Node file URLs. |
143
- | `eanAddOnPolicy` | Creation / one-shot | `"Ignore"` | `"Ignore"`, `"Read"`, `"Require"`; optional EAN/UPC supplement policy. |
144
- | `extendedBudget` | Scan / one-shot | `false` | Allow extra reader work for any format. Exact budgets may evolve. |
145
- | `debug` | Scan | `false` | Include search evidence under `result.debug`. Decoded polygons are always returned. |
146
-
147
- Format presets cover supported symbologies. Exports `commonFormats`, `commonLinearFormats`, `linearFormats`, `matrixFormats`
148
- and `retailFormats` let you compose custom selections; `formatBits` provides their
149
- native bit mapping. See [identifiers and coverage](../../docs/FORMATS.md).
150
- The four effort modes tune EAN13/UPCA, Common1D and QR Code; other matrix readers use fixed effort.
151
-
152
- Resolution, camera capture, preprocessing rotation, ROI, confidence thresholds,
153
- timeouts and exact work budgets are not public scan options. Demo capture and
154
- resize settings belong to the application.
339
+ | Option | Where | Default | Meaning |
340
+ | ------------------- | --------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
341
+ | `mode` | Creation | `"medium"` | `"low"`, `"medium"`, `"high"`, `"very-high"`. |
342
+ | `formats` | Creation / scan | `"retail"` | An identifier, `"retail"`, `"common1D"`, `"common"`, `"1D"`, `"2D"`, `"all"`, or a nonempty array. Per-call formats override the defaults. |
343
+ | `eanAddOnPolicy` | Creation | `"ignore"` | `"ignore"`, `"read"`, `"require"`; see [supplements](#eanupc-supplements). |
344
+ | `wasmBaseUrl` | Creation | Module-relative assets | Directory URL for the packaged WASM files. |
345
+ | `loadWasm` | Creation | Module-relative loader | `(url: URL) => Promise<ArrayBuffer>`. |
346
+ | `experimentalTurbo` | Creation | Unset | **Experimental:** `2`, `4`, `8` or `16`. Mutually exclusive with `mode`. |
347
+
348
+ `"retail"` selects EAN13, UPCA, EAN8 and UPCE. `"common1D"` adds Code128,
349
+ Code39 and ITF; `"common"` adds QRCode and DataMatrix. `"1D"` and `"2D"`
350
+ enable all linear or all 2D formats.
351
+ The exports `retailFormats`, `commonLinearFormats`, `commonFormats`,
352
+ `linearFormats` and `matrixFormats` help compose custom selections. See
353
+ [format coverage](https://github.com/kleinicke/tapirscan/blob/main/docs/FORMATS.md) for identifiers and variants.
155
354
 
156
355
  ## Image input
157
356
 
158
- `PixelImage` accepts `ImageData` (or its data/width/height fields) or an explicit
159
- `Image` buffer:
357
+ The core accepts `ImageData` (or an object with its `data`, `width` and `height`)
358
+ or an explicit pixel buffer:
160
359
 
161
- | Field | Type | Meaning |
162
- | ----------------- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
163
- | `data` | `Uint8Array` | Decoded pixels. ImageData instead uses `Uint8ClampedArray` and implies tightly packed RGBA. |
164
- | `width`, `height` | `number` | Integer input dimensions, at least 3 pixels each. |
165
- | `channels` | `1 \| 3 \| 4` | Grayscale, RGB or RGBA. Alpha is ignored. Required for explicit buffers. |
166
- | `stride` | `number` | Bytes between row starts, at least width × channels. Optional; defaults to width × channels. Padding is allowed. |
360
+ | Field | Type | Meaning |
361
+ | ----------------- | ------------- | ------------------------------------------------------------------------------------ |
362
+ | `data` | `Uint8Array` | Decoded pixels. `ImageData` uses `Uint8ClampedArray` with tightly packed RGBA. |
363
+ | `width`, `height` | `number` | Integer dimensions, at least 3 pixels each. |
364
+ | `channels` | `1 \| 3 \| 4` | Grayscale, RGB or RGBA. Alpha is ignored. Required for explicit buffers. |
365
+ | `stride` | `number` | Bytes between row starts; defaults to width × channels. Larger values allow padding. |
167
366
 
168
- Input is limited to 32 megapixels and 128 MiB of addressed pixels. Keep the buffer stable during the
169
- call. Convert DOM image elements or encoded images to pixels before scanning.
367
+ Input is limited to 32 megapixels and 128 MiB of addressed pixels. Keep the buffer
368
+ unchanged during the call. Coordinates start at the top left, x rightward and y
369
+ downward. If you resize or rotate before scanning, map coordinates back yourself.
170
370
 
171
371
  ## Results
172
372
 
173
- | Field | Type | Meaning |
174
- | ------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
175
- | `result.values` | `readonly string[]` | Decoded strings. |
176
- | `result.barcodes` | `readonly Barcode[]` | Decoded values with format and geometry. |
177
- | `result.best` | `Barcode \| undefined` | Highest-support read, or undefined when empty. |
178
- | `result.image` | `{ width: number, height: number }` | Dimensions of supplied pixels. |
179
- | `result.mode` | `Mode` | Selected effort. |
180
- | `result.elapsedMs` | `number` | Host scan time in milliseconds; excludes file loading and scanner initialization. |
181
- | `result.unfinished` | `boolean` | Incomplete work; returned reads may still be useful. |
182
- | `result.undecoded` | `readonly UndecodedRegion[]` | Localized proposals without accepted decodes; always available. |
183
- | `result.debug` | `Diagnostics \| undefined` | Requested diagnostic evidence; absent by default. |
184
- | `barcode.payloadBytes` | `readonly number[] \| undefined` | Original decoded matrix payload bytes when available; use `Uint8Array.from(...)` for an owned byte buffer. |
185
- | `barcode.text` | `string` | Decoded text. |
186
- | `barcode.format` | `Format \| "Unknown"` | Symbology identifier. |
187
- | `barcode.polygon` | `Quad` | Four `[x, y]` corners in input-image coordinates. |
188
- | `barcode.rect` | `{ left: number, top: number, width: number, height: number }` | Enclosing integer rectangle. |
189
- | `barcode.support` | `number` | Reader-specific ranking evidence; not confidence or a probability. |
190
- | `barcode.gs1` | `boolean \| undefined` | GS1 indicator when supplied by the reader. |
191
- | `barcode.readerInitialization` | `boolean \| undefined` | Reader initialization data indicator; never executed. |
192
- | `barcode.structuredAppend` | `StructuredAppend \| undefined` | Immutable multipart metadata: one-based `index`, `count`, optional `id` and `parity`. |
193
- | `barcode.eanAddOn` | `string \| undefined` | Optional EAN supplement; populated when `eanAddOnPolicy` is `"Read"` or `"Require"`. |
194
-
195
- Results, including nested geometry and requested diagnostics, are immutable at
196
- runtime and in TypeScript. Use `structuredClone(result)` if you need a mutable
197
- copy. `barcodes` and `values` are empty when nothing is decoded; `undecoded` may still
198
- contain proposals. Use `result.best` for
199
- one read, or `undefined` when empty. All decoded instances remain available,
200
- including separate copies of the same value. Coordinates start at the
201
- top left, x rightward and y downward. Geometry is returned, not a cropped bitmap.
202
- Map coordinates back yourself if you resize/rotate before scanning. Support is a
203
- ranking heuristic, not a probability.
204
- Polygon coordinates are Rust `f32` values exposed as JavaScript numbers. Their
205
- decimal string form may show the exact binary value instead of the shorter
206
- decimal spelling used by older package builds.
207
-
208
- The package exports `EanAddOnPolicy`, `ScannerOptions`, `ScanOptions`, `ScanResult`, `Barcode`,
209
- `PixelImage`, `Image`, `Quad`, `Mode`, `Format`, `FormatSelection`, `Diagnostics`,
210
- `StructuredAppend` and `DiagnosticBarcode` types. TypeScript infers results from calls; runtime
211
- checks still validate pixel buffers and dimensions.
373
+ `scan` returns a `ScanResult`; `inspect` returns an `InspectionResult` with the
374
+ same fields plus the ones marked _inspect_.
375
+
376
+ | Field | Type | Meaning |
377
+ | ------------------------------ | -------------------------------------------------------------- | --------------------------------------------------------------------------- |
378
+ | `result.values` | `readonly string[]` | Decoded strings, one per barcode. |
379
+ | `result.barcodes` | `readonly Barcode[]` | Decoded barcodes with format and geometry. |
380
+ | `result.best` | `Barcode \| undefined` | Highest-support barcode, first on ties. |
381
+ | `result.image` | `{ width, height }` | _inspect_: dimensions of the supplied pixels. |
382
+ | `result.mode` | `Mode \| undefined` | _inspect_: effort mode used; absent for Turbo presets. |
383
+ | `result.elapsedMs` | `number` | _inspect_: scan time, excluding image loading and scanner creation. |
384
+ | `result.undecoded` | `readonly UndecodedRegion[]` | _inspect_: [regions without a decode](#undecoded-regions). |
385
+ | `result.diagnostics` | `Diagnostics` | _inspect_: [engine evidence](#diagnostics). |
386
+ | `barcode.text` | `string` | Decoded text. |
387
+ | `barcode.format` | `Format` | Symbology identifier. |
388
+ | `barcode.polygon` | `Quad` | Four `[x, y]` corners in input-image coordinates. |
389
+ | `barcode.rect` | `{ left: number, top: number, width: number, height: number }` | Enclosing integer rectangle. |
390
+ | `barcode.support` | `number` | Reader-specific ranking evidence used by `best`. |
391
+ | `barcode.payloadBytes` | `readonly number[] \| undefined` | Decoded matrix payload bytes, when available. |
392
+ | `barcode.gs1` | `boolean \| undefined` | GS1 indicator, when supplied by the reader. |
393
+ | `barcode.readerInitialization` | `boolean \| undefined` | Data Matrix, PDF417, Aztec and MaxiCode reader-initialization flag. |
394
+ | `barcode.structuredAppend` | `StructuredAppend \| undefined` | Multipart metadata: one-based `index`, `count`, optional `id` and `parity`. |
395
+ | `barcode.eanAddOn` | `string \| undefined` | EAN/UPC supplement, with `eanAddOnPolicy` `"read"` or `"require"`. |
396
+
397
+ Separate labels with the same value stay separate entries. Results are deeply
398
+ frozen; use `structuredClone(result)` for a mutable copy. `JSON.stringify(result)`
399
+ gives plain JSON.
400
+
401
+ `support` is an uncalibrated, reader-specific ranking heuristic, not a
402
+ probability, and is not comparable across formats. In a mixed-format image,
403
+ `best` is therefore not necessarily the most reliable read; select by format or
404
+ payload when your application knows what it expects. Checksums reduce wrong
405
+ reads but cannot rule them out.
406
+
407
+ `payloadBytes` is supplied by QR Code, Data Matrix, Aztec, PDF417 and MaxiCode.
408
+ It holds decoded data bytes before character-set interpretation, not raw
409
+ codewords; Aztec Rune gives its value as decimal ASCII. Encoding `text` as UTF-8
410
+ does not reconstruct these bytes. Use `Uint8Array.from(barcode.payloadBytes)`
411
+ for a byte buffer.
412
+
413
+ All types (`ScanResult`, `InspectionResult`, `Barcode`, `ScannerOptions`,
414
+ `ScanOptions`, `Format` and others) are exported from both entries.
212
415
 
213
416
  ## EAN/UPC supplements
214
417
 
215
- Set `eanAddOnPolicy: "Read"` when creating a scanner or calling one-shot `scan()`.
216
- The policy is fixed for that scanner; its default is `"Ignore"`.
418
+ Set `eanAddOnPolicy` when creating a scanner or calling one-shot `scan` or `inspect`.
217
419
 
218
420
  | Policy | Behavior |
219
421
  | ----------- | ------------------------------------------------------------------------------------------------ |
220
- | `"Ignore"` | Decode the main barcode without reading its supplement. |
221
- | `"Read"` | Try reading the two- or five-digit supplement; keep the main barcode if none is readable. |
222
- | `"Require"` | Return an EAN/UPC barcode only when its supplement is readable. Other formats remain unaffected. |
223
-
224
- `barcode.polygon` and `barcode.rect` describe the main barcode, excluding the
225
- supplement. Supplement geometry is not exposed separately.
422
+ | `"ignore"` | Decode the main barcode without reading its supplement. |
423
+ | `"read"` | Try reading the two- or five-digit supplement; keep the main barcode if none is readable. |
424
+ | `"require"` | Return an EAN/UPC barcode only when its supplement is readable. Other formats remain unaffected. |
226
425
 
227
- The supplement appears separately in `barcode.eanAddOn`; `barcode.text` remains
228
- the main payload. Reading supplements enables additional decoding
229
- work independently of the effort mode. Retail reads rejected
230
- by `"Require"` remain available in `result.undecoded`.
426
+ The supplement appears in `barcode.eanAddOn`; `barcode.text` is the main payload,
427
+ and `polygon` and `rect` describe the main barcode. Reading supplements adds
428
+ decoding work. Retail reads rejected by `"require"` appear in `result.undecoded`.
231
429
 
232
- ## Evidence and work limits
233
-
234
- Most applications need `barcode.text`, `.format`, `.polygon` and `.rect`.
235
- `barcode.support` exposes the evidence used by `.best`. It is an uncalibrated,
236
- reader-specific ranking heuristic, not a certainty percentage; values are not
237
- comparable confidence across formats or effort modes. Consequently, `.best` means the largest support value,
238
- not the most reliable barcode in a mixed-format image. Select by the format or
239
- payload your application needs when that distinction matters. Checksums and consistency
240
- checks reduce wrong reads but cannot guarantee that every returned decode is correct.
241
-
242
- Select EAN13/UPCA, Common1D and QR Code search effort with `mode: "low"` through `"very-high"` at creation.
243
- Other matrix readers use fixed effort. `result.unfinished` is available without debug and
244
- combines reported decoding and localization limits. Returned reads are still usable.
245
- Candidate, retry and parsing caps are reported, including bounded searches that
246
- also returned reads. False does not promise exhaustive scanning. Exact budgets and interruptible timeouts are not public options.
430
+ ## Undecoded regions
247
431
 
248
- `debug: true` adds attempted search windows, localization proposals, candidate
249
- outcomes and engine traces. It is unnecessary for drawing decoded barcode locations.
432
+ `inspect` results list `undecoded` regions: localized proposals without an
433
+ accepted decode, each with a source-image `polygon` and a `format` hint. They
434
+ can overlap or be false candidates, and an empty list does not prove that every
435
+ barcode was found.
250
436
 
251
- ### Switching between retail and QR scanning
437
+ ## Diagnostics
252
438
 
253
439
  ```js
254
- const scanner = await Scanner.create({ formats: "common" });
255
- try {
256
- console.log(scanner.formats);
257
- const retail = scanner.scan(image, { formats: "retail" });
258
- const qr = scanner.scan(image, { formats: "QRCode" });
259
- } finally {
260
- scanner.dispose();
261
- }
440
+ const report = scanner.inspect(image);
441
+ console.log(report.diagnostics.regions.proposals, report.diagnostics.regions.searchWindows);
442
+ console.log(report.diagnostics.scan.barcodes);
262
443
  ```
263
444
 
264
- `payloadBytes` is supplied by QR Code, Data Matrix, Aztec, PDF417 and MaxiCode.
265
- It contains decoded data bytes before character-set interpretation, not raw symbol
266
- codewords. Aztec Rune represents its numeric value as decimal ASCII. Other readers
267
- leave it absent. Encoding `.text` as UTF-8 does not reconstruct original bytes.
268
- Unsupported character encodings can still prevent decoding; reader behavior is
269
- unchanged. The frozen number array is directly JSON-compatible.
445
+ `diagnostics.regions` has a stable shape: `proposals` and `searchWindows` are
446
+ `null` when a reader does not expose them, and empty arrays when it found nothing.
447
+ The other fields are the engine's own evidence; they vary by reader and may
448
+ change between releases. Candidate indices inside recovery crops are local to
449
+ the crop, not identifiers for tracking between frames.
270
450
 
271
- ## Diagnostics and errors
451
+ ## Errors
272
452
 
273
- ```js
274
- const result = scanner.scan(image, { debug: true });
275
- if (result.debug) {
276
- console.log(result.debug.regions.proposals, result.debug.regions.searchWindows);
277
- console.log(result.debug.regions.undecoded);
278
- console.log(result.debug.scan.barcodes);
279
- }
280
- ```
453
+ Invalid options throw `TypeError`. Engine and validation failures throw the
454
+ exported `ScannerError` with `.message` and a `.code`:
281
455
 
282
- `debug.regions` has a stable shape across creation formats: `proposals` and
283
- `searchWindows` contain evidence or null when unavailable, and `undecoded` contains
284
- unread source-image geometry as immutable `UndecodedRegion` objects (`format` hint
285
- and `polygon`, with no decoded text). Empty arrays mean available evidence with no entries.
286
- EAN evidence remains available when a scanner also enables additional readers.
287
-
288
- Diagnostics also retain the raw schema-2 result: `scan` includes support and candidate
289
- evidence, and `localizationLimited` reports localization limits. Depending on the
290
- reader, `localization`, `searchWindows`, `recovery` and `detailRegions` may be
291
- present. GS1, reader initialization and structured append are available directly on
292
- barcodes without debug; raw metadata also retains these fields where supported.
293
- Candidate indices inside recovery crops are local to the crop and are not
294
- identifiers for tracking between frames.
295
-
296
- Invalid options can raise TypeError. Scanner validation and engine failures can
297
- raise the exported `ScannerError` with a `.code` and `.message`. Loader/fetch
298
- errors propagate to the caller; creation and the one-shot helper reject their
299
- promises on failure. Always dispose reusable scanners with `finally`.
300
-
301
- ## Extended work budget
302
-
303
- Use `scanner.scan(image, { extendedBudget: true })` to allow additional reader work. The default
304
- is false. This option is valid for every format; the exact budgets and stages are
305
- implementation details that may evolve. Effort mode remains a separate setting.
306
-
307
- Today this relaxes shared EAN-13/UPC-A retry and association limits. Other readers
308
- currently retain their existing budgets. Per-candidate limits and intentional
309
- deferrals remain; `unfinished` can still be true. This is not unlimited search,
310
- an exhaustiveness guarantee or a wall-clock deadline. Custom primary-reader
311
- engines must support the extended-work capability or report an error.
456
+ | Code | Meaning |
457
+ | ----------------------------------------------------------- | --------------------------------------------------------- |
458
+ | `"invalid_input"` | The engine rejected the image or options. |
459
+ | `"disposed"` | The scanner was disposed. |
460
+ | `"engine"` | Internal scanner failure. |
461
+ | `"capacity"` | Too many live scanners in this WASM instance. |
462
+ | `"abi_shape"`, `"abi_version"`, `"abi_mode"`, `"abi_turbo"` | The loaded WASM file does not match this package or mode. |
463
+ | `"invalid_output"` | The engine returned unreadable output. |
464
+ | `"core_<n>"` | An unexpected engine status `n`. |
312
465
 
313
- ## Undecoded regions
466
+ Loader and fetch errors reject `Scanner.create` and the one-shot helpers. Dispose
467
+ reusable scanners in `finally`.
468
+
469
+ ## License
314
470
 
315
- `result.undecoded` is always available, independently of `debug`. Each entry has
316
- a source-image polygon and a format hint. It is a localized proposal without an
317
- accepted decode, not proof of a real or permanently unreadable barcode. Entries
318
- can overlap or describe false candidates. An empty collection does not prove
319
- that every barcode was found. Raw candidate attempts remain in debug diagnostics.
471
+ Tapirscan is dual-licensed under **MIT OR Apache-2.0**, at your option.
472
+ See the [full license texts](https://tapirscan.f-kleinicke.de/license/).
473
+ Third-party components retain their own licenses and notices.