@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gueshbzh-perso/scanner",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Fast QR code & barcode scanning for the web: camera, WebGL frame pipeline, Web Worker and Rust/WebAssembly decoder.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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
- /** Strategy plan of the next frame, for a `DecoderCore` configured the same way. */
93
- plan(): FramePlan {
94
- return this.active().plan();
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
- /** Decode on this thread (inline engine): plan, decode and commit at once. */
106
- process(pixels: Uint8Array, width: number, height: number, format: PixelFormat): FrameOutcome {
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
- // Write straight into WASM memory: avoids one more copy of the frame.
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
- /** Decode a camera frame. `buffer` is transferred and returned in the reply. */
32
- processFrame(buffer: ArrayBuffer, width: number, height: number, format: PixelFormat): Promise<FrameReply>;
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
- return { ...this.session.process(new Uint8Array(buffer), width, height, format), buffer };
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
+ }