@stll/scan-engine 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -2
- package/dist/review.d.ts +50 -0
- package/dist/review.js +82 -0
- package/dist/scan-engine.d.ts +57 -0
- package/dist/scan-engine.js +223 -0
- package/dist/scan-engine.worker.d.ts +1 -0
- package/dist/scan-engine.worker.js +60 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +5 -0
- package/generated/scan_engine.d.ts +91 -0
- package/generated/scan_engine.js +489 -0
- package/generated/scan_engine_bg.wasm +0 -0
- package/generated/scan_engine_bg.wasm.d.ts +32 -0
- package/package.json +73 -4
package/README.md
CHANGED
|
@@ -1,3 +1,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@stll/scan-engine`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Document scan cleanup engine implemented in Rust and compiled to WebAssembly:
|
|
4
|
+
deskew, content crop and grayscale or binary output for one rasterized page at
|
|
5
|
+
a time, plus an engine-independent review state machine for accepting or
|
|
6
|
+
rejecting the result page by page.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createScanEngine } from "@stll/scan-engine";
|
|
12
|
+
|
|
13
|
+
const engine = createScanEngine();
|
|
14
|
+
|
|
15
|
+
const result = await engine.clean(
|
|
16
|
+
{
|
|
17
|
+
pixels, // Uint8ClampedArray of RGBA pixels, e.g. ImageData.data
|
|
18
|
+
width,
|
|
19
|
+
height,
|
|
20
|
+
pageSize: { widthPoints: 595, heightPoints: 842 },
|
|
21
|
+
dpi: 150,
|
|
22
|
+
rotation: 0, // 0 | 90 | 180 | 270
|
|
23
|
+
},
|
|
24
|
+
{ output: "binary", contentCrop: "on", deskew: "on" },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (result.isErr()) {
|
|
28
|
+
// ScanEngineError: { type: "engine-failure" | "engine-timeout", message }
|
|
29
|
+
} else if (result.value.type === "cleaned") {
|
|
30
|
+
const { image, skewDegrees, contentBox, appliedQuad } = result.value;
|
|
31
|
+
// image.pixels holds one byte per pixel (image.width × image.height).
|
|
32
|
+
} else {
|
|
33
|
+
// { type: "unrecognized", reason }, e.g. "no-content" for a blank page.
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`clean` returns a `better-result` `Result` and never throws. Where `Worker`
|
|
38
|
+
exists (browsers, Bun), the engine runs in a module worker that it creates on
|
|
39
|
+
the first call; requests are serialized, and a request that takes longer than
|
|
40
|
+
10 seconds terminates the worker and fails with `engine-timeout`. The next call
|
|
41
|
+
starts a fresh worker. `createScanEngine(undefined, { requestTimeoutMs })`
|
|
42
|
+
changes the timeout. `scanEngine` is a shared instance of the default engine.
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
|
|
46
|
+
- `output`: `"grayscale"` or `"binary"` (pixels are strictly `0` or `255`).
|
|
47
|
+
- `contentCrop`: `"on"` crops to the detected content; `contentBox` gives
|
|
48
|
+
its bounds in the output image either way.
|
|
49
|
+
- `deskew`: `"on"` rotates the page straight; `skewDegrees` reports the
|
|
50
|
+
detected angle, and deskewing grows the output to contain the rotated page.
|
|
51
|
+
|
|
52
|
+
Types (`RasterizedScanPage`, `ScanOptions`, `CleanScanResult`, `ScanEngine`,
|
|
53
|
+
`ScanEngineError`, …) are exported from `@stll/scan-engine/types`.
|
|
54
|
+
|
|
55
|
+
## Review
|
|
56
|
+
|
|
57
|
+
`@stll/scan-engine/review` tracks each page through `pending`, `preview`,
|
|
58
|
+
`applied`, `rejected` and `unrecognized`, with undo. It works with any
|
|
59
|
+
`ScanEngine`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import {
|
|
63
|
+
applyAll,
|
|
64
|
+
applyOne,
|
|
65
|
+
createCleanScanReview,
|
|
66
|
+
rejectOne,
|
|
67
|
+
reviewPage,
|
|
68
|
+
setPageReview,
|
|
69
|
+
targetPageBox,
|
|
70
|
+
undoCleanScanReview,
|
|
71
|
+
} from "@stll/scan-engine/review";
|
|
72
|
+
|
|
73
|
+
let review = createCleanScanReview(["page-1", "page-2"]);
|
|
74
|
+
|
|
75
|
+
const pageReview = await reviewPage({
|
|
76
|
+
engine,
|
|
77
|
+
pageId: "page-1",
|
|
78
|
+
page,
|
|
79
|
+
options,
|
|
80
|
+
});
|
|
81
|
+
if (pageReview.isOk()) {
|
|
82
|
+
review = setPageReview(review, pageReview.value); // preview or unrecognized
|
|
83
|
+
}
|
|
84
|
+
review = applyOne(review, "page-1"); // or rejectOne, or applyAll
|
|
85
|
+
review = undoCleanScanReview(review);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Every transition returns a new review; one that changes nothing returns the
|
|
89
|
+
same object and records no undo step. `targetPageBox(pageSize, rotation)`
|
|
90
|
+
gives the page box, in points, to place a cleaned image into.
|
|
91
|
+
|
|
92
|
+
## Bundlers
|
|
93
|
+
|
|
94
|
+
The package loads `dist/scan-engine.worker.js` and
|
|
95
|
+
`generated/scan_engine_bg.wasm` with `new URL(…, import.meta.url)`, so the
|
|
96
|
+
bundler has to see those expressions in the installed package. Vite 8 rewrites
|
|
97
|
+
them in both `vite dev` and `vite build` with no configuration. If an older
|
|
98
|
+
Vite's dependency pre-bundling leaves them pointing into its cache directory
|
|
99
|
+
(the worker or the `.wasm` then fails to load in `vite dev` only), exclude the
|
|
100
|
+
package so the URLs resolve against its own files:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// vite.config.ts
|
|
104
|
+
export default defineConfig({
|
|
105
|
+
optimizeDeps: { exclude: ["@stll/scan-engine"] },
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
Run `bun run build:wasm` to build the WebAssembly target and its web bindings
|
|
112
|
+
(it fetches crates and the pinned `wasm-bindgen` CLI first unless
|
|
113
|
+
`CARGO_NET_OFFLINE=true`). To verify the committed copy, build into a scratch
|
|
114
|
+
directory and compare:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
export SCAN_ENGINE_WASM_OUT_DIR="$(mktemp -d)"
|
|
118
|
+
bun run build:wasm && bun run check:wasm --check
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Generated bindings in `generated/` are committed and ship in the package next
|
|
122
|
+
to `dist/`.
|
package/dist/review.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CleanScanResult, PhysicalPageSize, RasterizedScanPage, ScanEngine, ScanEngineError, ScanOptions, ScanPageRotation } from "./types.js";
|
|
2
|
+
import { Result } from "better-result";
|
|
3
|
+
//#region src/review.d.ts
|
|
4
|
+
type CleanedScan = Extract<CleanScanResult, {
|
|
5
|
+
type: "cleaned";
|
|
6
|
+
}>;
|
|
7
|
+
export type CleanScanPageReview = {
|
|
8
|
+
type: "pending";
|
|
9
|
+
pageId: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "preview";
|
|
12
|
+
pageId: string;
|
|
13
|
+
scan: CleanedScan;
|
|
14
|
+
} | {
|
|
15
|
+
type: "applied";
|
|
16
|
+
pageId: string;
|
|
17
|
+
scan: CleanedScan;
|
|
18
|
+
} | {
|
|
19
|
+
type: "rejected";
|
|
20
|
+
pageId: string;
|
|
21
|
+
scan: CleanedScan;
|
|
22
|
+
} | {
|
|
23
|
+
type: "unrecognized";
|
|
24
|
+
pageId: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
27
|
+
export type CleanScanReview = {
|
|
28
|
+
pages: readonly CleanScanPageReview[];
|
|
29
|
+
past: readonly (readonly CleanScanPageReview[])[];
|
|
30
|
+
};
|
|
31
|
+
type ReviewPageOptions = {
|
|
32
|
+
engine: ScanEngine;
|
|
33
|
+
pageId: string;
|
|
34
|
+
page: RasterizedScanPage;
|
|
35
|
+
options: ScanOptions;
|
|
36
|
+
};
|
|
37
|
+
export declare const reviewPage: ({ engine, pageId, page, options }: ReviewPageOptions) => Promise<Result<CleanScanPageReview, ScanEngineError>>;
|
|
38
|
+
export declare const createCleanScanReview: (pageIds: readonly string[]) => CleanScanReview;
|
|
39
|
+
export declare const applyOne: (review: CleanScanReview, pageId: string) => CleanScanReview;
|
|
40
|
+
export declare const applyAll: (review: CleanScanReview) => CleanScanReview;
|
|
41
|
+
export declare const rejectOne: (review: CleanScanReview, pageId: string) => CleanScanReview;
|
|
42
|
+
export declare const setPageReview: (review: CleanScanReview, pageReview: CleanScanPageReview) => CleanScanReview;
|
|
43
|
+
export declare const undoCleanScanReview: (review: CleanScanReview) => CleanScanReview;
|
|
44
|
+
export type TargetPageBox = PhysicalPageSize & {
|
|
45
|
+
xPoints: 0;
|
|
46
|
+
yPoints: 0;
|
|
47
|
+
rotation: ScanPageRotation;
|
|
48
|
+
};
|
|
49
|
+
export declare const targetPageBox: (pageSize: PhysicalPageSize, rotation: ScanPageRotation) => TargetPageBox;
|
|
50
|
+
//#endregion
|
package/dist/review.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Result, panic } from "better-result";
|
|
2
|
+
//#region src/review.ts
|
|
3
|
+
const reviewPage = async ({ engine, pageId, page, options }) => {
|
|
4
|
+
const result = await engine.clean(page, options);
|
|
5
|
+
if (result.isErr()) return Result.err(result.error);
|
|
6
|
+
switch (result.value.type) {
|
|
7
|
+
case "cleaned": return Result.ok({
|
|
8
|
+
type: "preview",
|
|
9
|
+
pageId,
|
|
10
|
+
scan: result.value
|
|
11
|
+
});
|
|
12
|
+
case "unrecognized": return Result.ok({
|
|
13
|
+
type: "unrecognized",
|
|
14
|
+
pageId,
|
|
15
|
+
reason: result.value.reason
|
|
16
|
+
});
|
|
17
|
+
default: return panic("Unhandled scan result", result.value);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const createCleanScanReview = (pageIds) => ({
|
|
21
|
+
pages: pageIds.map((pageId) => ({
|
|
22
|
+
type: "pending",
|
|
23
|
+
pageId
|
|
24
|
+
})),
|
|
25
|
+
past: []
|
|
26
|
+
});
|
|
27
|
+
const applyPage = (page) => {
|
|
28
|
+
switch (page.type) {
|
|
29
|
+
case "preview": return {
|
|
30
|
+
type: "applied",
|
|
31
|
+
pageId: page.pageId,
|
|
32
|
+
scan: page.scan
|
|
33
|
+
};
|
|
34
|
+
case "pending":
|
|
35
|
+
case "applied":
|
|
36
|
+
case "rejected":
|
|
37
|
+
case "unrecognized": return page;
|
|
38
|
+
default: return panic("Unhandled clean scan review state", page);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const rejectPage = (page) => {
|
|
42
|
+
switch (page.type) {
|
|
43
|
+
case "preview": return {
|
|
44
|
+
type: "rejected",
|
|
45
|
+
pageId: page.pageId,
|
|
46
|
+
scan: page.scan
|
|
47
|
+
};
|
|
48
|
+
case "pending":
|
|
49
|
+
case "applied":
|
|
50
|
+
case "rejected":
|
|
51
|
+
case "unrecognized": return page;
|
|
52
|
+
default: return panic("Unhandled clean scan review state", page);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const recordTransition = (review, pages) => {
|
|
56
|
+
if (pages.every((page, index) => page === review.pages[index])) return review;
|
|
57
|
+
return {
|
|
58
|
+
pages,
|
|
59
|
+
past: [...review.past, review.pages]
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const applyOne = (review, pageId) => recordTransition(review, review.pages.map((page) => page.pageId === pageId ? applyPage(page) : page));
|
|
63
|
+
const applyAll = (review) => recordTransition(review, review.pages.map(applyPage));
|
|
64
|
+
const rejectOne = (review, pageId) => recordTransition(review, review.pages.map((page) => page.pageId === pageId ? rejectPage(page) : page));
|
|
65
|
+
const setPageReview = (review, pageReview) => recordTransition(review, review.pages.map((page) => page.pageId === pageReview.pageId ? pageReview : page));
|
|
66
|
+
const undoCleanScanReview = (review) => {
|
|
67
|
+
const pages = review.past.at(-1);
|
|
68
|
+
if (!pages) return review;
|
|
69
|
+
return {
|
|
70
|
+
pages,
|
|
71
|
+
past: review.past.slice(0, -1)
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const targetPageBox = (pageSize, rotation) => ({
|
|
75
|
+
xPoints: 0,
|
|
76
|
+
yPoints: 0,
|
|
77
|
+
widthPoints: pageSize.widthPoints,
|
|
78
|
+
heightPoints: pageSize.heightPoints,
|
|
79
|
+
rotation
|
|
80
|
+
});
|
|
81
|
+
//#endregion
|
|
82
|
+
export { applyAll, applyOne, createCleanScanReview, rejectOne, reviewPage, setPageReview, targetPageBox, undoCleanScanReview };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { RasterizedScanPage, ScanEngine, ScanOptions } from "./types.js";
|
|
2
|
+
//#region src/worker-protocol.d.ts
|
|
3
|
+
type SerializedWasmResult = {
|
|
4
|
+
status: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
pixels: Uint8Array;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
skew_degrees: number;
|
|
10
|
+
/** Content bounds in the returned output image. */
|
|
11
|
+
content_box: Uint32Array;
|
|
12
|
+
/** Detected page corners in the source image, clockwise from top-left. */
|
|
13
|
+
applied_quad: Float32Array;
|
|
14
|
+
};
|
|
15
|
+
type WorkerRequest = {
|
|
16
|
+
page: Omit<RasterizedScanPage, "pixels">;
|
|
17
|
+
pixels: ArrayBuffer;
|
|
18
|
+
options: ScanOptions;
|
|
19
|
+
};
|
|
20
|
+
type WorkerResponse = {
|
|
21
|
+
type: "result";
|
|
22
|
+
result: SerializedWasmResult;
|
|
23
|
+
} | {
|
|
24
|
+
type: "error";
|
|
25
|
+
errorType: "runtime-trap" | "failure";
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/scan-engine.d.ts
|
|
30
|
+
type WasmScanResult = {
|
|
31
|
+
free: () => void;
|
|
32
|
+
readonly status: string;
|
|
33
|
+
readonly reason: string;
|
|
34
|
+
readonly pixels: Uint8Array;
|
|
35
|
+
readonly width: number;
|
|
36
|
+
readonly height: number;
|
|
37
|
+
readonly skew_degrees: number;
|
|
38
|
+
readonly content_box: Uint32Array;
|
|
39
|
+
readonly applied_quad: Float32Array;
|
|
40
|
+
};
|
|
41
|
+
type WasmBindings = {
|
|
42
|
+
clean_scan_rgba: (...args: [width: number, height: number, pixels: Uint8Array, dpi: number, output: string, contentCrop: boolean, deskew: boolean]) => WasmScanResult;
|
|
43
|
+
};
|
|
44
|
+
type WasmLoader = () => Promise<WasmBindings>;
|
|
45
|
+
type WorkerClient = {
|
|
46
|
+
addEventListener: (type: "message" | "error", listener: (event: MessageEvent<WorkerResponse> | ErrorEvent) => void) => void;
|
|
47
|
+
removeEventListener: (type: "message" | "error", listener: (event: MessageEvent<WorkerResponse> | ErrorEvent) => void) => void;
|
|
48
|
+
postMessage: (message: WorkerRequest, transfer: ArrayBuffer[]) => void;
|
|
49
|
+
terminate: () => void;
|
|
50
|
+
};
|
|
51
|
+
type WorkerEngineOptions = {
|
|
52
|
+
createWorker?: () => WorkerClient;
|
|
53
|
+
requestTimeoutMs?: number;
|
|
54
|
+
};
|
|
55
|
+
export declare const createScanEngine: (load?: WasmLoader, workerOptions?: WorkerEngineOptions) => ScanEngine;
|
|
56
|
+
export declare const scanEngine: ScanEngine;
|
|
57
|
+
//#endregion
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { ScanEngineError } from "./types.js";
|
|
2
|
+
import { Result } from "better-result";
|
|
3
|
+
import { fetchWithTimeout } from "@stll/fetch";
|
|
4
|
+
import { clean_scan_rgba, initSync } from "../generated/scan_engine.js";
|
|
5
|
+
//#region src/scan-engine.ts
|
|
6
|
+
const WASM_LOAD_TIMEOUT_MS = 1e4;
|
|
7
|
+
const WORKER_REQUEST_TIMEOUT_MS = 1e4;
|
|
8
|
+
const loadWasmBindings = async () => {
|
|
9
|
+
const response = await fetchWithTimeout(new URL("../generated/scan_engine_bg.wasm", import.meta.url), { timeout: {
|
|
10
|
+
type: "idle",
|
|
11
|
+
ms: WASM_LOAD_TIMEOUT_MS
|
|
12
|
+
} });
|
|
13
|
+
const module = await WebAssembly.compile(await response.arrayBuffer());
|
|
14
|
+
initSync({ module });
|
|
15
|
+
return { clean_scan_rgba };
|
|
16
|
+
};
|
|
17
|
+
const createWorkerEngine = ({ createWorker = () => new Worker(new URL("scan-engine.worker.js", import.meta.url), { type: "module" }), requestTimeoutMs = WORKER_REQUEST_TIMEOUT_MS } = {}) => {
|
|
18
|
+
let worker;
|
|
19
|
+
let queue = Promise.resolve();
|
|
20
|
+
const reset = () => {
|
|
21
|
+
worker?.terminate();
|
|
22
|
+
worker = void 0;
|
|
23
|
+
};
|
|
24
|
+
const clean = async (page, options) => {
|
|
25
|
+
const run = async () => {
|
|
26
|
+
worker ??= createWorker();
|
|
27
|
+
const activeWorker = worker;
|
|
28
|
+
const pixels = page.pixels.slice().buffer;
|
|
29
|
+
const request = {
|
|
30
|
+
page: {
|
|
31
|
+
width: page.width,
|
|
32
|
+
height: page.height,
|
|
33
|
+
pageSize: page.pageSize,
|
|
34
|
+
dpi: page.dpi,
|
|
35
|
+
rotation: page.rotation
|
|
36
|
+
},
|
|
37
|
+
pixels,
|
|
38
|
+
options
|
|
39
|
+
};
|
|
40
|
+
return await new Promise((resolve, reject) => {
|
|
41
|
+
let settled = false;
|
|
42
|
+
const settle = (complete) => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settled = true;
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
activeWorker.removeEventListener("message", onMessage);
|
|
47
|
+
activeWorker.removeEventListener("error", onError);
|
|
48
|
+
complete();
|
|
49
|
+
};
|
|
50
|
+
const onMessage = (event) => {
|
|
51
|
+
if (!(event instanceof MessageEvent)) return;
|
|
52
|
+
const { data } = event;
|
|
53
|
+
if (data.type === "result") {
|
|
54
|
+
settle(() => {
|
|
55
|
+
try {
|
|
56
|
+
resolve(convertResult({
|
|
57
|
+
...data.result,
|
|
58
|
+
free: () => void 0
|
|
59
|
+
}, options.output));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
reject(error instanceof ScanEngineError ? error : new ScanEngineError({
|
|
62
|
+
type: "engine-failure",
|
|
63
|
+
message: "The scan engine returned an invalid result"
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
reset();
|
|
70
|
+
settle(() => reject(data.errorType === "runtime-trap" ? new WebAssembly.RuntimeError(data.message) : new ScanEngineError({
|
|
71
|
+
type: "engine-failure",
|
|
72
|
+
message: data.message
|
|
73
|
+
})));
|
|
74
|
+
};
|
|
75
|
+
const onError = (event) => {
|
|
76
|
+
if (!(event instanceof ErrorEvent)) return;
|
|
77
|
+
reset();
|
|
78
|
+
settle(() => reject(new ScanEngineError({
|
|
79
|
+
type: "engine-failure",
|
|
80
|
+
message: event.message
|
|
81
|
+
})));
|
|
82
|
+
};
|
|
83
|
+
const timeout = setTimeout(() => {
|
|
84
|
+
reset();
|
|
85
|
+
settle(() => reject(new ScanEngineError({
|
|
86
|
+
type: "engine-timeout",
|
|
87
|
+
message: "The scan engine request timed out"
|
|
88
|
+
})));
|
|
89
|
+
}, requestTimeoutMs);
|
|
90
|
+
activeWorker.addEventListener("message", onMessage);
|
|
91
|
+
activeWorker.addEventListener("error", onError);
|
|
92
|
+
activeWorker.postMessage(request, [pixels]);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const result = queue.then(run, run);
|
|
96
|
+
queue = result.then(() => void 0, () => void 0);
|
|
97
|
+
return await Result.tryPromise({
|
|
98
|
+
try: async () => await result,
|
|
99
|
+
catch: (cause) => cause instanceof ScanEngineError ? cause : new ScanEngineError({
|
|
100
|
+
type: "engine-failure",
|
|
101
|
+
message: "The scan engine could not clean the page",
|
|
102
|
+
cause
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
return { clean };
|
|
107
|
+
};
|
|
108
|
+
const readContentBox = (values) => {
|
|
109
|
+
const x = values.at(0);
|
|
110
|
+
const y = values.at(1);
|
|
111
|
+
const width = values.at(2);
|
|
112
|
+
const height = values.at(3);
|
|
113
|
+
if (values.length !== 4 || x === void 0 || y === void 0 || width === void 0 || height === void 0) throw new ScanEngineError({
|
|
114
|
+
type: "engine-failure",
|
|
115
|
+
message: "The scan engine returned an invalid content box"
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
x,
|
|
119
|
+
y,
|
|
120
|
+
width,
|
|
121
|
+
height
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
const readQuad = (values) => {
|
|
125
|
+
if (values.length === 0) return null;
|
|
126
|
+
const coordinates = Array.from(values);
|
|
127
|
+
if (coordinates.length !== 8 || coordinates.some((value) => !Number.isFinite(value))) throw new ScanEngineError({
|
|
128
|
+
type: "engine-failure",
|
|
129
|
+
message: "The scan engine returned an invalid page quadrilateral"
|
|
130
|
+
});
|
|
131
|
+
const [x0, y0, x1, y1, x2, y2, x3, y3] = coordinates;
|
|
132
|
+
if (x0 === void 0 || y0 === void 0 || x1 === void 0 || y1 === void 0 || x2 === void 0 || y2 === void 0 || x3 === void 0 || y3 === void 0) throw new ScanEngineError({
|
|
133
|
+
type: "engine-failure",
|
|
134
|
+
message: "The scan engine returned an invalid page quadrilateral"
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
topLeft: {
|
|
138
|
+
x: x0,
|
|
139
|
+
y: y0
|
|
140
|
+
},
|
|
141
|
+
topRight: {
|
|
142
|
+
x: x1,
|
|
143
|
+
y: y1
|
|
144
|
+
},
|
|
145
|
+
bottomRight: {
|
|
146
|
+
x: x2,
|
|
147
|
+
y: y2
|
|
148
|
+
},
|
|
149
|
+
bottomLeft: {
|
|
150
|
+
x: x3,
|
|
151
|
+
y: y3
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
const convertResult = (result, output) => {
|
|
156
|
+
if (result.status === "unrecognized") {
|
|
157
|
+
if (result.reason.length === 0) throw new ScanEngineError({
|
|
158
|
+
type: "engine-failure",
|
|
159
|
+
message: "The scan engine returned an unrecognized page without a reason"
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
type: "unrecognized",
|
|
163
|
+
reason: result.reason
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (result.status !== "cleaned") throw new ScanEngineError({
|
|
167
|
+
type: "engine-failure",
|
|
168
|
+
message: `The scan engine returned an unknown status: ${result.status}`
|
|
169
|
+
});
|
|
170
|
+
if (result.pixels.length !== result.width * result.height) throw new ScanEngineError({
|
|
171
|
+
type: "engine-failure",
|
|
172
|
+
message: "The scan engine returned an invalid pixel buffer"
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
type: "cleaned",
|
|
176
|
+
image: {
|
|
177
|
+
pixels: result.pixels,
|
|
178
|
+
width: result.width,
|
|
179
|
+
height: result.height,
|
|
180
|
+
format: output
|
|
181
|
+
},
|
|
182
|
+
appliedQuad: readQuad(result.applied_quad),
|
|
183
|
+
skewDegrees: result.skew_degrees,
|
|
184
|
+
contentBox: readContentBox(result.content_box)
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
const createScanEngine = (load, workerOptions) => {
|
|
188
|
+
if (load === void 0 && typeof Worker !== "undefined") return createWorkerEngine(workerOptions);
|
|
189
|
+
const resolvedLoad = load ?? loadWasmBindings;
|
|
190
|
+
let bindings;
|
|
191
|
+
return { clean: async (page, options) => await Result.tryPromise({
|
|
192
|
+
try: async () => {
|
|
193
|
+
if (bindings === void 0) {
|
|
194
|
+
const pending = resolvedLoad().catch((error) => {
|
|
195
|
+
if (bindings === pending) bindings = void 0;
|
|
196
|
+
throw error;
|
|
197
|
+
});
|
|
198
|
+
bindings = pending;
|
|
199
|
+
}
|
|
200
|
+
const wasm = await bindings;
|
|
201
|
+
try {
|
|
202
|
+
let result;
|
|
203
|
+
try {
|
|
204
|
+
result = wasm.clean_scan_rgba(page.width, page.height, new Uint8Array(page.pixels.buffer, page.pixels.byteOffset, page.pixels.byteLength), page.dpi, options.output, options.contentCrop === "on", options.deskew === "on");
|
|
205
|
+
return convertResult(result, options.output);
|
|
206
|
+
} finally {
|
|
207
|
+
result?.free();
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof WebAssembly.RuntimeError) bindings = void 0;
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
catch: (cause) => cause instanceof ScanEngineError ? cause : new ScanEngineError({
|
|
215
|
+
type: "engine-failure",
|
|
216
|
+
message: "The scan engine could not clean the page",
|
|
217
|
+
cause
|
|
218
|
+
})
|
|
219
|
+
}) };
|
|
220
|
+
};
|
|
221
|
+
const scanEngine = createScanEngine();
|
|
222
|
+
//#endregion
|
|
223
|
+
export { createScanEngine, scanEngine };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fetchWithTimeout } from "@stll/fetch";
|
|
2
|
+
import initWasm, { clean_scan_rgba } from "../generated/scan_engine.js";
|
|
3
|
+
//#region src/scan-engine.worker.ts
|
|
4
|
+
const WASM_LOAD_TIMEOUT_MS = 1e4;
|
|
5
|
+
const post = (response, transfer = []) => {
|
|
6
|
+
Reflect.apply(postMessage, globalThis, [response, transfer]);
|
|
7
|
+
};
|
|
8
|
+
let initialized;
|
|
9
|
+
const initialize = async () => {
|
|
10
|
+
initialized ??= fetchWithTimeout(new URL("../generated/scan_engine_bg.wasm", import.meta.url), { timeout: {
|
|
11
|
+
type: "idle",
|
|
12
|
+
ms: WASM_LOAD_TIMEOUT_MS
|
|
13
|
+
} }).then(async (response) => await response.arrayBuffer()).then(async (module_or_path) => {
|
|
14
|
+
await initWasm({ module_or_path });
|
|
15
|
+
});
|
|
16
|
+
await initialized;
|
|
17
|
+
};
|
|
18
|
+
const handle = async ({ data }) => {
|
|
19
|
+
try {
|
|
20
|
+
await initialize();
|
|
21
|
+
let result;
|
|
22
|
+
try {
|
|
23
|
+
result = clean_scan_rgba(data.page.width, data.page.height, new Uint8Array(data.pixels), data.page.dpi, data.options.output, data.options.contentCrop === "on", data.options.deskew === "on");
|
|
24
|
+
const response = {
|
|
25
|
+
type: "result",
|
|
26
|
+
result: {
|
|
27
|
+
status: result.status,
|
|
28
|
+
reason: result.reason,
|
|
29
|
+
pixels: result.pixels,
|
|
30
|
+
width: result.width,
|
|
31
|
+
height: result.height,
|
|
32
|
+
skew_degrees: result.skew_degrees,
|
|
33
|
+
content_box: result.content_box,
|
|
34
|
+
applied_quad: result.applied_quad
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const buffer = response.result.pixels.buffer;
|
|
38
|
+
post(response, buffer instanceof ArrayBuffer ? [buffer] : []);
|
|
39
|
+
} finally {
|
|
40
|
+
result?.free();
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const response = {
|
|
44
|
+
type: "error",
|
|
45
|
+
errorType: error instanceof WebAssembly.RuntimeError ? "runtime-trap" : "failure",
|
|
46
|
+
message: error instanceof Error ? error.message : "Unknown worker failure"
|
|
47
|
+
};
|
|
48
|
+
post(response);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
addEventListener("message", (event) => {
|
|
52
|
+
handle(event).catch((error) => {
|
|
53
|
+
post({
|
|
54
|
+
type: "error",
|
|
55
|
+
errorType: "failure",
|
|
56
|
+
message: error instanceof Error ? error.message : "Unknown worker failure"
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
//#endregion
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
export type ScanPageRotation = 0 | 90 | 180 | 270;
|
|
4
|
+
export type PhysicalPageSize = {
|
|
5
|
+
widthPoints: number;
|
|
6
|
+
heightPoints: number;
|
|
7
|
+
};
|
|
8
|
+
export type RasterizedScanPage = {
|
|
9
|
+
pixels: Uint8ClampedArray;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
pageSize: PhysicalPageSize;
|
|
13
|
+
dpi: number;
|
|
14
|
+
rotation: ScanPageRotation;
|
|
15
|
+
};
|
|
16
|
+
export type ScanOptions = {
|
|
17
|
+
output: "grayscale" | "binary";
|
|
18
|
+
contentCrop: "on" | "off";
|
|
19
|
+
deskew: "on" | "off";
|
|
20
|
+
};
|
|
21
|
+
export type ContentBox = {
|
|
22
|
+
/** Bounds of the detected content in the returned output image. */
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
28
|
+
export type CleanedScanImage = {
|
|
29
|
+
pixels: Uint8Array;
|
|
30
|
+
/** Deskewing expands these dimensions to contain the complete rotated page. */
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
format: ScanOptions["output"];
|
|
34
|
+
};
|
|
35
|
+
export type CleanScanResult = {
|
|
36
|
+
type: "cleaned";
|
|
37
|
+
image: CleanedScanImage;
|
|
38
|
+
/** Corners of the detected page in the input image, clockwise from top-left. */
|
|
39
|
+
appliedQuad: {
|
|
40
|
+
topLeft: {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
};
|
|
44
|
+
topRight: {
|
|
45
|
+
x: number;
|
|
46
|
+
y: number;
|
|
47
|
+
};
|
|
48
|
+
bottomRight: {
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
};
|
|
52
|
+
bottomLeft: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
};
|
|
56
|
+
} | null;
|
|
57
|
+
skewDegrees: number;
|
|
58
|
+
contentBox: ContentBox;
|
|
59
|
+
} | {
|
|
60
|
+
type: "unrecognized";
|
|
61
|
+
reason: string;
|
|
62
|
+
};
|
|
63
|
+
declare const ScanEngineError_base: import("better-result").TaggedErrorClass<"ScanEngineError">;
|
|
64
|
+
export declare class ScanEngineError extends ScanEngineError_base<{
|
|
65
|
+
type: "engine-failure" | "engine-timeout";
|
|
66
|
+
message: string;
|
|
67
|
+
cause?: unknown;
|
|
68
|
+
}> {}
|
|
69
|
+
export type ScanEngine = {
|
|
70
|
+
clean: (page: RasterizedScanPage, options: ScanOptions) => Promise<Result<CleanScanResult, ScanEngineError>>;
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
|
|
3
|
+
export class WasmScanResult {
|
|
4
|
+
private constructor();
|
|
5
|
+
free(): void;
|
|
6
|
+
[Symbol.dispose](): void;
|
|
7
|
+
readonly applied_quad: Float32Array;
|
|
8
|
+
readonly content_box: Uint32Array;
|
|
9
|
+
readonly height: number;
|
|
10
|
+
readonly pixels: Uint8Array;
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
readonly skew_degrees: number;
|
|
13
|
+
readonly status: string;
|
|
14
|
+
readonly width: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function clean_scan_rgba(
|
|
18
|
+
width: number,
|
|
19
|
+
height: number,
|
|
20
|
+
pixels: Uint8Array,
|
|
21
|
+
dpi: number,
|
|
22
|
+
output: string,
|
|
23
|
+
content_crop: boolean,
|
|
24
|
+
deskew: boolean,
|
|
25
|
+
): WasmScanResult;
|
|
26
|
+
|
|
27
|
+
export type InitInput =
|
|
28
|
+
RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
29
|
+
|
|
30
|
+
export interface InitOutput {
|
|
31
|
+
readonly memory: WebAssembly.Memory;
|
|
32
|
+
readonly __wbg_wasmscanresult_free: (a: number, b: number) => void;
|
|
33
|
+
readonly clean_scan_rgba: (
|
|
34
|
+
a: number,
|
|
35
|
+
b: number,
|
|
36
|
+
c: number,
|
|
37
|
+
d: number,
|
|
38
|
+
e: number,
|
|
39
|
+
f: number,
|
|
40
|
+
g: number,
|
|
41
|
+
h: number,
|
|
42
|
+
i: number,
|
|
43
|
+
j: number,
|
|
44
|
+
) => void;
|
|
45
|
+
readonly wasmscanresult_applied_quad: (a: number, b: number) => void;
|
|
46
|
+
readonly wasmscanresult_content_box: (a: number, b: number) => void;
|
|
47
|
+
readonly wasmscanresult_pixels: (a: number, b: number) => void;
|
|
48
|
+
readonly wasmscanresult_reason: (a: number, b: number) => void;
|
|
49
|
+
readonly wasmscanresult_status: (a: number, b: number) => void;
|
|
50
|
+
readonly wasmscanresult_height: (a: number) => number;
|
|
51
|
+
readonly wasmscanresult_skew_degrees: (a: number) => number;
|
|
52
|
+
readonly wasmscanresult_width: (a: number) => number;
|
|
53
|
+
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
54
|
+
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
55
|
+
readonly __wbindgen_export2: (
|
|
56
|
+
a: number,
|
|
57
|
+
b: number,
|
|
58
|
+
c: number,
|
|
59
|
+
d: number,
|
|
60
|
+
) => number;
|
|
61
|
+
readonly __wbindgen_export3: (a: number, b: number, c: number) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
68
|
+
* a precompiled `WebAssembly.Module`.
|
|
69
|
+
*
|
|
70
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
71
|
+
*
|
|
72
|
+
* @returns {InitOutput}
|
|
73
|
+
*/
|
|
74
|
+
export function initSync(
|
|
75
|
+
module: { module: SyncInitInput } | SyncInitInput,
|
|
76
|
+
): InitOutput;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
80
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
81
|
+
*
|
|
82
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
83
|
+
*
|
|
84
|
+
* @returns {Promise<InitOutput>}
|
|
85
|
+
*/
|
|
86
|
+
export default function __wbg_init(
|
|
87
|
+
module_or_path?:
|
|
88
|
+
| { module_or_path: InitInput | Promise<InitInput> }
|
|
89
|
+
| InitInput
|
|
90
|
+
| Promise<InitInput>,
|
|
91
|
+
): Promise<InitOutput>;
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/* @ts-self-types="./scan_engine.d.ts" */
|
|
2
|
+
|
|
3
|
+
export class WasmScanResult {
|
|
4
|
+
static __wrap(ptr) {
|
|
5
|
+
const obj = Object.create(WasmScanResult.prototype);
|
|
6
|
+
obj.__wbg_ptr = ptr;
|
|
7
|
+
WasmScanResultFinalization.register(obj, obj.__wbg_ptr, obj);
|
|
8
|
+
return obj;
|
|
9
|
+
}
|
|
10
|
+
__destroy_into_raw() {
|
|
11
|
+
const ptr = this.__wbg_ptr;
|
|
12
|
+
this.__wbg_ptr = 0;
|
|
13
|
+
WasmScanResultFinalization.unregister(this);
|
|
14
|
+
return ptr;
|
|
15
|
+
}
|
|
16
|
+
free() {
|
|
17
|
+
const ptr = this.__destroy_into_raw();
|
|
18
|
+
wasm.__wbg_wasmscanresult_free(ptr, 0);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @returns {Float32Array}
|
|
22
|
+
*/
|
|
23
|
+
get applied_quad() {
|
|
24
|
+
try {
|
|
25
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
26
|
+
wasm.wasmscanresult_applied_quad(retptr, this.__wbg_ptr);
|
|
27
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
28
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
29
|
+
var v1 = getArrayF32FromWasm0(r0, r1).slice();
|
|
30
|
+
wasm.__wbindgen_export3(r0, r1 * 4, 4);
|
|
31
|
+
return v1;
|
|
32
|
+
} finally {
|
|
33
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @returns {Uint32Array}
|
|
38
|
+
*/
|
|
39
|
+
get content_box() {
|
|
40
|
+
try {
|
|
41
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
42
|
+
wasm.wasmscanresult_content_box(retptr, this.__wbg_ptr);
|
|
43
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
44
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
45
|
+
var v1 = getArrayU32FromWasm0(r0, r1).slice();
|
|
46
|
+
wasm.__wbindgen_export3(r0, r1 * 4, 4);
|
|
47
|
+
return v1;
|
|
48
|
+
} finally {
|
|
49
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
get height() {
|
|
56
|
+
const ret = wasm.wasmscanresult_height(this.__wbg_ptr);
|
|
57
|
+
return ret >>> 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* @returns {Uint8Array}
|
|
61
|
+
*/
|
|
62
|
+
get pixels() {
|
|
63
|
+
try {
|
|
64
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
65
|
+
wasm.wasmscanresult_pixels(retptr, this.__wbg_ptr);
|
|
66
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
67
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
68
|
+
var v1 = getArrayU8FromWasm0(r0, r1).slice();
|
|
69
|
+
wasm.__wbindgen_export3(r0, r1 * 1, 1);
|
|
70
|
+
return v1;
|
|
71
|
+
} finally {
|
|
72
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
get reason() {
|
|
79
|
+
let deferred1_0;
|
|
80
|
+
let deferred1_1;
|
|
81
|
+
try {
|
|
82
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
83
|
+
wasm.wasmscanresult_reason(retptr, this.__wbg_ptr);
|
|
84
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
85
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
86
|
+
deferred1_0 = r0;
|
|
87
|
+
deferred1_1 = r1;
|
|
88
|
+
return getStringFromWasm0(r0, r1);
|
|
89
|
+
} finally {
|
|
90
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
91
|
+
wasm.__wbindgen_export3(deferred1_0, deferred1_1, 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* @returns {number}
|
|
96
|
+
*/
|
|
97
|
+
get skew_degrees() {
|
|
98
|
+
const ret = wasm.wasmscanresult_skew_degrees(this.__wbg_ptr);
|
|
99
|
+
return ret;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
get status() {
|
|
105
|
+
let deferred1_0;
|
|
106
|
+
let deferred1_1;
|
|
107
|
+
try {
|
|
108
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
109
|
+
wasm.wasmscanresult_status(retptr, this.__wbg_ptr);
|
|
110
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
111
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
112
|
+
deferred1_0 = r0;
|
|
113
|
+
deferred1_1 = r1;
|
|
114
|
+
return getStringFromWasm0(r0, r1);
|
|
115
|
+
} finally {
|
|
116
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
117
|
+
wasm.__wbindgen_export3(deferred1_0, deferred1_1, 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* @returns {number}
|
|
122
|
+
*/
|
|
123
|
+
get width() {
|
|
124
|
+
const ret = wasm.wasmscanresult_width(this.__wbg_ptr);
|
|
125
|
+
return ret >>> 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (Symbol.dispose)
|
|
129
|
+
WasmScanResult.prototype[Symbol.dispose] = WasmScanResult.prototype.free;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {number} width
|
|
133
|
+
* @param {number} height
|
|
134
|
+
* @param {Uint8Array} pixels
|
|
135
|
+
* @param {number} dpi
|
|
136
|
+
* @param {string} output
|
|
137
|
+
* @param {boolean} content_crop
|
|
138
|
+
* @param {boolean} deskew
|
|
139
|
+
* @returns {WasmScanResult}
|
|
140
|
+
*/
|
|
141
|
+
export function clean_scan_rgba(
|
|
142
|
+
width,
|
|
143
|
+
height,
|
|
144
|
+
pixels,
|
|
145
|
+
dpi,
|
|
146
|
+
output,
|
|
147
|
+
content_crop,
|
|
148
|
+
deskew,
|
|
149
|
+
) {
|
|
150
|
+
try {
|
|
151
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
152
|
+
const ptr0 = passArray8ToWasm0(pixels, wasm.__wbindgen_export);
|
|
153
|
+
const len0 = WASM_VECTOR_LEN;
|
|
154
|
+
const ptr1 = passStringToWasm0(
|
|
155
|
+
output,
|
|
156
|
+
wasm.__wbindgen_export,
|
|
157
|
+
wasm.__wbindgen_export2,
|
|
158
|
+
);
|
|
159
|
+
const len1 = WASM_VECTOR_LEN;
|
|
160
|
+
wasm.clean_scan_rgba(
|
|
161
|
+
retptr,
|
|
162
|
+
width,
|
|
163
|
+
height,
|
|
164
|
+
ptr0,
|
|
165
|
+
len0,
|
|
166
|
+
dpi,
|
|
167
|
+
ptr1,
|
|
168
|
+
len1,
|
|
169
|
+
content_crop,
|
|
170
|
+
deskew,
|
|
171
|
+
);
|
|
172
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
173
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
174
|
+
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
|
175
|
+
if (r2) {
|
|
176
|
+
throw takeObject(r1);
|
|
177
|
+
}
|
|
178
|
+
return WasmScanResult.__wrap(r0);
|
|
179
|
+
} finally {
|
|
180
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function __wbg_get_imports() {
|
|
184
|
+
const import0 = {
|
|
185
|
+
__proto__: null,
|
|
186
|
+
__wbg___wbindgen_throw_344f42d3211c4765: function (arg0, arg1) {
|
|
187
|
+
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
188
|
+
},
|
|
189
|
+
__wbindgen_cast_0000000000000001: function (arg0, arg1) {
|
|
190
|
+
// Cast intrinsic for `Ref(String) -> Externref`.
|
|
191
|
+
const ret = getStringFromWasm0(arg0, arg1);
|
|
192
|
+
return addHeapObject(ret);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
__proto__: null,
|
|
197
|
+
"./scan_engine_bg.js": import0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const WasmScanResultFinalization =
|
|
202
|
+
typeof FinalizationRegistry === "undefined"
|
|
203
|
+
? { register: () => {}, unregister: () => {} }
|
|
204
|
+
: new FinalizationRegistry((ptr) => wasm.__wbg_wasmscanresult_free(ptr, 1));
|
|
205
|
+
|
|
206
|
+
function addHeapObject(obj) {
|
|
207
|
+
if (heap_next === heap.length) heap.push(heap.length + 1);
|
|
208
|
+
const idx = heap_next;
|
|
209
|
+
heap_next = heap[idx];
|
|
210
|
+
|
|
211
|
+
heap[idx] = obj;
|
|
212
|
+
return idx;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function dropObject(idx) {
|
|
216
|
+
if (idx < 1028) return;
|
|
217
|
+
heap[idx] = heap_next;
|
|
218
|
+
heap_next = idx;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getArrayF32FromWasm0(ptr, len) {
|
|
222
|
+
ptr = ptr >>> 0;
|
|
223
|
+
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getArrayU32FromWasm0(ptr, len) {
|
|
227
|
+
ptr = ptr >>> 0;
|
|
228
|
+
return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getArrayU8FromWasm0(ptr, len) {
|
|
232
|
+
ptr = ptr >>> 0;
|
|
233
|
+
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let cachedDataViewMemory0 = null;
|
|
237
|
+
function getDataViewMemory0() {
|
|
238
|
+
if (
|
|
239
|
+
cachedDataViewMemory0 === null ||
|
|
240
|
+
cachedDataViewMemory0.buffer.detached === true ||
|
|
241
|
+
(cachedDataViewMemory0.buffer.detached === undefined &&
|
|
242
|
+
cachedDataViewMemory0.buffer !== wasm.memory.buffer)
|
|
243
|
+
) {
|
|
244
|
+
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
|
245
|
+
}
|
|
246
|
+
return cachedDataViewMemory0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let cachedFloat32ArrayMemory0 = null;
|
|
250
|
+
function getFloat32ArrayMemory0() {
|
|
251
|
+
if (
|
|
252
|
+
cachedFloat32ArrayMemory0 === null ||
|
|
253
|
+
cachedFloat32ArrayMemory0.byteLength === 0
|
|
254
|
+
) {
|
|
255
|
+
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
|
|
256
|
+
}
|
|
257
|
+
return cachedFloat32ArrayMemory0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getStringFromWasm0(ptr, len) {
|
|
261
|
+
return decodeText(ptr >>> 0, len);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let cachedUint32ArrayMemory0 = null;
|
|
265
|
+
function getUint32ArrayMemory0() {
|
|
266
|
+
if (
|
|
267
|
+
cachedUint32ArrayMemory0 === null ||
|
|
268
|
+
cachedUint32ArrayMemory0.byteLength === 0
|
|
269
|
+
) {
|
|
270
|
+
cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer);
|
|
271
|
+
}
|
|
272
|
+
return cachedUint32ArrayMemory0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let cachedUint8ArrayMemory0 = null;
|
|
276
|
+
function getUint8ArrayMemory0() {
|
|
277
|
+
if (
|
|
278
|
+
cachedUint8ArrayMemory0 === null ||
|
|
279
|
+
cachedUint8ArrayMemory0.byteLength === 0
|
|
280
|
+
) {
|
|
281
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
282
|
+
}
|
|
283
|
+
return cachedUint8ArrayMemory0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getObject(idx) {
|
|
287
|
+
return heap[idx];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let heap = new Array(1024).fill(undefined);
|
|
291
|
+
heap.push(undefined, null, true, false);
|
|
292
|
+
|
|
293
|
+
let heap_next = heap.length;
|
|
294
|
+
|
|
295
|
+
function passArray8ToWasm0(arg, malloc) {
|
|
296
|
+
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
|
297
|
+
getUint8ArrayMemory0().set(arg, ptr / 1);
|
|
298
|
+
WASM_VECTOR_LEN = arg.length;
|
|
299
|
+
return ptr;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
303
|
+
if (realloc === undefined) {
|
|
304
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
305
|
+
const ptr = malloc(buf.length, 1) >>> 0;
|
|
306
|
+
getUint8ArrayMemory0()
|
|
307
|
+
.subarray(ptr, ptr + buf.length)
|
|
308
|
+
.set(buf);
|
|
309
|
+
WASM_VECTOR_LEN = buf.length;
|
|
310
|
+
return ptr;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let len = arg.length;
|
|
314
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
315
|
+
|
|
316
|
+
const mem = getUint8ArrayMemory0();
|
|
317
|
+
|
|
318
|
+
let offset = 0;
|
|
319
|
+
|
|
320
|
+
for (; offset < len; offset++) {
|
|
321
|
+
const code = arg.charCodeAt(offset);
|
|
322
|
+
if (code > 0x7f) break;
|
|
323
|
+
mem[ptr + offset] = code;
|
|
324
|
+
}
|
|
325
|
+
if (offset !== len) {
|
|
326
|
+
if (offset !== 0) {
|
|
327
|
+
arg = arg.slice(offset);
|
|
328
|
+
}
|
|
329
|
+
ptr = realloc(ptr, len, (len = offset + arg.length * 3), 1) >>> 0;
|
|
330
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
331
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
332
|
+
|
|
333
|
+
offset += ret.written;
|
|
334
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
WASM_VECTOR_LEN = offset;
|
|
338
|
+
return ptr;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function takeObject(idx) {
|
|
342
|
+
const ret = getObject(idx);
|
|
343
|
+
dropObject(idx);
|
|
344
|
+
return ret;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let cachedTextDecoder = new TextDecoder("utf-8", {
|
|
348
|
+
ignoreBOM: true,
|
|
349
|
+
fatal: true,
|
|
350
|
+
});
|
|
351
|
+
cachedTextDecoder.decode();
|
|
352
|
+
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
|
353
|
+
let numBytesDecoded = 0;
|
|
354
|
+
function decodeText(ptr, len) {
|
|
355
|
+
numBytesDecoded += len;
|
|
356
|
+
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
|
357
|
+
cachedTextDecoder = new TextDecoder("utf-8", {
|
|
358
|
+
ignoreBOM: true,
|
|
359
|
+
fatal: true,
|
|
360
|
+
});
|
|
361
|
+
cachedTextDecoder.decode();
|
|
362
|
+
numBytesDecoded = len;
|
|
363
|
+
}
|
|
364
|
+
return cachedTextDecoder.decode(
|
|
365
|
+
getUint8ArrayMemory0().subarray(ptr, ptr + len),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const cachedTextEncoder = new TextEncoder();
|
|
370
|
+
|
|
371
|
+
if (!("encodeInto" in cachedTextEncoder)) {
|
|
372
|
+
cachedTextEncoder.encodeInto = function (arg, view) {
|
|
373
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
374
|
+
view.set(buf);
|
|
375
|
+
return {
|
|
376
|
+
read: arg.length,
|
|
377
|
+
written: buf.length,
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let WASM_VECTOR_LEN = 0;
|
|
383
|
+
|
|
384
|
+
let wasmModule, wasmInstance, wasm;
|
|
385
|
+
function __wbg_finalize_init(instance, module) {
|
|
386
|
+
wasmInstance = instance;
|
|
387
|
+
wasm = instance.exports;
|
|
388
|
+
wasmModule = module;
|
|
389
|
+
cachedDataViewMemory0 = null;
|
|
390
|
+
cachedFloat32ArrayMemory0 = null;
|
|
391
|
+
cachedUint32ArrayMemory0 = null;
|
|
392
|
+
cachedUint8ArrayMemory0 = null;
|
|
393
|
+
return wasm;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function __wbg_load(module, imports) {
|
|
397
|
+
if (typeof Response === "function" && module instanceof Response) {
|
|
398
|
+
if (typeof WebAssembly.instantiateStreaming === "function") {
|
|
399
|
+
try {
|
|
400
|
+
return await WebAssembly.instantiateStreaming(module, imports);
|
|
401
|
+
} catch (e) {
|
|
402
|
+
const validResponse = module.ok && expectedResponseType(module.type);
|
|
403
|
+
|
|
404
|
+
if (
|
|
405
|
+
validResponse &&
|
|
406
|
+
module.headers.get("Content-Type") !== "application/wasm"
|
|
407
|
+
) {
|
|
408
|
+
console.warn(
|
|
409
|
+
"`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",
|
|
410
|
+
e,
|
|
411
|
+
);
|
|
412
|
+
} else {
|
|
413
|
+
throw e;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const bytes = await module.arrayBuffer();
|
|
419
|
+
return await WebAssembly.instantiate(bytes, imports);
|
|
420
|
+
} else {
|
|
421
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
422
|
+
|
|
423
|
+
if (instance instanceof WebAssembly.Instance) {
|
|
424
|
+
return { instance, module };
|
|
425
|
+
} else {
|
|
426
|
+
return instance;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function expectedResponseType(type) {
|
|
431
|
+
switch (type) {
|
|
432
|
+
case "basic":
|
|
433
|
+
case "cors":
|
|
434
|
+
case "default":
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function initSync(module) {
|
|
442
|
+
if (module !== undefined) {
|
|
443
|
+
if (Object.getPrototypeOf(module) === Object.prototype) {
|
|
444
|
+
({ module } = module);
|
|
445
|
+
} else {
|
|
446
|
+
console.warn(
|
|
447
|
+
"using deprecated parameters for `initSync()`; pass a single object instead",
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const imports = __wbg_get_imports();
|
|
453
|
+
if (!(module instanceof WebAssembly.Module)) {
|
|
454
|
+
module = new WebAssembly.Module(module);
|
|
455
|
+
}
|
|
456
|
+
const instance = new WebAssembly.Instance(module, imports);
|
|
457
|
+
return __wbg_finalize_init(instance, module);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function __wbg_init(module_or_path) {
|
|
461
|
+
if (module_or_path !== undefined) {
|
|
462
|
+
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
|
463
|
+
({ module_or_path } = module_or_path);
|
|
464
|
+
} else {
|
|
465
|
+
console.warn(
|
|
466
|
+
"using deprecated parameters for the initialization function; pass a single object instead",
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (module_or_path === undefined) {
|
|
472
|
+
module_or_path = new URL("scan_engine_bg.wasm", import.meta.url);
|
|
473
|
+
}
|
|
474
|
+
const imports = __wbg_get_imports();
|
|
475
|
+
|
|
476
|
+
if (
|
|
477
|
+
typeof module_or_path === "string" ||
|
|
478
|
+
(typeof Request === "function" && module_or_path instanceof Request) ||
|
|
479
|
+
(typeof URL === "function" && module_or_path instanceof URL)
|
|
480
|
+
) {
|
|
481
|
+
module_or_path = fetch(module_or_path);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
|
485
|
+
|
|
486
|
+
return __wbg_finalize_init(instance, module);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export { initSync, __wbg_init as default };
|
|
Binary file
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
export const memory: WebAssembly.Memory;
|
|
3
|
+
export const __wbg_wasmscanresult_free: (a: number, b: number) => void;
|
|
4
|
+
export const clean_scan_rgba: (
|
|
5
|
+
a: number,
|
|
6
|
+
b: number,
|
|
7
|
+
c: number,
|
|
8
|
+
d: number,
|
|
9
|
+
e: number,
|
|
10
|
+
f: number,
|
|
11
|
+
g: number,
|
|
12
|
+
h: number,
|
|
13
|
+
i: number,
|
|
14
|
+
j: number,
|
|
15
|
+
) => void;
|
|
16
|
+
export const wasmscanresult_applied_quad: (a: number, b: number) => void;
|
|
17
|
+
export const wasmscanresult_content_box: (a: number, b: number) => void;
|
|
18
|
+
export const wasmscanresult_pixels: (a: number, b: number) => void;
|
|
19
|
+
export const wasmscanresult_reason: (a: number, b: number) => void;
|
|
20
|
+
export const wasmscanresult_status: (a: number, b: number) => void;
|
|
21
|
+
export const wasmscanresult_height: (a: number) => number;
|
|
22
|
+
export const wasmscanresult_skew_degrees: (a: number) => number;
|
|
23
|
+
export const wasmscanresult_width: (a: number) => number;
|
|
24
|
+
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
25
|
+
export const __wbindgen_export: (a: number, b: number) => number;
|
|
26
|
+
export const __wbindgen_export2: (
|
|
27
|
+
a: number,
|
|
28
|
+
b: number,
|
|
29
|
+
c: number,
|
|
30
|
+
d: number,
|
|
31
|
+
) => number;
|
|
32
|
+
export const __wbindgen_export3: (a: number, b: number, c: number) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/scan-engine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Document scan cleanup engine (Rust, WebAssembly): deskew, content crop and binarization off the main thread, plus a per-page review state machine",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"binarization",
|
|
7
|
+
"deskew",
|
|
8
|
+
"document",
|
|
9
|
+
"scan",
|
|
10
|
+
"wasm",
|
|
11
|
+
"webassembly",
|
|
12
|
+
"worker"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/stella/stella/tree/main/packages/scan-engine",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/stella/stella/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/stella/stella.git",
|
|
22
|
+
"directory": "packages/scan-engine"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"generated"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/scan-engine.d.ts",
|
|
34
|
+
"import": "./dist/scan-engine.js"
|
|
35
|
+
},
|
|
36
|
+
"./review": {
|
|
37
|
+
"types": "./dist/review.d.ts",
|
|
38
|
+
"import": "./dist/review.js"
|
|
39
|
+
},
|
|
40
|
+
"./types": {
|
|
41
|
+
"types": "./dist/types.d.ts",
|
|
42
|
+
"import": "./dist/types.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"clean": "git clean -xdf dist target .cache .turbo node_modules",
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"pack:dry-run": "bun pm pack --dry-run",
|
|
52
|
+
"test": "bun run test:bun && cargo test",
|
|
53
|
+
"test:bun": "bun test src",
|
|
54
|
+
"typecheck": "bun check --no-pretty --all --project=tsconfig.json",
|
|
55
|
+
"lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --type-aware --type-check packages/scan-engine",
|
|
56
|
+
"lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/scan-engine",
|
|
57
|
+
"format:rust": "cargo fmt",
|
|
58
|
+
"build:wasm": "bash scripts/build-wasm.sh",
|
|
59
|
+
"check:wasm": "bun --preload ../../scripts/offline-network-preload.ts scripts/check-wasm.ts",
|
|
60
|
+
"generate:fixtures": "bun scripts/generate-fixtures.ts",
|
|
61
|
+
"check:fixtures": "bun scripts/generate-fixtures.ts --check",
|
|
62
|
+
"prepack": "bun run build"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@stll/fetch": "^0.1.0",
|
|
66
|
+
"better-result": "3.0.1"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@stll/typescript-config": "0.0.0",
|
|
70
|
+
"bun-types": "1.4.4",
|
|
71
|
+
"tsdown": "0.23.0"
|
|
72
|
+
},
|
|
73
|
+
"main": "./dist/scan-engine.js",
|
|
74
|
+
"types": "./dist/scan-engine.d.ts"
|
|
75
|
+
}
|