tapirscan 1.0.0 → 1.1.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 +173 -104
- package/dist/completion-host.d.mts +40 -0
- package/dist/completion-host.mjs +188 -0
- package/dist/detail-runtime/direct-recovery.d.mts +28 -0
- package/dist/detail-runtime/direct-recovery.mjs +142 -0
- package/dist/detail-runtime/host.d.mts +40 -0
- package/dist/detail-runtime/host.mjs +282 -0
- package/dist/detail-runtime/scanner.d.mts +12 -0
- package/dist/detail-runtime/scanner.mjs +70 -0
- package/dist/detail.d.ts +3 -4
- package/dist/detail.js +3 -6
- package/dist/index.d.ts +90 -33
- package/dist/index.js +169 -78
- package/dist/multiformat/coverage.d.ts +7 -0
- package/dist/multiformat/coverage.js +26 -0
- package/dist/multiformat/formats.d.ts +4 -2
- package/dist/multiformat/formats.js +13 -1
- package/dist/multiformat/geometry.d.ts +5 -0
- package/dist/multiformat/geometry.js +13 -3
- package/dist/multiformat/linear-duplicates.d.ts +16 -0
- package/dist/multiformat/linear-duplicates.js +163 -0
- package/dist/multiformat/scanner.d.ts +11 -2
- package/dist/multiformat/scanner.js +127 -24
- package/examples/camera.html +63 -0
- package/examples/scan-worker.mjs +20 -0
- package/examples/worker-client.mjs +63 -0
- package/package.json +7 -6
- package/wasm/{high-release-20260915.wasm → high-complete-release-20260916.wasm} +0 -0
- package/wasm/{low-release-20260915.wasm → low-complete-release-20260916.wasm} +0 -0
- package/wasm/{medium-release-20260915.wasm → medium-complete-release-20260916.wasm} +0 -0
- package/wasm/multiformat.json +2 -1
- package/wasm/multiformat.wasm +0 -0
- package/wasm/{very-high-release-20260915.wasm → very-high-complete-release-20260916.wasm} +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Tapirscan for JavaScript and TypeScript
|
|
2
2
|
|
|
3
|
+
This guide describes Tapirscan 1.1.0.
|
|
4
|
+
|
|
3
5
|
Scan image pixels in a browser or Node with the same Rust/WASM core.
|
|
4
|
-
[Quick start](#quick-start) · [WASM loading](#wasm-loading) · [Functions](#functions) · [All options](#all-options) · [Results](#results)
|
|
6
|
+
[Try the live demo](https://tapirscan.netlify.app) · [Quick start](#quick-start) · [WASM loading](#wasm-loading) · [Functions](#functions) · [All options](#all-options) · [Results](#results)
|
|
5
7
|
|
|
6
8
|
## Quick start
|
|
7
9
|
|
|
@@ -9,9 +11,9 @@ Scan image pixels in a browser or Node with the same Rust/WASM core.
|
|
|
9
11
|
npm install tapirscan
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
[
|
|
14
|
-
|
|
14
|
+
TypeScript declarations and WASM binaries are included in the
|
|
15
|
+
[npm package](https://www.npmjs.com/package/tapirscan). Your runtime must support
|
|
16
|
+
WebAssembly SIMD.
|
|
15
17
|
|
|
16
18
|
Pass a canvas's `ImageData` directly:
|
|
17
19
|
|
|
@@ -49,6 +51,11 @@ Here `image` is the `ImageData` above. `formats: "1D"` enables all supported lin
|
|
|
49
51
|
formats; additional readers are experimental. Settings also work with the helper:
|
|
50
52
|
`await scan(image, { mode: "high", formats: "1D" })`.
|
|
51
53
|
|
|
54
|
+
`formats: "retail"` selects EAN13, UPCA,
|
|
55
|
+
EAN8 and UPCE. `"common1D"` adds Code128, Code39 and ITF; `"common"` adds
|
|
56
|
+
QRCode and DataMatrix to `"common1D"`. See
|
|
57
|
+
[format presets and runtime behavior](../../docs/FORMATS.md).
|
|
58
|
+
|
|
52
59
|
## WASM loading
|
|
53
60
|
|
|
54
61
|
In Node, the default loader reads assets from the installed package. Decode your
|
|
@@ -69,17 +76,10 @@ mkdir -p public/tapirscan
|
|
|
69
76
|
cp node_modules/tapirscan/wasm/*.wasm public/tapirscan/
|
|
70
77
|
```
|
|
71
78
|
|
|
72
|
-
Then
|
|
79
|
+
Then point the scanner at that directory:
|
|
73
80
|
|
|
74
81
|
```js
|
|
75
|
-
const scanner = await Scanner.create({
|
|
76
|
-
loadWasm: async (url) => {
|
|
77
|
-
const filename = url.pathname.split("/").pop();
|
|
78
|
-
const response = await fetch(`/tapirscan/${filename}`);
|
|
79
|
-
if (!response.ok) throw new Error(`Could not load scanner: ${response.status}`);
|
|
80
|
-
return response.arrayBuffer();
|
|
81
|
-
},
|
|
82
|
-
});
|
|
82
|
+
const scanner = await Scanner.create({ wasmBaseUrl: "/tapirscan/" });
|
|
83
83
|
try {
|
|
84
84
|
console.log(scanner.scan(image).values);
|
|
85
85
|
} finally {
|
|
@@ -87,12 +87,30 @@ try {
|
|
|
87
87
|
}
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
The one-shot helper accepts the same option:
|
|
91
|
+
`await scan(image, { wasmBaseUrl: "/tapirscan/" })`.
|
|
92
|
+
`wasmBaseUrl` accepts a string or URL, with or without a trailing slash. Relative
|
|
93
|
+
URLs resolve against the page/worker URL in browsers and the package module in
|
|
94
|
+
Node; use an absolute URL for an unambiguous location. For authenticated requests
|
|
95
|
+
or custom storage, use `loadWasm: async (url) => arrayBuffer`. The callback receives
|
|
96
|
+
URLs resolved against `wasmBaseUrl` when both options are supplied. Only engines needed by the selected formats are loaded.
|
|
97
|
+
|
|
90
98
|
Use the deployed base path if your app is hosted below a subpath. Copy all current
|
|
91
|
-
WASMs: Medium/High/Very high also
|
|
99
|
+
WASMs: EAN13/UPCA scanning in Medium/High/Very high also loads the Low recovery decoder. The demo and its
|
|
92
100
|
comparison engines are not needed in your app.
|
|
93
101
|
|
|
94
102
|
## Camera and worker use
|
|
95
103
|
|
|
104
|
+
A standalone [worker client](examples/worker-client.mjs), [worker](examples/scan-worker.mjs)
|
|
105
|
+
and [camera page](examples/camera.html) are included in the package. From this
|
|
106
|
+
binding directory (or the installed package directory), run `python3 -m http.server`
|
|
107
|
+
and open `/examples/camera.html` on localhost. The example handles initialization,
|
|
108
|
+
frame ownership transfer, one frame in flight, errors and shutdown. It transfers
|
|
109
|
+
pixel buffers; callers must not reuse the transferred buffer. Worker messages
|
|
110
|
+
produce independent mutable result copies through structured cloning. Custom
|
|
111
|
+
`loadWasm` functions must be configured inside the worker; functions cannot be sent
|
|
112
|
+
in a message. Adjust the worker import and WASM asset path for your bundler.
|
|
113
|
+
|
|
96
114
|
Initialization is asynchronous; scanning is synchronous. For a responsive browser
|
|
97
115
|
UI, initialize one scanner inside a Web Worker and transfer an owned frame buffer.
|
|
98
116
|
Capture the next frame after the previous result arrives. Avoid racing scanner
|
|
@@ -100,77 +118,37 @@ initialization or modifying pixels during scanning. The [demo worker](../../demo
|
|
|
100
118
|
shows a complete integration. Camera capture belongs to your app and requires
|
|
101
119
|
HTTPS (localhost works for development).
|
|
102
120
|
|
|
103
|
-
## Moving from ZXing
|
|
104
|
-
|
|
105
|
-
For [`zxing-wasm`](https://github.com/Sec-ant/zxing-wasm), you can keep your
|
|
106
|
-
existing ImageData and replace the decoding call:
|
|
107
|
-
|
|
108
|
-
```js
|
|
109
|
-
// Before:
|
|
110
|
-
import { readBarcodes } from "zxing-wasm/reader";
|
|
111
|
-
const reads = await readBarcodes(image, { formats: ["EAN13"] });
|
|
112
|
-
const oldValues = reads.filter((read) => read.isValid).map((read) => read.text);
|
|
113
|
-
|
|
114
|
-
// After:
|
|
115
|
-
import { scan } from "tapirscan";
|
|
116
|
-
const result = await scan(image, { formats: ["EAN13"] });
|
|
117
|
-
const values = result.values;
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Here `image` is decoded ImageData. Keep an explicit format selection during
|
|
121
|
-
migration; Tapirscan defaults to EAN13. For successive frames, initialize a
|
|
122
|
-
`Scanner` once and scan inside a worker, as shown above.
|
|
123
|
-
|
|
124
|
-
| Existing ZXing integration | Tapirscan equivalent or difference |
|
|
125
|
-
| ---------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
126
|
-
| Array of reads | `result.barcodes`, or `result.values` for strings. |
|
|
127
|
-
| `read.text` | `barcode.text`. |
|
|
128
|
-
| `read.position` | `barcode.polygon` as four [x, y] corners, or `barcode.rect`. |
|
|
129
|
-
| `tryHarder`, `tryRotate`, `tryDownscale` | No direct option mapping. Select an effort mode and measure your images. |
|
|
130
|
-
| `maxNumberOfSymbols` | No arbitrary count limit; `multiple: false` selects one after the scan, without early exit. |
|
|
131
|
-
| Blob or encoded image input | Decode to ImageData or a supported byte buffer before scanning. |
|
|
132
|
-
| WASM overrides/asset paths | Use Tapirscan's `loadWasm` and packaged assets. |
|
|
133
|
-
|
|
134
|
-
[`@zxing/browser`](https://github.com/zxing-js/browser) also manages browser image
|
|
135
|
-
and video acquisition. Tapirscan's scanner accepts pixels; it does not replace
|
|
136
|
-
camera-device helpers or continuous-scan callbacks. Keep your capture loop,
|
|
137
|
-
draw frames to a canvas, and pass ImageData to a reusable scanner. Stop media
|
|
138
|
-
tracks when capture ends, and dispose the scanner when finished. See the
|
|
139
|
-
[demo](../../demo/README.md) for capture behavior.
|
|
140
|
-
|
|
141
|
-
Check supported formats and reader-specific metadata before switching. Additional
|
|
142
|
-
formats are experimental, and Tapirscan is not a drop-in replacement for every
|
|
143
|
-
ZXing package. Run both readers on representative inputs before replacing one.
|
|
144
|
-
|
|
145
121
|
## Functions
|
|
146
122
|
|
|
147
|
-
| Function | Return type
|
|
148
|
-
| ----------------------------------- |
|
|
149
|
-
| `scan(image, options = {})` | `Promise<ScanResult>`
|
|
150
|
-
| `Scanner.create(options = {})` | `Promise<Scanner>`
|
|
151
|
-
| `scanner.scan(image, options = {})` | `ScanResult`
|
|
152
|
-
| `scanner.
|
|
153
|
-
| `scanner.dispose()` | `void` | Release WASM sessions. Repeated disposal is safe; do not scan after disposal. |
|
|
123
|
+
| Function | Return type | Behavior |
|
|
124
|
+
| ----------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| `scan(image, options = {})` | `Promise<ScanResult>` | One image with automatic scanner creation and disposal, including on failure. Accepts creation and scan options together. |
|
|
126
|
+
| `Scanner.create(options = {})` | `Promise<Scanner>` | Initialize a reusable scanner. Mode is fixed; formats define defaults and allowed per-call subsets. |
|
|
127
|
+
| `scanner.scan(image, options = {})` | `ScanResult` | Synchronously scan pixels. Accepts scan options only. |
|
|
128
|
+
| `scanner.dispose()` | `void` | Release WASM sessions. Repeated disposal is safe; do not scan after disposal. |
|
|
154
129
|
|
|
155
130
|
`image` is required for either scan function. All options are optional. Reuse a
|
|
156
131
|
scanner for successive frames to avoid repeated initialization; create another
|
|
157
|
-
to change effort or formats
|
|
132
|
+
to change effort or enable formats outside its configured selection. A per-call
|
|
133
|
+
subset such as `scanner.scan(image, { formats: "EAN13" })` applies only to that
|
|
134
|
+
call and does not change the default formats. Previously returned results survive disposal. `scanner.formats` exposes the frozen creation selection.
|
|
158
135
|
|
|
159
136
|
## All options
|
|
160
137
|
|
|
161
|
-
| Option
|
|
162
|
-
|
|
|
163
|
-
| `mode`
|
|
164
|
-
| `formats`
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
|
|
170
|
-
|
|
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. |
|
|
147
|
+
|
|
148
|
+
Format presets cover supported symbologies. Exports `commonFormats`, `commonLinearFormats`, `linearFormats`, `matrixFormats`
|
|
171
149
|
and `retailFormats` let you compose custom selections; `formatBits` provides their
|
|
172
150
|
native bit mapping. See [identifiers and coverage](../../docs/FORMATS.md).
|
|
173
|
-
The four effort modes tune EAN13/UPCA;
|
|
151
|
+
The four effort modes tune EAN13/UPCA, Common1D and QR Code; other matrix readers use fixed effort.
|
|
174
152
|
|
|
175
153
|
Resolution, camera capture, preprocessing rotation, ROI, confidence thresholds,
|
|
176
154
|
timeouts and exact work budgets are not public scan options. Demo capture and
|
|
@@ -181,57 +159,133 @@ resize settings belong to the application.
|
|
|
181
159
|
`PixelImage` accepts `ImageData` (or its data/width/height fields) or an explicit
|
|
182
160
|
`Image` buffer:
|
|
183
161
|
|
|
184
|
-
| Field | Type | Meaning
|
|
185
|
-
| ----------------- | ------------- |
|
|
186
|
-
| `data` | `Uint8Array` | Decoded pixels. ImageData instead uses `Uint8ClampedArray` and implies tightly packed RGBA.
|
|
187
|
-
| `width`, `height` | `number` | Integer input dimensions, at least 3 pixels each.
|
|
188
|
-
| `channels` | `1 \| 3 \| 4` | Grayscale, RGB or RGBA. Alpha is ignored. Required for explicit buffers.
|
|
189
|
-
| `stride` | `number` | Bytes between row starts, at least width × channels.
|
|
162
|
+
| Field | Type | Meaning |
|
|
163
|
+
| ----------------- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
164
|
+
| `data` | `Uint8Array` | Decoded pixels. ImageData instead uses `Uint8ClampedArray` and implies tightly packed RGBA. |
|
|
165
|
+
| `width`, `height` | `number` | Integer input dimensions, at least 3 pixels each. |
|
|
166
|
+
| `channels` | `1 \| 3 \| 4` | Grayscale, RGB or RGBA. Alpha is ignored. Required for explicit buffers. |
|
|
167
|
+
| `stride` | `number` | Bytes between row starts, at least width × channels. Optional; defaults to width × channels. Padding is allowed. |
|
|
190
168
|
|
|
191
|
-
Input is limited to 128 MiB of addressed pixels. Keep the buffer stable during the
|
|
169
|
+
Input is limited to 32 megapixels and 128 MiB of addressed pixels. Keep the buffer stable during the
|
|
192
170
|
call. Convert DOM image elements or encoded images to pixels before scanning.
|
|
193
171
|
|
|
194
172
|
## Results
|
|
195
173
|
|
|
196
|
-
| Field
|
|
197
|
-
|
|
|
198
|
-
| `result.values`
|
|
199
|
-
| `result.barcodes`
|
|
200
|
-
| `result.best`
|
|
201
|
-
| `result.image`
|
|
202
|
-
| `result.mode`
|
|
203
|
-
| `result.elapsedMs`
|
|
204
|
-
| `result.unfinished`
|
|
205
|
-
| `result.debug`
|
|
206
|
-
| `barcode.
|
|
207
|
-
| `barcode.
|
|
208
|
-
| `barcode.
|
|
209
|
-
| `barcode.
|
|
210
|
-
|
|
211
|
-
|
|
174
|
+
| Field | Type | Meaning |
|
|
175
|
+
| ------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
176
|
+
| `result.values` | `readonly string[]` | Decoded strings. |
|
|
177
|
+
| `result.barcodes` | `readonly Barcode[]` | Decoded values with format and geometry. |
|
|
178
|
+
| `result.best` | `Barcode \| undefined` | Highest-support read, or undefined when empty. |
|
|
179
|
+
| `result.image` | `{ width: number, height: number }` | Dimensions of supplied pixels. |
|
|
180
|
+
| `result.mode` | `Mode` | Selected effort. |
|
|
181
|
+
| `result.elapsedMs` | `number` | Host scan time in milliseconds; excludes file loading and scanner initialization. |
|
|
182
|
+
| `result.unfinished` | `boolean` | Incomplete work; returned reads may still be useful. |
|
|
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. Both result arrays are empty when nothing is decoded. Use `result.best` for
|
|
198
|
+
one read, or `undefined` when empty. All decoded instances remain available,
|
|
199
|
+
including separate copies of the same value. Coordinates start at the
|
|
212
200
|
top left, x rightward and y downward. Geometry is returned, not a cropped bitmap.
|
|
213
201
|
Map coordinates back yourself if you resize/rotate before scanning. Support is a
|
|
214
202
|
ranking heuristic, not a probability.
|
|
215
203
|
|
|
216
|
-
The package exports `ScannerOptions`, `ScanOptions`, `ScanResult`, `Barcode`,
|
|
217
|
-
`PixelImage`, `Image`, `Quad`, `Mode`, `Format`, `FormatSelection`, `Diagnostics
|
|
218
|
-
and `DiagnosticBarcode` types. TypeScript infers results from calls; runtime
|
|
204
|
+
The package exports `EanAddOnPolicy`, `ScannerOptions`, `ScanOptions`, `ScanResult`, `Barcode`,
|
|
205
|
+
`PixelImage`, `Image`, `Quad`, `Mode`, `Format`, `FormatSelection`, `Diagnostics`,
|
|
206
|
+
`StructuredAppend` and `DiagnosticBarcode` types. TypeScript infers results from calls; runtime
|
|
219
207
|
checks still validate pixel buffers and dimensions.
|
|
220
208
|
|
|
209
|
+
## EAN/UPC supplements
|
|
210
|
+
|
|
211
|
+
Set `eanAddOnPolicy: "Read"` when creating a scanner or calling one-shot `scan()`.
|
|
212
|
+
The policy is fixed for that scanner; its default is `"Ignore"`.
|
|
213
|
+
|
|
214
|
+
| Policy | Behavior |
|
|
215
|
+
| ----------- | ------------------------------------------------------------------------------------------------ |
|
|
216
|
+
| `"Ignore"` | Decode the main barcode without reading its supplement. |
|
|
217
|
+
| `"Read"` | Try reading the two- or five-digit supplement; keep the main barcode if none is readable. |
|
|
218
|
+
| `"Require"` | Return an EAN/UPC barcode only when its supplement is readable. Other formats remain unaffected. |
|
|
219
|
+
|
|
220
|
+
`barcode.polygon` and `barcode.rect` describe the main barcode, excluding the
|
|
221
|
+
supplement. Supplement geometry is not exposed separately.
|
|
222
|
+
|
|
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.
|
|
227
|
+
|
|
228
|
+
## Evidence and work limits
|
|
229
|
+
|
|
230
|
+
Most applications need `barcode.text`, `.format`, `.polygon` and `.rect`.
|
|
231
|
+
`barcode.support` exposes the evidence used by `.best`. It is an uncalibrated,
|
|
232
|
+
reader-specific ranking heuristic, not a certainty percentage; values are not
|
|
233
|
+
comparable confidence across formats or effort modes. Consequently, `.best` means the largest support value,
|
|
234
|
+
not the most reliable barcode in a mixed-format image. Select by the format or
|
|
235
|
+
payload your application needs when that distinction matters. Checksums and consistency
|
|
236
|
+
checks reduce wrong reads but cannot guarantee that every returned decode is correct.
|
|
237
|
+
|
|
238
|
+
Select EAN13/UPCA, Common1D and QR Code search effort with `mode: "low"` through `"very-high"` at creation.
|
|
239
|
+
Other matrix readers use fixed effort. `result.unfinished` is available without debug and
|
|
240
|
+
combines reported decoding and localization limits. Returned reads are still usable.
|
|
241
|
+
Candidate, retry and parsing caps are reported, including bounded searches that
|
|
242
|
+
also returned reads. False does not promise exhaustive scanning. Exact budgets and interruptible timeouts are not public options.
|
|
243
|
+
|
|
244
|
+
`debug: true` adds attempted search windows, localization proposals, candidate
|
|
245
|
+
outcomes and engine traces. It is unnecessary for drawing decoded barcode locations.
|
|
246
|
+
|
|
247
|
+
### Switching between retail and QR scanning
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
const scanner = await Scanner.create({ formats: "common" });
|
|
251
|
+
try {
|
|
252
|
+
console.log(scanner.formats);
|
|
253
|
+
const retail = scanner.scan(image, { formats: "retail" });
|
|
254
|
+
const qr = scanner.scan(image, { formats: "QRCode" });
|
|
255
|
+
} finally {
|
|
256
|
+
scanner.dispose();
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`payloadBytes` is supplied by QR Code, Data Matrix, Aztec, PDF417 and MaxiCode.
|
|
261
|
+
It contains decoded data bytes before character-set interpretation, not raw symbol
|
|
262
|
+
codewords. Aztec Rune represents its numeric value as decimal ASCII. Other readers
|
|
263
|
+
leave it absent. Encoding `.text` as UTF-8 does not reconstruct original bytes.
|
|
264
|
+
Unsupported character encodings can still prevent decoding; reader behavior is
|
|
265
|
+
unchanged. The frozen number array is directly JSON-compatible.
|
|
266
|
+
|
|
221
267
|
## Diagnostics and errors
|
|
222
268
|
|
|
223
269
|
```js
|
|
224
270
|
const result = scanner.scan(image, { debug: true });
|
|
225
271
|
if (result.debug) {
|
|
226
|
-
console.log(result.debug.
|
|
272
|
+
console.log(result.debug.regions.proposals, result.debug.regions.searchWindows);
|
|
273
|
+
console.log(result.debug.regions.undecoded);
|
|
227
274
|
console.log(result.debug.scan.barcodes);
|
|
228
275
|
}
|
|
229
276
|
```
|
|
230
277
|
|
|
231
|
-
|
|
278
|
+
`debug.regions` has a stable shape across creation formats: `proposals` and
|
|
279
|
+
`searchWindows` contain evidence or null when unavailable, and `undecoded` contains
|
|
280
|
+
unread source-image geometry as immutable `UndecodedRegion` objects (`format` hint
|
|
281
|
+
and `polygon`, with no decoded text). Empty arrays mean available evidence with no entries.
|
|
282
|
+
EAN evidence remains available when a scanner also enables additional readers.
|
|
283
|
+
|
|
284
|
+
Diagnostics also retain the raw schema-2 result: `scan` includes support and candidate
|
|
232
285
|
evidence, and `localizationLimited` reports localization limits. Depending on the
|
|
233
286
|
reader, `localization`, `searchWindows`, `recovery` and `detailRegions` may be
|
|
234
|
-
present.
|
|
287
|
+
present. GS1, reader initialization and structured append are available directly on
|
|
288
|
+
barcodes without debug; raw metadata also retains these fields where supported.
|
|
235
289
|
Candidate indices inside recovery crops are local to the crop and are not
|
|
236
290
|
identifiers for tracking between frames.
|
|
237
291
|
|
|
@@ -239,3 +293,18 @@ Invalid options can raise TypeError. Scanner validation and engine failures can
|
|
|
239
293
|
raise the exported `ScannerError` with a `.code` and `.message`. Loader/fetch
|
|
240
294
|
errors propagate to the caller; creation and the one-shot helper reject their
|
|
241
295
|
promises on failure. Always dispose reusable scanners with `finally`.
|
|
296
|
+
|
|
297
|
+
## Finishing candidate work
|
|
298
|
+
|
|
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
|
+
|
|
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.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Explicit indices reject sparse arrays; nonfinite numeric geometry reaches Rust. */
|
|
2
|
+
export function isQuadShape(value: any): boolean;
|
|
3
|
+
/** Validate before asynchronous work; returned bytes are owned by the caller. */
|
|
4
|
+
export function snapshotImage(image: any): {
|
|
5
|
+
data: Uint8Array<any>;
|
|
6
|
+
width: any;
|
|
7
|
+
height: any;
|
|
8
|
+
channels: any;
|
|
9
|
+
stride: any;
|
|
10
|
+
};
|
|
11
|
+
export function snapshotPolicy(policy: any): any;
|
|
12
|
+
/** Uncalibrated support ordering; ties preserve spatial output order. */
|
|
13
|
+
export function rankBarcodes(barcodes: any): any[];
|
|
14
|
+
export class ScannerError extends Error {
|
|
15
|
+
constructor(code: any, message: any);
|
|
16
|
+
code: any;
|
|
17
|
+
}
|
|
18
|
+
export class IndependentScanner {
|
|
19
|
+
static create(bytes: any): Promise<IndependentScanner>;
|
|
20
|
+
constructor(exports: any);
|
|
21
|
+
/** All supplied regions receive the cheap pass; successful reads do not end scanning. */
|
|
22
|
+
scan(image: any, quads: any, policy?: {}): any;
|
|
23
|
+
/** Synchronous transaction: upload once, localize, decode the same owned pixels. */
|
|
24
|
+
scanLocalized(image: any, policy?: {}, fitLimit?: number, fullFrame?: boolean): {
|
|
25
|
+
localization: any;
|
|
26
|
+
searchWindows: {
|
|
27
|
+
kind: string;
|
|
28
|
+
polygon: number[][];
|
|
29
|
+
candidateIndex: any;
|
|
30
|
+
}[];
|
|
31
|
+
scan: any;
|
|
32
|
+
localizationMs: number;
|
|
33
|
+
decodingMs: number;
|
|
34
|
+
scanMs: number;
|
|
35
|
+
};
|
|
36
|
+
/** Separate convenience; it never changes find-all work or suppresses frame evidence. */
|
|
37
|
+
best(result: any): any;
|
|
38
|
+
dispose(): void;
|
|
39
|
+
#private;
|
|
40
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/** Explicit indices reject sparse arrays; nonfinite numeric geometry reaches Rust. */
|
|
2
|
+
export function isQuadShape(value) {
|
|
3
|
+
if (!Array.isArray(value) || value.length !== 4)
|
|
4
|
+
return false;
|
|
5
|
+
for (let i = 0; i < 4; i++) {
|
|
6
|
+
if (!Object.hasOwn(value, i))
|
|
7
|
+
return false;
|
|
8
|
+
const point = value[i];
|
|
9
|
+
if (!Array.isArray(point) || point.length !== 2)
|
|
10
|
+
return false;
|
|
11
|
+
for (let j = 0; j < 2; j++)
|
|
12
|
+
if (!Object.hasOwn(point, j) || typeof point[j] !== 'number')
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
export class ScannerError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code, message) { super(message); this.name = 'ScannerError'; this.code = code; }
|
|
20
|
+
}
|
|
21
|
+
function integer(value, min, max, name) {
|
|
22
|
+
if (!Number.isSafeInteger(value) || value < min || value > max)
|
|
23
|
+
throw new ScannerError('invalid_input', `Invalid ${name}`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function status(code) {
|
|
27
|
+
if (code !== 0)
|
|
28
|
+
throw new ScannerError(`core_${code}`, `Independent scanner rejected operation (${code})`);
|
|
29
|
+
}
|
|
30
|
+
function parseFrame(text, count) {
|
|
31
|
+
const v = JSON.parse(text);
|
|
32
|
+
if (!v || typeof v !== 'object')
|
|
33
|
+
throw new ScannerError('invalid_output', 'Expected frame object');
|
|
34
|
+
const f = v;
|
|
35
|
+
if (!Array.isArray(f.candidates) || f.candidates.length !== count || !Array.isArray(f.barcodes) || typeof f.unfinished !== 'boolean' || !f.reconciliation)
|
|
36
|
+
throw new ScannerError('invalid_output', 'Invalid frame shape');
|
|
37
|
+
for (let i = 0; i < f.candidates.length; i++) {
|
|
38
|
+
const c = f.candidates[i];
|
|
39
|
+
if (c.candidate_index !== i || !Array.isArray(c.coverage) || c.coverage.length !== 4 || !Array.isArray(c.observations) || !Array.isArray(c.detections) || typeof c.error !== 'boolean')
|
|
40
|
+
throw new ScannerError('invalid_output', 'Invalid candidate shape');
|
|
41
|
+
}
|
|
42
|
+
for (const b of f.barcodes)
|
|
43
|
+
if (!/^\d{13}$/.test(b.text) || !Array.isArray(b.polygon) || b.polygon.length !== 4 || !Array.isArray(b.candidate_indices) || b.candidate_indices.some(i => !Number.isInteger(i) || i < 0 || i >= count))
|
|
44
|
+
throw new ScannerError('invalid_output', 'Invalid barcode shape');
|
|
45
|
+
return f;
|
|
46
|
+
}
|
|
47
|
+
/** Validate before asynchronous work; returned bytes are owned by the caller. */
|
|
48
|
+
export function snapshotImage(image) {
|
|
49
|
+
if (!image || typeof image !== 'object')
|
|
50
|
+
throw new ScannerError('invalid_input', 'Invalid image');
|
|
51
|
+
const width = integer(image.width, 1, 0xffffffff, 'width'), height = integer(image.height, 1, 0xffffffff, 'height');
|
|
52
|
+
if (![1, 3, 4].includes(image.channels) || !(image.data instanceof Uint8Array))
|
|
53
|
+
throw new ScannerError('invalid_input', 'Invalid image buffer/channels');
|
|
54
|
+
const stride = integer(image.stride, width * image.channels, 0xffffffff, 'stride');
|
|
55
|
+
const required = integer((height - 1) * stride + width * image.channels, 1, 128 * 1024 * 1024, 'image length');
|
|
56
|
+
if (image.data.byteLength < required)
|
|
57
|
+
throw new ScannerError('invalid_input', 'Image buffer is too short');
|
|
58
|
+
return { data: new Uint8Array(image.data.subarray(0, required)), width, height, channels: image.channels, stride };
|
|
59
|
+
}
|
|
60
|
+
export function snapshotPolicy(policy) {
|
|
61
|
+
if (!policy || typeof policy !== 'object')
|
|
62
|
+
throw new ScannerError('invalid_input', 'Invalid policy');
|
|
63
|
+
const copy = { ...policy };
|
|
64
|
+
for (const key of ['transitionCleanup', 'sourceIdentity', 'interiorNormalization', 'guardBias', 'allowSingleRow'])
|
|
65
|
+
if (copy[key] !== undefined && typeof copy[key] !== 'boolean')
|
|
66
|
+
throw new ScannerError('invalid_input', `Invalid ${key}`);
|
|
67
|
+
integer(copy.maxRetryPathsPerCandidate ?? 512, 0, 4096, 'candidate budget');
|
|
68
|
+
integer(copy.maxRetryPathsPerFrame ?? 8192, 0, 65536, 'frame budget');
|
|
69
|
+
integer(copy.maxAssociationChecks ?? 200000, 0, 2000000, 'comparison budget');
|
|
70
|
+
integer(copy.maxAssociationPixels ?? 2000000, 0, 16000000, 'pixel budget');
|
|
71
|
+
integer(copy.maxResults ?? 1024, 1, 4096, 'result budget');
|
|
72
|
+
return copy;
|
|
73
|
+
}
|
|
74
|
+
/** Uncalibrated support ordering; ties preserve spatial output order. */
|
|
75
|
+
export function rankBarcodes(barcodes) {
|
|
76
|
+
return [...barcodes].sort((a, b) => b.support - a.support);
|
|
77
|
+
}
|
|
78
|
+
export class IndependentScanner {
|
|
79
|
+
#exports;
|
|
80
|
+
#handle;
|
|
81
|
+
constructor(exports) {
|
|
82
|
+
this.#exports = exports;
|
|
83
|
+
if (exports.regions_version() !== 1)
|
|
84
|
+
throw new ScannerError('abi_version', 'Unsupported scanner ABI');
|
|
85
|
+
this.#handle = exports.regions_new();
|
|
86
|
+
if (!this.#handle)
|
|
87
|
+
throw new ScannerError('capacity', 'Scanner handle capacity exhausted');
|
|
88
|
+
}
|
|
89
|
+
static async create(bytes) {
|
|
90
|
+
const module = await WebAssembly.compile(bytes);
|
|
91
|
+
const instance = await WebAssembly.instantiate(module, {});
|
|
92
|
+
const exports = instance.exports;
|
|
93
|
+
for (const name of ['regions_localize', 'regions_version', 'regions_new', 'regions_destroy', 'regions_prepare', 'regions_input_ptr', 'regions_input_len', 'regions_quads_ptr', 'regions_output_ptr', 'regions_output_len', 'regions_scan']) {
|
|
94
|
+
if (typeof instance.exports[name] !== 'function')
|
|
95
|
+
throw new ScannerError('abi_shape', `Missing ${name}`);
|
|
96
|
+
}
|
|
97
|
+
if (!(exports.memory instanceof WebAssembly.Memory))
|
|
98
|
+
throw new ScannerError('abi_shape', 'Missing memory');
|
|
99
|
+
return new IndependentScanner(exports);
|
|
100
|
+
}
|
|
101
|
+
/** All supplied regions receive the cheap pass; successful reads do not end scanning. */
|
|
102
|
+
scan(image, quads, policy = {}) {
|
|
103
|
+
return this.#scan(image, quads, policy, false);
|
|
104
|
+
}
|
|
105
|
+
/** Synchronous transaction: upload once, localize, decode the same owned pixels. */
|
|
106
|
+
scanLocalized(image, policy = {}, fitLimit = 8, fullFrame = false) {
|
|
107
|
+
const start = performance.now();
|
|
108
|
+
if (!this.#handle)
|
|
109
|
+
throw new ScannerError('disposed', 'Scanner is disposed');
|
|
110
|
+
if (!image || typeof image !== 'object')
|
|
111
|
+
throw new ScannerError('invalid_input', 'Invalid image');
|
|
112
|
+
const width = integer(image.width, 3, 0xffffffff, 'width'), height = integer(image.height, 3, 0xffffffff, 'height');
|
|
113
|
+
if (![1, 3, 4].includes(image.channels) || !(image.data instanceof Uint8Array))
|
|
114
|
+
throw new ScannerError('invalid_input', 'Invalid image buffer/channels');
|
|
115
|
+
const stride = integer(image.stride, width * image.channels, 0xffffffff, 'stride');
|
|
116
|
+
const required = integer((height - 1) * stride + width * image.channels, 1, 128 * 1024 * 1024, 'image length');
|
|
117
|
+
if (image.data.byteLength < required)
|
|
118
|
+
throw new ScannerError('invalid_input', 'Image buffer is too short');
|
|
119
|
+
integer(fitLimit, 0, 8, 'shear limit');
|
|
120
|
+
const e = this.#exports, id = this.#handle;
|
|
121
|
+
status(e.regions_prepare(id, width, height, image.channels, stride));
|
|
122
|
+
if (e.regions_input_len(id) !== required)
|
|
123
|
+
throw new ScannerError('abi_shape', 'Input allocation mismatch');
|
|
124
|
+
new Uint8Array(e.memory.buffer, e.regions_input_ptr(id), required).set(image.data.subarray(0, required));
|
|
125
|
+
status(e.regions_localize(id, fitLimit));
|
|
126
|
+
const localization = JSON.parse(new TextDecoder().decode(new Uint8Array(e.memory.buffer, e.regions_output_ptr(id), e.regions_output_len(id))));
|
|
127
|
+
if (!Array.isArray(localization.proposals) || localization.proposals.length > 32 || localization.proposals.some((p) => !isQuadShape(p.polygon)))
|
|
128
|
+
throw new ScannerError('abi_shape', 'Invalid localization');
|
|
129
|
+
const searchWindows = fullFrame ? [{ kind: 'full_frame_search', polygon: [[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], candidateIndex: localization.proposals.length }] : [];
|
|
130
|
+
const localizationMs = performance.now() - start, decodeStart = performance.now();
|
|
131
|
+
const scan = this.#scan(image, [...localization.proposals.map((p) => p.polygon), ...searchWindows.map(p => p.polygon)], policy, true);
|
|
132
|
+
return { localization, searchWindows, scan, localizationMs, decodingMs: performance.now() - decodeStart, scanMs: performance.now() - start };
|
|
133
|
+
}
|
|
134
|
+
#scan(image, quads, policy, prepared) {
|
|
135
|
+
const start = performance.now();
|
|
136
|
+
if (!this.#handle)
|
|
137
|
+
throw new ScannerError('disposed', 'Scanner is disposed');
|
|
138
|
+
if (!image || typeof image !== 'object' || !Array.isArray(quads) || !policy || typeof policy !== 'object')
|
|
139
|
+
throw new ScannerError('invalid_input', 'Invalid scan arguments');
|
|
140
|
+
for (const key of ['transitionCleanup', 'sourceIdentity', 'interiorNormalization', 'guardBias', 'allowSingleRow'])
|
|
141
|
+
if (policy[key] !== undefined && typeof policy[key] !== 'boolean')
|
|
142
|
+
throw new ScannerError('invalid_input', `Invalid ${key}`);
|
|
143
|
+
const e = this.#exports, id = this.#handle;
|
|
144
|
+
const width = integer(image.width, 1, 0xffffffff, 'width'), height = integer(image.height, 1, 0xffffffff, 'height');
|
|
145
|
+
if (![1, 3, 4].includes(image.channels) || !(image.data instanceof Uint8Array))
|
|
146
|
+
throw new ScannerError('invalid_input', 'Invalid image buffer/channels');
|
|
147
|
+
const stride = integer(image.stride, width * image.channels, 0xffffffff, 'stride');
|
|
148
|
+
const required = integer((height - 1) * stride + width * image.channels, 1, 128 * 1024 * 1024, 'image length');
|
|
149
|
+
if (image.data.byteLength < required)
|
|
150
|
+
throw new ScannerError('invalid_input', 'Image buffer is too short');
|
|
151
|
+
integer(quads.length, 0, 64, 'candidate count');
|
|
152
|
+
for (const q of quads)
|
|
153
|
+
if (!isQuadShape(q))
|
|
154
|
+
throw new ScannerError('invalid_input', 'Invalid quad shape');
|
|
155
|
+
const perCandidate = integer(policy.maxRetryPathsPerCandidate ?? 512, 0, 4096, 'candidate budget');
|
|
156
|
+
const perFrame = integer(policy.maxRetryPathsPerFrame ?? 8192, 0, 65536, 'frame budget');
|
|
157
|
+
const checks = integer(policy.maxAssociationChecks ?? 200000, 0, 2000000, 'comparison budget');
|
|
158
|
+
const pixels = integer(policy.maxAssociationPixels ?? 2000000, 0, 16000000, 'pixel budget');
|
|
159
|
+
const results = integer(policy.maxResults ?? 1024, 1, 4096, 'result budget');
|
|
160
|
+
if (policy.finishCandidates && e.regions_completion_supported?.() !== 1)
|
|
161
|
+
throw new Error("This WASM build does not support finishCandidates");
|
|
162
|
+
const flags = (policy.transitionCleanup ? 1 : 0) | (policy.sourceIdentity ? 2 : 0) | (policy.interiorNormalization ? 4 : 0) | (policy.guardBias ? 8 : 0) | (policy.allowSingleRow ? 16 : 0) | (policy.finishCandidates ? 32 : 0);
|
|
163
|
+
if (!prepared) {
|
|
164
|
+
status(e.regions_prepare(id, width, height, image.channels, stride));
|
|
165
|
+
if (e.regions_input_len(id) !== required)
|
|
166
|
+
throw new ScannerError('abi_shape', 'Input allocation mismatch');
|
|
167
|
+
new Uint8Array(e.memory.buffer, e.regions_input_ptr(id), required).set(image.data.subarray(0, required));
|
|
168
|
+
}
|
|
169
|
+
const coordinates = new Float64Array(e.memory.buffer, e.regions_quads_ptr(id), 512);
|
|
170
|
+
quads.forEach((q, i) => q.forEach((p, j) => { coordinates[i * 8 + j * 2] = p[0]; coordinates[i * 8 + j * 2 + 1] = p[1]; }));
|
|
171
|
+
status(e.regions_scan(id, quads.length, flags, perCandidate, perFrame, checks, pixels, results));
|
|
172
|
+
// Scan can grow memory. Never reuse the earlier input/coordinate views.
|
|
173
|
+
const output = new Uint8Array(e.memory.buffer, e.regions_output_ptr(id), e.regions_output_len(id)).slice();
|
|
174
|
+
const frame = parseFrame(new TextDecoder().decode(output), quads.length);
|
|
175
|
+
return { ...frame, candidateTimingsAvailable: false, elapsedMs: performance.now() - start };
|
|
176
|
+
}
|
|
177
|
+
/** Separate convenience; it never changes find-all work or suppresses frame evidence. */
|
|
178
|
+
best(result) {
|
|
179
|
+
return rankBarcodes(result.barcodes)[0];
|
|
180
|
+
}
|
|
181
|
+
dispose() {
|
|
182
|
+
if (this.#handle) {
|
|
183
|
+
const id = this.#handle;
|
|
184
|
+
this.#handle = 0;
|
|
185
|
+
status(this.#exports.regions_destroy(id));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|