tapirscan 1.2.1 → 1.3.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.
Files changed (84) hide show
  1. package/LICENSE +220 -0
  2. package/README.md +384 -226
  3. package/dist/browser-worker.d.ts +33 -0
  4. package/dist/browser-worker.js +83 -0
  5. package/dist/browser.d.ts +44 -0
  6. package/dist/browser.js +211 -0
  7. package/dist/freeze.d.ts +6 -0
  8. package/dist/freeze.js +7 -0
  9. package/dist/index.d.ts +53 -46
  10. package/dist/index.js +168 -209
  11. package/dist/layout.d.ts +19 -0
  12. package/dist/layout.js +27 -0
  13. package/dist/multiformat/format-registry.d.ts +26 -0
  14. package/dist/multiformat/format-registry.js +63 -0
  15. package/dist/multiformat/formats.d.ts +4 -24
  16. package/dist/multiformat/formats.js +15 -46
  17. package/dist/rust-session.d.ts +19 -0
  18. package/dist/rust-session.js +102 -0
  19. package/examples/camera.html +28 -29
  20. package/examples/vite/README.md +31 -0
  21. package/examples/vite/index.html +14 -0
  22. package/examples/vite/main.js +33 -0
  23. package/examples/vite/package.json +16 -0
  24. package/examples/vite/vite.config.js +6 -0
  25. package/package.json +28 -11
  26. package/wasm/build.json +295 -0
  27. package/wasm/experimental-turbo16.wasm +0 -0
  28. package/wasm/experimental-turbo2.wasm +0 -0
  29. package/wasm/experimental-turbo4.wasm +0 -0
  30. package/wasm/experimental-turbo8.wasm +0 -0
  31. package/wasm/high.wasm +0 -0
  32. package/wasm/low.wasm +0 -0
  33. package/wasm/medium.wasm +0 -0
  34. package/wasm/very-high.wasm +0 -0
  35. package/dist/completion-host.d.mts +0 -40
  36. package/dist/completion-host.mjs +0 -188
  37. package/dist/detail-20260914/continuity.d.mts +0 -2
  38. package/dist/detail-20260914/continuity.mjs +0 -62
  39. package/dist/detail-20260914/detail-proposals-rich.d.mts +0 -13
  40. package/dist/detail-20260914/detail-proposals-rich.mjs +0 -53
  41. package/dist/detail-20260914/direct-recovery.d.mts +0 -28
  42. package/dist/detail-20260914/direct-recovery.mjs +0 -139
  43. package/dist/detail-20260914/host.d.mts +0 -40
  44. package/dist/detail-20260914/host.mjs +0 -280
  45. package/dist/detail-20260914/scanner.d.mts +0 -12
  46. package/dist/detail-20260914/scanner.mjs +0 -64
  47. package/dist/detail-20260914/source-evidence.d.mts +0 -10
  48. package/dist/detail-20260914/source-evidence.mjs +0 -94
  49. package/dist/detail-canvas.d.ts +0 -35
  50. package/dist/detail-canvas.js +0 -73
  51. package/dist/detail-runtime/direct-recovery.d.mts +0 -28
  52. package/dist/detail-runtime/direct-recovery.mjs +0 -142
  53. package/dist/detail-runtime/host.d.mts +0 -40
  54. package/dist/detail-runtime/host.mjs +0 -284
  55. package/dist/detail-runtime/scanner.d.mts +0 -12
  56. package/dist/detail-runtime/scanner.mjs +0 -70
  57. package/dist/detail.d.ts +0 -20
  58. package/dist/detail.js +0 -84
  59. package/dist/host.d.ts +0 -112
  60. package/dist/host.js +0 -184
  61. package/dist/host64.d.ts +0 -112
  62. package/dist/host64.js +0 -184
  63. package/dist/multiformat/coverage.d.ts +0 -7
  64. package/dist/multiformat/coverage.js +0 -26
  65. package/dist/multiformat/geometry.d.ts +0 -37
  66. package/dist/multiformat/geometry.js +0 -132
  67. package/dist/multiformat/linear-duplicates.d.ts +0 -16
  68. package/dist/multiformat/linear-duplicates.js +0 -163
  69. package/dist/multiformat/pixels.d.ts +0 -3
  70. package/dist/multiformat/pixels.js +0 -36
  71. package/dist/multiformat/scanner.d.ts +0 -60
  72. package/dist/multiformat/scanner.js +0 -382
  73. package/dist/multiformat-host.d.ts +0 -121
  74. package/dist/multiformat-host.js +0 -207
  75. package/dist/policy.d.ts +0 -12
  76. package/dist/policy.js +0 -12
  77. package/examples/scan-worker.mjs +0 -20
  78. package/examples/worker-client.mjs +0 -63
  79. package/wasm/high-shared-retail-portable-20260919.wasm +0 -0
  80. package/wasm/low-shared-retail-portable-20260919.wasm +0 -0
  81. package/wasm/medium-shared-retail-portable-20260919.wasm +0 -0
  82. package/wasm/multiformat.json +0 -7
  83. package/wasm/multiformat.wasm +0 -0
  84. package/wasm/very-high-shared-retail-portable-20260919.wasm +0 -0
@@ -1,44 +1,10 @@
1
1
  /** Explicit opt-in formats for the independent Medium extension. */
2
- export const formatBits = {
3
- EAN13: 1,
4
- UPCA: 2,
5
- EAN8: 4,
6
- UPCE: 8,
7
- Code128: 16,
8
- Code39: 32,
9
- ITF: 64,
10
- Codabar: 128,
11
- Code93: 256,
12
- QRCode: 512,
13
- DataMatrix: 1024,
14
- PDF417: 2048,
15
- Aztec: 4096,
16
- DataBar: 8192,
17
- DataBarExpanded: 16384,
18
- MaxiCode: 131072,
19
- };
20
- export const retailFormats = ["EAN13", "UPCA", "EAN8", "UPCE"];
21
- export const commonLinearFormats = [
22
- ...retailFormats,
23
- "Code128",
24
- "Code39",
25
- "ITF",
26
- ];
27
- export const commonFormats = [...commonLinearFormats, "QRCode", "DataMatrix"];
28
- export const linearFormats = [
29
- ...commonLinearFormats,
30
- "Codabar",
31
- "Code93",
32
- "DataBar",
33
- "DataBarExpanded",
34
- ];
35
- export const matrixFormats = [
36
- "QRCode",
37
- "DataMatrix",
38
- "PDF417",
39
- "Aztec",
40
- "MaxiCode",
41
- ];
2
+ import { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, } from "./format-registry.js";
3
+ export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, };
4
+ /** Own-property check that works in Chrome 91 (`Object.hasOwn` needs Chrome 93). */
5
+ export function hasKey(object, key) {
6
+ return Object.prototype.hasOwnProperty.call(object, key);
7
+ }
42
8
  export function resolveFormats(input) {
43
9
  if (input === "retail")
44
10
  return [...retailFormats];
@@ -54,15 +20,18 @@ export function resolveFormats(input) {
54
20
  return [...linearFormats, ...matrixFormats];
55
21
  if (input === undefined)
56
22
  return [...retailFormats];
57
- if (typeof input === "string" && Object.hasOwn(formatBits, input))
58
- return [input];
23
+ if (typeof input === "string") {
24
+ if (hasKey(formatBits, input))
25
+ return [input];
26
+ throw new TypeError(`Unsupported barcode format or preset: ${input}`);
27
+ }
59
28
  if (!Array.isArray(input) || input.length === 0)
60
- throw Error("Choose at least one barcode format.");
29
+ throw new TypeError("Choose at least one barcode format.");
61
30
  for (const value of input) {
62
31
  if (typeof value !== "string")
63
- throw Error("Barcode format must be a string.");
64
- if (!Object.hasOwn(formatBits, value))
65
- throw Error(`Unsupported barcode format: ${value}`);
32
+ throw new TypeError("Barcode format must be a string.");
33
+ if (!hasKey(formatBits, value))
34
+ throw new TypeError(`Unsupported barcode format: ${value}`);
66
35
  }
67
36
  return [...new Set(input)];
68
37
  }
@@ -0,0 +1,19 @@
1
+ export declare class ScannerError extends Error {
2
+ readonly code: string;
3
+ constructor(code: string, message: string);
4
+ }
5
+ export declare class RustScannerSession {
6
+ private exports;
7
+ private handle;
8
+ private constructor();
9
+ static create(bytes: ArrayBuffer, mode: number, formats: number, addOnPolicy: number, experimentalTurbo?: number): Promise<RustScannerSession>;
10
+ scan(image: {
11
+ data: Uint8Array;
12
+ width: number;
13
+ height: number;
14
+ channels: number;
15
+ stride: number;
16
+ }, flags: number, formats: number): unknown;
17
+ dispose(): void;
18
+ private activeExports;
19
+ }
@@ -0,0 +1,102 @@
1
+ // Stateless for complete inputs, so one decoder serves every scan.
2
+ const outputDecoder = new TextDecoder();
3
+ export class ScannerError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = "ScannerError";
9
+ }
10
+ }
11
+ const exportNames = [
12
+ "tapirscan_abi_version",
13
+ "tapirscan_mode",
14
+ "tapirscan_create",
15
+ "tapirscan_destroy",
16
+ "tapirscan_prepare",
17
+ "tapirscan_input_ptr",
18
+ "tapirscan_input_len",
19
+ "tapirscan_scan",
20
+ "tapirscan_output_ptr",
21
+ "tapirscan_output_len",
22
+ ];
23
+ function status(code, operation) {
24
+ if (code === 0)
25
+ return;
26
+ const names = {
27
+ 1: "invalid_input",
28
+ 2: "disposed",
29
+ 4: "engine",
30
+ 5: "capacity",
31
+ };
32
+ throw new ScannerError(names[code] ?? `core_${String(code)}`, `${operation} failed (${String(code)})`);
33
+ }
34
+ export class RustScannerSession {
35
+ exports;
36
+ handle;
37
+ constructor(exports, handle) {
38
+ this.exports = exports;
39
+ this.handle = handle;
40
+ }
41
+ static async create(bytes, mode, formats, addOnPolicy, experimentalTurbo = 0) {
42
+ const instance = await WebAssembly.instantiate(bytes, {});
43
+ const exports = instance.instance.exports;
44
+ if (!(exports.memory instanceof WebAssembly.Memory))
45
+ throw new ScannerError("abi_shape", "Missing WASM memory");
46
+ for (const name of exportNames)
47
+ if (typeof exports[name] !== "function")
48
+ throw new ScannerError("abi_shape", `Missing WASM export: ${name}`);
49
+ if (exports.tapirscan_abi_version() !== 2)
50
+ throw new ScannerError("abi_version", "Unsupported scanner ABI");
51
+ if (exports.tapirscan_mode() !== mode)
52
+ throw new ScannerError("abi_mode", "Scanner WASM mode does not match the requested mode");
53
+ const tier = exports.tapirscan_experimental_turbo;
54
+ const actualTurbo = typeof tier === "function" ? tier() : 0;
55
+ if (actualTurbo !== experimentalTurbo)
56
+ throw new ScannerError("abi_turbo", "Scanner WASM does not match the requested experimentalTurbo preset");
57
+ const handle = exports.tapirscan_create(mode, formats, addOnPolicy);
58
+ if (!handle)
59
+ throw new ScannerError("capacity", "Could not create scanner session");
60
+ return new RustScannerSession(exports, handle);
61
+ }
62
+ scan(image, flags, formats) {
63
+ const exports = this.activeExports();
64
+ status(exports.tapirscan_prepare(this.handle, image.width, image.height, image.channels, image.stride), "Image preparation");
65
+ const length = exports.tapirscan_input_len(this.handle);
66
+ const expected = (image.height - 1) * image.stride + image.width * image.channels;
67
+ if (length !== expected)
68
+ throw new ScannerError("abi_shape", "Input allocation mismatch");
69
+ new Uint8Array(exports.memory.buffer, exports.tapirscan_input_ptr(this.handle), length).set(image.data.subarray(0, length));
70
+ const result = exports.tapirscan_scan(this.handle, flags, formats);
71
+ const output = new Uint8Array(exports.memory.buffer, exports.tapirscan_output_ptr(this.handle), exports.tapirscan_output_len(this.handle));
72
+ let decoded;
73
+ try {
74
+ decoded = JSON.parse(outputDecoder.decode(output));
75
+ }
76
+ catch {
77
+ throw new ScannerError("invalid_output", "Scanner returned invalid JSON");
78
+ }
79
+ if (result !== 0) {
80
+ const message = decoded && typeof decoded === "object" && "error" in decoded
81
+ ? String(decoded.error)
82
+ : `Scan failed (${String(result)})`;
83
+ throw new ScannerError(result === 4 ? "engine" : `core_${String(result)}`, message);
84
+ }
85
+ return decoded;
86
+ }
87
+ dispose() {
88
+ if (!this.handle || !this.exports)
89
+ return;
90
+ const exports = this.exports;
91
+ const handle = this.handle;
92
+ // WASM memory never shrinks: drop the instance so a retained scanner frees it.
93
+ this.exports = undefined;
94
+ this.handle = 0;
95
+ status(exports.tapirscan_destroy(handle), "Scanner disposal");
96
+ }
97
+ activeExports() {
98
+ if (!this.handle || !this.exports)
99
+ throw new ScannerError("disposed", "Scanner is disposed");
100
+ return this.exports;
101
+ }
102
+ }
@@ -1,60 +1,59 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
3
  <meta charset="utf-8" />
4
- <title>Tapirscan worker example</title>
4
+ <title>Tapirscan camera example</title>
5
5
  <button id="start">Start camera</button>
6
6
  <button id="stop" disabled>Stop</button>
7
7
  <video autoplay muted playsinline style="max-width: 100%"></video>
8
8
  <pre role="status"></pre>
9
9
  <script type="module">
10
- import { createScannerWorker } from "./worker-client.mjs";
10
+ // Serve this package directory and open /examples/camera.html on localhost.
11
+ import { Scanner } from "../dist/browser.js";
11
12
  const video = document.querySelector("video");
12
13
  const status = document.querySelector("pre");
13
14
  const start = document.querySelector("#start");
14
15
  const stop = document.querySelector("#stop");
15
- const canvas = document.createElement("canvas");
16
- const context = canvas.getContext("2d", { willReadFrequently: true });
17
- let running = false;
18
16
  let scanner;
19
17
  let stream;
18
+
20
19
  function shutdown() {
21
- running = false;
22
20
  scanner?.dispose();
23
- scanner = undefined;
24
21
  stream?.getTracks().forEach((track) => track.stop());
25
- stream = undefined;
22
+ scanner = stream = undefined;
26
23
  video.srcObject = null;
24
+ start.disabled = false;
25
+ stop.disabled = true;
27
26
  }
27
+
28
28
  start.onclick = async () => {
29
29
  start.disabled = true;
30
- running = true;
30
+ let current;
31
31
  try {
32
- scanner = await createScannerWorker({ formats: ["EAN13", "QRCode"] });
33
- if (!running) return;
34
- stream = await navigator.mediaDevices.getUserMedia({
32
+ // Loads in a worker while the camera starts.
33
+ current = scanner = new Scanner({ formats: ["EAN13", "QRCode"] });
34
+ const camera = await navigator.mediaDevices.getUserMedia({
35
35
  video: { facingMode: "environment" },
36
36
  });
37
- if (!running) return;
37
+ if (scanner !== current) {
38
+ camera.getTracks().forEach((track) => track.stop());
39
+ return;
40
+ }
41
+ stream = camera;
38
42
  video.srcObject = stream;
39
- await video.play();
40
43
  stop.disabled = false;
41
- while (running) {
42
- canvas.width = video.videoWidth;
43
- canvas.height = video.videoHeight;
44
- context.drawImage(video, 0, 0);
45
- const result = await scanner.scan(
46
- context.getImageData(0, 0, canvas.width, canvas.height),
47
- );
48
- status.textContent = JSON.stringify(result.values);
49
- // Backpressure: capture only after the previous scan has completed.
50
- await new Promise(requestAnimationFrame);
44
+ while (scanner === current) {
45
+ // Scan each new frame at most once; awaiting the scan avoids a backlog.
46
+ await new Promise((resolve) => video.requestVideoFrameCallback(resolve));
47
+ if (scanner !== current) break;
48
+ const barcodes = await current.scan(video);
49
+ if (scanner === current) status.textContent = JSON.stringify(barcodes.values);
51
50
  }
52
51
  } catch (error) {
53
- if (running) status.textContent = error.message;
54
- } finally {
55
- shutdown();
56
- start.disabled = false;
57
- stop.disabled = true;
52
+ // Also reached when construction fails: report it and re-enable Start.
53
+ if (scanner === current) {
54
+ status.textContent = error.message;
55
+ shutdown();
56
+ }
58
57
  }
59
58
  };
60
59
  stop.onclick = shutdown;
@@ -0,0 +1,31 @@
1
+ # Vite example
2
+
3
+ Copy this directory into your application workspace, then run:
4
+
5
+ ```sh
6
+ npm install
7
+ npm run dev
8
+ ```
9
+
10
+ Choose a barcode photo. `tapirscan/browser` decodes the file and scans it in a
11
+ worker; Vite bundles the worker and WASM files without plugins or copy steps.
12
+ The only configuration is `optimizeDeps.exclude` in `vite.config.js`, which the
13
+ development server needs to serve those files from the package. Retail formats
14
+ are enabled by default.
15
+
16
+ To test a locally built package, install its npm tarball instead of
17
+ the registry dependency before running the example:
18
+
19
+ ```sh
20
+ npm install /absolute/path/to/tapirscan-X.Y.Z.tgz
21
+ ```
22
+
23
+ Test production hosting, including a subpath:
24
+
25
+ ```sh
26
+ npm run build -- --base=/scanner/
27
+ npm run preview -- --base=/scanner/
28
+ ```
29
+
30
+ Open `/scanner/` on the preview server. For Svelte and SvelteKit, see the
31
+ [package README](../../README.md#browser-apps-react-and-svelte).
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Tapirscan with Vite</title>
7
+ </head>
8
+ <body>
9
+ <h1>Scan a barcode photo</h1>
10
+ <input type="file" accept="image/*" aria-label="Barcode photo" disabled />
11
+ <pre role="status">Loading scanner…</pre>
12
+ <script type="module" src="/main.js"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,33 @@
1
+ import { Scanner } from "tapirscan/browser";
2
+
3
+ const input = document.querySelector("input");
4
+ const status = document.querySelector('[role="status"]');
5
+ // Scanning runs in a worker, so the page stays responsive.
6
+ const scanner = new Scanner();
7
+
8
+ scanner.ready.then(
9
+ () => {
10
+ input.disabled = false;
11
+ status.textContent = "Choose a photo.";
12
+ },
13
+ (error) => {
14
+ status.textContent = error.message;
15
+ },
16
+ );
17
+ // Only the latest selection may update the page; scans can finish out of order.
18
+ let latest = 0;
19
+ input.onchange = async () => {
20
+ const file = input.files[0];
21
+ if (!file) return;
22
+ const request = ++latest;
23
+ status.textContent = "Scanning…";
24
+ try {
25
+ const { barcodes } = await scanner.scan(file);
26
+ if (request !== latest) return;
27
+ status.textContent = barcodes.length
28
+ ? barcodes.map((barcode) => `${barcode.format} ${barcode.text}`).join("\n")
29
+ : "No barcode found.";
30
+ } catch (error) {
31
+ if (request === latest) status.textContent = error.message;
32
+ }
33
+ };
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "tapirscan-vite-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "tapirscan": "^1.3.0"
12
+ },
13
+ "devDependencies": {
14
+ "vite": "^6.3.5"
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+
3
+ export default defineConfig({
4
+ // The dev server must serve Tapirscan's worker and WASM files from the package.
5
+ optimizeDeps: { exclude: ["tapirscan"] },
6
+ });
package/package.json CHANGED
@@ -1,31 +1,48 @@
1
1
  {
2
2
  "name": "tapirscan",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Orientation-aware barcode scanning with a Rust/WASM core and four effort modes",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./dist/index.d.ts",
9
9
  "import": "./dist/index.js"
10
- }
10
+ },
11
+ "./browser": {
12
+ "types": "./dist/browser.d.ts",
13
+ "import": "./dist/browser.js"
14
+ },
15
+ "./wasm/low.wasm": "./wasm/low.wasm",
16
+ "./wasm/medium.wasm": "./wasm/medium.wasm",
17
+ "./wasm/high.wasm": "./wasm/high.wasm",
18
+ "./wasm/very-high.wasm": "./wasm/very-high.wasm",
19
+ "./wasm/experimental-turbo2.wasm": "./wasm/experimental-turbo2.wasm",
20
+ "./wasm/experimental-turbo4.wasm": "./wasm/experimental-turbo4.wasm",
21
+ "./wasm/experimental-turbo8.wasm": "./wasm/experimental-turbo8.wasm",
22
+ "./wasm/experimental-turbo16.wasm": "./wasm/experimental-turbo16.wasm"
11
23
  },
12
24
  "files": [
13
25
  "dist",
14
26
  "examples",
15
- "wasm/multiformat.json",
16
27
  "THIRD_PARTY_NOTICES.md",
17
- "wasm/low-shared-retail-portable-20260919.wasm",
18
- "wasm/medium-shared-retail-portable-20260919.wasm",
19
- "wasm/high-shared-retail-portable-20260919.wasm",
20
- "wasm/very-high-shared-retail-portable-20260919.wasm",
21
- "wasm/multiformat.wasm"
28
+ "wasm/low.wasm",
29
+ "wasm/medium.wasm",
30
+ "wasm/high.wasm",
31
+ "wasm/very-high.wasm",
32
+ "wasm/experimental-turbo2.wasm",
33
+ "wasm/experimental-turbo4.wasm",
34
+ "wasm/experimental-turbo8.wasm",
35
+ "wasm/experimental-turbo16.wasm",
36
+ "wasm/build.json"
22
37
  ],
23
38
  "scripts": {
24
- "build": "tsc -p tsconfig.json",
25
- "test": "node --test test/scanner.mjs test/linear-duplicates.mjs test/shared-retail.mjs && tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext test/api-types.ts",
39
+ "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.json",
40
+ "test": "node --test test/camera-example.mjs test/scanner.mjs test/shared-retail.mjs test/loading.mjs test/turbo.mjs test/packaging.mjs && tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext test/api-types.ts",
41
+ "test:browser": "node --test --test-timeout=120000 test/browser.mjs",
26
42
  "prepack": "npm run build && node scripts/verify-package.mjs"
27
43
  },
28
44
  "devDependencies": {
45
+ "playwright-core": "1.63.0",
29
46
  "typescript": "5.8.3"
30
47
  },
31
48
  "keywords": [
@@ -39,7 +56,7 @@
39
56
  "engines": {
40
57
  "node": ">=20"
41
58
  },
42
- "license": "MIT",
59
+ "license": "MIT OR Apache-2.0",
43
60
  "author": "Florian Nick",
44
61
  "repository": {
45
62
  "type": "git",