tapirscan 1.0.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/LICENSE +21 -0
- package/README.md +241 -0
- package/THIRD_PARTY_NOTICES.md +656 -0
- package/dist/detail-20260914/continuity.d.mts +2 -0
- package/dist/detail-20260914/continuity.mjs +62 -0
- package/dist/detail-20260914/detail-proposals-rich.d.mts +13 -0
- package/dist/detail-20260914/detail-proposals-rich.mjs +53 -0
- package/dist/detail-20260914/direct-recovery.d.mts +28 -0
- package/dist/detail-20260914/direct-recovery.mjs +139 -0
- package/dist/detail-20260914/host.d.mts +40 -0
- package/dist/detail-20260914/host.mjs +280 -0
- package/dist/detail-20260914/scanner.d.mts +12 -0
- package/dist/detail-20260914/scanner.mjs +64 -0
- package/dist/detail-20260914/source-evidence.d.mts +10 -0
- package/dist/detail-20260914/source-evidence.mjs +94 -0
- package/dist/detail-canvas.d.ts +35 -0
- package/dist/detail-canvas.js +73 -0
- package/dist/detail.d.ts +19 -0
- package/dist/detail.js +87 -0
- package/dist/host.d.ts +112 -0
- package/dist/host.js +184 -0
- package/dist/host64.d.ts +112 -0
- package/dist/host64.js +184 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +199 -0
- package/dist/multiformat/formats.d.ts +26 -0
- package/dist/multiformat/formats.js +59 -0
- package/dist/multiformat/geometry.d.ts +32 -0
- package/dist/multiformat/geometry.js +122 -0
- package/dist/multiformat/pixels.d.ts +3 -0
- package/dist/multiformat/pixels.js +36 -0
- package/dist/multiformat/scanner.d.ts +51 -0
- package/dist/multiformat/scanner.js +258 -0
- package/dist/multiformat-host.d.ts +121 -0
- package/dist/multiformat-host.js +207 -0
- package/dist/policy.d.ts +12 -0
- package/dist/policy.js +12 -0
- package/package.json +52 -0
- package/wasm/high-release-20260915.wasm +0 -0
- package/wasm/low-release-20260915.wasm +0 -0
- package/wasm/medium-release-20260915.wasm +0 -0
- package/wasm/multiformat.json +6 -0
- package/wasm/multiformat.wasm +0 -0
- package/wasm/very-high-release-20260915.wasm +0 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { ReleaseDetailScanner, fitLimits } from "./detail.js";
|
|
2
|
+
import { policy } from "./policy.js";
|
|
3
|
+
import { MediumMultiformatScanner } from "./multiformat/scanner.js";
|
|
4
|
+
import { resolveFormats } from "./multiformat/formats.js";
|
|
5
|
+
export { formatBits, linearFormats, matrixFormats, retailFormats } from "./multiformat/formats.js";
|
|
6
|
+
import { IndependentScanner } from "./host.js";
|
|
7
|
+
export { ScannerError } from "./host.js";
|
|
8
|
+
function pixels(image) {
|
|
9
|
+
if ("channels" in image)
|
|
10
|
+
return image;
|
|
11
|
+
if (!(image.data instanceof Uint8ClampedArray))
|
|
12
|
+
throw new TypeError("Use ImageData or an explicit buffer with channels and stride");
|
|
13
|
+
return {
|
|
14
|
+
data: new Uint8Array(image.data.buffer, image.data.byteOffset, image.data.byteLength),
|
|
15
|
+
width: image.width,
|
|
16
|
+
height: image.height,
|
|
17
|
+
channels: 4,
|
|
18
|
+
stride: image.width * 4,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function loadDefault(url) {
|
|
22
|
+
if (url.protocol === "file:") {
|
|
23
|
+
const nodeFs = "node:fs/promises";
|
|
24
|
+
const fs = (await import(/* @vite-ignore */ nodeFs));
|
|
25
|
+
return new Uint8Array(await fs.readFile(url)).buffer;
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(url);
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
throw new Error(`WASM load failed: ${String(response.status)}`);
|
|
30
|
+
return response.arrayBuffer();
|
|
31
|
+
}
|
|
32
|
+
function publicResult(raw, image, debug) {
|
|
33
|
+
const barcodes = raw.scan.barcodes.map(({ text, format, polygon }) => {
|
|
34
|
+
const left = Math.floor(Math.min(...polygon.map((p) => p[0])));
|
|
35
|
+
const top = Math.floor(Math.min(...polygon.map((p) => p[1])));
|
|
36
|
+
return {
|
|
37
|
+
text,
|
|
38
|
+
format,
|
|
39
|
+
polygon,
|
|
40
|
+
rect: {
|
|
41
|
+
left,
|
|
42
|
+
top,
|
|
43
|
+
width: Math.ceil(Math.max(...polygon.map((p) => p[0]))) - left,
|
|
44
|
+
height: Math.ceil(Math.max(...polygon.map((p) => p[1]))) - top,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
let bestIndex = -1;
|
|
49
|
+
for (let i = 0; i < barcodes.length; i++)
|
|
50
|
+
if (bestIndex < 0 || raw.scan.barcodes[i].support > raw.scan.barcodes[bestIndex].support)
|
|
51
|
+
bestIndex = i;
|
|
52
|
+
return {
|
|
53
|
+
barcodes,
|
|
54
|
+
values: barcodes.map((b) => b.text),
|
|
55
|
+
best: barcodes[bestIndex],
|
|
56
|
+
image: { width: image.width, height: image.height },
|
|
57
|
+
mode: raw.mode,
|
|
58
|
+
elapsedMs: raw.elapsedMs,
|
|
59
|
+
unfinished: raw.scan.unfinished,
|
|
60
|
+
...(debug ? { debug: raw } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const modes = {
|
|
64
|
+
low: "low-release-20260915.wasm",
|
|
65
|
+
medium: "medium-release-20260915.wasm",
|
|
66
|
+
high: "high-release-20260915.wasm",
|
|
67
|
+
"very-high": "very-high-release-20260915.wasm",
|
|
68
|
+
};
|
|
69
|
+
/** Mode selects a compiled implementation. Create another instance to switch. */
|
|
70
|
+
export class Scanner {
|
|
71
|
+
host;
|
|
72
|
+
mode;
|
|
73
|
+
additional;
|
|
74
|
+
formats;
|
|
75
|
+
constructor(host, mode, additional, formats = ["EAN13"]) {
|
|
76
|
+
this.host = host;
|
|
77
|
+
this.mode = mode;
|
|
78
|
+
this.additional = additional;
|
|
79
|
+
this.formats = formats;
|
|
80
|
+
}
|
|
81
|
+
static async create(options = {}) {
|
|
82
|
+
const input = options;
|
|
83
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
84
|
+
throw new TypeError("Invalid scanner options");
|
|
85
|
+
for (const key of Object.keys(options))
|
|
86
|
+
if (!["mode", "formats", "loadWasm"].includes(key))
|
|
87
|
+
throw new TypeError(`Unknown scanner option: ${key}`);
|
|
88
|
+
const mode = options.mode ?? "medium";
|
|
89
|
+
if (!Object.hasOwn(modes, mode))
|
|
90
|
+
throw new TypeError("Unknown scanner mode");
|
|
91
|
+
const extraAsset = "../wasm/multiformat.wasm";
|
|
92
|
+
const formats = resolveFormats(options.formats);
|
|
93
|
+
const url = new URL("../wasm/" + modes[mode], import.meta.url);
|
|
94
|
+
const load = options.loadWasm ?? loadDefault;
|
|
95
|
+
const bytes = await load(url);
|
|
96
|
+
const recovery = mode === "low" ? undefined : await load(new URL("../wasm/" + modes.low, import.meta.url));
|
|
97
|
+
const host = recovery
|
|
98
|
+
? await ReleaseDetailScanner.create(bytes, recovery, mode)
|
|
99
|
+
: await IndependentScanner.create(bytes);
|
|
100
|
+
try {
|
|
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
|
+
}
|
|
113
|
+
}
|
|
114
|
+
scan(inputImage, options = {}) {
|
|
115
|
+
const image = pixels(inputImage);
|
|
116
|
+
const input = options;
|
|
117
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
118
|
+
throw new TypeError("Invalid scan options");
|
|
119
|
+
for (const key of Object.keys(options)) {
|
|
120
|
+
if (key !== "multiple" && key !== "includeRegions" && key !== "debug")
|
|
121
|
+
throw new TypeError(`Unknown scan option: ${key}`);
|
|
122
|
+
}
|
|
123
|
+
for (const key of ["multiple", "includeRegions", "debug"]) {
|
|
124
|
+
if (options[key] !== undefined && typeof options[key] !== "boolean")
|
|
125
|
+
throw new TypeError(`Invalid ${key}`);
|
|
126
|
+
}
|
|
127
|
+
const multiple = options.multiple ?? true;
|
|
128
|
+
if (options.debug !== undefined &&
|
|
129
|
+
options.includeRegions !== undefined &&
|
|
130
|
+
options.debug !== options.includeRegions)
|
|
131
|
+
throw new TypeError("debug and includeRegions disagree");
|
|
132
|
+
const includeRegions = options.debug ?? options.includeRegions ?? false;
|
|
133
|
+
if (this.additional) {
|
|
134
|
+
const frame = this.additional.scan(image, this.formats);
|
|
135
|
+
const barcodes = multiple ? frame.barcodes : frame.barcodes.slice(0, 1);
|
|
136
|
+
return publicResult({
|
|
137
|
+
schemaVersion: 2,
|
|
138
|
+
mode: this.mode,
|
|
139
|
+
multiple,
|
|
140
|
+
elapsedMs: frame.scanMs,
|
|
141
|
+
localizationLimited: frame.unfinished,
|
|
142
|
+
scan: {
|
|
143
|
+
barcodes,
|
|
144
|
+
unfinished: frame.unfinished,
|
|
145
|
+
...(includeRegions ? { regions: frame.regions } : {}),
|
|
146
|
+
},
|
|
147
|
+
}, image, includeRegions);
|
|
148
|
+
}
|
|
149
|
+
const full = this.host.scanLocalized(image, policy, fitLimits[this.mode], true);
|
|
150
|
+
// Isolate the imported host's JSON result at the public ABI type boundary.
|
|
151
|
+
const localization = full.localization;
|
|
152
|
+
const best = multiple ? undefined : this.host.best(full.scan);
|
|
153
|
+
const barcodes = (multiple ? full.scan.barcodes : best ? [best] : []).map((b) => ({
|
|
154
|
+
...b,
|
|
155
|
+
format: "EAN13",
|
|
156
|
+
}));
|
|
157
|
+
return publicResult({
|
|
158
|
+
schemaVersion: 2,
|
|
159
|
+
mode: this.mode,
|
|
160
|
+
multiple,
|
|
161
|
+
elapsedMs: full.scanMs,
|
|
162
|
+
localizationLimited: localization.workLimited,
|
|
163
|
+
scan: includeRegions
|
|
164
|
+
? { ...full.scan, barcodes }
|
|
165
|
+
: { barcodes, unfinished: full.scan.unfinished },
|
|
166
|
+
...(includeRegions
|
|
167
|
+
? {
|
|
168
|
+
localization,
|
|
169
|
+
searchWindows: full.searchWindows,
|
|
170
|
+
...("recovery" in full && "detailRegions" in full
|
|
171
|
+
? {
|
|
172
|
+
recovery: full.recovery,
|
|
173
|
+
detailRegions: full.detailRegions,
|
|
174
|
+
}
|
|
175
|
+
: {}),
|
|
176
|
+
}
|
|
177
|
+
: {}),
|
|
178
|
+
}, image, includeRegions);
|
|
179
|
+
}
|
|
180
|
+
/** Convenience alias for result.best. */
|
|
181
|
+
best(result) {
|
|
182
|
+
return result.best;
|
|
183
|
+
}
|
|
184
|
+
dispose() {
|
|
185
|
+
this.additional?.dispose();
|
|
186
|
+
this.host.dispose();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Scan one image with automatic cleanup. Reuse Scanner for a stream of images. */
|
|
190
|
+
export async function scan(image, options = {}) {
|
|
191
|
+
const { multiple, debug, includeRegions, ...creation } = options;
|
|
192
|
+
const scanner = await Scanner.create(creation);
|
|
193
|
+
try {
|
|
194
|
+
return scanner.scan(image, { multiple, debug, includeRegions });
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
scanner.dispose();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Explicit opt-in formats for the independent Medium extension. */
|
|
2
|
+
export declare const formatBits: {
|
|
3
|
+
readonly EAN13: 1;
|
|
4
|
+
readonly UPCA: 2;
|
|
5
|
+
readonly EAN8: 4;
|
|
6
|
+
readonly UPCE: 8;
|
|
7
|
+
readonly Code128: 16;
|
|
8
|
+
readonly Code39: 32;
|
|
9
|
+
readonly ITF: 64;
|
|
10
|
+
readonly Codabar: 128;
|
|
11
|
+
readonly Code93: 256;
|
|
12
|
+
readonly QRCode: 512;
|
|
13
|
+
readonly DataMatrix: 1024;
|
|
14
|
+
readonly PDF417: 2048;
|
|
15
|
+
readonly Aztec: 4096;
|
|
16
|
+
readonly DataBar: 8192;
|
|
17
|
+
readonly DataBarExpanded: 16384;
|
|
18
|
+
readonly MaxiCode: 131072;
|
|
19
|
+
};
|
|
20
|
+
export type Format = keyof typeof formatBits;
|
|
21
|
+
export declare const retailFormats: readonly Format[];
|
|
22
|
+
export declare const linearFormats: readonly Format[];
|
|
23
|
+
export declare const matrixFormats: readonly Format[];
|
|
24
|
+
export type FormatSelection = readonly Format[] | "1D" | "2D" | "all";
|
|
25
|
+
export declare function resolveFormats(input?: readonly string[] | "1D" | "2D" | "all"): Format[];
|
|
26
|
+
export declare function maskFor(formats: readonly Format[]): number;
|
|
@@ -0,0 +1,59 @@
|
|
|
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 linearFormats = [
|
|
22
|
+
...retailFormats,
|
|
23
|
+
"Code128",
|
|
24
|
+
"Code39",
|
|
25
|
+
"ITF",
|
|
26
|
+
"Codabar",
|
|
27
|
+
"Code93",
|
|
28
|
+
"DataBar",
|
|
29
|
+
"DataBarExpanded",
|
|
30
|
+
];
|
|
31
|
+
export const matrixFormats = [
|
|
32
|
+
"QRCode",
|
|
33
|
+
"DataMatrix",
|
|
34
|
+
"PDF417",
|
|
35
|
+
"Aztec",
|
|
36
|
+
"MaxiCode",
|
|
37
|
+
];
|
|
38
|
+
export function resolveFormats(input) {
|
|
39
|
+
if (input === "1D")
|
|
40
|
+
return [...linearFormats];
|
|
41
|
+
if (input === "2D")
|
|
42
|
+
return [...matrixFormats];
|
|
43
|
+
if (input === "all")
|
|
44
|
+
return [...linearFormats, ...matrixFormats];
|
|
45
|
+
if (input === undefined)
|
|
46
|
+
return ["EAN13"];
|
|
47
|
+
if (!Array.isArray(input) || input.length === 0)
|
|
48
|
+
throw Error("Choose at least one barcode format.");
|
|
49
|
+
for (const value of input) {
|
|
50
|
+
if (typeof value !== "string")
|
|
51
|
+
throw Error("Barcode format must be a string.");
|
|
52
|
+
if (!Object.hasOwn(formatBits, value))
|
|
53
|
+
throw Error(`Unsupported barcode format: ${value}`);
|
|
54
|
+
}
|
|
55
|
+
return [...new Set(input)];
|
|
56
|
+
}
|
|
57
|
+
export function maskFor(formats) {
|
|
58
|
+
return formats.reduce((mask, f) => mask | formatBits[f], 0);
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Quad } from "../host.js";
|
|
2
|
+
export type Transform = readonly number[];
|
|
3
|
+
export declare function project(t: Transform, x: number, y: number): [number, number];
|
|
4
|
+
export declare function transformFor(quad: Quad, width: number, height: number): Transform;
|
|
5
|
+
export declare function rectify(gray: Uint8Array, width: number, height: number, quad: Quad): {
|
|
6
|
+
data: Uint8Array<ArrayBuffer>;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
transform: number[];
|
|
10
|
+
};
|
|
11
|
+
/** Convex intersection, including opposite winding and projective quadrilaterals. */
|
|
12
|
+
export declare function polygonOverlap(a: Quad, b: Quad): {
|
|
13
|
+
iou: number;
|
|
14
|
+
smaller: number;
|
|
15
|
+
a: number;
|
|
16
|
+
b: number;
|
|
17
|
+
};
|
|
18
|
+
/** Suppress overlapping reads of the same physical symbol, never by text alone. */
|
|
19
|
+
export declare function distinctReads<T extends {
|
|
20
|
+
text: string;
|
|
21
|
+
format: string;
|
|
22
|
+
polygon: Quad;
|
|
23
|
+
support: number;
|
|
24
|
+
eanAddOn?: string;
|
|
25
|
+
readerInitialization?: boolean;
|
|
26
|
+
structuredAppend?: {
|
|
27
|
+
index: number;
|
|
28
|
+
count: number;
|
|
29
|
+
id?: string;
|
|
30
|
+
parity?: number;
|
|
31
|
+
};
|
|
32
|
+
}>(reads: readonly T[]): T[];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export function project(t, x, y) {
|
|
2
|
+
const z = t[6] * x + t[7] * y + 1;
|
|
3
|
+
return [(t[0] * x + t[1] * y + t[2]) / z, (t[3] * x + t[4] * y + t[5]) / z];
|
|
4
|
+
}
|
|
5
|
+
export function transformFor(quad, width, height) {
|
|
6
|
+
const source = [
|
|
7
|
+
[0, 0],
|
|
8
|
+
[width, 0],
|
|
9
|
+
[width, height],
|
|
10
|
+
[0, height],
|
|
11
|
+
];
|
|
12
|
+
const a = [];
|
|
13
|
+
for (let i = 0; i < 4; i++) {
|
|
14
|
+
const [x, y] = source[i], [u, v] = quad[i];
|
|
15
|
+
a.push([x, y, 1, 0, 0, 0, -u * x, -u * y, u], [0, 0, 0, x, y, 1, -v * x, -v * y, v]);
|
|
16
|
+
}
|
|
17
|
+
for (let col = 0; col < 8; col++) {
|
|
18
|
+
let pivot = col;
|
|
19
|
+
for (let i = col + 1; i < 8; i++)
|
|
20
|
+
if (Math.abs(a[i][col]) > Math.abs(a[pivot][col]))
|
|
21
|
+
pivot = i;
|
|
22
|
+
if (Math.abs(a[pivot][col]) < 1e-9)
|
|
23
|
+
throw Error("Degenerate barcode region.");
|
|
24
|
+
[a[col], a[pivot]] = [a[pivot], a[col]];
|
|
25
|
+
const divisor = a[col][col];
|
|
26
|
+
for (let j = col; j <= 8; j++)
|
|
27
|
+
a[col][j] /= divisor;
|
|
28
|
+
for (let i = 0; i < 8; i++) {
|
|
29
|
+
if (i === col)
|
|
30
|
+
continue;
|
|
31
|
+
const scale = a[i][col];
|
|
32
|
+
for (let j = col; j <= 8; j++)
|
|
33
|
+
a[i][j] -= scale * a[col][j];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return a.map((row) => row[8]);
|
|
37
|
+
}
|
|
38
|
+
export function rectify(gray, width, height, quad) {
|
|
39
|
+
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
40
|
+
const w = Math.max(8, Math.ceil(Math.max(distance(quad[0], quad[1]), distance(quad[3], quad[2]))));
|
|
41
|
+
const h = Math.max(8, Math.ceil(Math.max(distance(quad[0], quad[3]), distance(quad[1], quad[2]))));
|
|
42
|
+
if (w * h > 8 * 1024 * 1024)
|
|
43
|
+
throw Error("Barcode region exceeds rectification budget.");
|
|
44
|
+
const base = transformFor(quad, w, h);
|
|
45
|
+
const pad = 8, divisor = 1 - pad * (base[6] + base[7]);
|
|
46
|
+
const t = [
|
|
47
|
+
base[0] / divisor,
|
|
48
|
+
base[1] / divisor,
|
|
49
|
+
(base[2] - pad * (base[0] + base[1])) / divisor,
|
|
50
|
+
base[3] / divisor,
|
|
51
|
+
base[4] / divisor,
|
|
52
|
+
(base[5] - pad * (base[3] + base[4])) / divisor,
|
|
53
|
+
base[6] / divisor,
|
|
54
|
+
base[7] / divisor,
|
|
55
|
+
];
|
|
56
|
+
const paddedWidth = w + 2 * pad, paddedHeight = h + 2 * pad;
|
|
57
|
+
const data = new Uint8Array(paddedWidth * paddedHeight);
|
|
58
|
+
for (let y = 0; y < paddedHeight; y++)
|
|
59
|
+
for (let x = 0; x < paddedWidth; x++) {
|
|
60
|
+
const [xx, yy] = project(t, x + 0.5, y + 0.5);
|
|
61
|
+
if (xx < 0 || yy < 0 || xx > width - 1 || yy > height - 1) {
|
|
62
|
+
data[y * paddedWidth + x] = 255;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const x0 = Math.floor(xx), y0 = Math.floor(yy), fx = xx - x0, fy = yy - y0;
|
|
66
|
+
const x1 = Math.min(width - 1, x0 + 1), y1 = Math.min(height - 1, y0 + 1);
|
|
67
|
+
data[y * paddedWidth + x] = Math.round((gray[y0 * width + x0] * (1 - fx) + gray[y0 * width + x1] * fx) * (1 - fy) +
|
|
68
|
+
(gray[y1 * width + x0] * (1 - fx) + gray[y1 * width + x1] * fx) * fy);
|
|
69
|
+
}
|
|
70
|
+
return { data, width: paddedWidth, height: paddedHeight, transform: t };
|
|
71
|
+
}
|
|
72
|
+
/** Convex intersection, including opposite winding and projective quadrilaterals. */
|
|
73
|
+
export function polygonOverlap(a, b) {
|
|
74
|
+
const signedArea = (p) => p.reduce((sum, q, i) => {
|
|
75
|
+
const r = p[(i + 1) % p.length];
|
|
76
|
+
return sum + q[0] * r[1] - q[1] * r[0];
|
|
77
|
+
}, 0) / 2;
|
|
78
|
+
const aa = Math.abs(signedArea(a)), bb = Math.abs(signedArea(b));
|
|
79
|
+
if (Math.min(aa, bb) < 1e-6)
|
|
80
|
+
return { iou: 0, smaller: 0, a: 0, b: 0 };
|
|
81
|
+
const winding = Math.sign(signedArea(b));
|
|
82
|
+
let points = [...a];
|
|
83
|
+
for (let i = 0; i < 4 && points.length; i++) {
|
|
84
|
+
const p = b[i], q = b[(i + 1) % 4];
|
|
85
|
+
const side = (r) => winding * ((q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]));
|
|
86
|
+
const clipped = [];
|
|
87
|
+
for (let j = 0; j < points.length; j++) {
|
|
88
|
+
const u = points[j], v = points[(j + 1) % points.length], su = side(u), sv = side(v);
|
|
89
|
+
if (su >= 0)
|
|
90
|
+
clipped.push(u);
|
|
91
|
+
if (su >= 0 !== sv >= 0) {
|
|
92
|
+
const t = su / (su - sv);
|
|
93
|
+
clipped.push([u[0] + t * (v[0] - u[0]), u[1] + t * (v[1] - u[1])]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
points = clipped;
|
|
97
|
+
}
|
|
98
|
+
const intersection = Math.min(aa, bb, Math.abs(signedArea(points)));
|
|
99
|
+
return {
|
|
100
|
+
iou: intersection / (aa + bb - intersection),
|
|
101
|
+
smaller: intersection / Math.min(aa, bb),
|
|
102
|
+
a: intersection / aa,
|
|
103
|
+
b: intersection / bb,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Suppress overlapping reads of the same physical symbol, never by text alone. */
|
|
107
|
+
export function distinctReads(reads) {
|
|
108
|
+
const result = [];
|
|
109
|
+
for (const read of [...reads].sort((a, b) => b.support - a.support)) {
|
|
110
|
+
if (!result.some((r) => r.text === read.text &&
|
|
111
|
+
r.format === read.format &&
|
|
112
|
+
r.eanAddOn === read.eanAddOn &&
|
|
113
|
+
Boolean(r.readerInitialization) === Boolean(read.readerInitialization) &&
|
|
114
|
+
r.structuredAppend?.index === read.structuredAppend?.index &&
|
|
115
|
+
r.structuredAppend?.count === read.structuredAppend?.count &&
|
|
116
|
+
r.structuredAppend?.id === read.structuredAppend?.id &&
|
|
117
|
+
r.structuredAppend?.parity === read.structuredAppend?.parity &&
|
|
118
|
+
polygonOverlap(r.polygon, read.polygon).smaller >= 0.65))
|
|
119
|
+
result.push(read);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const littleEndian = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
2
|
+
/** Input dimensions are validated by the scanner. Preserve its integer luma rule. */
|
|
3
|
+
export function toGray({ data, width, height, channels, stride }) {
|
|
4
|
+
const count = width * height;
|
|
5
|
+
if (channels === 1 && stride === width)
|
|
6
|
+
return data.slice(0, count);
|
|
7
|
+
const gray = new Uint8Array(count);
|
|
8
|
+
if (channels === 4 && stride === width * 4 && littleEndian && data.byteOffset % 4 === 0) {
|
|
9
|
+
const words = new Uint32Array(data.buffer, data.byteOffset, count);
|
|
10
|
+
for (let i = 0; i < count; i++) {
|
|
11
|
+
const value = words[i];
|
|
12
|
+
gray[i] =
|
|
13
|
+
((value & 255) * 77 + ((value >>> 8) & 255) * 150 + ((value >>> 16) & 255) * 29 + 128) >>>
|
|
14
|
+
8;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else if (channels === 4 && stride === width * 4) {
|
|
18
|
+
for (let i = 0, j = 0; j < count; i += 4, j++)
|
|
19
|
+
gray[j] = (data[i] * 77 + data[i + 1] * 150 + data[i + 2] * 29 + 128) >> 8;
|
|
20
|
+
}
|
|
21
|
+
else if (channels === 3 && stride === width * 3) {
|
|
22
|
+
for (let i = 0, j = 0; j < count; i += 3, j++)
|
|
23
|
+
gray[j] = (data[i] * 77 + data[i + 1] * 150 + data[i + 2] * 29 + 128) >> 8;
|
|
24
|
+
}
|
|
25
|
+
else if (channels === 1) {
|
|
26
|
+
for (let y = 0; y < height; y++)
|
|
27
|
+
gray.set(data.subarray(y * stride, y * stride + width), y * width);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
for (let y = 0; y < height; y++) {
|
|
31
|
+
for (let x = 0, i = y * stride, j = y * width; x < width; x++, i += channels, j++)
|
|
32
|
+
gray[j] = (data[i] * 77 + data[i + 1] * 150 + data[i + 2] * 29 + 128) >> 8;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return gray;
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Image, type Quad } from "../multiformat-host.js";
|
|
2
|
+
import type { Mode } from "../index.js";
|
|
3
|
+
import { type Format } from "./formats.js";
|
|
4
|
+
export interface Barcode {
|
|
5
|
+
format: Format | "Unknown";
|
|
6
|
+
text: string;
|
|
7
|
+
polygon: Quad;
|
|
8
|
+
support: number;
|
|
9
|
+
localizationScore?: number;
|
|
10
|
+
gs1?: boolean;
|
|
11
|
+
readerInitialization?: boolean;
|
|
12
|
+
structuredAppend?: {
|
|
13
|
+
index: number;
|
|
14
|
+
count: number;
|
|
15
|
+
id?: string;
|
|
16
|
+
parity?: number;
|
|
17
|
+
};
|
|
18
|
+
eanAddOn?: string;
|
|
19
|
+
error?: number;
|
|
20
|
+
rank?: number;
|
|
21
|
+
}
|
|
22
|
+
export type EanAddOnSymbol = "Ignore" | "Read" | "Require";
|
|
23
|
+
export interface Frame {
|
|
24
|
+
eanAddOnSymbol: EanAddOnSymbol;
|
|
25
|
+
barcodes: Barcode[];
|
|
26
|
+
regions: Barcode[];
|
|
27
|
+
formats: Format[];
|
|
28
|
+
unfinished: boolean;
|
|
29
|
+
scanMs: number;
|
|
30
|
+
mediumMs: number;
|
|
31
|
+
additionalMs: number;
|
|
32
|
+
preparationMs: number;
|
|
33
|
+
localizationMs: number;
|
|
34
|
+
linearStrategy: "scanlines" | "medium-localized";
|
|
35
|
+
}
|
|
36
|
+
/** Frozen EAN13 Medium plus project-owned opt-in readers. No reference decoder. */
|
|
37
|
+
export declare class MediumMultiformatScanner {
|
|
38
|
+
private medium?;
|
|
39
|
+
private mode;
|
|
40
|
+
private extra?;
|
|
41
|
+
private handle;
|
|
42
|
+
private disposed;
|
|
43
|
+
private constructor();
|
|
44
|
+
static create(mediumBytes: ArrayBuffer, extraBytes?: ArrayBuffer, mode?: Mode, recoveryBytes?: ArrayBuffer): Promise<MediumMultiformatScanner>;
|
|
45
|
+
scan(image: Image, inputFormats?: readonly string[], options?: {
|
|
46
|
+
linearStrategy?: "scanlines" | "medium-localized";
|
|
47
|
+
eanAddOnSymbol?: EanAddOnSymbol;
|
|
48
|
+
}): Frame;
|
|
49
|
+
private scanExtra;
|
|
50
|
+
dispose(): void;
|
|
51
|
+
}
|