tapirscan 1.0.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 +184 -105
- 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 +91 -33
- package/dist/index.js +174 -86
- 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,12 @@
|
|
|
1
1
|
# Tapirscan for JavaScript and TypeScript
|
|
2
2
|
|
|
3
|
+
This guide describes the 1.2.0 API revision. See [migration](../../docs/API_MIGRATION.md).
|
|
4
|
+
Build/install this checkout using [the development guide](../../docs/DEVELOPMENT.md)
|
|
5
|
+
to use these changes before publication; older registry packages use their own
|
|
6
|
+
versioned API.
|
|
7
|
+
|
|
3
8
|
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)
|
|
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)
|
|
5
10
|
|
|
6
11
|
## Quick start
|
|
7
12
|
|
|
@@ -9,9 +14,9 @@ Scan image pixels in a browser or Node with the same Rust/WASM core.
|
|
|
9
14
|
npm install tapirscan
|
|
10
15
|
```
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
[
|
|
14
|
-
|
|
17
|
+
TypeScript declarations and WASM binaries are included in the
|
|
18
|
+
[npm package](https://www.npmjs.com/package/tapirscan). Your runtime must support
|
|
19
|
+
WebAssembly SIMD.
|
|
15
20
|
|
|
16
21
|
Pass a canvas's `ImageData` directly:
|
|
17
22
|
|
|
@@ -46,9 +51,14 @@ try {
|
|
|
46
51
|
```
|
|
47
52
|
|
|
48
53
|
Here `image` is the `ImageData` above. `formats: "1D"` enables all supported linear
|
|
49
|
-
formats;
|
|
54
|
+
formats; readers outside the retail group remain experimental. Settings also work with the helper:
|
|
50
55
|
`await scan(image, { mode: "high", formats: "1D" })`.
|
|
51
56
|
|
|
57
|
+
`formats: "retail"` selects EAN13, UPCA,
|
|
58
|
+
EAN8 and UPCE. `"common1D"` adds Code128, Code39 and ITF; `"common"` adds
|
|
59
|
+
QRCode and DataMatrix to `"common1D"`. See
|
|
60
|
+
[format presets and runtime behavior](../../docs/FORMATS.md).
|
|
61
|
+
|
|
52
62
|
## WASM loading
|
|
53
63
|
|
|
54
64
|
In Node, the default loader reads assets from the installed package. Decode your
|
|
@@ -69,17 +79,10 @@ mkdir -p public/tapirscan
|
|
|
69
79
|
cp node_modules/tapirscan/wasm/*.wasm public/tapirscan/
|
|
70
80
|
```
|
|
71
81
|
|
|
72
|
-
Then
|
|
82
|
+
Then point the scanner at that directory:
|
|
73
83
|
|
|
74
84
|
```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
|
-
});
|
|
85
|
+
const scanner = await Scanner.create({ wasmBaseUrl: "/tapirscan/" });
|
|
83
86
|
try {
|
|
84
87
|
console.log(scanner.scan(image).values);
|
|
85
88
|
} finally {
|
|
@@ -87,12 +90,30 @@ try {
|
|
|
87
90
|
}
|
|
88
91
|
```
|
|
89
92
|
|
|
93
|
+
The one-shot helper accepts the same option:
|
|
94
|
+
`await scan(image, { wasmBaseUrl: "/tapirscan/" })`.
|
|
95
|
+
`wasmBaseUrl` accepts a string or URL, with or without a trailing slash. Relative
|
|
96
|
+
URLs resolve against the page/worker URL in browsers and the package module in
|
|
97
|
+
Node; use an absolute URL for an unambiguous location. For authenticated requests
|
|
98
|
+
or custom storage, use `loadWasm: async (url) => arrayBuffer`. The callback receives
|
|
99
|
+
URLs resolved against `wasmBaseUrl` when both options are supplied. Only engines needed by the selected formats are loaded.
|
|
100
|
+
|
|
90
101
|
Use the deployed base path if your app is hosted below a subpath. Copy all current
|
|
91
|
-
WASMs: Medium/High/Very high also
|
|
102
|
+
WASMs: EAN13/UPCA scanning in Medium/High/Very high also loads the Low recovery decoder. The demo and its
|
|
92
103
|
comparison engines are not needed in your app.
|
|
93
104
|
|
|
94
105
|
## Camera and worker use
|
|
95
106
|
|
|
107
|
+
A standalone [worker client](examples/worker-client.mjs), [worker](examples/scan-worker.mjs)
|
|
108
|
+
and [camera page](examples/camera.html) are included in the package. From this
|
|
109
|
+
binding directory (or the installed package directory), run `python3 -m http.server`
|
|
110
|
+
and open `/examples/camera.html` on localhost. The example handles initialization,
|
|
111
|
+
frame ownership transfer, one frame in flight, errors and shutdown. It transfers
|
|
112
|
+
pixel buffers; callers must not reuse the transferred buffer. Worker messages
|
|
113
|
+
produce independent mutable result copies through structured cloning. Custom
|
|
114
|
+
`loadWasm` functions must be configured inside the worker; functions cannot be sent
|
|
115
|
+
in a message. Adjust the worker import and WASM asset path for your bundler.
|
|
116
|
+
|
|
96
117
|
Initialization is asynchronous; scanning is synchronous. For a responsive browser
|
|
97
118
|
UI, initialize one scanner inside a Web Worker and transfer an owned frame buffer.
|
|
98
119
|
Capture the next frame after the previous result arrives. Avoid racing scanner
|
|
@@ -100,77 +121,37 @@ initialization or modifying pixels during scanning. The [demo worker](../../demo
|
|
|
100
121
|
shows a complete integration. Camera capture belongs to your app and requires
|
|
101
122
|
HTTPS (localhost works for development).
|
|
102
123
|
|
|
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
124
|
## Functions
|
|
146
125
|
|
|
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. |
|
|
126
|
+
| Function | Return type | Behavior |
|
|
127
|
+
| ----------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
128
|
+
| `scan(image, options = {})` | `Promise<ScanResult>` | One image with automatic scanner creation and disposal, including on failure. Accepts creation and scan options together. |
|
|
129
|
+
| `Scanner.create(options = {})` | `Promise<Scanner>` | Initialize a reusable scanner. Mode is fixed; formats define defaults and allowed per-call subsets. |
|
|
130
|
+
| `scanner.scan(image, options = {})` | `ScanResult` | Synchronously scan pixels. Accepts scan options only. |
|
|
131
|
+
| `scanner.dispose()` | `void` | Release WASM sessions. Repeated disposal is safe; do not scan after disposal. |
|
|
154
132
|
|
|
155
133
|
`image` is required for either scan function. All options are optional. Reuse a
|
|
156
134
|
scanner for successive frames to avoid repeated initialization; create another
|
|
157
|
-
to change effort or formats
|
|
135
|
+
to change effort or enable formats outside its configured selection. A per-call
|
|
136
|
+
subset such as `scanner.scan(image, { formats: "EAN13" })` applies only to that
|
|
137
|
+
call and does not change the default formats. Previously returned results survive disposal. `scanner.formats` exposes the frozen creation selection.
|
|
158
138
|
|
|
159
139
|
## All options
|
|
160
140
|
|
|
161
|
-
| Option | Where
|
|
162
|
-
| ---------------- |
|
|
163
|
-
| `mode` | Creation
|
|
164
|
-
| `formats` | Creation | `["EAN13"]` | `"1D"`, `"2D"`, `"all"`, or a nonempty array of
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
|
|
170
|
-
|
|
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. |
|
|
150
|
+
|
|
151
|
+
Format presets cover supported symbologies. Exports `commonFormats`, `commonLinearFormats`, `linearFormats`, `matrixFormats`
|
|
171
152
|
and `retailFormats` let you compose custom selections; `formatBits` provides their
|
|
172
153
|
native bit mapping. See [identifiers and coverage](../../docs/FORMATS.md).
|
|
173
|
-
The four effort modes tune EAN13/UPCA;
|
|
154
|
+
The four effort modes tune EAN13/UPCA, Common1D and QR Code; other matrix readers use fixed effort.
|
|
174
155
|
|
|
175
156
|
Resolution, camera capture, preprocessing rotation, ROI, confidence thresholds,
|
|
176
157
|
timeouts and exact work budgets are not public scan options. Demo capture and
|
|
@@ -181,57 +162,135 @@ resize settings belong to the application.
|
|
|
181
162
|
`PixelImage` accepts `ImageData` (or its data/width/height fields) or an explicit
|
|
182
163
|
`Image` buffer:
|
|
183
164
|
|
|
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.
|
|
165
|
+
| Field | Type | Meaning |
|
|
166
|
+
| ----------------- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
167
|
+
| `data` | `Uint8Array` | Decoded pixels. ImageData instead uses `Uint8ClampedArray` and implies tightly packed RGBA. |
|
|
168
|
+
| `width`, `height` | `number` | Integer input dimensions, at least 3 pixels each. |
|
|
169
|
+
| `channels` | `1 \| 3 \| 4` | Grayscale, RGB or RGBA. Alpha is ignored. Required for explicit buffers. |
|
|
170
|
+
| `stride` | `number` | Bytes between row starts, at least width × channels. Optional; defaults to width × channels. Padding is allowed. |
|
|
190
171
|
|
|
191
|
-
Input is limited to 128 MiB of addressed pixels. Keep the buffer stable during the
|
|
172
|
+
Input is limited to 32 megapixels and 128 MiB of addressed pixels. Keep the buffer stable during the
|
|
192
173
|
call. Convert DOM image elements or encoded images to pixels before scanning.
|
|
193
174
|
|
|
194
175
|
## Results
|
|
195
176
|
|
|
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.
|
|
206
|
-
| `
|
|
207
|
-
| `barcode.
|
|
208
|
-
| `barcode.
|
|
209
|
-
| `barcode.
|
|
210
|
-
|
|
211
|
-
|
|
177
|
+
| Field | Type | Meaning |
|
|
178
|
+
| ------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
179
|
+
| `result.values` | `readonly string[]` | Decoded strings. |
|
|
180
|
+
| `result.barcodes` | `readonly Barcode[]` | Decoded values with format and geometry. |
|
|
181
|
+
| `result.best` | `Barcode \| undefined` | Highest-support read, or undefined when empty. |
|
|
182
|
+
| `result.image` | `{ width: number, height: number }` | Dimensions of supplied pixels. |
|
|
183
|
+
| `result.mode` | `Mode` | Selected effort. |
|
|
184
|
+
| `result.elapsedMs` | `number` | Host scan time in milliseconds; excludes file loading and scanner initialization. |
|
|
185
|
+
| `result.unfinished` | `boolean` | Incomplete work; returned reads may still be useful. |
|
|
186
|
+
| `result.undecoded` | `readonly UndecodedRegion[]` | Localized proposals without accepted decodes; always available. |
|
|
187
|
+
| `result.debug` | `Diagnostics \| undefined` | Requested diagnostic evidence; absent by default. |
|
|
188
|
+
| `barcode.payloadBytes` | `readonly number[] \| undefined` | Original decoded matrix payload bytes when available; use `Uint8Array.from(...)` for an owned byte buffer. |
|
|
189
|
+
| `barcode.text` | `string` | Decoded text. |
|
|
190
|
+
| `barcode.format` | `Format \| "Unknown"` | Symbology identifier. |
|
|
191
|
+
| `barcode.polygon` | `Quad` | Four `[x, y]` corners in input-image coordinates. |
|
|
192
|
+
| `barcode.rect` | `{ left: number, top: number, width: number, height: number }` | Enclosing integer rectangle. |
|
|
193
|
+
| `barcode.support` | `number` | Reader-specific ranking evidence; not confidence or a probability. |
|
|
194
|
+
| `barcode.gs1` | `boolean \| undefined` | GS1 indicator when supplied by the reader. |
|
|
195
|
+
| `barcode.readerInitialization` | `boolean \| undefined` | Reader initialization data indicator; never executed. |
|
|
196
|
+
| `barcode.structuredAppend` | `StructuredAppend \| undefined` | Immutable multipart metadata: one-based `index`, `count`, optional `id` and `parity`. |
|
|
197
|
+
| `barcode.eanAddOn` | `string \| undefined` | Optional EAN supplement; populated when `eanAddOnPolicy` is `"Read"` or `"Require"`. |
|
|
198
|
+
|
|
199
|
+
Results, including nested geometry and requested diagnostics, are immutable at
|
|
200
|
+
runtime and in TypeScript. Use `structuredClone(result)` if you need a mutable
|
|
201
|
+
copy. `barcodes` and `values` are empty when nothing is decoded; `undecoded` may still
|
|
202
|
+
contain proposals. Use `result.best` for
|
|
203
|
+
one read, or `undefined` when empty. All decoded instances remain available,
|
|
204
|
+
including separate copies of the same value. Coordinates start at the
|
|
212
205
|
top left, x rightward and y downward. Geometry is returned, not a cropped bitmap.
|
|
213
206
|
Map coordinates back yourself if you resize/rotate before scanning. Support is a
|
|
214
207
|
ranking heuristic, not a probability.
|
|
215
208
|
|
|
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
|
|
209
|
+
The package exports `EanAddOnPolicy`, `ScannerOptions`, `ScanOptions`, `ScanResult`, `Barcode`,
|
|
210
|
+
`PixelImage`, `Image`, `Quad`, `Mode`, `Format`, `FormatSelection`, `Diagnostics`,
|
|
211
|
+
`StructuredAppend` and `DiagnosticBarcode` types. TypeScript infers results from calls; runtime
|
|
219
212
|
checks still validate pixel buffers and dimensions.
|
|
220
213
|
|
|
214
|
+
## EAN/UPC supplements
|
|
215
|
+
|
|
216
|
+
Set `eanAddOnPolicy: "Read"` when creating a scanner or calling one-shot `scan()`.
|
|
217
|
+
The policy is fixed for that scanner; its default is `"Ignore"`.
|
|
218
|
+
|
|
219
|
+
| Policy | Behavior |
|
|
220
|
+
| ----------- | ------------------------------------------------------------------------------------------------ |
|
|
221
|
+
| `"Ignore"` | Decode the main barcode without reading its supplement. |
|
|
222
|
+
| `"Read"` | Try reading the two- or five-digit supplement; keep the main barcode if none is readable. |
|
|
223
|
+
| `"Require"` | Return an EAN/UPC barcode only when its supplement is readable. Other formats remain unaffected. |
|
|
224
|
+
|
|
225
|
+
`barcode.polygon` and `barcode.rect` describe the main barcode, excluding the
|
|
226
|
+
supplement. Supplement geometry is not exposed separately.
|
|
227
|
+
|
|
228
|
+
The supplement appears separately in `barcode.eanAddOn`; `barcode.text` remains
|
|
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`.
|
|
232
|
+
|
|
233
|
+
## Evidence and work limits
|
|
234
|
+
|
|
235
|
+
Most applications need `barcode.text`, `.format`, `.polygon` and `.rect`.
|
|
236
|
+
`barcode.support` exposes the evidence used by `.best`. It is an uncalibrated,
|
|
237
|
+
reader-specific ranking heuristic, not a certainty percentage; values are not
|
|
238
|
+
comparable confidence across formats or effort modes. Consequently, `.best` means the largest support value,
|
|
239
|
+
not the most reliable barcode in a mixed-format image. Select by the format or
|
|
240
|
+
payload your application needs when that distinction matters. Checksums and consistency
|
|
241
|
+
checks reduce wrong reads but cannot guarantee that every returned decode is correct.
|
|
242
|
+
|
|
243
|
+
Select EAN13/UPCA, Common1D and QR Code search effort with `mode: "low"` through `"very-high"` at creation.
|
|
244
|
+
Other matrix readers use fixed effort. `result.unfinished` is available without debug and
|
|
245
|
+
combines reported decoding and localization limits. Returned reads are still usable.
|
|
246
|
+
Candidate, retry and parsing caps are reported, including bounded searches that
|
|
247
|
+
also returned reads. False does not promise exhaustive scanning. Exact budgets and interruptible timeouts are not public options.
|
|
248
|
+
|
|
249
|
+
`debug: true` adds attempted search windows, localization proposals, candidate
|
|
250
|
+
outcomes and engine traces. It is unnecessary for drawing decoded barcode locations.
|
|
251
|
+
|
|
252
|
+
### Switching between retail and QR scanning
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
const scanner = await Scanner.create({ formats: "common" });
|
|
256
|
+
try {
|
|
257
|
+
console.log(scanner.formats);
|
|
258
|
+
const retail = scanner.scan(image, { formats: "retail" });
|
|
259
|
+
const qr = scanner.scan(image, { formats: "QRCode" });
|
|
260
|
+
} finally {
|
|
261
|
+
scanner.dispose();
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`payloadBytes` is supplied by QR Code, Data Matrix, Aztec, PDF417 and MaxiCode.
|
|
266
|
+
It contains decoded data bytes before character-set interpretation, not raw symbol
|
|
267
|
+
codewords. Aztec Rune represents its numeric value as decimal ASCII. Other readers
|
|
268
|
+
leave it absent. Encoding `.text` as UTF-8 does not reconstruct original bytes.
|
|
269
|
+
Unsupported character encodings can still prevent decoding; reader behavior is
|
|
270
|
+
unchanged. The frozen number array is directly JSON-compatible.
|
|
271
|
+
|
|
221
272
|
## Diagnostics and errors
|
|
222
273
|
|
|
223
274
|
```js
|
|
224
275
|
const result = scanner.scan(image, { debug: true });
|
|
225
276
|
if (result.debug) {
|
|
226
|
-
console.log(result.debug.
|
|
277
|
+
console.log(result.debug.regions.proposals, result.debug.regions.searchWindows);
|
|
278
|
+
console.log(result.debug.regions.undecoded);
|
|
227
279
|
console.log(result.debug.scan.barcodes);
|
|
228
280
|
}
|
|
229
281
|
```
|
|
230
282
|
|
|
231
|
-
|
|
283
|
+
`debug.regions` has a stable shape across creation formats: `proposals` and
|
|
284
|
+
`searchWindows` contain evidence or null when unavailable, and `undecoded` contains
|
|
285
|
+
unread source-image geometry as immutable `UndecodedRegion` objects (`format` hint
|
|
286
|
+
and `polygon`, with no decoded text). Empty arrays mean available evidence with no entries.
|
|
287
|
+
EAN evidence remains available when a scanner also enables additional readers.
|
|
288
|
+
|
|
289
|
+
Diagnostics also retain the raw schema-2 result: `scan` includes support and candidate
|
|
232
290
|
evidence, and `localizationLimited` reports localization limits. Depending on the
|
|
233
291
|
reader, `localization`, `searchWindows`, `recovery` and `detailRegions` may be
|
|
234
|
-
present.
|
|
292
|
+
present. GS1, reader initialization and structured append are available directly on
|
|
293
|
+
barcodes without debug; raw metadata also retains these fields where supported.
|
|
235
294
|
Candidate indices inside recovery crops are local to the crop and are not
|
|
236
295
|
identifiers for tracking between frames.
|
|
237
296
|
|
|
@@ -239,3 +298,23 @@ Invalid options can raise TypeError. Scanner validation and engine failures can
|
|
|
239
298
|
raise the exported `ScannerError` with a `.code` and `.message`. Loader/fetch
|
|
240
299
|
errors propagate to the caller; creation and the one-shot helper reject their
|
|
241
300
|
promises on failure. Always dispose reusable scanners with `finally`.
|
|
301
|
+
|
|
302
|
+
## Extended work budget
|
|
303
|
+
|
|
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.
|
|
307
|
+
|
|
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.
|
|
@@ -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
|
+
}
|