id_card_ocr_web 2.1.0
id_card_ocr_web: ^2.1.0 copied to clipboard
A client-side OCR package for Flutter Web using Tesseract.js.
ID CARD OCR for WEB #
Client-side ID card OCR for Flutter Web, powered by Tesseract.js. Runs entirely in the browser — image bytes are never uploaded to a server.
Currently supports Indonesian KTP (national ID card). Malaysian MyKad support is planned.
Features #
- Pure client-side OCR — no backend, no image upload
- Indonesian KTP parsing out of the box
- Image pre-processing (resize + binarization) for better recognition
- Works in a normal browser and inside a WebView / mobile browser (with self-hosted assets — see Using inside a WebView or on mobile)
Platform support #
| Platform | Supported |
|---|---|
| Flutter Web (desktop Chrome, Edge, Safari, Firefox) | ✅ |
| Flutter Web inside a WebView (Android/iOS) | ✅ with self-hosted assets |
| Mobile browsers (iOS Safari, Android Chrome) | ✅ with self-hosted assets |
| Android / iOS native (non-web) | ❌ |
This package relies on browser APIs (Canvas, Blob, Web Workers, WASM) and is web-only. It runs only when your Flutter target is web, including a web build embedded in a WebView.
Installation #
dependencies:
id_card_ocr_web: ^2.1.0
flutter pub get
Quick start (default — loads assets from CDN) #
import 'dart:typed_data';
import 'package:id_card_ocr_web/id_card_ocr_web.dart';
// Optional: warm up early (e.g. in main()) so the first scan is faster.
IdCardOcr.initialize();
Future<void> scan(Uint8List imageBytes) async {
final KtpResult result = await IdCardOcr.processKtp(imageBytes);
if (result.isSuccess) {
print(result.toJson());
} else {
print('Extraction failed');
}
}
With no extra configuration, all Tesseract assets load from a public CDN (jsDelivr). This is enough for desktop browsers. For WebViews and mobile browsers, see the next section.
How it works #
Tesseract.js loads four assets, at different times and (by default) from a CDN:
tesseract.min.js— the main script, loaded when the engine initializesworker.min.js— the Web Worker- the core WASM (
tesseract-core-*.wasm.js+.wasm) — loaded by the worker at recognition time ind.traineddata.gz— the Indonesian language data
The heavy assets (3 and 4) are cached in the browser (IndexedDB) after the first scan, so subsequent scans don't re-download them. Nothing downloads at app startup — the first scan is when the core and language data are fetched.
Using inside a WebView or on mobile #
If you serve your Flutter web build inside a WebView, or run it in a mobile browser (notably iOS Safari / WKWebView), OCR may return empty results even though it works in desktop Chrome.
Why #
The default assets come from a CDN — a different origin than your page. Some environments block cross-origin resource requests:
- iOS Safari / WKWebView, content blockers, or Private Relay can silently drop cross-site requests (the core WASM fetch fails with no response).
- A page or server Content Security Policy may not allow the CDN.
- Cross-origin Web Workers fall back to
blob:workers, which strict WebViews block.
When any required asset can't load, OCR silently returns nothing.
Fix: self-host the assets on your own origin #
Host the Tesseract files on the same origin as your Flutter build, then point the library at them. Same requests, same-origin — nothing to block.
1. Place the files in your app's web/ folder (the app you build and serve — not this package; a package can't install files onto your origin):
web/tesseract/
tesseract.min.js # from tesseract.js@7.0.0 dist
worker.min.js # from tesseract.js@7.0.0 dist
core/ # ALL tesseract.js-core@7.0.0 files (a directory)
lang/
ind.traineddata.gz # Indonesian language data (keep it gzipped)
⚠️ Use matching versions.
tesseract.min.js,worker.min.js, and thecore/files must all be 7.0.0. Mismatched versions cause subtle runtime failures.
⚠️
core/must be a directory containing all the core builds, not a single file. Tesseract picks the right build (SIMD / relaxed-SIMD / plain) per device; pointing at one file breaks some devices.
2. Rebuild so the files are copied into the output, and confirm they're there:
flutter build web
ls build/web/tesseract/ # tesseract.min.js, worker.min.js, core/, lang/
ls build/web/tesseract/core/ # the core builds
3. Pass the paths. They must resolve to your served URLs. If you build with a base-href (e.g. --base-href=/my/app/), include that prefix:
// warm-up in main() — pass scriptSrc here too, since init is idempotent
IdCardOcr.initialize(
scriptSrc: '/my/app/tesseract/tesseract.min.js',
);
await IdCardOcr.processKtp(
bytes,
scriptSrc: '/my/app/tesseract/tesseract.min.js',
workerPath: '/my/app/tesseract/worker.min.js',
corePath: '/my/app/tesseract/core',
langPath: '/my/app/tesseract/lang',
);
Any path you leave out falls back to the CDN, so you can self-host only what you need. At minimum, self-host corePath and langPath — those are the requests most often blocked on mobile.
Server requirements #
- Serve
.wasmfiles withContent-Type: application/wasm. If the server returnsoctet-streamor an HTML error page, Safari refuses to compile the WASM. (iOS is stricter than desktop here.) - Serve
ind.traineddata.gzas-is (gzipped). Don't decompress or rename it; Tesseract expects<langPath>/ind.traineddata.gzand decompresses it itself. - Make sure the
tesseract/assets are not behind an auth gate — static files on a login-protected path can return a redirect/login page instead of the file.
Content Security Policy #
If you keep any asset on the CDN and your WebView enforces a CSP, allow it in your app's web/index.html:
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://unpkg.com;
worker-src 'self' blob:;
connect-src 'self' https://cdn.jsdelivr.net https://tessdata.projectnaptha.com;
img-src 'self' data: blob:;
">
A
<meta>CSP can only make policy stricter, never looser. If a server-sent CSP header (or the WebView container itself) is doing the blocking, it must be changed there, not inindex.html. Self-hosting all assets avoids CSP entirely.
API #
IdCardOcr.initialize({String? scriptSrc}) #
Loads the Tesseract.js script. Optional to call directly — processKtp calls it for you — but calling it early (e.g. in main()) warms up the engine. Idempotent.
| Parameter | Description |
|---|---|
scriptSrc |
Optional. URL to a self-hosted tesseract.min.js. Defaults to the CDN. If you warm up in main(), pass it here too. |
IdCardOcr.processKtp(...) #
static Future<KtpResult> processKtp(
Uint8List imageBytes, {
String? scriptSrc,
String? workerPath,
String? corePath,
String? langPath,
})
| Parameter | Description |
|---|---|
imageBytes |
Raw image bytes of the KTP. |
scriptSrc |
Optional. Self-hosted tesseract.min.js URL. Defaults to CDN. |
workerPath |
Optional. Self-hosted worker.min.js URL. When set, blob workers are disabled. Defaults to CDN. |
corePath |
Optional. Directory URL containing the tesseract.js-core files. Defaults to CDN. |
langPath |
Optional. Directory URL containing ind.traineddata.gz. Defaults to CDN. |
Returns a KtpResult with the parsed fields, an isSuccess flag, and toJson().
⚠️ OCR output is a draft for human review, not a verified value. Always let a person confirm/correct extracted fields before they are used.
Tips for better accuracy #
- Use a sharp, well-lit, straight-on photo. Glare and skew hurt more than resolution.
- Crop to the card where possible — surrounding text confuses parsing.
- Very large phone photos are memory-heavy on mobile browsers; a reasonable capture size scans faster and more reliably.
Troubleshooting #
| Symptom | Likely cause |
|---|---|
| Works on desktop, empty result on iPhone / in WebView | A cross-origin asset (usually the core WASM) is blocked. Self-host corePath + langPath. |
| Works without paths, fails when paths are set | Wrong URL — a path 404s. Verify each file opens directly in a browser at its full URL; check the base-href prefix. |
| Empty result, no visible error | Errors are caught internally. While debugging, temporarily surface them (see below). |
| WASM fails to load / compile | .wasm served with wrong Content-Type. Serve as application/wasm. |
| Slow first scan | First run downloads core + language data, then caches. Later scans are fast. |
| Blank page entirely | Not OCR — usually cleartext (http) blocked, wrong host address, or an asset behind an auth gate. |
Surfacing the real error #
OCR failures are caught internally and returned as an empty string. While debugging, temporarily change the internal catch to rethrow, or log the error, so the real message reaches the console. Then inspect the running page:
- Android WebView — desktop Chrome →
chrome://inspect/#devices→ inspect. - iOS WKWebView / Safari — Safari (Mac) → Develop → [device] → the page.
In the Network tab, confirm each asset (tesseract.min.js, worker.min.js, the core *.wasm.js + .wasm, ind.traineddata.gz) returns 200 from the expected origin. A blocked cross-origin request shows as failed/empty with no response.
Roadmap #
- ❌ Malaysian MyKad support
- ❌ Reusable worker (skip per-call worker creation for batch scanning)
Acknowledgements #
OCR powered by Tesseract.js and tesseract.js-core. Language data from the Tesseract tessdata project.
License #
See LICENSE.