@gueshbzh-perso/scanner 0.9.0 → 0.11.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 +17 -2
- package/dist/core.d.ts +12 -4
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +24 -11
- package/dist/core.js.map +1 -1
- package/dist/engine.d.ts +13 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +24 -4
- package/dist/engine.js.map +1 -1
- package/dist/native.d.ts +48 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +114 -0
- package/dist/native.js.map +1 -0
- package/dist/reader.d.ts +43 -1
- package/dist/reader.d.ts.map +1 -1
- package/dist/reader.js +211 -30
- package/dist/reader.js.map +1 -1
- package/dist/types.d.ts +31 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/wasm/scanner_wasm.d.ts +20 -0
- package/dist/wasm/scanner_wasm.js +53 -0
- package/dist/wasm/scanner_wasm_bg.wasm +0 -0
- package/dist/wasm/scanner_wasm_bg.wasm.d.ts +2 -0
- package/package.json +1 -1
- package/src/core.ts +24 -11
- package/src/engine.ts +43 -5
- package/src/native.ts +147 -0
- package/src/reader.ts +231 -34
- package/src/types.ts +31 -2
|
@@ -16,10 +16,12 @@ export const wasmscanner_scanRgb: (a: number, b: number, c: number, d: number, e
|
|
|
16
16
|
export const wasmscanner_scanRgba: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
|
17
17
|
export const wasmscansession_clearCooldown: (a: number, b: number, c: number) => void;
|
|
18
18
|
export const wasmscansession_commit: (a: number, b: number) => number;
|
|
19
|
+
export const wasmscansession_commitExternal: (a: number, b: number, c: number, d: number) => void;
|
|
19
20
|
export const wasmscansession_frameBuffer: (a: number, b: number) => number;
|
|
20
21
|
export const wasmscansession_lastDetections: (a: number) => number;
|
|
21
22
|
export const wasmscansession_new: (a: number) => number;
|
|
22
23
|
export const wasmscansession_plan: (a: number) => number;
|
|
24
|
+
export const wasmscansession_planFallbackOnly: (a: number) => number;
|
|
23
25
|
export const wasmscansession_processFrameBuffer: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
|
24
26
|
export const wasmscansession_processFrameLuma: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
25
27
|
export const wasmscansession_processFrameRgba: (a: number, b: number, c: number, d: number, e: number) => number;
|
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// - `SessionCore`: the session. Cheap calls (`plan`, `commit`), main thread.
|
|
7
7
|
// - `DecoderCore`: the expensive decoding, one per Web Worker.
|
|
8
8
|
// The inline engine (no Worker support) uses both on the main thread.
|
|
9
|
+
// Codes read by the browser's native `BarcodeDetector` enter the session with
|
|
10
|
+
// `SessionCore.commitExternal` (see `native.ts`).
|
|
9
11
|
import init, { WasmFrameDecoder, WasmScanner, WasmScanSession, version } from './wasm/scanner_wasm.js';
|
|
10
12
|
import type { FramePlan, FrameReport, PixelFormat, ScanResult, WasmScanConfig } from './wasm/scanner_wasm.js';
|
|
11
13
|
import type { Detection, EngineOptions } from './types.js';
|
|
@@ -72,6 +74,12 @@ export function toDetection(r: ScanResult): Detection {
|
|
|
72
74
|
return d;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
function toScanResult(d: Detection): ScanResult {
|
|
78
|
+
const r: ScanResult = { text: d.text, format: d.format, points: d.points, timestampMs: d.timestamp };
|
|
79
|
+
if (d.rawBytes) r.rawBytes = d.rawBytes;
|
|
80
|
+
return r;
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
export interface FrameOutcome {
|
|
76
84
|
/** New codes, or `null`. */
|
|
77
85
|
results: Detection[] | null;
|
|
@@ -89,9 +97,14 @@ export class SessionCore {
|
|
|
89
97
|
this.session = new WasmScanSession(streamConfig(options));
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Strategy plan of the next frame, for a `DecoderCore` configured the same
|
|
102
|
+
* way. `fastPath: false` when the native `BarcodeDetector` already searched
|
|
103
|
+
* the frame: the decoder then runs the fallback strategy only.
|
|
104
|
+
*/
|
|
105
|
+
plan(fastPath = true): FramePlan {
|
|
106
|
+
const session = this.active();
|
|
107
|
+
return fastPath ? session.plan() : session.planFallbackOnly();
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
/** Record a decoded frame; returns the new codes and the live ones. */
|
|
@@ -102,16 +115,16 @@ export class SessionCore {
|
|
|
102
115
|
return { results, live: results ?? session.lastDetections.map(toDetection), decodeMs: report.elapsedMs };
|
|
103
116
|
}
|
|
104
117
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Record codes read by the native `BarcodeDetector`: same format filter,
|
|
120
|
+
* confirmation and de-duplication as decoded frames. Points are kept as
|
|
121
|
+
* given (video pixels).
|
|
122
|
+
*/
|
|
123
|
+
commitExternal(codes: Detection[], elapsedMs: number): FrameOutcome {
|
|
107
124
|
const session = this.active();
|
|
108
|
-
|
|
109
|
-
session.frameBuffer(pixels.length).set(pixels);
|
|
110
|
-
const t0 = performance.now();
|
|
111
|
-
const fresh = session.processFrameBuffer(width, height, format);
|
|
112
|
-
const decodeMs = performance.now() - t0;
|
|
125
|
+
const fresh = session.commitExternal(codes.map(toScanResult), elapsedMs);
|
|
113
126
|
const results = fresh ? fresh.map(toDetection) : null;
|
|
114
|
-
return { results, live: results ?? session.lastDetections.map(toDetection), decodeMs };
|
|
127
|
+
return { results, live: results ?? session.lastDetections.map(toDetection), decodeMs: elapsedMs };
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
reset(): void {
|
package/src/engine.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// with many slow cores (Galaxy A14: decode ~125 ms) the pool multiplies the
|
|
10
10
|
// frame rate.
|
|
11
11
|
import { DecoderCore, SessionCore, compileWasm, defaultWasmUrl, loadWasm } from './core.js';
|
|
12
|
-
import type { FramePlan, FrameReport } from './core.js';
|
|
12
|
+
import type { FrameOutcome, FramePlan, FrameReport } from './core.js';
|
|
13
13
|
import type { WorkerRequest, WorkerResponse } from './protocol.js';
|
|
14
14
|
import type { PixelFormat } from './wasm/scanner_wasm.js';
|
|
15
15
|
import type { AssetOptions, Detection, EngineOptions } from './types.js';
|
|
@@ -28,8 +28,24 @@ export interface Engine {
|
|
|
28
28
|
/** Frames that can be decoded at the same time (number of workers). */
|
|
29
29
|
readonly concurrency: number;
|
|
30
30
|
configure(options: EngineOptions): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Decode a camera frame. `buffer` is transferred and returned in the reply.
|
|
33
|
+
* `fastPath: false` runs the fallback strategy only, for a frame the
|
|
34
|
+
* native `BarcodeDetector` already searched without finding anything.
|
|
35
|
+
*/
|
|
36
|
+
processFrame(
|
|
37
|
+
buffer: ArrayBuffer,
|
|
38
|
+
width: number,
|
|
39
|
+
height: number,
|
|
40
|
+
format: PixelFormat,
|
|
41
|
+
fastPath?: boolean,
|
|
42
|
+
): Promise<FrameReply>;
|
|
43
|
+
/**
|
|
44
|
+
* Record codes read by the native `BarcodeDetector` in a frame: same
|
|
45
|
+
* format filter, multi-frame confirmation and de-duplication as decoded
|
|
46
|
+
* frames. Points are returned as given.
|
|
47
|
+
*/
|
|
48
|
+
commitExternal(codes: Detection[], elapsedMs: number): Promise<FrameOutcome>;
|
|
33
49
|
/** One-shot scan of a still image, running every strategy. */
|
|
34
50
|
scanImage(
|
|
35
51
|
buffer: ArrayBuffer,
|
|
@@ -186,10 +202,11 @@ class WorkerPoolEngine implements Engine {
|
|
|
186
202
|
width: number,
|
|
187
203
|
height: number,
|
|
188
204
|
format: PixelFormat,
|
|
205
|
+
fastPath = true,
|
|
189
206
|
): Promise<FrameReply> {
|
|
190
207
|
if (!this.configured) throw new Error('Engine not configured');
|
|
191
208
|
const generation = this.generation;
|
|
192
|
-
const plan = this.session.plan();
|
|
209
|
+
const plan = this.session.plan(fastPath);
|
|
193
210
|
let res: OkResponse;
|
|
194
211
|
try {
|
|
195
212
|
res = await this.idleWorker().ok({ type: 'decode', buffer, width, height, format, plan }, [buffer]);
|
|
@@ -205,6 +222,11 @@ class WorkerPoolEngine implements Engine {
|
|
|
205
222
|
return { ...this.session.commit(res.report), buffer: returned };
|
|
206
223
|
}
|
|
207
224
|
|
|
225
|
+
async commitExternal(codes: Detection[], elapsedMs: number) {
|
|
226
|
+
if (!this.configured) throw new Error('Engine not configured');
|
|
227
|
+
return this.session.commitExternal(codes, elapsedMs);
|
|
228
|
+
}
|
|
229
|
+
|
|
208
230
|
async scanImage(
|
|
209
231
|
buffer: ArrayBuffer,
|
|
210
232
|
width: number,
|
|
@@ -265,7 +287,10 @@ class InlineEngine implements Engine {
|
|
|
265
287
|
constructor(readonly version: string) {}
|
|
266
288
|
|
|
267
289
|
async configure(options: EngineOptions) {
|
|
290
|
+
// Same configuration for both: a plan is an index in the decoder's list
|
|
291
|
+
// of fallback strategies (as with workers).
|
|
268
292
|
this.session.configure(options);
|
|
293
|
+
this.decoder.configure(options);
|
|
269
294
|
}
|
|
270
295
|
|
|
271
296
|
async processFrame(
|
|
@@ -273,8 +298,21 @@ class InlineEngine implements Engine {
|
|
|
273
298
|
width: number,
|
|
274
299
|
height: number,
|
|
275
300
|
format: PixelFormat,
|
|
301
|
+
fastPath = true,
|
|
276
302
|
): Promise<FrameReply> {
|
|
277
|
-
|
|
303
|
+
const plan = this.session.plan(fastPath);
|
|
304
|
+
let report: FrameReport;
|
|
305
|
+
try {
|
|
306
|
+
report = this.decoder.decode(new Uint8Array(buffer), width, height, format, plan);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
this.session.commit(emptyReport(plan));
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
return { ...this.session.commit(report), buffer };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async commitExternal(codes: Detection[], elapsedMs: number) {
|
|
315
|
+
return this.session.commitExternal(codes, elapsedMs);
|
|
278
316
|
}
|
|
279
317
|
|
|
280
318
|
async scanImage(
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The browser's native barcode decoder (`BarcodeDetector`, Shape Detection API).
|
|
2
|
+
//
|
|
3
|
+
// Available in Chrome on Android (Google Play Services), macOS (Apple Vision)
|
|
4
|
+
// and ChromeOS; not in Safari, Firefox, nor Chrome on Windows / Linux. Where it
|
|
5
|
+
// exists it decodes outside the page's threads, so it can replace the engine's
|
|
6
|
+
// **fast path**: the engine then only runs its fallback strategies (Linear,
|
|
7
|
+
// CLAHE, Sauvola, inverted…) on the frames the native decoder found nothing
|
|
8
|
+
// in. The native decoder has no such preprocessing, and those strategies are
|
|
9
|
+
// what reads the hard codes (laser-engraved tags, labels in shadow).
|
|
10
|
+
import type { Rect } from './grabber.js';
|
|
11
|
+
import type { BarcodeFormat, Detection, Point } from './types.js';
|
|
12
|
+
|
|
13
|
+
// The API is not in TypeScript's `lib.dom` yet: minimal local declarations.
|
|
14
|
+
|
|
15
|
+
/** A code found by `BarcodeDetector.detect()`. */
|
|
16
|
+
export interface NativeBarcode {
|
|
17
|
+
rawValue: string;
|
|
18
|
+
format: string;
|
|
19
|
+
/** Corners in pixels of the source (for a `<video>`: `videoWidth` space). */
|
|
20
|
+
cornerPoints: readonly Point[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface BarcodeDetectorInstance {
|
|
24
|
+
detect(source: CanvasImageSource): Promise<NativeBarcode[]>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The `BarcodeDetector` constructor. */
|
|
28
|
+
export interface BarcodeDetectorClass {
|
|
29
|
+
new (options?: { formats?: string[] }): BarcodeDetectorInstance;
|
|
30
|
+
getSupportedFormats(): Promise<string[]>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Formats of this SDK, all spelled like the native ones (`qr_code`, `ean_13`…)
|
|
35
|
+
* except `micro_qr`, which the native API does not have. With the engine
|
|
36
|
+
* fallback it is still read by the fallback strategies (`Hard` decodes the
|
|
37
|
+
* raw frame with every format); without it, see `NativeDetector.create`.
|
|
38
|
+
*/
|
|
39
|
+
const NATIVE_FORMATS: readonly BarcodeFormat[] = [
|
|
40
|
+
'qr_code',
|
|
41
|
+
'data_matrix',
|
|
42
|
+
'aztec',
|
|
43
|
+
'pdf417',
|
|
44
|
+
'code_128',
|
|
45
|
+
'code_39',
|
|
46
|
+
'code_93',
|
|
47
|
+
'ean_13',
|
|
48
|
+
'ean_8',
|
|
49
|
+
'upc_a',
|
|
50
|
+
'upc_e',
|
|
51
|
+
'itf',
|
|
52
|
+
'codabar',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** Native decoder serving one reader configuration. */
|
|
56
|
+
export class NativeDetector {
|
|
57
|
+
private constructor(
|
|
58
|
+
private detector: BarcodeDetectorInstance,
|
|
59
|
+
/** Formats the detector looks for. */
|
|
60
|
+
readonly formats: readonly BarcodeFormat[],
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A detector for `formats` (default: all), or `undefined` when the native
|
|
65
|
+
* decoder cannot replace the engine's fast path: API absent, or one of the
|
|
66
|
+
* requested formats not supported natively (codes of that format would
|
|
67
|
+
* then be read by the slow fallbacks at best).
|
|
68
|
+
*
|
|
69
|
+
* `micro_qr`, absent from the native API: with `engineFallback` the
|
|
70
|
+
* fallbacks read it. Without, a list that names it explicitly keeps the
|
|
71
|
+
* engine (the application asked for it, it must not be lost silently),
|
|
72
|
+
* while the default list (every format) accepts not reading it.
|
|
73
|
+
*/
|
|
74
|
+
static async create(
|
|
75
|
+
formats: readonly BarcodeFormat[] | undefined,
|
|
76
|
+
api: BarcodeDetectorClass | undefined = nativeApi(),
|
|
77
|
+
engineFallback = false,
|
|
78
|
+
): Promise<NativeDetector | undefined> {
|
|
79
|
+
if (!api) return undefined;
|
|
80
|
+
if (!engineFallback && formats?.includes('micro_qr')) return undefined;
|
|
81
|
+
let supported: string[];
|
|
82
|
+
try {
|
|
83
|
+
supported = await api.getSupportedFormats();
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
// On Android the list is empty until Google Play Services has downloaded
|
|
88
|
+
// its barcode module (first use on the device, in the background): the
|
|
89
|
+
// detector is unavailable *for now*, the engine does the whole job.
|
|
90
|
+
const wanted = (formats?.length ? formats : NATIVE_FORMATS).filter((f) => f !== 'micro_qr');
|
|
91
|
+
if (wanted.length === 0 || !wanted.every((f) => supported.includes(f))) return undefined;
|
|
92
|
+
try {
|
|
93
|
+
return new NativeDetector(new api({ formats: [...wanted] }), wanted);
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Codes in `source` whose center lies in `rect` (the region of interest, in
|
|
101
|
+
* source pixels), as detections in source pixels. Throws when the native
|
|
102
|
+
* decoder fails.
|
|
103
|
+
*/
|
|
104
|
+
async detect(source: CanvasImageSource, rect: Rect): Promise<Detection[]> {
|
|
105
|
+
const found = await this.detector.detect(source);
|
|
106
|
+
const timestamp = Date.now();
|
|
107
|
+
return found.flatMap((code) => {
|
|
108
|
+
const format = code.format as BarcodeFormat;
|
|
109
|
+
if (!this.formats.includes(format) || code.cornerPoints.length === 0) return [];
|
|
110
|
+
if (!contains(rect, center(code.cornerPoints))) return [];
|
|
111
|
+
const points = code.cornerPoints.map((p) => ({ x: p.x, y: p.y }));
|
|
112
|
+
return [{ text: code.rawValue, format, points, timestamp }];
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The browser's own `BarcodeDetector`, if any. Feature detection only (no
|
|
119
|
+
* list of browsers or devices): any browser that ships the API is used.
|
|
120
|
+
*
|
|
121
|
+
* Polyfills are rejected: packages such as `barcode-detector` install a
|
|
122
|
+
* `BarcodeDetector` that runs ZXing in WebAssembly on the main thread,
|
|
123
|
+
* slower than this SDK's own engine in its workers. A built-in class
|
|
124
|
+
* stringifies as `function BarcodeDetector() { [native code] }`.
|
|
125
|
+
*/
|
|
126
|
+
function nativeApi(): BarcodeDetectorClass | undefined {
|
|
127
|
+
const api: unknown = Reflect.get(globalThis, 'BarcodeDetector');
|
|
128
|
+
if (typeof api !== 'function' || !isBuiltIn(api)) return undefined;
|
|
129
|
+
return api as BarcodeDetectorClass;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** `true` for a function implemented by the browser, not in JavaScript. */
|
|
133
|
+
export function isBuiltIn(fn: object): boolean {
|
|
134
|
+
return /\{\s*\[native code\]\s*\}\s*$/.test(Function.prototype.toString.call(fn));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function center(points: readonly Point[]): Point {
|
|
138
|
+
const n = points.length;
|
|
139
|
+
return {
|
|
140
|
+
x: points.reduce((s, p) => s + p.x, 0) / n,
|
|
141
|
+
y: points.reduce((s, p) => s + p.y, 0) / n,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function contains(r: Rect, p: Point): boolean {
|
|
146
|
+
return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
|
|
147
|
+
}
|