tapirscan 1.0.0 → 1.2.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 +184 -105
- 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 +91 -33
- package/dist/index.js +174 -86
- 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
|
+
/** Allow reader-specific extra work. Supported for every format; exact budgets may evolve. */
|
|
29
|
+
extendedBudget?: 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,75 @@ 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
|
-
|
|
117
|
+
readonly mode: Mode;
|
|
118
|
+
readonly elapsedMs: number;
|
|
119
|
+
readonly unfinished: boolean;
|
|
120
|
+
readonly undecoded: readonly UndecodedRegion[];
|
|
121
|
+
readonly debug?: Diagnostics;
|
|
71
122
|
}
|
|
123
|
+
export type EanAddOnPolicy = "Ignore" | "Read" | "Require";
|
|
72
124
|
export interface ScannerOptions {
|
|
125
|
+
/** Optional EAN/UPC supplement policy, fixed at creation. */
|
|
126
|
+
eanAddOnPolicy?: EanAddOnPolicy;
|
|
73
127
|
mode?: Mode;
|
|
74
128
|
formats?: FormatSelection;
|
|
129
|
+
/** Directory containing the packaged WASMs; relative to the page in browsers. */
|
|
130
|
+
wasmBaseUrl?: string | URL;
|
|
131
|
+
/** Advanced loader, receiving URLs resolved against wasmBaseUrl. */
|
|
75
132
|
loadWasm?: (url: URL) => Promise<ArrayBuffer>;
|
|
76
133
|
}
|
|
77
134
|
export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
|
|
@@ -79,13 +136,14 @@ export type PixelImage = Image | Pick<ImageData, "data" | "width" | "height">;
|
|
|
79
136
|
export declare class Scanner {
|
|
80
137
|
private readonly host;
|
|
81
138
|
readonly mode: Mode;
|
|
82
|
-
private readonly
|
|
83
|
-
private readonly
|
|
139
|
+
private readonly configuredFormats;
|
|
140
|
+
private readonly addOnPolicy;
|
|
84
141
|
private constructor();
|
|
142
|
+
/** Formats available for scanning, fixed at creation. */
|
|
143
|
+
get formats(): readonly Format[];
|
|
144
|
+
get eanAddOnPolicy(): EanAddOnPolicy;
|
|
85
145
|
static create(options?: ScannerOptions): Promise<Scanner>;
|
|
86
146
|
scan(inputImage: PixelImage, options?: ScanOptions): ScanResult;
|
|
87
|
-
/** Convenience alias for result.best. */
|
|
88
|
-
best(result: ScanResult): Barcode | undefined;
|
|
89
147
|
dispose(): void;
|
|
90
148
|
}
|
|
91
149
|
/** 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,149 +111,175 @@ 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
|
-
|
|
114
|
+
const regions = regionEvidence(raw);
|
|
115
|
+
return freeze({
|
|
53
116
|
barcodes,
|
|
54
117
|
values: barcodes.map((b) => b.text),
|
|
55
118
|
best: barcodes[bestIndex],
|
|
119
|
+
undecoded: regions.undecoded,
|
|
56
120
|
image: { width: image.width, height: image.height },
|
|
57
121
|
mode: raw.mode,
|
|
58
122
|
elapsedMs: raw.elapsedMs,
|
|
59
|
-
unfinished: raw.scan.unfinished,
|
|
60
|
-
...(debug ? { debug: raw } : {}),
|
|
61
|
-
};
|
|
123
|
+
unfinished: raw.scan.unfinished || raw.localizationLimited || (raw.localization?.omitted ?? 0) > 0,
|
|
124
|
+
...(debug ? { debug: { ...raw, regions } } : {}),
|
|
125
|
+
});
|
|
62
126
|
}
|
|
63
127
|
const modes = {
|
|
64
|
-
low: "low-release-
|
|
65
|
-
medium: "medium-release-
|
|
66
|
-
high: "high-release-
|
|
67
|
-
"very-high": "very-high-release-
|
|
128
|
+
low: "low-complete-release-20260916.wasm",
|
|
129
|
+
medium: "medium-complete-release-20260916.wasm",
|
|
130
|
+
high: "high-complete-release-20260916.wasm",
|
|
131
|
+
"very-high": "very-high-complete-release-20260916.wasm",
|
|
68
132
|
};
|
|
69
133
|
/** Mode selects a compiled implementation. Create another instance to switch. */
|
|
70
134
|
export class Scanner {
|
|
71
135
|
host;
|
|
72
136
|
mode;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
constructor(host, mode,
|
|
137
|
+
configuredFormats;
|
|
138
|
+
addOnPolicy;
|
|
139
|
+
constructor(host, mode, configuredFormats = ["EAN13"], addOnPolicy = "Ignore") {
|
|
76
140
|
this.host = host;
|
|
77
141
|
this.mode = mode;
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
142
|
+
this.configuredFormats = configuredFormats;
|
|
143
|
+
this.addOnPolicy = addOnPolicy;
|
|
144
|
+
Object.freeze(configuredFormats);
|
|
145
|
+
}
|
|
146
|
+
/** Formats available for scanning, fixed at creation. */
|
|
147
|
+
get formats() {
|
|
148
|
+
return this.configuredFormats;
|
|
149
|
+
}
|
|
150
|
+
get eanAddOnPolicy() {
|
|
151
|
+
return this.addOnPolicy;
|
|
80
152
|
}
|
|
81
153
|
static async create(options = {}) {
|
|
82
154
|
const input = options;
|
|
83
155
|
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
84
156
|
throw new TypeError("Invalid scanner options");
|
|
85
157
|
for (const key of Object.keys(options))
|
|
86
|
-
if (!["mode", "formats", "loadWasm"].includes(key))
|
|
158
|
+
if (!["mode", "formats", "wasmBaseUrl", "loadWasm", "eanAddOnPolicy"].includes(key))
|
|
87
159
|
throw new TypeError(`Unknown scanner option: ${key}`);
|
|
88
160
|
const mode = options.mode ?? "medium";
|
|
89
161
|
if (!Object.hasOwn(modes, mode))
|
|
90
162
|
throw new TypeError("Unknown scanner mode");
|
|
91
|
-
const extraAsset = "../wasm/multiformat.wasm";
|
|
92
163
|
const formats = resolveFormats(options.formats);
|
|
93
|
-
const
|
|
164
|
+
const addonPolicy = options.eanAddOnPolicy === undefined ? "Ignore" : options.eanAddOnPolicy;
|
|
165
|
+
if (!["Ignore", "Read", "Require"].includes(addonPolicy))
|
|
166
|
+
throw new TypeError("eanAddOnPolicy must be Ignore, Read or Require");
|
|
167
|
+
if (options.loadWasm !== undefined && typeof options.loadWasm !== "function")
|
|
168
|
+
throw new TypeError("loadWasm must be a function");
|
|
169
|
+
if (options.wasmBaseUrl !== undefined &&
|
|
170
|
+
typeof options.wasmBaseUrl !== "string" &&
|
|
171
|
+
!(options.wasmBaseUrl instanceof URL))
|
|
172
|
+
throw new TypeError("wasmBaseUrl must be a string or URL");
|
|
173
|
+
const base = options.wasmBaseUrl === undefined
|
|
174
|
+
? new URL(/* @vite-ignore */ "../wasm/", import.meta.url)
|
|
175
|
+
: new URL(options.wasmBaseUrl, typeof location === "undefined" ? import.meta.url : location.href);
|
|
176
|
+
if (!base.pathname.endsWith("/"))
|
|
177
|
+
base.pathname += "/";
|
|
94
178
|
const load = options.loadWasm ?? loadDefault;
|
|
95
|
-
const
|
|
96
|
-
const
|
|
179
|
+
const needsPrimary = formats.some((f) => f === "EAN13" || f === "UPCA");
|
|
180
|
+
const bytes = needsPrimary ? await load(new URL(modes[mode], base)) : undefined;
|
|
181
|
+
const recovery = !needsPrimary || mode === "low" ? undefined : await load(new URL(modes.low, base));
|
|
182
|
+
const multiformat = addonPolicy !== "Ignore" || formats.length !== 1 || formats[0] !== "EAN13";
|
|
183
|
+
const extra = addonPolicy !== "Ignore" || formats.some((f) => f !== "EAN13" && f !== "UPCA")
|
|
184
|
+
? await load(new URL("multiformat.wasm", base))
|
|
185
|
+
: undefined;
|
|
186
|
+
if (multiformat)
|
|
187
|
+
return new Scanner(await MediumMultiformatScanner.create(bytes, extra, mode, recovery), mode, formats, addonPolicy);
|
|
188
|
+
if (!bytes)
|
|
189
|
+
throw new Error("EAN13 engine was not loaded");
|
|
97
190
|
const host = recovery
|
|
98
191
|
? await ReleaseDetailScanner.create(bytes, recovery, mode)
|
|
99
192
|
: 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
|
-
}
|
|
193
|
+
return new Scanner(host, mode, formats);
|
|
113
194
|
}
|
|
114
195
|
scan(inputImage, options = {}) {
|
|
115
196
|
const image = pixels(inputImage);
|
|
116
197
|
const input = options;
|
|
117
198
|
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
118
199
|
throw new TypeError("Invalid scan options");
|
|
119
|
-
for (const key of Object.keys(options))
|
|
120
|
-
if (key !== "
|
|
200
|
+
for (const key of Object.keys(options))
|
|
201
|
+
if (key !== "debug" && key !== "formats" && key !== "extendedBudget")
|
|
121
202
|
throw new TypeError(`Unknown scan option: ${key}`);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
throw new TypeError("
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
203
|
+
if (options.debug !== undefined && typeof options.debug !== "boolean")
|
|
204
|
+
throw new TypeError("debug must be a boolean");
|
|
205
|
+
if (options.extendedBudget !== undefined && typeof options.extendedBudget !== "boolean")
|
|
206
|
+
throw new TypeError("extendedBudget must be a boolean");
|
|
207
|
+
const finishCandidates = options.extendedBudget ?? false;
|
|
208
|
+
const debug = options.debug ?? false;
|
|
209
|
+
const scanPolicy = { ...policy, finishCandidates };
|
|
210
|
+
const formats = options.formats === undefined ? this.formats : resolveFormats(options.formats);
|
|
211
|
+
if (formats.some((format) => !this.formats.includes(format)))
|
|
212
|
+
throw new TypeError(`Scan formats must be a subset of configured formats. Requested: ${formats.join(", ")}; configured: ${this.formats.join(", ")}`);
|
|
213
|
+
if (this.host instanceof MediumMultiformatScanner) {
|
|
214
|
+
const frame = this.host.scan(image, formats, {
|
|
215
|
+
eanAddOnSymbol: this.eanAddOnPolicy,
|
|
216
|
+
finishCandidates,
|
|
217
|
+
});
|
|
218
|
+
const barcodes = frame.barcodes;
|
|
219
|
+
const primary = frame.primary;
|
|
220
|
+
const localization = primary?.localization;
|
|
136
221
|
return publicResult({
|
|
137
222
|
schemaVersion: 2,
|
|
138
223
|
mode: this.mode,
|
|
139
|
-
multiple,
|
|
224
|
+
multiple: true,
|
|
140
225
|
elapsedMs: frame.scanMs,
|
|
226
|
+
// Preserve the aggregate limit flag when additional readers cannot separate causes.
|
|
141
227
|
localizationLimited: frame.unfinished,
|
|
142
228
|
scan: {
|
|
229
|
+
...primary?.scan,
|
|
143
230
|
barcodes,
|
|
144
231
|
unfinished: frame.unfinished,
|
|
145
|
-
|
|
232
|
+
regions: frame.regions,
|
|
146
233
|
},
|
|
147
|
-
|
|
234
|
+
...(primary
|
|
235
|
+
? {
|
|
236
|
+
localization,
|
|
237
|
+
searchWindows: primary.searchWindows,
|
|
238
|
+
...("recovery" in primary ? { recovery: primary.recovery } : {}),
|
|
239
|
+
...("detailRegions" in primary ? { detailRegions: primary.detailRegions } : {}),
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
242
|
+
}, image, debug);
|
|
148
243
|
}
|
|
149
|
-
const full = this.host.scanLocalized(image,
|
|
244
|
+
const full = this.host.scanLocalized(image, scanPolicy, fitLimits[this.mode], true);
|
|
150
245
|
// Isolate the imported host's JSON result at the public ABI type boundary.
|
|
151
246
|
const localization = full.localization;
|
|
152
|
-
const
|
|
153
|
-
const barcodes = (multiple ? full.scan.barcodes : best ? [best] : []).map((b) => ({
|
|
247
|
+
const barcodes = mergeLinearDuplicates(full.scan.barcodes.map((b) => ({
|
|
154
248
|
...b,
|
|
155
249
|
format: "EAN13",
|
|
156
|
-
}));
|
|
250
|
+
})), image);
|
|
157
251
|
return publicResult({
|
|
158
252
|
schemaVersion: 2,
|
|
159
253
|
mode: this.mode,
|
|
160
|
-
multiple,
|
|
254
|
+
multiple: true,
|
|
161
255
|
elapsedMs: full.scanMs,
|
|
162
|
-
localizationLimited: localization.workLimited,
|
|
163
|
-
scan:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
...(
|
|
256
|
+
localizationLimited: localization.workLimited || localization.omitted > 0,
|
|
257
|
+
scan: { ...full.scan, barcodes },
|
|
258
|
+
localization,
|
|
259
|
+
searchWindows: full.searchWindows,
|
|
260
|
+
...("recovery" in full && "detailRegions" in full
|
|
167
261
|
? {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
...("recovery" in full && "detailRegions" in full
|
|
171
|
-
? {
|
|
172
|
-
recovery: full.recovery,
|
|
173
|
-
detailRegions: full.detailRegions,
|
|
174
|
-
}
|
|
175
|
-
: {}),
|
|
262
|
+
recovery: full.recovery,
|
|
263
|
+
detailRegions: full.detailRegions,
|
|
176
264
|
}
|
|
177
265
|
: {}),
|
|
178
|
-
}, image,
|
|
179
|
-
}
|
|
180
|
-
/** Convenience alias for result.best. */
|
|
181
|
-
best(result) {
|
|
182
|
-
return result.best;
|
|
266
|
+
}, image, debug);
|
|
183
267
|
}
|
|
184
268
|
dispose() {
|
|
185
|
-
this.additional?.dispose();
|
|
186
269
|
this.host.dispose();
|
|
187
270
|
}
|
|
188
271
|
}
|
|
189
272
|
/** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
|
|
190
273
|
export async function scan(image, options = {}) {
|
|
191
|
-
const
|
|
274
|
+
const input = options;
|
|
275
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
276
|
+
throw new TypeError("Invalid scan options");
|
|
277
|
+
const { debug, extendedBudget, ...creation } = options;
|
|
278
|
+
if (debug !== undefined && typeof debug !== "boolean")
|
|
279
|
+
throw new TypeError("debug must be a boolean");
|
|
192
280
|
const scanner = await Scanner.create(creation);
|
|
193
281
|
try {
|
|
194
|
-
return scanner.scan(image, {
|
|
282
|
+
return scanner.scan(image, { debug, extendedBudget });
|
|
195
283
|
}
|
|
196
284
|
finally {
|
|
197
285
|
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;
|