@jieonist/vision-camera-ocr-scanner
v0.1.0
Published
On-device structured OCR scanner (MRZ passport, credit card, business card) for React Native — built on VisionCamera + Nitro
Maintainers
Readme
@jieonist/vision-camera-ocr-scanner
On-device, structured OCR scanner for React Native — scan a passport (MRZ), credit card, or business card with the camera and get back structured data, not just raw text.
Built as a react-native-vision-camera frame processor plugin on top of Apple Vision (iOS) and Google ML Kit (Android), powered by Nitro Modules for zero-copy, high-FPS frame processing.
📖 Documentation: https://g1d5ny.github.io/vision-camera-ocr-scanner/ (한국어)
🚧 Status: early development. MRZ (passport), credit-card, and business-card scanning work today on iOS and Android; the receipt mode is on the Roadmap. Full docs: https://g1d5ny.github.io/vision-camera-ocr-scanner/
Why this library
- 🆓 Free & on-device — no API keys, no servers, no per-scan fees. Nothing leaves the device.
- 🔒 Privacy-first — great for apps that can't send card/ID/receipt images to a cloud OCR service.
- 🏗️ New Architecture native — Nitro + Fabric, fast per-frame processing.
- 🧩 Structured output — MRZ fields, card number, contact fields — already parsed.
Not trying to be a paid enterprise KYC SDK (BlinkID, Dynamsoft, Scanbot). This is the "good enough, free, on-device" option for indie and small apps.
Requirements
- React Native New Architecture (Fabric + TurboModules/Nitro) — required, no old-architecture support.
react-native-vision-camerav5+.- iOS 15+ / Android (ML Kit).
Platforms
| Platform | Supported | |---|---| | Bare React Native (CLI) | ✅ | | Expo dev build / prebuild | ✅ (autolinking — a dedicated config plugin is on the roadmap) | | Expo Go | ❌ (uses custom native code) |
Installation
npm install @jieonist/vision-camera-ocr-scanner react-native-vision-camera react-native-nitro-modules react-native-nitro-image react-native-worklets react-native-vision-camera-worklets# or with yarn
yarn add @jieonist/vision-camera-ocr-scanner react-native-vision-camera react-native-nitro-modules react-native-nitro-image react-native-worklets react-native-vision-camera-workletsiOS:
cd ios && pod installExpo (dev build) — this package needs no config plugin of its own (it autolinks); only vision-camera's plugin is required:
// app.json / app.config.js
{
"expo": {
"plugins": [
["react-native-vision-camera", { "cameraPermissionText": "Allow camera to scan documents" }]
]
}
}npx expo prebuild && npx expo run:ios # or run:androidUsage
The native side does OCR only — scan(frame, options?) returns recognized text lines. You then structure them on the JS thread with parseMrz / parseCard (the parsers aren't worklet-safe). This keeps every mode on one native call, with no extra native dependency.
scan() performs real OCR on every call (no internal throttling) and by default recognizes only the central band of the frame — the middle half under a centered guide box, which preserves glyph detail and drops background text. Pass { roi: 'full' } to recognize the whole frame, and throttle calls yourself in the worklet (see below).
import { useCallback, useMemo, useState } from 'react';
import {
Camera,
useCameraDevice,
useCameraPermission,
useFrameOutput,
} from 'react-native-vision-camera';
import { scheduleOnRN } from 'react-native-worklets';
import {
getOcrScanner,
parseCard,
type CardResult,
} from '@jieonist/vision-camera-ocr-scanner';
export function CardScanner() {
const { hasPermission } = useCameraPermission();
const device = useCameraDevice('back');
const scanner = useMemo(() => getOcrScanner(), []);
const [card, setCard] = useState<CardResult | null>(null);
const onLines = useCallback((lines: string[]) => {
const parsed = parseCard(lines); // or parseMrz(lines)
if (parsed?.number) setCard(parsed);
}, []);
const frameOutput = useFrameOutput({
pixelFormat: 'yuv',
onFrame: (frame) => {
'worklet';
try {
// scan() runs real OCR (hundreds of ms) on every call — throttle it.
const g = globalThis as unknown as { __ocrFrameCount?: number };
g.__ocrFrameCount = (g.__ocrFrameCount ?? 0) + 1;
if (g.__ocrFrameCount % 5 !== 0) return;
const ocr = scanner.scan(frame); // native OCR → { text, lines }
if (ocr.lines.length > 0) scheduleOnRN(onLines, ocr.lines);
} finally {
frame.dispose();
}
},
});
if (!hasPermission || device == null) return null;
return (
<Camera style={{ flex: 1 }} device={device} isActive outputs={[frameOutput]} />
);
}Parsers
| Function | Returns | Self-validation |
|---|---|---|
| parseMrz(lines) | MrzResult \| null — passport/ID fields | ICAO 9303 check digits |
| parseCard(lines) | CardResult \| null — number, brand, expiry, holder | Luhn checksum |
| parseBusinessCard(lines) | BusinessCardResult \| null — name, company, title, phones, email, website, address | none (heuristics) — pair with its scan session |
| detectDocument(lines) | DetectedDocument \| null — auto-detect MRZ, card, or business card | validates MRZ/card; bizcard needs an email |
| detectBrand(number) | brand string from a known card number | — |
For a "scan any document" flow, detectDocument(lines) returns { type: 'mrz' | 'card', valid, data } or { type: 'bizcard', data }. Self-validating documents win (MRZ over card on ambiguous ties); a business card is the guarded last resort and only detects when an email is present — a phone number alone would make almost any text "detect" as one. When your screen already knows the type, call the specific parser — fewer false positives and a mode-specific guide box.
See the docs for the full MrzResult, CardResult, and BusinessCardResult shapes.
Native result type
interface OcrResult {
/** Recognized text joined with newlines. */
text: string;
/** Recognized lines, ordered top-to-bottom. Feed these to parseMrz / parseCard. */
lines: string[];
/**
* Layout box per line (same order as `lines`): x/y/width/height normalized
* 0..1 against the scanned region, upright, top-left origin. Pass to
* parseBusinessCard(lines, lineItems) so text size becomes a signal.
*/
lineItems: OcrLine[];
}Permissions
This library uses the camera via VisionCamera — request permission as usual:
import { Camera } from 'react-native-vision-camera';
const status = await Camera.requestCameraPermission();Add the platform strings:
- iOS —
NSCameraUsageDescriptioninInfo.plist. - Android —
android.permission.CAMERAinAndroidManifest.xml.
Accuracy & limitations
- On-device OCR is good enough for autofill / convenience, not guaranteed for high-stakes verification. Always let users confirm/edit results.
- Embossed (raised) card numbers, glare, and worn documents reduce accuracy.
- For full multi-template ID parsing or certified liveness/KYC, use a commercial SDK.
Handling sensitive data
[!WARNING] Never log the result objects.
CardResult.lines/MrzResult.linescontain the raw OCR lines — including the full card number (PAN) and passport fields. Logging a result object whole (e.g.console.log(parsed)) leaks that data into device logs and crash/analytics pipelines. Log only what you need (e.g.parsed.valid,parsed.brand), and drop the result from state as soon as the flow is done.
Roadmap
- [x] MRZ mode (checksum self-validates) — iOS & Android
- [x] Credit card mode (number + expiry + brand, Luhn) — iOS & Android
- [x] Business card → contact (name / company / title / phones / email / address)
- [ ] Receipt → merchant / date / total
- [ ] Expo config plugin
- [x] First npm release (
0.1.0)
Documentation
Full guides live at https://g1d5ny.github.io/vision-camera-ocr-scanner/ (English) / https://g1d5ny.github.io/vision-camera-ocr-scanner/ko/ (한국어):
Contributing
License
MIT © jieonist
Made with create-react-native-library · powered by Nitro Modules and VisionCamera
