flutter_receipt_scanner 0.5.0
flutter_receipt_scanner: ^0.5.0 copied to clipboard
On-device receipt capture (camera + gallery) with crop, EXIF, auto-rotate, and Korean/English OCR quality gating.
flutter_receipt_scanner #
On-device receipt image acquisition, crop, orientation normalization, JPEG compression, EXIF extraction, and OCR (raw string) for Flutter, on iOS and Android.
This is a federated plugin. The native layers own image primitives only — receipt domain parsing (store name, amount, date), upload transport, cloud OCR, and duplicate detection belong in the consuming app, not here.
- iOS: VisionKit document scanner + PHPicker → Vision OCR.
- Android: GMS document scanner + system photo picker → ML Kit OCR.
Usage #
import 'package:flutter_receipt_scanner/flutter_receipt_scanner.dart';
final result = await scan(options: const ScanReceiptOptions(maxPages: 1));
switch (result.status) {
case ScanStatus.success:
for (final image in result.images) {
print('${image.fileName}: ${image.ocrQuality?.textLength ?? 0} OCR chars');
}
case ScanStatus.cancelled:
// The user dismissed the scanner.
case ScanStatus.rejected:
// Every capture fell below the OCR floor; see result.rejectedImages.
}
The output file:// JPEG URIs are stable until the next scan() call and do not survive app restarts (the OS clears the cache directory).
Long receipts (multi-page OCR merge) #
Very long receipts should be captured as multiple overlapping camera pages, not one tall photo. Enable the opt-in merge to get one ordered OCR string with proven page-seam duplicates removed:
final result = await scan(
options: const ScanReceiptOptions(maxPages: 6),
mergeOcrPages: true,
);
final merged = result.mergedOcr;
if (merged != null) {
if (merged.isComplete) {
print(merged.text); // One ordered OCR string, seam duplicates removed.
} else {
// Diagnostics: merged.unmatchedBoundaryIndexes, merged.rejectedPageIndexes.
}
}
mergeOcrPages: true requires ocr: true (the default), source: ScanSource.camera, and maxPages >= 2; any other combination throws ArgumentError before the native scanner opens.
Support contract #
- The supported logical receipt content aspect ratio is
contentHeight / contentWidth <= 11.0. - The physical interpretation is a receipt up to 600 mm long on paper at least 57.0 mm wide (
600 / 57.0 = 10.53). - The reference capture layout is six portrait pages with approximately 20% vertical overlap between adjacent pages.
- The 11.0 limit is a tested capability claim, not a runtime physical-length measurement — a scan is never rejected because a physical size cannot be inferred from pixels.
Capture guidance #
Capture consecutive sections top-to-bottom in one camera session, overlapping each page with the previous one by roughly 20%. The overlap is what lets the merger prove each adjacent seam and remove the duplicated lines exactly once.
Exact page division is not required — a seam is proven when the next capture re-shows the last few lines of the previous one (two or three receipt lines are usually enough); the ~20% figure is the tested reference layout with margin. A gap between adjacent captures surfaces as an unmatched boundary, but a missed receipt top or bottom cannot be detected — start at the very first printed line and finish past the last one.
Set maxPages to the ceiling (10) when merging.
iOS cannot enforce a page limit in the VisionKit scanner UI, so pages captured beyond maxPages are discarded before processing without native recourse; ScanReceiptResult.discardedPageCount reports how many were dropped, and a positive count always makes the merged result incomplete.
Prefer the scanner's manual shutter while sectioning a long receipt — the automatic shutter can fire before a section is framed.
On iOS, use the Auto/Manual toggle inside the scanner UI; the package cannot switch it programmatically.
There is no capture-mode scan option on either platform: Android's GmsDocumentScannerOptions declares CaptureMode constants but its public builder has no capture-mode setter (googlesamples/mlkit#846), and those constants must never be passed to setScannerMode, whose integer namespace is unrelated.
Android's in-scanner capture behavior is pending physical-device verification.
What the merge does and does not do #
- The merge assembles OCR text only. No stitched bitmap, PDF, or tall composite image is returned; each page keeps its own JPEG in
result.images. - Gallery-selected images are not merged in version 1 —
mergeOcrPagesworks withScanSource.cameraonly. - An unproven seam or an OCR-rejected page never throws after a completed scan.
The result comes back with
isComplete == false, the seam recorded inunmatchedBoundaryIndexes(indexiis the seam between pagesiandi + 1), the page recorded inrejectedPageIndexes, and all text preserved — the merger never deletes uncertain content to make a result look complete.
Multilingual OCR #
The scanner extracts text using the languages requested in ScanReceiptOptions.ocrLanguages.
By default, this is ['ko-KR', 'en-US'] (Korean and Latin text).
final result = await scan(
options: const ScanReceiptOptions(
ocrLanguages: ['ko-KR', 'en-US'], // BCP 47 language tags
),
);
Checking Capabilities #
You can query current OCR support without opening the scanner or downloading models:
final capabilities = await getOcrCapabilities();
if (capabilities is IosOcrCapabilities) {
// Vision OCR exact supported languages
print(capabilities.supportedLanguages);
} else if (capabilities is AndroidOcrCapabilities) {
// ML Kit OCR script families and download states
for (final model in capabilities.models) {
print('${model.script} is ${model.status.name}');
}
}
- iOS: The system language bundle handles OCR. If a requested language is not supported by the active Vision framework revision, it throws
PlatformException('OCR_LANGUAGE_NOT_SUPPORTED'). - Android: The Korean module (
com.google.mlkit:text-recognition-korean, which also reads Latin) is bundled with the plugin, so the default language list works offline. Latin, Japanese, Chinese, and Devanagari use theplay-services-mlkit-text-recognition*modules delivered dynamically by Google Play services. If a script family is not installed, the firstscan()will trigger a download and may throwPlatformException('OCR_MODEL_INSTALL_FAILED')if offline. At most one non-Latin script can be requested at a time, or it throwsPlatformException('OCR_LANGUAGE_COMBINATION_NOT_SUPPORTED').
Note: The 11.0 aspect ratio and seam-matching metrics were calibrated exclusively on Korean+Latin text. Other scripts are natively supported but uncalibrated.
Host app permissions #
- iOS
Info.plist:NSCameraUsageDescription(camera scan). No photo-library key is needed — the gallery flow uses the permissionlessPHPickerViewController. - Android: none — the GMS document scanner and the system photo picker run out-of-process and require no app-declared permission.
Platform baselines #
- iOS deployment target: 16.0.
- Android
minSdk: 24.
See the repository for the full documentation and design specs.