appbrain-scanner-sdk
v0.2.0
Published
Web-first scanner SDK: live camera capture, document vision/auto-capture, OCR and multi-format barcode reading behind one package.
Maintainers
Readme
appbrain-scanner-sdk
Web-first, modular scanner SDK for browser apps. It captures frames from a live camera, runs a vision/quality stage, then runs independent barcode and OCR readers and returns the raw results. Domain rules (what an ID card, invoice or receipt means) stay in your code — the SDK is deliberately domain-agnostic.
One install gives you the whole SDK:
npm install appbrain-scanner-sdkappbrain-scanner-sdk is a facade over the @appbrain/scanner-* packages. You
never need to install those directly.
Requirements
- Runtime: a modern browser with
navigator.mediaDevices.getUserMedia(Chrome/Edge/Firefox/Safari, current versions). Camera access needs HTTPS orlocalhost. - React:
>= 19.2(peer dependency) — only if you use the<Scanner>component. The lower-level APIs are framework-agnostic. - Bundler: any modern ESM bundler (Vite, Next.js, etc.). The package is ESM
only (
"type": "module") and ships TypeScript declarations. - Node.js is not a runtime requirement — this SDK runs in the browser.
Features
Implemented and covered by tests:
| Capability | Status | Notes |
|---|---|---|
| Live camera capture (getUserMedia) | implemented | WebCameraSource, environment-facing by default |
| Barcode reading via ZXing | implemented | PDF417 by default; QR, Data Matrix, Aztec, EAN, UPC, Code 39/93/128, ITF, Codabar available by config |
| OCR via Tesseract.js | implemented | Spanish (spa) model by default; any Tesseract language |
| Document detection + perspective normalization | implemented | ScanicFrameAnalyzer (Scanic engine, behind SDK contracts) |
| Corner-stability tracking | implemented | CornerStabilityTracker |
| Threshold auto-capture policy | implemented | ThresholdAutoCapturePolicy — deterministic, configurable thresholds |
| React <Scanner> component with auto-capture loop | implemented | imperative handle: start, stop, reset, scanOnce, warmUp |
| Generic raw-to-JSON parsers | implemented | DelimitedParser, OcrKeyValueParser (optional, not wired into the engine) |
Not included: any document-specific parser (ID cards, invoices…) or a native
Capacitor plugin (the web adapter runs inside a Capacitor WebView as-is).
ScanicFrameAnalyzer exposes the optional ML detector for evaluation, but the
default pipeline remains classical. The ML model's asset, license, privacy and
hosting review is still pending, so it is not an accepted production default.
Quick start (React)
"use client";
import { useMemo } from "react";
import {
Scanner,
createDefaultReaders,
createDefaultVisionPipeline,
type ScanResult,
} from "appbrain-scanner-sdk";
export function DocumentScanner() {
// Keep these stable across renders — the component does not own their lifecycle.
const readers = useMemo(() => createDefaultReaders(), []);
const visionPipeline = useMemo(() => createDefaultVisionPipeline(), []);
return (
<Scanner
readers={readers}
visionPipeline={visionPipeline}
mode="both"
autoCapture
cameraOptions={{ facingMode: "environment" }}
onResult={(result: ScanResult) => {
for (const reading of result.readings) {
if (reading.kind === "code") console.log("barcode:", reading.raw);
if (reading.kind === "ocr") console.log("text:", reading.raw);
}
}}
onError={(err) => console.error(err)}
/>
);
}createDefaultReaders() returns a ZXing PDF417 reader plus a Spanish Tesseract
OCR reader. createDefaultVisionPipeline() returns a ready VisionPipeline
(analyzer + normalizer + stability tracker + auto-capture policy).
Use cameraOptions.facingMode to select the browser camera type:
<Scanner
readers={readers}
visionPipeline={visionPipeline}
cameraOptions={{ facingMode: "user" }}
/>"environment" requests the rear camera and remains the default. "user"
requests the front camera. You can combine it with resolution hints, for example
{ facingMode: "environment", width: 1920, height: 1080 }.
For devices with several rear lenses, enumerate inputs after permission and pass
an exact deviceId; it takes priority over facingMode:
import { WebCameraSource } from "appbrain-scanner-sdk";
const inputs = await WebCameraSource.listVideoInputs();
const camera = new WebCameraSource({ deviceId: inputs[0]?.deviceId });focusMode, focusDistance and pointOfInterest are browser/media-device
hints. They may be ignored when the selected camera does not support them. See
the camera selection and focus guide.
OCR assets
By default Tesseract.js downloads its worker, WASM core and language data from a public CDN on first use. To self-host them (recommended for production and for mobile testing behind an HTTPS tunnel), serve the files from your app and point the reader at them:
const readers = createDefaultReaders({
codes: { formats: ["PDF_417"] },
ocr: {
language: "spa",
assets: {
workerPath: "/tesseract/worker.min.js",
corePath: "/tesseract/core",
langPath: "/tesseract/lang",
},
},
});The repo's demo has a prepare:ocr-assets script that copies these files from
tesseract.js, tesseract.js-core and @tesseract.js-data/spa into
public/tesseract/ — mirror it in your app.
Barcode scanning
The ZXing reader decodes PDF_417 only unless you configure formats:
import { ZxingCodeReader } from "appbrain-scanner-sdk";
const reader = new ZxingCodeReader({ formats: ["QR_CODE", "EAN_13", "CODE_128"] });For harder frames, enable ZXing's TRY_HARDER hint without importing ZXing
types:
const reader = new ZxingCodeReader({ formats: ["PDF_417"], tryHarder: true });
const readers = createDefaultReaders({
codes: { formats: ["PDF_417"], tryHarder: true },
});tryHarder defaults to false because it can increase per-frame decode work.
tryRotated defaults to true and remains independent; keep it enabled for
PDF417 cards that may appear sideways in a portrait camera view.
Supported CodeFormat values: PDF_417, QR_CODE, MICRO_QR_CODE, AZTEC,
DATA_MATRIX, MAXICODE, EAN_13, EAN_8, UPC_A, UPC_E, CODE_39,
CODE_93, CODE_128, CODABAR, ITF, RSS_14, RSS_EXPANDED.
ZxingCodeData exposes both text and, when the payload is byte-oriented,
bytes (lossless — useful for PDF417 payloads that carry packed binary after the
printed fields).
OCR
import { TesseractOcrReader } from "appbrain-scanner-sdk";
const ocr = new TesseractOcrReader({
language: "spa+eng",
includeBlocks: true,
contrastIntensity: 1,
preserve_interword_spaces: true,
});Returns OcrData: { text, confidence?, blocks?, debugImage? }. blocks
(word/line/block geometry) is present only with includeBlocks: true;
debugImage requires debugPreprocessedImage: true and should be used only for
diagnostics because PNG encoding adds work and result size.
With grayscale enabled, the default preprocessing is a global contrast stretch.
useAdaptiveContrast: true currently selects global Otsu binarization despite
the option name; it is not CLAHE or local adaptive histogram equalization.
Whitelist/blacklist options are forwarded to Tesseract, and
minBlockConfidence filters data.text while preserving the unfiltered raw.
See the OCR troubleshooting guide.
Call ocr.dispose() when done to terminate the worker.
Auto capture
<Scanner autoCapture> analyzes preview frames a few times per second at a
reduced resolution (analysisMaxDimension, 640px default), tracks the document
corners, and captures a full-resolution frame the moment
ThresholdAutoCapturePolicy reports the frame is ready. Manual capture is always
available through the ref (scannerRef.current.scanOnce()).
both mode: per-capability completion
In mode="both" each capability finishes on its own terms — the loop does not
stop at the first accepted frame. A code reader keeps retrying fresh frames until
it decodes; an OCR reader keeps going until its reading is good enough.
<Scanner
readers={readers}
visionPipeline={visionPipeline}
mode="both"
bothStrategy="sequential" // default: PDF417 first (full cadence), then OCR
readerAcceptance={{
// "zxing": a non-null result is enough (the default)
"tesseract": (r) => (r.confidence ?? 0) >= 80 && !!r.data.text?.trim(),
}}
captureTimeoutMs={15000} // give up on a stuck capability, then move on
onReading={(r) => console.log(`${r.kind} done`)}
onResult={(result) => { /* both capabilities settled (or timed out) */ }}
/>bothStrategy—"sequential"(default) runs the readers one at a time inreadersorder;"parallel"dispatches every pending reader on each accepted frame. Sequential is the reliable choice for a dense PDF417: ZXing decodes synchronously on the main thread, so giving it the loop's full cadence before OCR starts matters.readerAcceptance—Record<readerId, (r: ReaderResult) => boolean>. When a reader has no entry, any non-null result counts. An OCR reader always returns something, so pass a predicate (a confidence gate) or it is treated as done on its first frame.captureTimeoutMs(default15000,0= forever) andmaxCaptureAttempts(default0= unbounded) bound the wait — per stage in"sequential", for the whole run in"parallel". On timeout the loop reports whatever it collected.onReading(result)fires as each capability closes, beforeonResult.
mode="ocr" / mode="codes" are the single-capability case and behave as
before. scanOnce is unchanged: one frame, one pass, reports whatever it got.
Tune thresholds by constructing the policy yourself:
import {
ScanicFrameAnalyzer,
CornerStabilityTracker,
ThresholdAutoCapturePolicy,
} from "appbrain-scanner-sdk";
const analyzer = new ScanicFrameAnalyzer({ maxProcessingDimension: 800 });
const visionPipeline = {
analyzer,
normalizer: analyzer,
stabilityTracker: new CornerStabilityTracker(),
autoCapturePolicy: new ThresholdAutoCapturePolicy({
minSharpness: 0.2,
minCoverage: 0.25,
}),
};The values above deliberately relax the current policy defaults
(minSharpness: 0.6, minCoverage: 0.3). Record target-device measurements
before changing capture gates.
Result
onResult (and scanOnce) give you a ScanResult:
interface ScanResult {
capturedFrame: Frame; // full-resolution frame that was read
processedFrame: Frame; // perspective-normalized frame handed to readers
vision?: VisionResult; // analysis + acceptance for this frame
readings: ReaderResult[]; // one per reader that produced something
failures: ReaderFailure[]; // reader id + message for readers that threw
}
interface ReaderResult<T = unknown> {
readerId: string;
kind: "ocr" | "code";
data: T; // ZxingCodeData | OcrData
raw?: string; // raw text payload
confidence?: number;
metadata?: Record<string, unknown>;
}A frame that was read but matched nothing still resolves to a ScanResult with
empty readings — deciding whether that is worth surfacing is your call.
In a both-mode auto-capture, readings can come from different frames (each
capability finishes on its own); capturedFrame/processedFrame are then the
last frame processed. A single engine.process / processPrepared call and
scanOnce always read one frame.
Without React
Use ScannerEngine directly with your own capture loop:
import { ScannerEngine, createDefaultReaders } from "appbrain-scanner-sdk";
const engine = new ScannerEngine({ readers: createDefaultReaders(), mode: "both" });
await engine.warmUp(); // pre-download models/WASM (optional)
const result = await engine.process(frame); // frame: { imageData, width, height, capturedAt }WebCameraSource (from the same package) produces Frames from an
HTMLVideoElement.
engine.process(frame, signal?) runs vision then all mode-enabled readers.
engine.processPrepared(captured, processed, options?) skips the vision stage
(for a frame already accepted by a VisionPipeline); options is
{ signal?, vision?, only?, frameFor? } — only restricts to given reader ids,
frameFor(reader) picks the frame per reader.
Parsing results
The SDK returns raw text. @appbrain/scanner-parser (re-exported here) has small
generic helpers — DelimitedParser, OcrKeyValueParser — implementing the
ResultParser<I, O> contract. Document-specific parsing belongs in your app.
React / Capacitor
- React / Next.js: import
Scannerin a client component ("use client"). - Capacitor: the web camera adapter works inside a Capacitor WebView with no native plugin. Grant camera permission in the native project as usual.
Examples
A complete working demo (Next.js + React) is included in the repository at
apps/scanner-demo.
Clone the repo, npm install, then npm run dev to see it in action.
Public API
Re-exported from appbrain-scanner-sdk:
- Facade helpers:
createDefaultReaders,createDefaultVisionPipeline - React:
Scanner,ScannerProps,ScannerHandle - Engine & contracts:
ScannerEngine, and the typesFrame,ScanResult,ReaderResult,ReaderFailure,ScannerReader,ProcessPreparedOptions,VisionPipeline,VisionResult,FrameAnalysis,CaptureDecision,ScannerMode,ScannerState, … - Camera:
WebCameraSource,WebCameraError,WebCameraOptions,VideoInputDevice - Readers:
ZxingCodeReader,CodeFormat,ZxingCodeData,TesseractOcrReader,OcrData - Vision:
ScanicFrameAnalyzer,CornerStabilityTracker,ThresholdAutoCapturePolicy - Parsers:
ResultParser,DelimitedParser,OcrKeyValueParser
Changelog
See CHANGELOG.md.
License
MIT © AppBrain. Bundled engines: ZXing (@zxing/library, Apache-2.0),
Tesseract.js (Apache-2.0), Scanic (MIT) — all installed as transitive
dependencies.
