@stll/scan-engine 0.0.1-placeholder.0 → 0.1.1

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 ADDED
@@ -0,0 +1,122 @@
1
+ # `@stll/scan-engine`
2
+
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/`.
@@ -0,0 +1,51 @@
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
51
+ export {};
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,58 @@
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
58
+ export {};
@@ -0,0 +1,224 @@
1
+ import { ScanEngineError } from "./types.js";
2
+ import { Result } from "better-result";
3
+ import { fetchWithTimeout } from "@stll/fetch";
4
+ import { readBodyWithIdleDeadline } from "@stll/fetch/body-reader";
5
+ import { clean_scan_rgba, initSync } from "../generated/scan_engine.js";
6
+ //#region src/scan-engine.ts
7
+ const WASM_LOAD_TIMEOUT_MS = 1e4;
8
+ const WORKER_REQUEST_TIMEOUT_MS = 1e4;
9
+ const loadWasmBindings = async () => {
10
+ const response = await fetchWithTimeout(new URL("../generated/scan_engine_bg.wasm", import.meta.url), { timeout: {
11
+ type: "headers",
12
+ ms: WASM_LOAD_TIMEOUT_MS
13
+ } });
14
+ const module = await WebAssembly.compile(await readBodyWithIdleDeadline(response, { idleMs: WASM_LOAD_TIMEOUT_MS }));
15
+ initSync({ module });
16
+ return { clean_scan_rgba };
17
+ };
18
+ const createWorkerEngine = ({ createWorker = () => new Worker(new URL("scan-engine.worker.js", import.meta.url), { type: "module" }), requestTimeoutMs = WORKER_REQUEST_TIMEOUT_MS } = {}) => {
19
+ let worker;
20
+ let queue = Promise.resolve();
21
+ const reset = () => {
22
+ worker?.terminate();
23
+ worker = void 0;
24
+ };
25
+ const clean = async (page, options) => {
26
+ const run = async () => {
27
+ worker ??= createWorker();
28
+ const activeWorker = worker;
29
+ const pixels = page.pixels.slice().buffer;
30
+ const request = {
31
+ page: {
32
+ width: page.width,
33
+ height: page.height,
34
+ pageSize: page.pageSize,
35
+ dpi: page.dpi,
36
+ rotation: page.rotation
37
+ },
38
+ pixels,
39
+ options
40
+ };
41
+ return await new Promise((resolve, reject) => {
42
+ let settled = false;
43
+ const settle = (complete) => {
44
+ if (settled) return;
45
+ settled = true;
46
+ clearTimeout(timeout);
47
+ activeWorker.removeEventListener("message", onMessage);
48
+ activeWorker.removeEventListener("error", onError);
49
+ complete();
50
+ };
51
+ const onMessage = (event) => {
52
+ if (!(event instanceof MessageEvent)) return;
53
+ const { data } = event;
54
+ if (data.type === "result") {
55
+ settle(() => {
56
+ try {
57
+ resolve(convertResult({
58
+ ...data.result,
59
+ free: () => void 0
60
+ }, options.output));
61
+ } catch (error) {
62
+ reject(error instanceof ScanEngineError ? error : new ScanEngineError({
63
+ type: "engine-failure",
64
+ message: "The scan engine returned an invalid result"
65
+ }));
66
+ }
67
+ });
68
+ return;
69
+ }
70
+ reset();
71
+ settle(() => reject(data.errorType === "runtime-trap" ? new WebAssembly.RuntimeError(data.message) : new ScanEngineError({
72
+ type: "engine-failure",
73
+ message: data.message
74
+ })));
75
+ };
76
+ const onError = (event) => {
77
+ if (!(event instanceof ErrorEvent)) return;
78
+ reset();
79
+ settle(() => reject(new ScanEngineError({
80
+ type: "engine-failure",
81
+ message: event.message
82
+ })));
83
+ };
84
+ const timeout = setTimeout(() => {
85
+ reset();
86
+ settle(() => reject(new ScanEngineError({
87
+ type: "engine-timeout",
88
+ message: "The scan engine request timed out"
89
+ })));
90
+ }, requestTimeoutMs);
91
+ activeWorker.addEventListener("message", onMessage);
92
+ activeWorker.addEventListener("error", onError);
93
+ activeWorker.postMessage(request, [pixels]);
94
+ });
95
+ };
96
+ const result = queue.then(run, run);
97
+ queue = result.then(() => void 0, () => void 0);
98
+ return await Result.tryPromise({
99
+ try: async () => await result,
100
+ catch: (cause) => cause instanceof ScanEngineError ? cause : new ScanEngineError({
101
+ type: "engine-failure",
102
+ message: "The scan engine could not clean the page",
103
+ cause
104
+ })
105
+ });
106
+ };
107
+ return { clean };
108
+ };
109
+ const readContentBox = (values) => {
110
+ const x = values.at(0);
111
+ const y = values.at(1);
112
+ const width = values.at(2);
113
+ const height = values.at(3);
114
+ if (values.length !== 4 || x === void 0 || y === void 0 || width === void 0 || height === void 0) throw new ScanEngineError({
115
+ type: "engine-failure",
116
+ message: "The scan engine returned an invalid content box"
117
+ });
118
+ return {
119
+ x,
120
+ y,
121
+ width,
122
+ height
123
+ };
124
+ };
125
+ const readQuad = (values) => {
126
+ if (values.length === 0) return null;
127
+ const coordinates = Array.from(values);
128
+ if (coordinates.length !== 8 || coordinates.some((value) => !Number.isFinite(value))) throw new ScanEngineError({
129
+ type: "engine-failure",
130
+ message: "The scan engine returned an invalid page quadrilateral"
131
+ });
132
+ const [x0, y0, x1, y1, x2, y2, x3, y3] = coordinates;
133
+ 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({
134
+ type: "engine-failure",
135
+ message: "The scan engine returned an invalid page quadrilateral"
136
+ });
137
+ return {
138
+ topLeft: {
139
+ x: x0,
140
+ y: y0
141
+ },
142
+ topRight: {
143
+ x: x1,
144
+ y: y1
145
+ },
146
+ bottomRight: {
147
+ x: x2,
148
+ y: y2
149
+ },
150
+ bottomLeft: {
151
+ x: x3,
152
+ y: y3
153
+ }
154
+ };
155
+ };
156
+ const convertResult = (result, output) => {
157
+ if (result.status === "unrecognized") {
158
+ if (result.reason.length === 0) throw new ScanEngineError({
159
+ type: "engine-failure",
160
+ message: "The scan engine returned an unrecognized page without a reason"
161
+ });
162
+ return {
163
+ type: "unrecognized",
164
+ reason: result.reason
165
+ };
166
+ }
167
+ if (result.status !== "cleaned") throw new ScanEngineError({
168
+ type: "engine-failure",
169
+ message: `The scan engine returned an unknown status: ${result.status}`
170
+ });
171
+ if (result.pixels.length !== result.width * result.height) throw new ScanEngineError({
172
+ type: "engine-failure",
173
+ message: "The scan engine returned an invalid pixel buffer"
174
+ });
175
+ return {
176
+ type: "cleaned",
177
+ image: {
178
+ pixels: result.pixels,
179
+ width: result.width,
180
+ height: result.height,
181
+ format: output
182
+ },
183
+ appliedQuad: readQuad(result.applied_quad),
184
+ skewDegrees: result.skew_degrees,
185
+ contentBox: readContentBox(result.content_box)
186
+ };
187
+ };
188
+ const createScanEngine = (load, workerOptions) => {
189
+ if (load === void 0 && typeof Worker !== "undefined") return createWorkerEngine(workerOptions);
190
+ const resolvedLoad = load ?? loadWasmBindings;
191
+ let bindings;
192
+ return { clean: async (page, options) => await Result.tryPromise({
193
+ try: async () => {
194
+ if (bindings === void 0) {
195
+ const pending = resolvedLoad().catch((error) => {
196
+ if (bindings === pending) bindings = void 0;
197
+ throw error;
198
+ });
199
+ bindings = pending;
200
+ }
201
+ const wasm = await bindings;
202
+ try {
203
+ let result;
204
+ try {
205
+ 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");
206
+ return convertResult(result, options.output);
207
+ } finally {
208
+ result?.free();
209
+ }
210
+ } catch (error) {
211
+ if (error instanceof WebAssembly.RuntimeError) bindings = void 0;
212
+ throw error;
213
+ }
214
+ },
215
+ catch: (cause) => cause instanceof ScanEngineError ? cause : new ScanEngineError({
216
+ type: "engine-failure",
217
+ message: "The scan engine could not clean the page",
218
+ cause
219
+ })
220
+ }) };
221
+ };
222
+ const scanEngine = createScanEngine();
223
+ //#endregion
224
+ export { createScanEngine, scanEngine };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,61 @@
1
+ import { fetchWithTimeout } from "@stll/fetch";
2
+ import { readBodyWithIdleDeadline } from "@stll/fetch/body-reader";
3
+ import initWasm, { clean_scan_rgba } from "../generated/scan_engine.js";
4
+ //#region src/scan-engine.worker.ts
5
+ const WASM_LOAD_TIMEOUT_MS = 1e4;
6
+ const post = (response, transfer = []) => {
7
+ Reflect.apply(postMessage, globalThis, [response, transfer]);
8
+ };
9
+ let initialized;
10
+ const initialize = async () => {
11
+ initialized ??= fetchWithTimeout(new URL("../generated/scan_engine_bg.wasm", import.meta.url), { timeout: {
12
+ type: "headers",
13
+ ms: WASM_LOAD_TIMEOUT_MS
14
+ } }).then(async (response) => await readBodyWithIdleDeadline(response, { idleMs: WASM_LOAD_TIMEOUT_MS })).then(async (module_or_path) => {
15
+ await initWasm({ module_or_path });
16
+ });
17
+ await initialized;
18
+ };
19
+ const handle = async ({ data }) => {
20
+ try {
21
+ await initialize();
22
+ let result;
23
+ try {
24
+ 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");
25
+ const response = {
26
+ type: "result",
27
+ result: {
28
+ status: result.status,
29
+ reason: result.reason,
30
+ pixels: result.pixels,
31
+ width: result.width,
32
+ height: result.height,
33
+ skew_degrees: result.skew_degrees,
34
+ content_box: result.content_box,
35
+ applied_quad: result.applied_quad
36
+ }
37
+ };
38
+ const buffer = response.result.pixels.buffer;
39
+ post(response, buffer instanceof ArrayBuffer ? [buffer] : []);
40
+ } finally {
41
+ result?.free();
42
+ }
43
+ } catch (error) {
44
+ const response = {
45
+ type: "error",
46
+ errorType: error instanceof WebAssembly.RuntimeError ? "runtime-trap" : "failure",
47
+ message: error instanceof Error ? error.message : "Unknown worker failure"
48
+ };
49
+ post(response);
50
+ }
51
+ };
52
+ addEventListener("message", (event) => {
53
+ handle(event).catch((error) => {
54
+ post({
55
+ type: "error",
56
+ errorType: "failure",
57
+ message: error instanceof Error ? error.message : "Unknown worker failure"
58
+ });
59
+ });
60
+ });
61
+ //#endregion
@@ -0,0 +1,73 @@
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
73
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ import { TaggedError } from "better-result";
2
+ //#region src/types.ts
3
+ var ScanEngineError = class extends TaggedError("ScanEngineError") {};
4
+ //#endregion
5
+ export { ScanEngineError };
@@ -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.0.1-placeholder.0",
4
- "description": "Placeholder to bootstrap npm trusted publishing. Real releases are published from stella/stella via GitHub Actions.",
5
- "license": "Apache-2.0"
3
+ "version": "0.1.1",
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.2.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.1"
72
+ },
73
+ "main": "./dist/scan-engine.js",
74
+ "types": "./dist/scan-engine.d.ts"
6
75
  }