wascanner
v0.3.0
Published
WebAssembly-powered document scanner — a WASM (Go) reimagining of jscanify, as a TypeScript library and a Vue component.
Maintainers
Readme
wascanner
WebAssembly-powered document scanner — a WASM (Go) reimagining of jscanify.
wascanner detects a document (a sheet of paper, receipt, card, …) in an image
or camera frame and can perspective-correct ("dewarp") it into a flat scan. The
web layer only handles the camera and drawing the overlay — all image
analysis runs in a compact Go/WebAssembly binary for speed and portability.
It ships in two forms:
wascanner— a framework-agnostic TypeScript/JS library.wascanner/vue— a<DocumentScanner>Vue 3 component with a built-in camera + live overlay, importable straight into a Vite app.
How it works
camera / <img> / <canvas> ── JS (this library) ──▶ RGBA pixels
│
▼
overlay + dewarped scan ◀── JS ── corners / warped image WASM (Go)The Go core (wasm/) runs the classic scanning pipeline entirely in
WebAssembly:
grayscale → downscale → gaussian blur → Otsu threshold → morphological close →
connected components → largest blob → extreme-point corner estimation →
perspective dewarp
No native/OpenCV dependency — pure Go, cross-compiled to js/wasm.
Install
npm install wascanner
# peer (only needed for the Vue component):
npm install vueThe package ships the prebuilt dist/wascanner.wasm and dist/wasm_exec.js.
Quick start — core library
import { WAScanner } from 'wascanner';
const scanner = await WAScanner.load();
// Any ImageData | <canvas> | <img> | <video> | ImageBitmap source:
const result = scanner.detect(imageEl);
if (result.found) {
console.log(result.corners); // { tl, tr, br, bl }
const overlay = scanner.highlight(imageEl); // -> HTMLCanvasElement
const flat = scanner.extract(imageEl); // -> dewarped HTMLCanvasElement
document.body.append(overlay, flat);
}Off the main thread (recommended for live camera)
WAScannerWorker mirrors WAScanner with async methods and runs all WASM
analysis in a Web Worker, so detection never stalls the camera preview or UI:
import { WAScannerWorker } from 'wascanner';
const scanner = await WAScannerWorker.load();
const result = await scanner.detect(videoEl); // Promise<DetectResult>
const flat = await scanner.extract(videoEl); // Promise<HTMLCanvasElement>
scanner.dispose(); // terminates the workerThe Vue <DocumentScanner> uses the worker automatically when Workers are
available, falling back to in-thread analysis otherwise.
With Vite / explicit asset URLs
WAScanner.load() resolves the .wasm next to its own module, which works when
the package is served as-is. In a bundled app the most reliable path is to pass
the URLs explicitly using Vite's ?url imports:
import wasmUrl from 'wascanner/wasm?url';
import wasmExecUrl from 'wascanner/wasm-exec?url';
import workerUrl from 'wascanner/worker?url'; // worker variant only
import { WAScanner, WAScannerWorker } from 'wascanner';
const scanner = await WAScanner.load({ wasmUrl, wasmExecUrl });
// or, worker-backed (dist/worker.js is self-contained, safe to copy as an asset):
const workerScanner = await WAScannerWorker.load({ workerUrl, wasmUrl, wasmExecUrl });Quick start — Vue component
<script setup lang="ts">
import { ref } from 'vue';
import { DocumentScanner } from 'wascanner/vue';
import 'wascanner/style.css'; // optional cosmetic styles
const scanner = ref();
const shot = ref('');
</script>
<template>
<DocumentScanner
ref="scanner"
facing-mode="environment"
:options="{ color: '#22c55e', fill: 'rgba(34,197,94,0.15)' }"
@capture="({ dataUrl }) => (shot = dataUrl)"
/>
<button @click="scanner.capture()">Capture</button>
<img v-if="shot" :src="shot" />
</template>Props: facingMode, constraints, autostart, options
(processMaxDim, minAreaRatio, invert, color, lineWidth, fill,
handles), interval (ms between detections, default 200), smoothing
(overlay easing time constant in ms, default 180; the overlay animates every
frame and eases toward the latest detection), hold (ms to keep the last quad
through missed detections, default 800), wasm.
Events: ready, detect(result), capture({ canvas, dataUrl }), error.
Exposed methods: start(), stop(), capture().
API
| Method | Returns | Notes |
| --- | --- | --- |
| WAScanner.load(assets?) | Promise<WAScanner> | Loads + caches the WASM core (in-thread). |
| .detect(source, opts?) | DetectResult | { found, score, corners, width, height }. |
| .highlight(source, opts?) | HTMLCanvasElement | Source with the detected outline drawn. |
| .extract(source, corners?, w?, h?) | HTMLCanvasElement | Perspective-corrected document. |
| WAScannerWorker.load(assets?) | Promise<WAScannerWorker> | Same pipeline in a Web Worker. |
| .detect(source, opts?) | Promise<DetectResult> | Async; pixels are transferred, not copied. |
| .extract(source, corners?, w?, h?) | Promise<HTMLCanvasElement> | Detects first when corners omitted. |
| .dispose() | void | Terminates the worker. |
DetectOptions: processMaxDim (default 480), minAreaRatio (default 0.12),
invert (for dark-on-light documents).
Building from source
compile.sh checks for the Go and Node toolchains, installs npm deps, and
builds into dist/:
./compile.sh # build both WASM + JS (alias: --all)
./compile.sh --wasm # only the Go WebAssembly binary (+ wasm_exec.js)
./compile.sh --js # only the TS/Vue library (Vite)
./compile.sh --deps # just check/install dependencies
./compile.sh --clean # remove dist/Requires Go ≥ 1.21 and Node ≥ 18.
Example app
./compile.sh --all
npm run example
# core demo → https://localhost:5173/
# vue demo → https://localhost:5173/vue.htmlThe example is a zero-dependency Node static server showcasing image upload, a
synthetic sample, live camera detection, and document extraction. It listens on
all interfaces and serves over HTTPS by default (auto-generating a
self-signed cert covering localhost and your LAN IPs) so the camera works both
locally and from other devices — accept the one-time browser warning. Serve
plain http with HTTPS=0 npm run example.
Tests
npm test # runs the Go pipeline unit testsNotes & limitations
- Detection runs three complementary segmentations and picks the best-scoring
quad: color distance from the background (handles a neutral page on a
cream/wood surface where brightness barely differs), an edge-enclosed
region (finds the page from its boundary/shadow regardless of contrast),
and a classic brightness blob (fast path for a page on a darker surface).
It still assumes a single, roughly rectangular document that is the most
prominent object — not arbitrary scenes. For Apple-
VNDocument-grade robustness on cluttered backgrounds you'd want an ML detector (a natural future addition behind the same API). - Camera access requires a secure context (
https://orlocalhost).
License
MIT © andor83
