@cflorioluis/barcodereader 0.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 +79 -0
- package/js/camera.d.ts +66 -0
- package/js/camera.js +163 -0
- package/js/index.d.ts +92 -0
- package/js/index.js +277 -0
- package/js/worker.d.ts +1 -0
- package/js/worker.js +65 -0
- package/package.json +27 -0
- package/wasm/pkg/barcode_wasm.d.ts +74 -0
- package/wasm/pkg/barcode_wasm.js +337 -0
- package/wasm/pkg/barcode_wasm_bg.wasm +0 -0
- package/wasm/pkg/barcode_wasm_bg.wasm.d.ts +16 -0
- package/wasm/pkg/package.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# BarCodeReader
|
|
2
|
+
|
|
3
|
+
A dependency-free 1D/2D barcode & QR decoder with **one core** that runs
|
|
4
|
+
everywhere JavaScript runs — and, later, everywhere native FFI reaches.
|
|
5
|
+
|
|
6
|
+
## The idea
|
|
7
|
+
|
|
8
|
+
The decoding logic is written **once** in pure Rust (`core/`). It knows nothing
|
|
9
|
+
about cameras, files or platforms: it takes a grayscale pixel buffer and returns
|
|
10
|
+
the decoded symbols. Every platform is a thin binding on top:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
core (Rust, pure, no I/O)
|
|
14
|
+
decode_luma(pixels, width, height) -> Vec<Symbol>
|
|
15
|
+
│
|
|
16
|
+
├── wasm/ → wasm-pack → npm → Angular, React, Vue, Electron, Capacitor ← we are here
|
|
17
|
+
├── ffi/ → .so/.dll/.dylib (C ABI) → Flutter (dart:ffi), PHP (FFI), Python (step 2)
|
|
18
|
+
└── uniffi → Swift + Kotlin → native iOS / Android (step 3)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Each platform only supplies pixels its own way (getUserMedia on web,
|
|
22
|
+
AVFoundation on iOS, CameraX on Android). The hard part — finder patterns,
|
|
23
|
+
perspective sampling, Reed-Solomon, payload decoding — lives in the core and is
|
|
24
|
+
tested once.
|
|
25
|
+
|
|
26
|
+
## Layout
|
|
27
|
+
|
|
28
|
+
| Path | What it is |
|
|
29
|
+
|------|-----------|
|
|
30
|
+
| `core/` | Pure-Rust decoder. Image handling, binarization, QR pipeline. |
|
|
31
|
+
| `wasm/` | `wasm-bindgen` wrappers → npm package (via `wasm-pack`). |
|
|
32
|
+
| `js/` | Framework-agnostic camera glue + high-level `BarcodeScanner`. |
|
|
33
|
+
| `docs/` | Integration guides (Angular, React, Capacitor, WASM bundling). |
|
|
34
|
+
| `demo/` | Runnable apps: React, Angular, Capacitor (+ `examples/web`). |
|
|
35
|
+
| `examples/web/` | Minimal browser demo driving the camera, zero build. |
|
|
36
|
+
| `ROADMAP.md` | Staged build plan and current status of each pipeline stage. |
|
|
37
|
+
|
|
38
|
+
## Status
|
|
39
|
+
|
|
40
|
+
QR decoding works end-to-end from camera/image pixels: binarization (adaptive +
|
|
41
|
+
Otsu), finder detection, affine + perspective sampling, format info, Reed-Solomon,
|
|
42
|
+
and payload decode (numeric / alphanumeric / byte / ECI). Verified against
|
|
43
|
+
generated QR codes across versions 1–27, all EC levels, and a real photographed
|
|
44
|
+
thermal-receipt fixture. 1D symbologies and native FFI bindings are next — see
|
|
45
|
+
`ROADMAP.md`.
|
|
46
|
+
|
|
47
|
+
**New here? Start with [docs/getting-started.md](docs/getting-started.md).**
|
|
48
|
+
|
|
49
|
+
## Build (once the Rust toolchain is installed)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# 1. Toolchain (one-time)
|
|
53
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
54
|
+
cargo install wasm-pack
|
|
55
|
+
|
|
56
|
+
# 2. Run core tests
|
|
57
|
+
cargo test -p barcode-core
|
|
58
|
+
|
|
59
|
+
# 3. Build the npm/WASM package
|
|
60
|
+
wasm-pack build wasm --target web --out-dir pkg
|
|
61
|
+
# → wasm/pkg/ is a ready-to-import ES module + .wasm
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then import from any framework:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { BarcodeScanner } from "@cflorioluis/barcodereader";
|
|
68
|
+
|
|
69
|
+
const scanner = new BarcodeScanner();
|
|
70
|
+
await scanner.start((r) => console.log(r.format, r.text)); // call from a click
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Platform notes
|
|
74
|
+
|
|
75
|
+
- **iOS Safari**: HTTPS (or localhost) + a user gesture to start. `playsinline`
|
|
76
|
+
is set on the video element (required).
|
|
77
|
+
- **Android Chrome / desktop**: works out of the box on HTTPS/localhost.
|
|
78
|
+
- **Capacitor**: runs in the WebView using the same WASM path.
|
|
79
|
+
- **Electron**: same WASM path (Chromium runtime).
|
package/js/camera.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface CameraOptions {
|
|
2
|
+
/** "environment" = rear camera (default for scanning), "user" = front.
|
|
3
|
+
* Ignored when `deviceId` is set. */
|
|
4
|
+
facingMode?: "environment" | "user";
|
|
5
|
+
/** Open a specific camera by id (from `listCameras()`). Overrides facingMode. */
|
|
6
|
+
deviceId?: string;
|
|
7
|
+
/** Desired capture width; the browser may pick the nearest supported. */
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Frame {
|
|
12
|
+
data: Uint8ClampedArray;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
/** A camera the device exposes. `label` is empty until permission is granted. */
|
|
17
|
+
export interface CameraInfo {
|
|
18
|
+
deviceId: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Opens the camera and yields frames on demand. iOS constraints: must run on
|
|
23
|
+
* HTTPS (or localhost) and be started from a user gesture (a click/tap).
|
|
24
|
+
*/
|
|
25
|
+
export declare class CameraSource {
|
|
26
|
+
private stream;
|
|
27
|
+
private video;
|
|
28
|
+
private canvas;
|
|
29
|
+
private ctx;
|
|
30
|
+
private opts;
|
|
31
|
+
private keepAlive;
|
|
32
|
+
constructor();
|
|
33
|
+
private ensureAttached;
|
|
34
|
+
/** The live `<video>` element. Append it to the DOM to show the feed. */
|
|
35
|
+
get element(): HTMLVideoElement;
|
|
36
|
+
/** True while a camera stream is open. */
|
|
37
|
+
get active(): boolean;
|
|
38
|
+
start(opts?: CameraOptions): Promise<void>;
|
|
39
|
+
private open;
|
|
40
|
+
private stopTracks;
|
|
41
|
+
/** List available video cameras. Call after a first grant so labels fill in. */
|
|
42
|
+
listCameras(): Promise<CameraInfo[]>;
|
|
43
|
+
/** Switch to another camera without tearing down the video element. */
|
|
44
|
+
switchTo(deviceId: string): Promise<void>;
|
|
45
|
+
private videoTrack;
|
|
46
|
+
/** Whether the active camera supports the torch/flash (Android rear cameras;
|
|
47
|
+
* not iOS Safari). Returns false if no camera is open. */
|
|
48
|
+
torchAvailable(): boolean;
|
|
49
|
+
/** Turn the torch on/off. Returns true if applied, false if unsupported. */
|
|
50
|
+
setTorch(on: boolean): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Grab the current frame as a transferable `ImageBitmap`. This is GPU-backed
|
|
53
|
+
* and does no pixel copy on the main thread, so the heavy drawImage +
|
|
54
|
+
* getImageData work can be handed to a worker (via `OffscreenCanvas`) and the
|
|
55
|
+
* preview never stutters. The caller downscales when rasterising.
|
|
56
|
+
*/
|
|
57
|
+
grabBitmap(): Promise<ImageBitmap>;
|
|
58
|
+
/**
|
|
59
|
+
* Grab the current frame as RGBA pixels, optionally downscaled so the largest
|
|
60
|
+
* side is at most `maxSize`. Decoding a full 1080p+ frame every tick saturates
|
|
61
|
+
* the main thread and makes the preview stutter; QR detection is happy at
|
|
62
|
+
* ~600–800px, which is far cheaper.
|
|
63
|
+
*/
|
|
64
|
+
grab(maxSize?: number): Frame;
|
|
65
|
+
stop(): void;
|
|
66
|
+
}
|
package/js/camera.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Cross-platform camera glue for the web target.
|
|
2
|
+
//
|
|
3
|
+
// This is the "how each platform gets pixels" layer described in the README.
|
|
4
|
+
// It uses only standard web APIs (getUserMedia + canvas), so it behaves the
|
|
5
|
+
// same in Chrome (Android/desktop), Safari (iOS 14.3+), Electron and any
|
|
6
|
+
// Capacitor WebView. It never touches decoding — it hands raw pixels to WASM.
|
|
7
|
+
/**
|
|
8
|
+
* Opens the camera and yields frames on demand. iOS constraints: must run on
|
|
9
|
+
* HTTPS (or localhost) and be started from a user gesture (a click/tap).
|
|
10
|
+
*/
|
|
11
|
+
export class CameraSource {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.stream = null;
|
|
14
|
+
this.opts = {};
|
|
15
|
+
this.keepAlive = null;
|
|
16
|
+
this.video = document.createElement("video");
|
|
17
|
+
this.video.setAttribute("playsinline", "true"); // required by iOS Safari
|
|
18
|
+
this.video.muted = true;
|
|
19
|
+
this.video.autoplay = true;
|
|
20
|
+
this.video.style.width = "100%";
|
|
21
|
+
this.video.style.height = "100%";
|
|
22
|
+
this.video.style.objectFit = "cover";
|
|
23
|
+
this.canvas = document.createElement("canvas");
|
|
24
|
+
const ctx = this.canvas.getContext("2d", { willReadFrequently: true });
|
|
25
|
+
if (!ctx)
|
|
26
|
+
throw new Error("2D canvas context unavailable");
|
|
27
|
+
this.ctx = ctx;
|
|
28
|
+
}
|
|
29
|
+
// A tiny, off-screen host that keeps the <video> attached to the DOM. Mobile
|
|
30
|
+
// browsers (iOS especially) pause a video that isn't in the document, which
|
|
31
|
+
// freezes frame capture — so we always park it here when the app isn't
|
|
32
|
+
// showing it. drawImage still reads full-resolution frames regardless of the
|
|
33
|
+
// element's on-screen size.
|
|
34
|
+
ensureAttached() {
|
|
35
|
+
if (typeof document === "undefined" || this.video.isConnected)
|
|
36
|
+
return;
|
|
37
|
+
if (!this.keepAlive) {
|
|
38
|
+
const host = document.createElement("div");
|
|
39
|
+
host.style.cssText =
|
|
40
|
+
"position:fixed;top:0;left:0;width:1px;height:1px;overflow:hidden;" +
|
|
41
|
+
"opacity:0;pointer-events:none;z-index:-1;";
|
|
42
|
+
document.body.appendChild(host);
|
|
43
|
+
this.keepAlive = host;
|
|
44
|
+
}
|
|
45
|
+
this.keepAlive.appendChild(this.video);
|
|
46
|
+
void this.video.play().catch(() => { });
|
|
47
|
+
}
|
|
48
|
+
/** The live `<video>` element. Append it to the DOM to show the feed. */
|
|
49
|
+
get element() {
|
|
50
|
+
return this.video;
|
|
51
|
+
}
|
|
52
|
+
/** True while a camera stream is open. */
|
|
53
|
+
get active() {
|
|
54
|
+
return this.stream !== null;
|
|
55
|
+
}
|
|
56
|
+
async start(opts = {}) {
|
|
57
|
+
this.opts = opts;
|
|
58
|
+
await this.open();
|
|
59
|
+
}
|
|
60
|
+
async open() {
|
|
61
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
62
|
+
throw new Error("getUserMedia is not available in this environment");
|
|
63
|
+
}
|
|
64
|
+
const video = {
|
|
65
|
+
width: this.opts.width ? { ideal: this.opts.width } : undefined,
|
|
66
|
+
height: this.opts.height ? { ideal: this.opts.height } : undefined,
|
|
67
|
+
};
|
|
68
|
+
if (this.opts.deviceId) {
|
|
69
|
+
video.deviceId = { exact: this.opts.deviceId };
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
video.facingMode = this.opts.facingMode ?? "environment";
|
|
73
|
+
}
|
|
74
|
+
this.stream = await navigator.mediaDevices.getUserMedia({ audio: false, video });
|
|
75
|
+
this.video.srcObject = this.stream;
|
|
76
|
+
this.ensureAttached(); // keep it live even if the app never shows it
|
|
77
|
+
await this.video.play();
|
|
78
|
+
this.canvas.width = this.video.videoWidth;
|
|
79
|
+
this.canvas.height = this.video.videoHeight;
|
|
80
|
+
}
|
|
81
|
+
stopTracks() {
|
|
82
|
+
this.stream?.getTracks().forEach((t) => t.stop());
|
|
83
|
+
this.stream = null;
|
|
84
|
+
}
|
|
85
|
+
/** List available video cameras. Call after a first grant so labels fill in. */
|
|
86
|
+
async listCameras() {
|
|
87
|
+
if (!navigator.mediaDevices?.enumerateDevices)
|
|
88
|
+
return [];
|
|
89
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
90
|
+
return devices
|
|
91
|
+
.filter((d) => d.kind === "videoinput")
|
|
92
|
+
.map((d, i) => ({ deviceId: d.deviceId, label: d.label || `Camera ${i + 1}` }));
|
|
93
|
+
}
|
|
94
|
+
/** Switch to another camera without tearing down the video element. */
|
|
95
|
+
async switchTo(deviceId) {
|
|
96
|
+
this.opts = { ...this.opts, deviceId };
|
|
97
|
+
this.stopTracks();
|
|
98
|
+
await this.open();
|
|
99
|
+
}
|
|
100
|
+
videoTrack() {
|
|
101
|
+
return this.stream?.getVideoTracks()[0];
|
|
102
|
+
}
|
|
103
|
+
/** Whether the active camera supports the torch/flash (Android rear cameras;
|
|
104
|
+
* not iOS Safari). Returns false if no camera is open. */
|
|
105
|
+
torchAvailable() {
|
|
106
|
+
const caps = this.videoTrack()?.getCapabilities?.();
|
|
107
|
+
return !!caps?.torch;
|
|
108
|
+
}
|
|
109
|
+
/** Turn the torch on/off. Returns true if applied, false if unsupported. */
|
|
110
|
+
async setTorch(on) {
|
|
111
|
+
const track = this.videoTrack();
|
|
112
|
+
if (!track || !this.torchAvailable())
|
|
113
|
+
return false;
|
|
114
|
+
await track.applyConstraints({ advanced: [{ torch: on }] });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Grab the current frame as a transferable `ImageBitmap`. This is GPU-backed
|
|
119
|
+
* and does no pixel copy on the main thread, so the heavy drawImage +
|
|
120
|
+
* getImageData work can be handed to a worker (via `OffscreenCanvas`) and the
|
|
121
|
+
* preview never stutters. The caller downscales when rasterising.
|
|
122
|
+
*/
|
|
123
|
+
async grabBitmap() {
|
|
124
|
+
this.ensureAttached();
|
|
125
|
+
const { videoWidth: w, videoHeight: h } = this.video;
|
|
126
|
+
if (w === 0 || h === 0) {
|
|
127
|
+
throw new Error("Camera not ready yet");
|
|
128
|
+
}
|
|
129
|
+
return createImageBitmap(this.video);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Grab the current frame as RGBA pixels, optionally downscaled so the largest
|
|
133
|
+
* side is at most `maxSize`. Decoding a full 1080p+ frame every tick saturates
|
|
134
|
+
* the main thread and makes the preview stutter; QR detection is happy at
|
|
135
|
+
* ~600–800px, which is far cheaper.
|
|
136
|
+
*/
|
|
137
|
+
grab(maxSize) {
|
|
138
|
+
// If the app removed the video from the DOM (e.g. "hide preview"), park it
|
|
139
|
+
// back in the keep-alive host so it keeps producing frames.
|
|
140
|
+
this.ensureAttached();
|
|
141
|
+
const { videoWidth: vw, videoHeight: vh } = this.video;
|
|
142
|
+
if (vw === 0 || vh === 0) {
|
|
143
|
+
throw new Error("Camera not ready yet");
|
|
144
|
+
}
|
|
145
|
+
const scale = maxSize ? Math.min(1, maxSize / Math.max(vw, vh)) : 1;
|
|
146
|
+
const w = Math.max(1, Math.round(vw * scale));
|
|
147
|
+
const h = Math.max(1, Math.round(vh * scale));
|
|
148
|
+
if (this.canvas.width !== w || this.canvas.height !== h) {
|
|
149
|
+
this.canvas.width = w;
|
|
150
|
+
this.canvas.height = h;
|
|
151
|
+
}
|
|
152
|
+
this.ctx.drawImage(this.video, 0, 0, w, h);
|
|
153
|
+
const img = this.ctx.getImageData(0, 0, w, h);
|
|
154
|
+
return { data: img.data, width: w, height: h };
|
|
155
|
+
}
|
|
156
|
+
stop() {
|
|
157
|
+
this.stopTracks();
|
|
158
|
+
this.video.srcObject = null;
|
|
159
|
+
// Tear down the keep-alive host so nothing lingers in the DOM.
|
|
160
|
+
this.keepAlive?.remove();
|
|
161
|
+
this.keepAlive = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
package/js/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type CameraOptions, type CameraInfo } from "./camera.js";
|
|
2
|
+
export interface ScanResult {
|
|
3
|
+
format: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export type OnResult = (result: ScanResult) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Optional one-time configuration. Call before the first scan.
|
|
9
|
+
*
|
|
10
|
+
* `wasmPath` points at the `barcode_wasm_bg.wasm` binary. Set it when your
|
|
11
|
+
* bundler (Angular, webpack, some Vite setups) serves the WASM as a static
|
|
12
|
+
* asset instead of resolving the package's relative path — e.g. after copying
|
|
13
|
+
* `wasm/pkg/barcode_wasm_bg.wasm` into your app's assets:
|
|
14
|
+
*
|
|
15
|
+
* configure({ wasmPath: "/assets/barcode_wasm_bg.wasm" });
|
|
16
|
+
*/
|
|
17
|
+
export declare function configure(opts: {
|
|
18
|
+
wasmPath?: string;
|
|
19
|
+
}): void;
|
|
20
|
+
/** Decode any drawable image source (image element, bitmap, canvas, video). */
|
|
21
|
+
export declare function scanImageSource(source: CanvasImageSource, width: number, height: number): Promise<ScanResult[]>;
|
|
22
|
+
/** Decode a still image from a File/Blob (e.g. an <input type=file>). */
|
|
23
|
+
export declare function scanFile(file: Blob): Promise<ScanResult[]>;
|
|
24
|
+
/** Decode a still image from a URL. */
|
|
25
|
+
export declare function scanUrl(url: string): Promise<ScanResult[]>;
|
|
26
|
+
export interface ScanOptions extends CameraOptions {
|
|
27
|
+
/** Stop the camera automatically after the first successful decode. Ideal
|
|
28
|
+
* for the "scan a code, run one action, close" flow. Default: false. */
|
|
29
|
+
once?: boolean;
|
|
30
|
+
/** Suppress repeated identical results within this many milliseconds, so a
|
|
31
|
+
* code held in view fires your action once, not every frame. Default: 1500.
|
|
32
|
+
* Set to 0 to fire on every frame. */
|
|
33
|
+
dedupeMs?: number;
|
|
34
|
+
/** Downscale each frame to this max side before decoding. Keeps the preview
|
|
35
|
+
* smooth on high-res phone cameras; QR detection is fine at this size.
|
|
36
|
+
* Default: 800. */
|
|
37
|
+
maxScanSize?: number;
|
|
38
|
+
/** Minimum milliseconds between decode attempts. Decoding every animation
|
|
39
|
+
* frame saturates the main thread and makes the preview lag; 10 fps is
|
|
40
|
+
* plenty for scanning. Default: 100. */
|
|
41
|
+
scanIntervalMs?: number;
|
|
42
|
+
/** Decode in a Web Worker (off the main thread) so the preview never stutters.
|
|
43
|
+
* Default: true. Falls back to the main thread automatically if a worker
|
|
44
|
+
* can't be created. Set false to force main-thread decoding. */
|
|
45
|
+
useWorker?: boolean;
|
|
46
|
+
/** Restrict which symbologies fire `onResult`, by their `format` string
|
|
47
|
+
* (e.g. `["QR"]`, `["EAN-13", "UPC-A"]`). Omit to accept every format. */
|
|
48
|
+
formats?: string[];
|
|
49
|
+
}
|
|
50
|
+
export declare class BarcodeScanner {
|
|
51
|
+
private camera;
|
|
52
|
+
private running;
|
|
53
|
+
private lastText;
|
|
54
|
+
private lastAt;
|
|
55
|
+
private lastDecodeAt;
|
|
56
|
+
private onResult;
|
|
57
|
+
private opts;
|
|
58
|
+
private worker;
|
|
59
|
+
private workerReady;
|
|
60
|
+
private workerOffscreen;
|
|
61
|
+
private busy;
|
|
62
|
+
/** The live `<video>` element — append it to the DOM to show the feed. */
|
|
63
|
+
get video(): HTMLVideoElement;
|
|
64
|
+
/** True while the camera is open and scanning. */
|
|
65
|
+
get active(): boolean;
|
|
66
|
+
/** List the device's cameras. Call after `start()` so labels are populated. */
|
|
67
|
+
listCameras(): Promise<CameraInfo[]>;
|
|
68
|
+
/** Switch to another camera by id (from `listCameras()`) while scanning. */
|
|
69
|
+
switchCamera(deviceId: string): Promise<void>;
|
|
70
|
+
/** Whether the active camera has a torch/flash (Android rear; not iOS). */
|
|
71
|
+
torchAvailable(): boolean;
|
|
72
|
+
/** Turn the torch on/off. Returns true if applied, false if unsupported. */
|
|
73
|
+
setTorch(on: boolean): Promise<boolean>;
|
|
74
|
+
/**
|
|
75
|
+
* Start scanning. `onResult` fires when a code is decoded — this is where you
|
|
76
|
+
* run your action (validate a ticket, open a URL, check someone in, ...).
|
|
77
|
+
*
|
|
78
|
+
* The library gives you the primitive; your UI (the button, the overlay) is
|
|
79
|
+
* entirely yours. A minimal "scan once then act" flow:
|
|
80
|
+
*
|
|
81
|
+
* await scanner.start(r => myAction(r.text), { once: true });
|
|
82
|
+
*/
|
|
83
|
+
start(onResult: OnResult, opts?: ScanOptions): Promise<void>;
|
|
84
|
+
private initWorker;
|
|
85
|
+
private now;
|
|
86
|
+
private hidden;
|
|
87
|
+
private handle;
|
|
88
|
+
private loop;
|
|
89
|
+
stop(): void;
|
|
90
|
+
}
|
|
91
|
+
export { CameraSource } from "./camera.js";
|
|
92
|
+
export type { CameraOptions, CameraInfo } from "./camera.js";
|
package/js/index.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// High-level scanner API for the web target.
|
|
2
|
+
//
|
|
3
|
+
// Combines the WASM decoder with two pixel sources:
|
|
4
|
+
// * live camera (BarcodeScanner) — for real devices.
|
|
5
|
+
// * still images (scanImageSource / scanFile) — decode a photo, screenshot
|
|
6
|
+
// or uploaded file. Works everywhere, no camera needed.
|
|
7
|
+
//
|
|
8
|
+
// Any framework (Angular, React, Vue, Electron, Capacitor) can drive these.
|
|
9
|
+
// The WASM module is loaded lazily and shared across both paths.
|
|
10
|
+
import { CameraSource } from "./camera.js";
|
|
11
|
+
let wasmPromise = null;
|
|
12
|
+
let wasmPath;
|
|
13
|
+
/**
|
|
14
|
+
* Optional one-time configuration. Call before the first scan.
|
|
15
|
+
*
|
|
16
|
+
* `wasmPath` points at the `barcode_wasm_bg.wasm` binary. Set it when your
|
|
17
|
+
* bundler (Angular, webpack, some Vite setups) serves the WASM as a static
|
|
18
|
+
* asset instead of resolving the package's relative path — e.g. after copying
|
|
19
|
+
* `wasm/pkg/barcode_wasm_bg.wasm` into your app's assets:
|
|
20
|
+
*
|
|
21
|
+
* configure({ wasmPath: "/assets/barcode_wasm_bg.wasm" });
|
|
22
|
+
*/
|
|
23
|
+
export function configure(opts) {
|
|
24
|
+
wasmPath = opts.wasmPath;
|
|
25
|
+
}
|
|
26
|
+
async function wasm() {
|
|
27
|
+
if (!wasmPromise) {
|
|
28
|
+
wasmPromise = (async () => {
|
|
29
|
+
// Path is the wasm-pack output package, relative to this module.
|
|
30
|
+
const mod = (await import("../wasm/pkg/barcode_wasm.js"));
|
|
31
|
+
// With no explicit path the glue fetches the .wasm next to itself, which
|
|
32
|
+
// works for direct ES-module use and most Vite setups.
|
|
33
|
+
await mod.default(wasmPath ? { module_or_path: wasmPath } : undefined);
|
|
34
|
+
return mod;
|
|
35
|
+
})();
|
|
36
|
+
}
|
|
37
|
+
return wasmPromise;
|
|
38
|
+
}
|
|
39
|
+
// A canvas reused for still-image decoding.
|
|
40
|
+
let scratch = null;
|
|
41
|
+
function scratchCtx(w, h) {
|
|
42
|
+
scratch ?? (scratch = document.createElement("canvas"));
|
|
43
|
+
scratch.width = w;
|
|
44
|
+
scratch.height = h;
|
|
45
|
+
const ctx = scratch.getContext("2d", { willReadFrequently: true });
|
|
46
|
+
if (!ctx)
|
|
47
|
+
throw new Error("2D canvas context unavailable");
|
|
48
|
+
return ctx;
|
|
49
|
+
}
|
|
50
|
+
// Decode RGBA pixels on the main thread, returning plain objects and freeing
|
|
51
|
+
// the wasm-bindgen results so they don't accumulate over a long session.
|
|
52
|
+
async function decodeRgba(rgba, w, h) {
|
|
53
|
+
const m = await wasm();
|
|
54
|
+
const raw = m.scan_rgba(rgba, w, h);
|
|
55
|
+
return raw.map((r) => {
|
|
56
|
+
const out = { format: r.format, text: r.text };
|
|
57
|
+
r.free?.();
|
|
58
|
+
return out;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Decode any drawable image source (image element, bitmap, canvas, video). */
|
|
62
|
+
export async function scanImageSource(source, width, height) {
|
|
63
|
+
const ctx = scratchCtx(width, height);
|
|
64
|
+
ctx.drawImage(source, 0, 0, width, height);
|
|
65
|
+
const img = ctx.getImageData(0, 0, width, height);
|
|
66
|
+
return decodeRgba(new Uint8Array(img.data.buffer), width, height);
|
|
67
|
+
}
|
|
68
|
+
/** Decode a still image from a File/Blob (e.g. an <input type=file>). */
|
|
69
|
+
export async function scanFile(file) {
|
|
70
|
+
const bitmap = await createImageBitmap(file);
|
|
71
|
+
try {
|
|
72
|
+
return await scanImageSource(bitmap, bitmap.width, bitmap.height);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
bitmap.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Decode a still image from a URL. */
|
|
79
|
+
export async function scanUrl(url) {
|
|
80
|
+
const res = await fetch(url);
|
|
81
|
+
return scanFile(await res.blob());
|
|
82
|
+
}
|
|
83
|
+
export class BarcodeScanner {
|
|
84
|
+
constructor() {
|
|
85
|
+
this.camera = new CameraSource();
|
|
86
|
+
this.running = false;
|
|
87
|
+
this.lastText = "";
|
|
88
|
+
this.lastAt = 0;
|
|
89
|
+
this.lastDecodeAt = 0;
|
|
90
|
+
this.onResult = () => { };
|
|
91
|
+
this.opts = {};
|
|
92
|
+
this.worker = null;
|
|
93
|
+
this.workerReady = false;
|
|
94
|
+
this.workerOffscreen = false; // worker can rasterise ImageBitmaps itself
|
|
95
|
+
this.busy = false; // a frame is currently being decoded by the worker
|
|
96
|
+
}
|
|
97
|
+
/** The live `<video>` element — append it to the DOM to show the feed. */
|
|
98
|
+
get video() {
|
|
99
|
+
return this.camera.element;
|
|
100
|
+
}
|
|
101
|
+
/** True while the camera is open and scanning. */
|
|
102
|
+
get active() {
|
|
103
|
+
return this.camera.active;
|
|
104
|
+
}
|
|
105
|
+
/** List the device's cameras. Call after `start()` so labels are populated. */
|
|
106
|
+
listCameras() {
|
|
107
|
+
return this.camera.listCameras();
|
|
108
|
+
}
|
|
109
|
+
/** Switch to another camera by id (from `listCameras()`) while scanning. */
|
|
110
|
+
switchCamera(deviceId) {
|
|
111
|
+
return this.camera.switchTo(deviceId);
|
|
112
|
+
}
|
|
113
|
+
/** Whether the active camera has a torch/flash (Android rear; not iOS). */
|
|
114
|
+
torchAvailable() {
|
|
115
|
+
return this.camera.torchAvailable();
|
|
116
|
+
}
|
|
117
|
+
/** Turn the torch on/off. Returns true if applied, false if unsupported. */
|
|
118
|
+
setTorch(on) {
|
|
119
|
+
return this.camera.setTorch(on);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Start scanning. `onResult` fires when a code is decoded — this is where you
|
|
123
|
+
* run your action (validate a ticket, open a URL, check someone in, ...).
|
|
124
|
+
*
|
|
125
|
+
* The library gives you the primitive; your UI (the button, the overlay) is
|
|
126
|
+
* entirely yours. A minimal "scan once then act" flow:
|
|
127
|
+
*
|
|
128
|
+
* await scanner.start(r => myAction(r.text), { once: true });
|
|
129
|
+
*/
|
|
130
|
+
async start(onResult, opts = {}) {
|
|
131
|
+
this.onResult = onResult;
|
|
132
|
+
this.opts = opts;
|
|
133
|
+
await wasm(); // warms the still-image path + main-thread fallback
|
|
134
|
+
await this.camera.start(opts);
|
|
135
|
+
this.running = true;
|
|
136
|
+
this.lastText = "";
|
|
137
|
+
this.lastAt = 0;
|
|
138
|
+
this.lastDecodeAt = 0;
|
|
139
|
+
this.busy = false;
|
|
140
|
+
if (opts.useWorker !== false)
|
|
141
|
+
await this.initWorker();
|
|
142
|
+
this.loop();
|
|
143
|
+
}
|
|
144
|
+
// Spin up the decode worker and wait until its WASM is ready. On any failure
|
|
145
|
+
// (workers blocked, bundler couldn't emit it) we silently fall back to the
|
|
146
|
+
// main thread — scanning still works, just without the off-thread win.
|
|
147
|
+
async initWorker() {
|
|
148
|
+
try {
|
|
149
|
+
const worker = new Worker(new URL("./worker.js", import.meta.url), {
|
|
150
|
+
type: "module",
|
|
151
|
+
});
|
|
152
|
+
await new Promise((resolve, reject) => {
|
|
153
|
+
const timer = setTimeout(() => reject(new Error("worker init timeout")), 5000);
|
|
154
|
+
worker.addEventListener("message", (e) => {
|
|
155
|
+
const data = e.data;
|
|
156
|
+
if (data?.type === "ready") {
|
|
157
|
+
this.workerOffscreen = data.offscreen === true;
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
resolve();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
worker.addEventListener("error", (e) => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
reject(e);
|
|
165
|
+
});
|
|
166
|
+
worker.postMessage({ type: "init", wasmPath });
|
|
167
|
+
});
|
|
168
|
+
worker.addEventListener("message", (e) => {
|
|
169
|
+
const data = e.data;
|
|
170
|
+
if (data?.type === "result") {
|
|
171
|
+
this.busy = false;
|
|
172
|
+
this.handle(data.results ?? []);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
this.worker = worker;
|
|
176
|
+
this.workerReady = true;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
this.worker = null;
|
|
180
|
+
this.workerReady = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
now() {
|
|
184
|
+
return typeof performance !== "undefined" ? performance.now() : 0;
|
|
185
|
+
}
|
|
186
|
+
hidden() {
|
|
187
|
+
return typeof document !== "undefined" && document.hidden;
|
|
188
|
+
}
|
|
189
|
+
// Apply dedupe, fire the caller's action, honour `once`.
|
|
190
|
+
handle(results) {
|
|
191
|
+
const dedupeMs = this.opts.dedupeMs ?? 1500;
|
|
192
|
+
const formats = this.opts.formats;
|
|
193
|
+
for (const r of results) {
|
|
194
|
+
if (!r.text)
|
|
195
|
+
continue;
|
|
196
|
+
if (formats && formats.length > 0 && !formats.includes(r.format))
|
|
197
|
+
continue;
|
|
198
|
+
const t = this.now();
|
|
199
|
+
if (r.text === this.lastText && t - this.lastAt < dedupeMs)
|
|
200
|
+
continue;
|
|
201
|
+
this.lastText = r.text;
|
|
202
|
+
this.lastAt = t;
|
|
203
|
+
this.onResult(r);
|
|
204
|
+
if (this.opts.once) {
|
|
205
|
+
this.stop();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async loop() {
|
|
211
|
+
if (!this.running)
|
|
212
|
+
return;
|
|
213
|
+
const intervalMs = this.opts.scanIntervalMs ?? 100;
|
|
214
|
+
const maxScanSize = this.opts.maxScanSize ?? 800;
|
|
215
|
+
const now = this.now();
|
|
216
|
+
// Pause decoding while the page is hidden — saves battery. The camera stays
|
|
217
|
+
// open so resuming is instant.
|
|
218
|
+
if (!this.hidden() && now - this.lastDecodeAt >= intervalMs) {
|
|
219
|
+
if (this.worker && this.workerReady && this.workerOffscreen && typeof createImageBitmap !== "undefined") {
|
|
220
|
+
// Lightest path: hand the worker a GPU-backed ImageBitmap and let it do
|
|
221
|
+
// the drawImage + getImageData on its own OffscreenCanvas. The main
|
|
222
|
+
// thread does almost nothing per frame, so the preview never stutters.
|
|
223
|
+
if (!this.busy) {
|
|
224
|
+
this.busy = true;
|
|
225
|
+
this.lastDecodeAt = now;
|
|
226
|
+
this.camera
|
|
227
|
+
.grabBitmap()
|
|
228
|
+
.then((bitmap) => {
|
|
229
|
+
this.worker.postMessage({ type: "decodeBitmap", bitmap, maxSize: maxScanSize }, [bitmap]);
|
|
230
|
+
})
|
|
231
|
+
.catch(() => {
|
|
232
|
+
this.busy = false; // camera not ready yet
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (this.worker && this.workerReady) {
|
|
237
|
+
// Off-thread but no OffscreenCanvas: rasterise on the main thread and
|
|
238
|
+
// transfer the pixel buffer (zero-copy).
|
|
239
|
+
if (!this.busy) {
|
|
240
|
+
try {
|
|
241
|
+
const frame = this.camera.grab(maxScanSize);
|
|
242
|
+
this.busy = true;
|
|
243
|
+
this.lastDecodeAt = now;
|
|
244
|
+
this.worker.postMessage({ type: "decode", buf: frame.data.buffer, w: frame.width, h: frame.height }, [frame.data.buffer]);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
this.busy = false; // camera not ready yet
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Main-thread fallback.
|
|
253
|
+
this.lastDecodeAt = now;
|
|
254
|
+
try {
|
|
255
|
+
const frame = this.camera.grab(maxScanSize);
|
|
256
|
+
const results = await decodeRgba(new Uint8Array(frame.data.buffer), frame.width, frame.height);
|
|
257
|
+
this.handle(results);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// not ready
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
requestAnimationFrame(() => this.loop());
|
|
265
|
+
}
|
|
266
|
+
stop() {
|
|
267
|
+
this.running = false;
|
|
268
|
+
this.busy = false;
|
|
269
|
+
this.camera.stop();
|
|
270
|
+
if (this.worker) {
|
|
271
|
+
this.worker.terminate();
|
|
272
|
+
this.worker = null;
|
|
273
|
+
this.workerReady = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export { CameraSource } from "./camera.js";
|
package/js/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/js/worker.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Decoder Web Worker: runs the WASM decode OFF the main thread so the camera
|
|
2
|
+
// preview never stutters, no matter how heavy a frame is.
|
|
3
|
+
//
|
|
4
|
+
// Two input paths:
|
|
5
|
+
// * decodeBitmap — the main thread transfers a GPU-backed ImageBitmap; this
|
|
6
|
+
// worker rasterises it on an OffscreenCanvas and reads the pixels here, so
|
|
7
|
+
// the (costly) drawImage + getImageData never touch the UI thread.
|
|
8
|
+
// * decode — a pre-rasterised RGBA buffer, transferred zero-copy. Fallback
|
|
9
|
+
// for engines without OffscreenCanvas.
|
|
10
|
+
//
|
|
11
|
+
// It also frees each wasm-bindgen `ScanResult` after reading it — otherwise the
|
|
12
|
+
// Rust-side objects accumulate over a long session and the app slows down.
|
|
13
|
+
import init, { scan_rgba } from "../wasm/pkg/barcode_wasm.js";
|
|
14
|
+
const ctx = self;
|
|
15
|
+
let ready = false;
|
|
16
|
+
const hasOffscreen = typeof OffscreenCanvas !== "undefined";
|
|
17
|
+
let canvas = null;
|
|
18
|
+
let canvasCtx = null;
|
|
19
|
+
function decodeRgba(rgba, w, h) {
|
|
20
|
+
const raw = scan_rgba(rgba, w, h);
|
|
21
|
+
return raw.map((r) => {
|
|
22
|
+
const out = { format: r.format, text: r.text };
|
|
23
|
+
r.free?.(); // release the Rust-side object deterministically
|
|
24
|
+
return out;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
ctx.addEventListener("message", async (e) => {
|
|
28
|
+
const msg = e.data;
|
|
29
|
+
if (msg.type === "init") {
|
|
30
|
+
await init(msg.wasmPath ? { module_or_path: msg.wasmPath } : undefined);
|
|
31
|
+
ready = true;
|
|
32
|
+
ctx.postMessage({ type: "ready", offscreen: hasOffscreen });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (msg.type === "decode") {
|
|
36
|
+
const results = ready ? decodeRgba(new Uint8Array(msg.buf), msg.w, msg.h) : [];
|
|
37
|
+
ctx.postMessage({ type: "result", results });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (msg.type === "decodeBitmap") {
|
|
41
|
+
let results = [];
|
|
42
|
+
if (ready && hasOffscreen) {
|
|
43
|
+
const bmp = msg.bitmap;
|
|
44
|
+
const scale = Math.min(1, msg.maxSize / Math.max(bmp.width, bmp.height));
|
|
45
|
+
const w = Math.max(1, Math.round(bmp.width * scale));
|
|
46
|
+
const h = Math.max(1, Math.round(bmp.height * scale));
|
|
47
|
+
if (!canvas) {
|
|
48
|
+
canvas = new OffscreenCanvas(w, h);
|
|
49
|
+
canvasCtx = canvas.getContext("2d", {
|
|
50
|
+
willReadFrequently: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
canvas.width = w;
|
|
54
|
+
canvas.height = h;
|
|
55
|
+
canvasCtx.drawImage(bmp, 0, 0, w, h);
|
|
56
|
+
bmp.close();
|
|
57
|
+
const img = canvasCtx.getImageData(0, 0, w, h);
|
|
58
|
+
results = decodeRgba(new Uint8Array(img.data.buffer), w, h);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
msg.bitmap.close?.();
|
|
62
|
+
}
|
|
63
|
+
ctx.postMessage({ type: "result", results });
|
|
64
|
+
}
|
|
65
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cflorioluis/barcodereader",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dependency-free 1D/2D barcode & QR decoder. One Rust core, WASM for web (Angular/React/Vue/Electron/Capacitor), native FFI later.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "js/index.js",
|
|
7
|
+
"types": "js/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./js/index.d.ts",
|
|
11
|
+
"default": "./js/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["js/**/*.js", "js/**/*.d.ts", "wasm/pkg", "README.md"],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build:wasm": "wasm-pack build wasm --target web --out-dir pkg && rm -f wasm/pkg/.gitignore",
|
|
20
|
+
"build:js": "tsc -p tsconfig.json",
|
|
21
|
+
"build": "npm run build:wasm && npm run build:js",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"test:core": "cargo test -p barcode-core"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["qr", "barcode", "scanner", "wasm", "rust", "camera"],
|
|
26
|
+
"license": "MIT"
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A decoded symbol, shaped for JS consumption.
|
|
6
|
+
*/
|
|
7
|
+
export class ScanResult {
|
|
8
|
+
private constructor();
|
|
9
|
+
free(): void;
|
|
10
|
+
[Symbol.dispose](): void;
|
|
11
|
+
/**
|
|
12
|
+
* Symbology, e.g. "QR".
|
|
13
|
+
*/
|
|
14
|
+
format: string;
|
|
15
|
+
/**
|
|
16
|
+
* Decoded text payload.
|
|
17
|
+
*/
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decode barcodes from a grayscale luminance buffer (one byte per pixel).
|
|
23
|
+
*
|
|
24
|
+
* Callers that have RGBA (e.g. from a `<canvas>` `ImageData`) should use
|
|
25
|
+
* `scan_rgba` instead, which converts to luminance first.
|
|
26
|
+
*/
|
|
27
|
+
export function scan_luma(pixels: Uint8Array, width: number, height: number): ScanResult[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decode barcodes directly from RGBA pixels (canvas `ImageData.data`).
|
|
31
|
+
* Converts to luminance with the Rec. 601 weights, then decodes.
|
|
32
|
+
*/
|
|
33
|
+
export function scan_rgba(rgba: Uint8Array, width: number, height: number): ScanResult[];
|
|
34
|
+
|
|
35
|
+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
36
|
+
|
|
37
|
+
export interface InitOutput {
|
|
38
|
+
readonly memory: WebAssembly.Memory;
|
|
39
|
+
readonly __wbg_get_scanresult_format: (a: number) => [number, number];
|
|
40
|
+
readonly __wbg_get_scanresult_text: (a: number) => [number, number];
|
|
41
|
+
readonly __wbg_scanresult_free: (a: number, b: number) => void;
|
|
42
|
+
readonly __wbg_set_scanresult_format: (a: number, b: number, c: number) => void;
|
|
43
|
+
readonly __wbg_set_scanresult_text: (a: number, b: number, c: number) => void;
|
|
44
|
+
readonly scan_luma: (a: number, b: number, c: number, d: number) => [number, number];
|
|
45
|
+
readonly scan_rgba: (a: number, b: number, c: number, d: number) => [number, number];
|
|
46
|
+
readonly __wbindgen_externrefs: WebAssembly.Table;
|
|
47
|
+
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
48
|
+
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
|
49
|
+
readonly __externref_drop_slice: (a: number, b: number) => void;
|
|
50
|
+
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
51
|
+
readonly __wbindgen_start: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
58
|
+
* a precompiled `WebAssembly.Module`.
|
|
59
|
+
*
|
|
60
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
61
|
+
*
|
|
62
|
+
* @returns {InitOutput}
|
|
63
|
+
*/
|
|
64
|
+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
68
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
69
|
+
*
|
|
70
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
71
|
+
*
|
|
72
|
+
* @returns {Promise<InitOutput>}
|
|
73
|
+
*/
|
|
74
|
+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/* @ts-self-types="./barcode_wasm.d.ts" */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A decoded symbol, shaped for JS consumption.
|
|
5
|
+
*/
|
|
6
|
+
export class ScanResult {
|
|
7
|
+
static __wrap(ptr) {
|
|
8
|
+
const obj = Object.create(ScanResult.prototype);
|
|
9
|
+
obj.__wbg_ptr = ptr;
|
|
10
|
+
ScanResultFinalization.register(obj, obj.__wbg_ptr, obj);
|
|
11
|
+
return obj;
|
|
12
|
+
}
|
|
13
|
+
__destroy_into_raw() {
|
|
14
|
+
const ptr = this.__wbg_ptr;
|
|
15
|
+
this.__wbg_ptr = 0;
|
|
16
|
+
ScanResultFinalization.unregister(this);
|
|
17
|
+
return ptr;
|
|
18
|
+
}
|
|
19
|
+
free() {
|
|
20
|
+
const ptr = this.__destroy_into_raw();
|
|
21
|
+
wasm.__wbg_scanresult_free(ptr, 0);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Symbology, e.g. "QR".
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
get format() {
|
|
28
|
+
let deferred1_0;
|
|
29
|
+
let deferred1_1;
|
|
30
|
+
try {
|
|
31
|
+
const ret = wasm.__wbg_get_scanresult_format(this.__wbg_ptr);
|
|
32
|
+
deferred1_0 = ret[0];
|
|
33
|
+
deferred1_1 = ret[1];
|
|
34
|
+
return getStringFromWasm0(ret[0], ret[1]);
|
|
35
|
+
} finally {
|
|
36
|
+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Decoded text payload.
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
get text() {
|
|
44
|
+
let deferred1_0;
|
|
45
|
+
let deferred1_1;
|
|
46
|
+
try {
|
|
47
|
+
const ret = wasm.__wbg_get_scanresult_text(this.__wbg_ptr);
|
|
48
|
+
deferred1_0 = ret[0];
|
|
49
|
+
deferred1_1 = ret[1];
|
|
50
|
+
return getStringFromWasm0(ret[0], ret[1]);
|
|
51
|
+
} finally {
|
|
52
|
+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Symbology, e.g. "QR".
|
|
57
|
+
* @param {string} arg0
|
|
58
|
+
*/
|
|
59
|
+
set format(arg0) {
|
|
60
|
+
const ptr0 = passStringToWasm0(arg0, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
61
|
+
const len0 = WASM_VECTOR_LEN;
|
|
62
|
+
wasm.__wbg_set_scanresult_format(this.__wbg_ptr, ptr0, len0);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Decoded text payload.
|
|
66
|
+
* @param {string} arg0
|
|
67
|
+
*/
|
|
68
|
+
set text(arg0) {
|
|
69
|
+
const ptr0 = passStringToWasm0(arg0, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
70
|
+
const len0 = WASM_VECTOR_LEN;
|
|
71
|
+
wasm.__wbg_set_scanresult_text(this.__wbg_ptr, ptr0, len0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (Symbol.dispose) ScanResult.prototype[Symbol.dispose] = ScanResult.prototype.free;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Decode barcodes from a grayscale luminance buffer (one byte per pixel).
|
|
78
|
+
*
|
|
79
|
+
* Callers that have RGBA (e.g. from a `<canvas>` `ImageData`) should use
|
|
80
|
+
* `scan_rgba` instead, which converts to luminance first.
|
|
81
|
+
* @param {Uint8Array} pixels
|
|
82
|
+
* @param {number} width
|
|
83
|
+
* @param {number} height
|
|
84
|
+
* @returns {ScanResult[]}
|
|
85
|
+
*/
|
|
86
|
+
export function scan_luma(pixels, width, height) {
|
|
87
|
+
const ptr0 = passArray8ToWasm0(pixels, wasm.__wbindgen_malloc);
|
|
88
|
+
const len0 = WASM_VECTOR_LEN;
|
|
89
|
+
const ret = wasm.scan_luma(ptr0, len0, width, height);
|
|
90
|
+
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
|
91
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
|
92
|
+
return v2;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Decode barcodes directly from RGBA pixels (canvas `ImageData.data`).
|
|
97
|
+
* Converts to luminance with the Rec. 601 weights, then decodes.
|
|
98
|
+
* @param {Uint8Array} rgba
|
|
99
|
+
* @param {number} width
|
|
100
|
+
* @param {number} height
|
|
101
|
+
* @returns {ScanResult[]}
|
|
102
|
+
*/
|
|
103
|
+
export function scan_rgba(rgba, width, height) {
|
|
104
|
+
const ptr0 = passArray8ToWasm0(rgba, wasm.__wbindgen_malloc);
|
|
105
|
+
const len0 = WASM_VECTOR_LEN;
|
|
106
|
+
const ret = wasm.scan_rgba(ptr0, len0, width, height);
|
|
107
|
+
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
|
108
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
|
109
|
+
return v2;
|
|
110
|
+
}
|
|
111
|
+
function __wbg_get_imports() {
|
|
112
|
+
const import0 = {
|
|
113
|
+
__proto__: null,
|
|
114
|
+
__wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) {
|
|
115
|
+
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
116
|
+
},
|
|
117
|
+
__wbg_scanresult_new: function(arg0) {
|
|
118
|
+
const ret = ScanResult.__wrap(arg0);
|
|
119
|
+
return ret;
|
|
120
|
+
},
|
|
121
|
+
__wbindgen_init_externref_table: function() {
|
|
122
|
+
const table = wasm.__wbindgen_externrefs;
|
|
123
|
+
const offset = table.grow(4);
|
|
124
|
+
table.set(0, undefined);
|
|
125
|
+
table.set(offset + 0, undefined);
|
|
126
|
+
table.set(offset + 1, null);
|
|
127
|
+
table.set(offset + 2, true);
|
|
128
|
+
table.set(offset + 3, false);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
__proto__: null,
|
|
133
|
+
"./barcode_wasm_bg.js": import0,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ScanResultFinalization = (typeof FinalizationRegistry === 'undefined')
|
|
138
|
+
? { register: () => {}, unregister: () => {} }
|
|
139
|
+
: new FinalizationRegistry(ptr => wasm.__wbg_scanresult_free(ptr, 1));
|
|
140
|
+
|
|
141
|
+
function getArrayJsValueFromWasm0(ptr, len) {
|
|
142
|
+
ptr = ptr >>> 0;
|
|
143
|
+
const mem = getDataViewMemory0();
|
|
144
|
+
const result = [];
|
|
145
|
+
for (let i = ptr; i < ptr + 4 * len; i += 4) {
|
|
146
|
+
result.push(wasm.__wbindgen_externrefs.get(mem.getUint32(i, true)));
|
|
147
|
+
}
|
|
148
|
+
wasm.__externref_drop_slice(ptr, len);
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let cachedDataViewMemory0 = null;
|
|
153
|
+
function getDataViewMemory0() {
|
|
154
|
+
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
|
155
|
+
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
|
156
|
+
}
|
|
157
|
+
return cachedDataViewMemory0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getStringFromWasm0(ptr, len) {
|
|
161
|
+
return decodeText(ptr >>> 0, len);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let cachedUint8ArrayMemory0 = null;
|
|
165
|
+
function getUint8ArrayMemory0() {
|
|
166
|
+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
|
167
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
168
|
+
}
|
|
169
|
+
return cachedUint8ArrayMemory0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function passArray8ToWasm0(arg, malloc) {
|
|
173
|
+
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
|
174
|
+
getUint8ArrayMemory0().set(arg, ptr / 1);
|
|
175
|
+
WASM_VECTOR_LEN = arg.length;
|
|
176
|
+
return ptr;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
180
|
+
if (realloc === undefined) {
|
|
181
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
182
|
+
const ptr = malloc(buf.length, 1) >>> 0;
|
|
183
|
+
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
|
184
|
+
WASM_VECTOR_LEN = buf.length;
|
|
185
|
+
return ptr;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let len = arg.length;
|
|
189
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
190
|
+
|
|
191
|
+
const mem = getUint8ArrayMemory0();
|
|
192
|
+
|
|
193
|
+
let offset = 0;
|
|
194
|
+
|
|
195
|
+
for (; offset < len; offset++) {
|
|
196
|
+
const code = arg.charCodeAt(offset);
|
|
197
|
+
if (code > 0x7F) break;
|
|
198
|
+
mem[ptr + offset] = code;
|
|
199
|
+
}
|
|
200
|
+
if (offset !== len) {
|
|
201
|
+
if (offset !== 0) {
|
|
202
|
+
arg = arg.slice(offset);
|
|
203
|
+
}
|
|
204
|
+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
205
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
206
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
207
|
+
|
|
208
|
+
offset += ret.written;
|
|
209
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
WASM_VECTOR_LEN = offset;
|
|
213
|
+
return ptr;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
217
|
+
cachedTextDecoder.decode();
|
|
218
|
+
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
|
219
|
+
let numBytesDecoded = 0;
|
|
220
|
+
function decodeText(ptr, len) {
|
|
221
|
+
numBytesDecoded += len;
|
|
222
|
+
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
|
223
|
+
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
224
|
+
cachedTextDecoder.decode();
|
|
225
|
+
numBytesDecoded = len;
|
|
226
|
+
}
|
|
227
|
+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const cachedTextEncoder = new TextEncoder();
|
|
231
|
+
|
|
232
|
+
if (!('encodeInto' in cachedTextEncoder)) {
|
|
233
|
+
cachedTextEncoder.encodeInto = function (arg, view) {
|
|
234
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
235
|
+
view.set(buf);
|
|
236
|
+
return {
|
|
237
|
+
read: arg.length,
|
|
238
|
+
written: buf.length
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let WASM_VECTOR_LEN = 0;
|
|
244
|
+
|
|
245
|
+
let wasmModule, wasmInstance, wasm;
|
|
246
|
+
function __wbg_finalize_init(instance, module) {
|
|
247
|
+
wasmInstance = instance;
|
|
248
|
+
wasm = instance.exports;
|
|
249
|
+
wasmModule = module;
|
|
250
|
+
cachedDataViewMemory0 = null;
|
|
251
|
+
cachedUint8ArrayMemory0 = null;
|
|
252
|
+
wasm.__wbindgen_start();
|
|
253
|
+
return wasm;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function __wbg_load(module, imports) {
|
|
257
|
+
if (typeof Response === 'function' && module instanceof Response) {
|
|
258
|
+
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
|
259
|
+
try {
|
|
260
|
+
return await WebAssembly.instantiateStreaming(module, imports);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
const validResponse = module.ok && expectedResponseType(module.type);
|
|
263
|
+
|
|
264
|
+
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
|
265
|
+
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
|
266
|
+
|
|
267
|
+
} else { throw e; }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const bytes = await module.arrayBuffer();
|
|
272
|
+
return await WebAssembly.instantiate(bytes, imports);
|
|
273
|
+
} else {
|
|
274
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
275
|
+
|
|
276
|
+
if (instance instanceof WebAssembly.Instance) {
|
|
277
|
+
return { instance, module };
|
|
278
|
+
} else {
|
|
279
|
+
return instance;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function expectedResponseType(type) {
|
|
284
|
+
switch (type) {
|
|
285
|
+
case 'basic': case 'cors': case 'default': return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function initSync(module) {
|
|
292
|
+
if (wasm !== undefined) return wasm;
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if (module !== undefined) {
|
|
296
|
+
if (Object.getPrototypeOf(module) === Object.prototype) {
|
|
297
|
+
({module} = module)
|
|
298
|
+
} else {
|
|
299
|
+
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const imports = __wbg_get_imports();
|
|
304
|
+
if (!(module instanceof WebAssembly.Module)) {
|
|
305
|
+
module = new WebAssembly.Module(module);
|
|
306
|
+
}
|
|
307
|
+
const instance = new WebAssembly.Instance(module, imports);
|
|
308
|
+
return __wbg_finalize_init(instance, module);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function __wbg_init(module_or_path) {
|
|
312
|
+
if (wasm !== undefined) return wasm;
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if (module_or_path !== undefined) {
|
|
316
|
+
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
|
317
|
+
({module_or_path} = module_or_path)
|
|
318
|
+
} else {
|
|
319
|
+
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (module_or_path === undefined) {
|
|
324
|
+
module_or_path = new URL('barcode_wasm_bg.wasm', import.meta.url);
|
|
325
|
+
}
|
|
326
|
+
const imports = __wbg_get_imports();
|
|
327
|
+
|
|
328
|
+
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
|
329
|
+
module_or_path = fetch(module_or_path);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
|
333
|
+
|
|
334
|
+
return __wbg_finalize_init(instance, module);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export { initSync, __wbg_init as default };
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const __wbg_get_scanresult_format: (a: number) => [number, number];
|
|
5
|
+
export const __wbg_get_scanresult_text: (a: number) => [number, number];
|
|
6
|
+
export const __wbg_scanresult_free: (a: number, b: number) => void;
|
|
7
|
+
export const __wbg_set_scanresult_format: (a: number, b: number, c: number) => void;
|
|
8
|
+
export const __wbg_set_scanresult_text: (a: number, b: number, c: number) => void;
|
|
9
|
+
export const scan_luma: (a: number, b: number, c: number, d: number) => [number, number];
|
|
10
|
+
export const scan_rgba: (a: number, b: number, c: number, d: number) => [number, number];
|
|
11
|
+
export const __wbindgen_externrefs: WebAssembly.Table;
|
|
12
|
+
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
13
|
+
export const __wbindgen_malloc: (a: number, b: number) => number;
|
|
14
|
+
export const __externref_drop_slice: (a: number, b: number) => void;
|
|
15
|
+
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
16
|
+
export const __wbindgen_start: () => void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "barcode-wasm",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"description": "WASM bindings for barcode-core (wasm-pack -> npm).",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"barcode_wasm_bg.wasm",
|
|
9
|
+
"barcode_wasm.js",
|
|
10
|
+
"barcode_wasm.d.ts"
|
|
11
|
+
],
|
|
12
|
+
"main": "barcode_wasm.js",
|
|
13
|
+
"types": "barcode_wasm.d.ts",
|
|
14
|
+
"sideEffects": [
|
|
15
|
+
"./snippets/*"
|
|
16
|
+
]
|
|
17
|
+
}
|