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

  1. The image is decoded and pre-processed (resized to max 1600px, converted to high-contrast black & white) to improve recognition.
  2. On first use, the package loads tesseract.js and, at recognition time, downloads the OCR engine core (WASM) and the language data (ind.traineddata) from a CDN.
  3. 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.

  1. 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.

  2. Place it in your app's web folder (the app you build and serve — not this package):

    web/tesseract/worker.min.js
    
  3. Rebuild so the file is copied into the output:

    flutter build web
    # confirm it's there:
    ls build/web/tesseract/
    
  4. 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/#devicesinspect. (Enable once with WebView.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 support
  • Reusable worker (skip per-call worker creation for batch scanning)
  • Optional self-hosted core/language helpers

Acknowledgements

OCR powered by Tesseract.js.

License

See LICENSE.

Libraries

id_card_ocr_web