tapirscan 1.0.0 → 1.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 +173 -104
- package/dist/completion-host.d.mts +40 -0
- package/dist/completion-host.mjs +188 -0
- package/dist/detail-runtime/direct-recovery.d.mts +28 -0
- package/dist/detail-runtime/direct-recovery.mjs +142 -0
- package/dist/detail-runtime/host.d.mts +40 -0
- package/dist/detail-runtime/host.mjs +282 -0
- package/dist/detail-runtime/scanner.d.mts +12 -0
- package/dist/detail-runtime/scanner.mjs +70 -0
- package/dist/detail.d.ts +3 -4
- package/dist/detail.js +3 -6
- package/dist/index.d.ts +90 -33
- package/dist/index.js +169 -78
- package/dist/multiformat/coverage.d.ts +7 -0
- package/dist/multiformat/coverage.js +26 -0
- package/dist/multiformat/formats.d.ts +4 -2
- package/dist/multiformat/formats.js +13 -1
- package/dist/multiformat/geometry.d.ts +5 -0
- package/dist/multiformat/geometry.js +13 -3
- package/dist/multiformat/linear-duplicates.d.ts +16 -0
- package/dist/multiformat/linear-duplicates.js +163 -0
- package/dist/multiformat/scanner.d.ts +11 -2
- package/dist/multiformat/scanner.js +127 -24
- package/examples/camera.html +63 -0
- package/examples/scan-worker.mjs +20 -0
- package/examples/worker-client.mjs +63 -0
- package/package.json +7 -6
- package/wasm/{high-release-20260915.wasm → high-complete-release-20260916.wasm} +0 -0
- package/wasm/{low-release-20260915.wasm → low-complete-release-20260916.wasm} +0 -0
- package/wasm/{medium-release-20260915.wasm → medium-complete-release-20260916.wasm} +0 -0
- package/wasm/multiformat.json +2 -1
- package/wasm/multiformat.wasm +0 -0
- package/wasm/{very-high-release-20260915.wasm → very-high-complete-release-20260916.wasm} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
import type { Recovery, DetailRegion } from "./detail-20260914/scanner.mjs";
|
|
2
2
|
import { type Barcode as FormatBarcode } from "./multiformat/scanner.js";
|
|
3
3
|
import { type Format, type FormatSelection } from "./multiformat/formats.js";
|
|
4
|
-
export { formatBits, linearFormats, matrixFormats, retailFormats } from "./multiformat/formats.js";
|
|
4
|
+
export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, } from "./multiformat/formats.js";
|
|
5
5
|
export type { Format, FormatSelection } from "./multiformat/formats.js";
|
|
6
|
-
import { type
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
6
|
+
import { type ScanFrame, type Quad as HostQuad } from "./completion-host.mjs";
|
|
7
|
+
/** Source-image corners, in pixels. */
|
|
8
|
+
export type Quad = readonly [
|
|
9
|
+
readonly [number, number],
|
|
10
|
+
readonly [number, number],
|
|
11
|
+
readonly [number, number],
|
|
12
|
+
readonly [number, number]
|
|
13
|
+
];
|
|
14
|
+
/** Decoded pixels; stride defaults to width * channels. Alpha is ignored. */
|
|
15
|
+
export interface Image {
|
|
16
|
+
readonly data: Uint8Array;
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
readonly channels: 1 | 3 | 4;
|
|
20
|
+
readonly stride?: number;
|
|
21
|
+
}
|
|
22
|
+
export { ScannerError } from "./completion-host.mjs";
|
|
23
|
+
type RawDiagnosticBarcode = FormatBarcode | (ScanFrame["barcodes"][number] & {
|
|
10
24
|
format: "EAN13";
|
|
11
25
|
});
|
|
12
26
|
export type Mode = "low" | "medium" | "high" | "very-high";
|
|
13
27
|
export interface ScanOptions {
|
|
14
|
-
|
|
28
|
+
/** Finish effort-selected EAN13/UPCA work beyond shared frame budgets. */
|
|
29
|
+
finishCandidates?: boolean;
|
|
30
|
+
/** Per-call subset of the formats configured at creation. */
|
|
31
|
+
formats?: FormatSelection;
|
|
15
32
|
debug?: boolean;
|
|
16
|
-
/** Compatibility alias; use debug. Decoded polygons are always returned. */
|
|
17
|
-
includeRegions?: boolean;
|
|
18
33
|
}
|
|
19
|
-
|
|
34
|
+
interface RawDiagnostics {
|
|
20
35
|
schemaVersion: 2;
|
|
21
36
|
mode: Mode;
|
|
22
37
|
multiple: boolean;
|
|
23
38
|
elapsedMs: number;
|
|
24
39
|
localizationLimited: boolean;
|
|
25
40
|
scan: {
|
|
26
|
-
barcodes:
|
|
41
|
+
barcodes: RawDiagnosticBarcode[];
|
|
27
42
|
unfinished: boolean;
|
|
28
43
|
regions?: FormatBarcode[];
|
|
29
44
|
} & Partial<Omit<ScanFrame, "barcodes" | "unfinished">>;
|
|
30
45
|
localization?: {
|
|
31
46
|
proposals: {
|
|
32
|
-
polygon:
|
|
47
|
+
polygon: HostQuad;
|
|
33
48
|
score: number;
|
|
34
49
|
text: string;
|
|
35
50
|
}[];
|
|
@@ -45,33 +60,74 @@ export interface Diagnostics {
|
|
|
45
60
|
candidateIndex: number;
|
|
46
61
|
}[];
|
|
47
62
|
}
|
|
63
|
+
/** Deeply immutable scan evidence, independent of the scanner lifetime. */
|
|
64
|
+
type ReadonlyDeep<T> = T extends object ? {
|
|
65
|
+
readonly [K in keyof T]: ReadonlyDeep<T[K]>;
|
|
66
|
+
} : T;
|
|
67
|
+
export type DiagnosticBarcode = ReadonlyDeep<RawDiagnosticBarcode>;
|
|
68
|
+
/** Source geometry with no accepted decode; format is a reader hint. */
|
|
69
|
+
export interface UndecodedRegion {
|
|
70
|
+
readonly format: Format | "Unknown";
|
|
71
|
+
readonly polygon: Quad;
|
|
72
|
+
}
|
|
73
|
+
/** Stable region evidence. null means this reader did not expose that evidence. */
|
|
74
|
+
export interface RegionEvidence {
|
|
75
|
+
readonly proposals: ReadonlyDeep<NonNullable<RawDiagnostics["localization"]>["proposals"]> | null;
|
|
76
|
+
readonly searchWindows: ReadonlyDeep<NonNullable<RawDiagnostics["searchWindows"]>> | null;
|
|
77
|
+
readonly undecoded: readonly UndecodedRegion[];
|
|
78
|
+
}
|
|
79
|
+
export type Diagnostics = ReadonlyDeep<RawDiagnostics> & {
|
|
80
|
+
readonly regions: RegionEvidence;
|
|
81
|
+
};
|
|
82
|
+
export interface StructuredAppend {
|
|
83
|
+
/** One-based symbol index; symbols are not automatically assembled. */
|
|
84
|
+
readonly index: number;
|
|
85
|
+
readonly count: number;
|
|
86
|
+
readonly id?: string;
|
|
87
|
+
readonly parity?: number;
|
|
88
|
+
}
|
|
48
89
|
export interface Barcode {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
/** Payload bytes before character-set interpretation; absent when unavailable. */
|
|
91
|
+
readonly payloadBytes?: readonly number[];
|
|
92
|
+
readonly text: string;
|
|
93
|
+
readonly format: Format | "Unknown";
|
|
94
|
+
/** Reader-specific ranking evidence, not a probability or cross-reader confidence. */
|
|
95
|
+
readonly support: number;
|
|
96
|
+
readonly gs1?: boolean;
|
|
97
|
+
readonly readerInitialization?: boolean;
|
|
98
|
+
readonly structuredAppend?: StructuredAppend;
|
|
99
|
+
readonly eanAddOn?: string;
|
|
100
|
+
readonly polygon: Quad;
|
|
101
|
+
readonly rect: {
|
|
102
|
+
readonly left: number;
|
|
103
|
+
readonly top: number;
|
|
104
|
+
readonly width: number;
|
|
105
|
+
readonly height: number;
|
|
57
106
|
};
|
|
58
107
|
}
|
|
59
108
|
export interface ScanResult {
|
|
60
|
-
barcodes: Barcode[];
|
|
61
|
-
values: string[];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
109
|
+
readonly barcodes: readonly Barcode[];
|
|
110
|
+
readonly values: readonly string[];
|
|
111
|
+
/** Largest reader-specific support; not a cross-format confidence comparison. */
|
|
112
|
+
readonly best: Barcode | undefined;
|
|
113
|
+
readonly image: {
|
|
114
|
+
readonly width: number;
|
|
115
|
+
readonly height: number;
|
|
66
116
|
};
|
|
67
|
-
mode: Mode;
|
|
68
|
-
elapsedMs: number;
|
|
69
|
-
unfinished: boolean;
|
|
70
|
-
debug?: Diagnostics;
|
|
117
|
+
readonly mode: Mode;
|
|
118
|
+
readonly elapsedMs: number;
|
|
119
|
+
readonly unfinished: boolean;
|
|
120
|
+
readonly debug?: Diagnostics;
|
|
71
121
|
}
|
|
122
|
+
export type EanAddOnPolicy = "Ignore" | "Read" | "Require";
|
|
72
123
|
export interface ScannerOptions {
|
|
124
|
+
/** Optional EAN/UPC supplement policy, fixed at creation. */
|
|
125
|
+
eanAddOnPolicy?: EanAddOnPolicy;
|
|
73
126
|
mode?: Mode;
|
|
74
127
|
formats?: FormatSelection;
|
|
128
|
+
/** Directory containing the packaged WASMs; relative to the page in browsers. */
|
|
129
|
+
wasmBaseUrl?: string | URL;
|
|
130
|
+
/** Advanced loader, receiving URLs resolved against wasmBaseUrl. */
|
|
75
131
|
loadWasm?: (url: URL) => Promise<ArrayBuffer>;
|
|
76
132
|
}
|
|
77
133
|
export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
|
|
@@ -79,13 +135,14 @@ export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
|
|
|
79
135
|
export declare class Scanner {
|
|
80
136
|
private readonly host;
|
|
81
137
|
readonly mode: Mode;
|
|
82
|
-
private readonly
|
|
83
|
-
private readonly
|
|
138
|
+
private readonly configuredFormats;
|
|
139
|
+
private readonly addOnPolicy;
|
|
84
140
|
private constructor();
|
|
141
|
+
/** Formats available for scanning, fixed at creation. */
|
|
142
|
+
get formats(): readonly Format[];
|
|
143
|
+
get eanAddOnPolicy(): EanAddOnPolicy;
|
|
85
144
|
static create(options?: ScannerOptions): Promise<Scanner>;
|
|
86
145
|
scan(inputImage: PixelImage, options?: ScanOptions): ScanResult;
|
|
87
|
-
/** Convenience alias for result.best. */
|
|
88
|
-
best(result: ScanResult): Barcode | undefined;
|
|
89
146
|
dispose(): void;
|
|
90
147
|
}
|
|
91
148
|
/** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,48 @@
|
|
|
1
|
+
import { mergeLinearDuplicates } from "./multiformat/linear-duplicates.js";
|
|
1
2
|
import { ReleaseDetailScanner, fitLimits } from "./detail.js";
|
|
2
3
|
import { policy } from "./policy.js";
|
|
3
4
|
import { MediumMultiformatScanner } from "./multiformat/scanner.js";
|
|
4
5
|
import { resolveFormats } from "./multiformat/formats.js";
|
|
5
|
-
export { formatBits, linearFormats, matrixFormats, retailFormats } from "./multiformat/formats.js";
|
|
6
|
-
import { IndependentScanner } from "./host.
|
|
7
|
-
export { ScannerError } from "./host.
|
|
6
|
+
export { commonFormats, commonLinearFormats, formatBits, linearFormats, matrixFormats, retailFormats, } from "./multiformat/formats.js";
|
|
7
|
+
import { IndependentScanner, } from "./completion-host.mjs";
|
|
8
|
+
export { ScannerError } from "./completion-host.mjs";
|
|
8
9
|
function pixels(image) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const input = image;
|
|
11
|
+
if (input === null || typeof input !== "object")
|
|
12
|
+
throw new TypeError("Expected ImageData or decoded pixels");
|
|
13
|
+
const explicit = "channels" in image;
|
|
14
|
+
if (!explicit && !(image.data instanceof Uint8ClampedArray))
|
|
15
|
+
throw new TypeError("Use ImageData or an explicit buffer with channels");
|
|
16
|
+
const channels = explicit ? image.channels : 4;
|
|
17
|
+
const stride = explicit ? (image.stride ?? image.width * channels) : image.width * 4;
|
|
18
|
+
const required = (image.height - 1) * stride + image.width * channels;
|
|
19
|
+
if (!Number.isSafeInteger(image.width) ||
|
|
20
|
+
!Number.isSafeInteger(image.height) ||
|
|
21
|
+
image.width < 3 ||
|
|
22
|
+
image.height < 3 ||
|
|
23
|
+
image.width * image.height > 32 * 1024 * 1024 ||
|
|
24
|
+
![1, 3, 4].includes(channels) ||
|
|
25
|
+
!Number.isSafeInteger(stride) ||
|
|
26
|
+
stride < image.width * channels ||
|
|
27
|
+
required > 128 * 1024 * 1024 ||
|
|
28
|
+
!(image.data instanceof Uint8Array || image.data instanceof Uint8ClampedArray) ||
|
|
29
|
+
image.data.byteLength < required)
|
|
30
|
+
throw new TypeError("Invalid image dimensions, channels, stride or buffer (maximum 128 MiB)");
|
|
13
31
|
return {
|
|
14
32
|
data: new Uint8Array(image.data.buffer, image.data.byteOffset, image.data.byteLength),
|
|
15
33
|
width: image.width,
|
|
16
34
|
height: image.height,
|
|
17
|
-
channels
|
|
18
|
-
stride
|
|
35
|
+
channels,
|
|
36
|
+
stride,
|
|
19
37
|
};
|
|
20
38
|
}
|
|
39
|
+
/** Freeze only plain result data, never caller-owned input buffers. */
|
|
40
|
+
function freeze(value) {
|
|
41
|
+
for (const child of Object.values(value))
|
|
42
|
+
if (child !== null && typeof child === "object")
|
|
43
|
+
freeze(child);
|
|
44
|
+
return Object.freeze(value);
|
|
45
|
+
}
|
|
21
46
|
async function loadDefault(url) {
|
|
22
47
|
if (url.protocol === "file:") {
|
|
23
48
|
const nodeFs = "node:fs/promises";
|
|
@@ -29,14 +54,51 @@ async function loadDefault(url) {
|
|
|
29
54
|
throw new Error(`WASM load failed: ${String(response.status)}`);
|
|
30
55
|
return response.arrayBuffer();
|
|
31
56
|
}
|
|
57
|
+
function regionEvidence(raw) {
|
|
58
|
+
const undecoded = [];
|
|
59
|
+
if (raw.scan.regions) {
|
|
60
|
+
for (const region of raw.scan.regions) {
|
|
61
|
+
if (!raw.scan.barcodes.includes(region))
|
|
62
|
+
undecoded.push({ format: region.format, polygon: region.polygon });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const decoded = new Set(raw.scan.barcodes.flatMap((b) => ("candidate_indices" in b ? b.candidate_indices : [])));
|
|
67
|
+
raw.localization?.proposals.forEach((p, i) => {
|
|
68
|
+
if (!decoded.has(i))
|
|
69
|
+
undecoded.push({ format: "Unknown", polygon: p.polygon });
|
|
70
|
+
});
|
|
71
|
+
for (const attempt of raw.recovery?.attempts ?? []) {
|
|
72
|
+
const decoded = new Set(attempt.reads.flatMap((b) => b.candidate_indices));
|
|
73
|
+
attempt.proposals.forEach((p, i) => {
|
|
74
|
+
if (!decoded.has(i))
|
|
75
|
+
undecoded.push({ format: "Unknown", polygon: p.polygon });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
proposals: raw.localization?.proposals ?? null,
|
|
81
|
+
searchWindows: raw.searchWindows ?? null,
|
|
82
|
+
undecoded,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
32
85
|
function publicResult(raw, image, debug) {
|
|
33
|
-
const barcodes = raw.scan.barcodes.map((
|
|
86
|
+
const barcodes = raw.scan.barcodes.map((read) => {
|
|
87
|
+
const { text, format, polygon, support } = read;
|
|
34
88
|
const left = Math.floor(Math.min(...polygon.map((p) => p[0])));
|
|
35
89
|
const top = Math.floor(Math.min(...polygon.map((p) => p[1])));
|
|
36
90
|
return {
|
|
37
91
|
text,
|
|
38
92
|
format,
|
|
39
93
|
polygon,
|
|
94
|
+
support,
|
|
95
|
+
...("bytes" in read ? { payloadBytes: read.bytes } : {}),
|
|
96
|
+
...("gs1" in read ? { gs1: read.gs1 } : {}),
|
|
97
|
+
...("readerInitialization" in read
|
|
98
|
+
? { readerInitialization: read.readerInitialization }
|
|
99
|
+
: {}),
|
|
100
|
+
...("structuredAppend" in read ? { structuredAppend: read.structuredAppend } : {}),
|
|
101
|
+
...("eanAddOn" in read ? { eanAddOn: read.eanAddOn } : {}),
|
|
40
102
|
rect: {
|
|
41
103
|
left,
|
|
42
104
|
top,
|
|
@@ -49,121 +111,150 @@ function publicResult(raw, image, debug) {
|
|
|
49
111
|
for (let i = 0; i < barcodes.length; i++)
|
|
50
112
|
if (bestIndex < 0 || raw.scan.barcodes[i].support > raw.scan.barcodes[bestIndex].support)
|
|
51
113
|
bestIndex = i;
|
|
52
|
-
return {
|
|
114
|
+
return freeze({
|
|
53
115
|
barcodes,
|
|
54
116
|
values: barcodes.map((b) => b.text),
|
|
55
117
|
best: barcodes[bestIndex],
|
|
56
118
|
image: { width: image.width, height: image.height },
|
|
57
119
|
mode: raw.mode,
|
|
58
120
|
elapsedMs: raw.elapsedMs,
|
|
59
|
-
unfinished: raw.scan.unfinished,
|
|
60
|
-
...(debug ? { debug: raw } : {}),
|
|
61
|
-
};
|
|
121
|
+
unfinished: raw.scan.unfinished || raw.localizationLimited || (raw.localization?.omitted ?? 0) > 0,
|
|
122
|
+
...(debug ? { debug: { ...raw, regions: regionEvidence(raw) } } : {}),
|
|
123
|
+
});
|
|
62
124
|
}
|
|
63
125
|
const modes = {
|
|
64
|
-
low: "low-release-
|
|
65
|
-
medium: "medium-release-
|
|
66
|
-
high: "high-release-
|
|
67
|
-
"very-high": "very-high-release-
|
|
126
|
+
low: "low-complete-release-20260916.wasm",
|
|
127
|
+
medium: "medium-complete-release-20260916.wasm",
|
|
128
|
+
high: "high-complete-release-20260916.wasm",
|
|
129
|
+
"very-high": "very-high-complete-release-20260916.wasm",
|
|
68
130
|
};
|
|
69
131
|
/** Mode selects a compiled implementation. Create another instance to switch. */
|
|
70
132
|
export class Scanner {
|
|
71
133
|
host;
|
|
72
134
|
mode;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
constructor(host, mode,
|
|
135
|
+
configuredFormats;
|
|
136
|
+
addOnPolicy;
|
|
137
|
+
constructor(host, mode, configuredFormats = ["EAN13"], addOnPolicy = "Ignore") {
|
|
76
138
|
this.host = host;
|
|
77
139
|
this.mode = mode;
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
140
|
+
this.configuredFormats = configuredFormats;
|
|
141
|
+
this.addOnPolicy = addOnPolicy;
|
|
142
|
+
Object.freeze(configuredFormats);
|
|
143
|
+
}
|
|
144
|
+
/** Formats available for scanning, fixed at creation. */
|
|
145
|
+
get formats() {
|
|
146
|
+
return this.configuredFormats;
|
|
147
|
+
}
|
|
148
|
+
get eanAddOnPolicy() {
|
|
149
|
+
return this.addOnPolicy;
|
|
80
150
|
}
|
|
81
151
|
static async create(options = {}) {
|
|
82
152
|
const input = options;
|
|
83
153
|
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
84
154
|
throw new TypeError("Invalid scanner options");
|
|
85
155
|
for (const key of Object.keys(options))
|
|
86
|
-
if (!["mode", "formats", "loadWasm"].includes(key))
|
|
156
|
+
if (!["mode", "formats", "wasmBaseUrl", "loadWasm", "eanAddOnPolicy"].includes(key))
|
|
87
157
|
throw new TypeError(`Unknown scanner option: ${key}`);
|
|
88
158
|
const mode = options.mode ?? "medium";
|
|
89
159
|
if (!Object.hasOwn(modes, mode))
|
|
90
160
|
throw new TypeError("Unknown scanner mode");
|
|
91
|
-
const extraAsset = "../wasm/multiformat.wasm";
|
|
92
161
|
const formats = resolveFormats(options.formats);
|
|
93
|
-
const
|
|
162
|
+
const addonPolicy = options.eanAddOnPolicy === undefined ? "Ignore" : options.eanAddOnPolicy;
|
|
163
|
+
if (!["Ignore", "Read", "Require"].includes(addonPolicy))
|
|
164
|
+
throw new TypeError("eanAddOnPolicy must be Ignore, Read or Require");
|
|
165
|
+
if (options.loadWasm !== undefined && typeof options.loadWasm !== "function")
|
|
166
|
+
throw new TypeError("loadWasm must be a function");
|
|
167
|
+
if (options.wasmBaseUrl !== undefined &&
|
|
168
|
+
typeof options.wasmBaseUrl !== "string" &&
|
|
169
|
+
!(options.wasmBaseUrl instanceof URL))
|
|
170
|
+
throw new TypeError("wasmBaseUrl must be a string or URL");
|
|
171
|
+
const base = options.wasmBaseUrl === undefined
|
|
172
|
+
? new URL(/* @vite-ignore */ "../wasm/", import.meta.url)
|
|
173
|
+
: new URL(options.wasmBaseUrl, typeof location === "undefined" ? import.meta.url : location.href);
|
|
174
|
+
if (!base.pathname.endsWith("/"))
|
|
175
|
+
base.pathname += "/";
|
|
94
176
|
const load = options.loadWasm ?? loadDefault;
|
|
95
|
-
const
|
|
96
|
-
const
|
|
177
|
+
const needsPrimary = formats.some((f) => f === "EAN13" || f === "UPCA");
|
|
178
|
+
const bytes = needsPrimary ? await load(new URL(modes[mode], base)) : undefined;
|
|
179
|
+
const recovery = !needsPrimary || mode === "low" ? undefined : await load(new URL(modes.low, base));
|
|
180
|
+
const multiformat = addonPolicy !== "Ignore" || formats.length !== 1 || formats[0] !== "EAN13";
|
|
181
|
+
const extra = addonPolicy !== "Ignore" || formats.some((f) => f !== "EAN13" && f !== "UPCA")
|
|
182
|
+
? await load(new URL("multiformat.wasm", base))
|
|
183
|
+
: undefined;
|
|
184
|
+
if (multiformat)
|
|
185
|
+
return new Scanner(await MediumMultiformatScanner.create(bytes, extra, mode, recovery), mode, formats, addonPolicy);
|
|
186
|
+
if (!bytes)
|
|
187
|
+
throw new Error("EAN13 engine was not loaded");
|
|
97
188
|
const host = recovery
|
|
98
189
|
? await ReleaseDetailScanner.create(bytes, recovery, mode)
|
|
99
190
|
: await IndependentScanner.create(bytes);
|
|
100
|
-
|
|
101
|
-
const extra = formats.some((f) => f !== "EAN13" && f !== "UPCA")
|
|
102
|
-
? await load(new URL(extraAsset, import.meta.url))
|
|
103
|
-
: undefined;
|
|
104
|
-
const additional = formats.length !== 1 || formats[0] !== "EAN13"
|
|
105
|
-
? await MediumMultiformatScanner.create(bytes, extra, mode, recovery)
|
|
106
|
-
: undefined;
|
|
107
|
-
return new Scanner(host, mode, additional, formats);
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
host.dispose();
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
191
|
+
return new Scanner(host, mode, formats);
|
|
113
192
|
}
|
|
114
193
|
scan(inputImage, options = {}) {
|
|
115
194
|
const image = pixels(inputImage);
|
|
116
195
|
const input = options;
|
|
117
196
|
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
118
197
|
throw new TypeError("Invalid scan options");
|
|
119
|
-
for (const key of Object.keys(options))
|
|
120
|
-
if (key !== "
|
|
198
|
+
for (const key of Object.keys(options))
|
|
199
|
+
if (key !== "debug" && key !== "formats" && key !== "finishCandidates")
|
|
121
200
|
throw new TypeError(`Unknown scan option: ${key}`);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const frame = this.
|
|
135
|
-
|
|
201
|
+
if (options.debug !== undefined && typeof options.debug !== "boolean")
|
|
202
|
+
throw new TypeError("debug must be a boolean");
|
|
203
|
+
if (options.finishCandidates !== undefined && typeof options.finishCandidates !== "boolean")
|
|
204
|
+
throw new TypeError("finishCandidates must be a boolean");
|
|
205
|
+
const debug = options.debug ?? false;
|
|
206
|
+
const scanPolicy = { ...policy, finishCandidates: options.finishCandidates ?? false };
|
|
207
|
+
const formats = options.formats === undefined ? this.formats : resolveFormats(options.formats);
|
|
208
|
+
if (formats.some((format) => !this.formats.includes(format)))
|
|
209
|
+
throw new TypeError(`Scan formats must be a subset of configured formats. Requested: ${formats.join(", ")}; configured: ${this.formats.join(", ")}`);
|
|
210
|
+
if (this.host instanceof MediumMultiformatScanner) {
|
|
211
|
+
if (options.finishCandidates && !formats.some((f) => f === "EAN13" || f === "UPCA"))
|
|
212
|
+
throw new TypeError("finishCandidates requires EAN13 or UPCA");
|
|
213
|
+
const frame = this.host.scan(image, formats, {
|
|
214
|
+
eanAddOnSymbol: this.eanAddOnPolicy,
|
|
215
|
+
finishCandidates: options.finishCandidates,
|
|
216
|
+
});
|
|
217
|
+
const barcodes = frame.barcodes;
|
|
218
|
+
const primary = frame.primary;
|
|
219
|
+
const localization = primary?.localization;
|
|
136
220
|
return publicResult({
|
|
137
221
|
schemaVersion: 2,
|
|
138
222
|
mode: this.mode,
|
|
139
|
-
multiple,
|
|
223
|
+
multiple: true,
|
|
140
224
|
elapsedMs: frame.scanMs,
|
|
225
|
+
// Preserve the aggregate limit flag when additional readers cannot separate causes.
|
|
141
226
|
localizationLimited: frame.unfinished,
|
|
142
227
|
scan: {
|
|
228
|
+
...(debug ? primary?.scan : {}),
|
|
143
229
|
barcodes,
|
|
144
230
|
unfinished: frame.unfinished,
|
|
145
|
-
...(
|
|
231
|
+
...(debug ? { regions: frame.regions } : {}),
|
|
146
232
|
},
|
|
147
|
-
|
|
233
|
+
...(debug && primary
|
|
234
|
+
? {
|
|
235
|
+
localization,
|
|
236
|
+
searchWindows: primary.searchWindows,
|
|
237
|
+
...("recovery" in primary ? { recovery: primary.recovery } : {}),
|
|
238
|
+
...("detailRegions" in primary ? { detailRegions: primary.detailRegions } : {}),
|
|
239
|
+
}
|
|
240
|
+
: {}),
|
|
241
|
+
}, image, debug);
|
|
148
242
|
}
|
|
149
|
-
const full = this.host.scanLocalized(image,
|
|
243
|
+
const full = this.host.scanLocalized(image, scanPolicy, fitLimits[this.mode], true);
|
|
150
244
|
// Isolate the imported host's JSON result at the public ABI type boundary.
|
|
151
245
|
const localization = full.localization;
|
|
152
|
-
const
|
|
153
|
-
const barcodes = (multiple ? full.scan.barcodes : best ? [best] : []).map((b) => ({
|
|
246
|
+
const barcodes = mergeLinearDuplicates(full.scan.barcodes.map((b) => ({
|
|
154
247
|
...b,
|
|
155
248
|
format: "EAN13",
|
|
156
|
-
}));
|
|
249
|
+
})), image);
|
|
157
250
|
return publicResult({
|
|
158
251
|
schemaVersion: 2,
|
|
159
252
|
mode: this.mode,
|
|
160
|
-
multiple,
|
|
253
|
+
multiple: true,
|
|
161
254
|
elapsedMs: full.scanMs,
|
|
162
|
-
localizationLimited: localization.workLimited,
|
|
163
|
-
scan:
|
|
164
|
-
|
|
165
|
-
: { barcodes, unfinished: full.scan.unfinished },
|
|
166
|
-
...(includeRegions
|
|
255
|
+
localizationLimited: localization.workLimited || localization.omitted > 0,
|
|
256
|
+
scan: debug ? { ...full.scan, barcodes } : { barcodes, unfinished: full.scan.unfinished },
|
|
257
|
+
...(debug
|
|
167
258
|
? {
|
|
168
259
|
localization,
|
|
169
260
|
searchWindows: full.searchWindows,
|
|
@@ -175,23 +266,23 @@ export class Scanner {
|
|
|
175
266
|
: {}),
|
|
176
267
|
}
|
|
177
268
|
: {}),
|
|
178
|
-
}, image,
|
|
179
|
-
}
|
|
180
|
-
/** Convenience alias for result.best. */
|
|
181
|
-
best(result) {
|
|
182
|
-
return result.best;
|
|
269
|
+
}, image, debug);
|
|
183
270
|
}
|
|
184
271
|
dispose() {
|
|
185
|
-
this.additional?.dispose();
|
|
186
272
|
this.host.dispose();
|
|
187
273
|
}
|
|
188
274
|
}
|
|
189
275
|
/** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
|
|
190
276
|
export async function scan(image, options = {}) {
|
|
191
|
-
const
|
|
277
|
+
const input = options;
|
|
278
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
279
|
+
throw new TypeError("Invalid scan options");
|
|
280
|
+
const { debug, finishCandidates, ...creation } = options;
|
|
281
|
+
if (debug !== undefined && typeof debug !== "boolean")
|
|
282
|
+
throw new TypeError("debug must be a boolean");
|
|
192
283
|
const scanner = await Scanner.create(creation);
|
|
193
284
|
try {
|
|
194
|
-
return scanner.scan(image, {
|
|
285
|
+
return scanner.scan(image, { debug, finishCandidates });
|
|
195
286
|
}
|
|
196
287
|
finally {
|
|
197
288
|
scanner.dispose();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Quad } from "../host.js";
|
|
2
|
+
/** Convex containment, including the boundary; reject degenerate coverage. */
|
|
3
|
+
export declare function containsPoint(point: readonly number[], quad: Quad): boolean;
|
|
4
|
+
/** Only deep retries are deferred. Normal proposals and the full-frame bit remain. */
|
|
5
|
+
export declare function uncoveredRetryMask(proposals: readonly {
|
|
6
|
+
polygon: Quad;
|
|
7
|
+
}[], coverage: readonly Quad[], initial?: readonly number[]): [number, number];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Convex containment, including the boundary; reject degenerate coverage. */
|
|
2
|
+
export function containsPoint(point, quad) {
|
|
3
|
+
if (!point.every(Number.isFinite) || quad.some((p) => !p.every(Number.isFinite)))
|
|
4
|
+
return false;
|
|
5
|
+
let positive = false;
|
|
6
|
+
let negative = false;
|
|
7
|
+
let area = 0;
|
|
8
|
+
for (let i = 0; i < 4; i++) {
|
|
9
|
+
const a = quad[i], b = quad[(i + 1) % 4];
|
|
10
|
+
const cross = (b[0] - a[0]) * (point[1] - a[1]) - (b[1] - a[1]) * (point[0] - a[0]);
|
|
11
|
+
positive ||= cross > 1e-6;
|
|
12
|
+
negative ||= cross < -1e-6;
|
|
13
|
+
area += a[0] * b[1] - b[0] * a[1];
|
|
14
|
+
}
|
|
15
|
+
return Math.abs(area) > 1e-6 && !(positive && negative);
|
|
16
|
+
}
|
|
17
|
+
/** Only deep retries are deferred. Normal proposals and the full-frame bit remain. */
|
|
18
|
+
export function uncoveredRetryMask(proposals, coverage, initial = [0xffffffff, 0xffffffff]) {
|
|
19
|
+
const mask = [initial[0] >>> 0, initial[1] >>> 0];
|
|
20
|
+
proposals.forEach((proposal, index) => {
|
|
21
|
+
if (coverage.some((quad) => proposal.polygon.every((point) => containsPoint(point, quad)))) {
|
|
22
|
+
mask[index >>> 5] = (mask[index >>> 5] & ~(1 << (index & 31))) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return mask;
|
|
26
|
+
}
|
|
@@ -19,8 +19,10 @@ export declare const formatBits: {
|
|
|
19
19
|
};
|
|
20
20
|
export type Format = keyof typeof formatBits;
|
|
21
21
|
export declare const retailFormats: readonly Format[];
|
|
22
|
+
export declare const commonLinearFormats: readonly Format[];
|
|
23
|
+
export declare const commonFormats: readonly Format[];
|
|
22
24
|
export declare const linearFormats: readonly Format[];
|
|
23
25
|
export declare const matrixFormats: readonly Format[];
|
|
24
|
-
export type FormatSelection = readonly Format[] | "1D" | "2D" | "all";
|
|
25
|
-
export declare function resolveFormats(input?: readonly string[] |
|
|
26
|
+
export type FormatSelection = Format | readonly Format[] | "retail" | "common1D" | "common" | "1D" | "2D" | "all";
|
|
27
|
+
export declare function resolveFormats(input?: readonly string[] | string): Format[];
|
|
26
28
|
export declare function maskFor(formats: readonly Format[]): number;
|
|
@@ -18,11 +18,15 @@ export const formatBits = {
|
|
|
18
18
|
MaxiCode: 131072,
|
|
19
19
|
};
|
|
20
20
|
export const retailFormats = ["EAN13", "UPCA", "EAN8", "UPCE"];
|
|
21
|
-
export const
|
|
21
|
+
export const commonLinearFormats = [
|
|
22
22
|
...retailFormats,
|
|
23
23
|
"Code128",
|
|
24
24
|
"Code39",
|
|
25
25
|
"ITF",
|
|
26
|
+
];
|
|
27
|
+
export const commonFormats = [...commonLinearFormats, "QRCode", "DataMatrix"];
|
|
28
|
+
export const linearFormats = [
|
|
29
|
+
...commonLinearFormats,
|
|
26
30
|
"Codabar",
|
|
27
31
|
"Code93",
|
|
28
32
|
"DataBar",
|
|
@@ -36,6 +40,12 @@ export const matrixFormats = [
|
|
|
36
40
|
"MaxiCode",
|
|
37
41
|
];
|
|
38
42
|
export function resolveFormats(input) {
|
|
43
|
+
if (input === "retail")
|
|
44
|
+
return [...retailFormats];
|
|
45
|
+
if (input === "common1D")
|
|
46
|
+
return [...commonLinearFormats];
|
|
47
|
+
if (input === "common")
|
|
48
|
+
return [...commonFormats];
|
|
39
49
|
if (input === "1D")
|
|
40
50
|
return [...linearFormats];
|
|
41
51
|
if (input === "2D")
|
|
@@ -44,6 +54,8 @@ export function resolveFormats(input) {
|
|
|
44
54
|
return [...linearFormats, ...matrixFormats];
|
|
45
55
|
if (input === undefined)
|
|
46
56
|
return ["EAN13"];
|
|
57
|
+
if (typeof input === "string" && Object.hasOwn(formatBits, input))
|
|
58
|
+
return [input];
|
|
47
59
|
if (!Array.isArray(input) || input.length === 0)
|
|
48
60
|
throw Error("Choose at least one barcode format.");
|
|
49
61
|
for (const value of input) {
|
|
@@ -2,6 +2,11 @@ import type { Quad } from "../host.js";
|
|
|
2
2
|
export type Transform = readonly number[];
|
|
3
3
|
export declare function project(t: Transform, x: number, y: number): [number, number];
|
|
4
4
|
export declare function transformFor(quad: Quad, width: number, height: number): Transform;
|
|
5
|
+
export declare function rectificationPlan(quad: Quad): {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
transform: number[];
|
|
9
|
+
};
|
|
5
10
|
export declare function rectify(gray: Uint8Array, width: number, height: number, quad: Quad): {
|
|
6
11
|
data: Uint8Array<ArrayBuffer>;
|
|
7
12
|
width: number;
|