id_card_ocr_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 (with one extra step — see Using inside a WebView)
Platform support
| Platform | Supported |
|---|---|
| Flutter Web (Chrome, Edge, Safari, Firefox) | ✅ |
| Flutter Web inside a WebView (Android/iOS) | ✅ (see WebView notes) |
| Android / iOS native (non-web) | ❌ |
This package relies on browser APIs (Canvas, Blob, Web Workers, WASM) and is web-only. It does not run on the Flutter mobile/desktop engines directly — only when your Flutter target is web, including a web build embedded in a WebView.
Installation
Add the dependency:
dependencies:
id_card_ocr_web: ^2.0.0
Then:
flutter pub get
Quick start
import 'dart:typed_data';
import 'package:id_card_ocr_web/id_card_ocr_web.dart';
// Optional: warm up the engine early (e.g. in main()) so the first
// scan is faster. Safe to call multiple times.
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');
}
}
processKtp takes the raw image bytes (Uint8List), runs OCR, parses the result, and returns a KtpResult. The image bytes can come from anywhere — image_picker, a file input, a network fetch, etc.
How it works
- The image is decoded and pre-processed (resized to max 1600px, converted to high-contrast black & white) to improve recognition.
- On first use, the package loads
tesseract.jsand, at recognition time, downloads the OCR engine core (WASM) and the language data (ind.traineddata) from a CDN. - The heavy assets are cached by the browser (IndexedDB) after the first run, so subsequent scans don't re-download them.
By default everything loads from the CDN — no setup required. This is enough for a normal browser and for many WebViews.
Using inside a WebView
If you serve your Flutter web build inside a WebView (webview_flutter, Android WebView, iOS WKWebView), OCR may return empty results even though it works in Chrome.
Why
Tesseract loads its Web Worker from a different origin (the CDN), so it falls back to creating the worker from a blob: URL. Some WebViews — most often iOS WKWebView, or any WebView with a strict Content Security Policy — block cross-origin or blob workers. When the worker can't start, OCR silently returns nothing.
Fix: host the worker file on your own origin
You only need to self-host one small file (worker.min.js, tens of KB). The heavy core and language data keep loading from the CDN.
-
Download the worker matching this package's Tesseract version:
https://unpkg.com/tesseract.js@7.0.0/dist/worker.min.js⚠️ Use the same version (
7.0.0). A mismatched worker can load but then fail at runtime. -
Place it in your app's web folder (the app you build and serve — not this package):
web/tesseract/worker.min.js -
Rebuild so the file is copied into the output:
flutter build web # confirm it's there: ls build/web/tesseract/ -
Pass the path when calling the OCR:
final result = await IdCardOcr.processKtp( bytes, workerPath: '/tesseract/worker.min.js', );
When workerPath is provided, the package loads the worker same-origin and disables the blob worker (workerBlobURL: false) — the configuration WebViews reliably accept.
If your app is served under a sub-path (a non-root
base href), use a relative path (tesseract/worker.min.js, no leading slash) so it resolves correctly.
Still failing? (CSP)
If your WebView enforces a Content Security Policy, allow the worker and CDN in your app's web/index.html:
<meta http-equiv="Content-Security-Policy" content="
worker-src 'self' blob:;
script-src 'self' blob: https://cdn.jsdelivr.net https://unpkg.com;
connect-src 'self' https://cdn.jsdelivr.net https://tessdata.projectnaptha.com;
">
Fully offline / air-gapped
To remove the CDN dependency entirely (e.g. offline-first apps), also self-host the engine core and language data and pass their paths. Host tesseract.js-core (all 4 build files) and ind.traineddata.gz on your origin, then extend the call accordingly. See the Tesseract.js local-installation guide for the file list.
API
IdCardOcr.initialize()
static Future<void> initialize()
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 so the first scan is faster. Idempotent.
IdCardOcr.processKtp(...)
static Future<KtpResult> processKtp(
Uint8List imageBytes, {
String? workerPath,
})
| Parameter | Type | Description |
|---|---|---|
imageBytes |
Uint8List |
Raw image bytes of the KTP. |
workerPath |
String? |
Optional. Same-origin URL to a hosted worker.min.js. Required for strict WebViews — see Using inside a WebView. When omitted, the CDN worker is used. |
Returns a KtpResult with the parsed fields, an isSuccess flag, and a toJson() method.
Tips for better accuracy
- Use a sharp, well-lit, straight-on photo of the card. Glare and skew hurt recognition more than resolution.
- Higher-resolution source images generally parse better; the package downscales internally only above 1600px.
- Crop to the card if possible — surrounding background text can confuse parsing.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Works in Chrome, empty result in WebView | Blob worker blocked — host worker.min.js same-origin and pass workerPath. |
| Empty result, no error shown | Errors are caught internally and return an empty string. Wrap your call site in logging, or temporarily surface errors while debugging. |
worker.min.js 404 |
File not in the served build — place it in your app's web/tesseract/, then flutter build web again. |
| Slow first scan | First run downloads the core + language data (a few MB), then caches them. Subsequent scans are fast. |
| Blank WebView page entirely | Not an OCR issue — usually cleartext (http) blocked, wrong host address, or the dev server not reachable. |
Debugging a WebView
Inspect the page running inside the WebView with full DevTools:
- Android — desktop Chrome →
chrome://inspect/#devices→ inspect. (Enable once withWebView.setWebContentsDebuggingEnabled(true)in debug builds.) - iOS — Safari → Develop → [device] → the page.
Check the Console for the real error and the Network tab to confirm worker.min.js loads from your origin and the core/language data load from the CDN.
Roadmap
Malaysian MyKad supportReusable worker (skip per-call worker creation for batch scanning)Optional self-hosted core/language helpers
Acknowledgements
OCR powered by Tesseract.js.
License
See LICENSE.