Google search engine
HomeDevelopersHow to Add Text Recognition (OCR) in Flutter Apps: Complete Guide

How to Add Text Recognition (OCR) in Flutter Apps: Complete Guide

How to Add Text Recognition (OCR) in Flutter Apps:

Complete Guide !!

If you’re looking for the best Flutter app development company for your mobile application then feel free to

Introduction

What Is OCR and Why Does It Matter in Flutter

Choosing the Right OCR Package

Setting Up Google ML Kit Text Recognition

Handling Runtime Permissions

Scanning Text from Camera in Real Time

Scanning Text from Gallery Images

Handling Multi-Language OCR

Tesseract OCR as an Alternative

Post-Processing Recognized Text

Real-World Use Cases

Common Mistakes to Avoid

Conclusion

References

Introduction:

Text recognition, commonly known as OCR (Optical Character Recognition), is one of the most powerful capabilities you can add to a Flutter mobile application. From scanning business cards and extracting invoice data to reading printed documents and digitizing handwritten notes, OCR opens a wide range of intelligent features that make your app significantly more useful.

Flutter, backed by Google’s ecosystem, has excellent support for on-device OCR through the ML Kit plugin. This guide walks you through adding text recognition to your Flutter app from scratch, covering setup, camera integration, gallery image scanning, multilingual support, permission handling, and real-world architecture patterns. Every step is explained with clean code examples and practical insights so you can ship this feature confidently.

What you’ll learn:

Setting up Google ML Kit for text recognition

Handling camera and storage permissions properly

Implementing real-time camera OCR with performance optimization

Processing images from gallery and camera

Extracting structured data (emails, phone numbers, URLs)

Multi-language support

Common pitfalls and how to avoid them

What Is OCR and Why Does It Matter in Flutter:

OCR is the process of converting images containing printed or handwritten text into machine-readable string data. In a Flutter context, this means your app can take a photo — either from the live camera or from the user’s gallery — and extract all readable text from it automatically.

This capability matters because it removes manual data entry from user workflows. Instead of typing a long product code, an email address from a business card, or a tracking number from a label, the user simply points their camera at it. The app handles the rest. This dramatically improves user experience, reduces input errors, and enables automation at the edge, directly on the user’s device without any server call required.

Key Benefits:

Improved User Experience: Eliminates tedious manual typing

Reduced Errors: Automated extraction is more accurate than manual entry

Offline Capability: Works fully on-device without internet connection

Privacy-Friendly: No data sent to external servers

Fast Processing: Real-time recognition with ML Kit

Choosing the Right OCR Package:

Flutter developers have two primary choices for OCR:

Google ML Kit (Recommended):

Package: google_mlkit_text_recognition

Pros:

Fast, accurate, on-device recognition

Supports Latin and non-Latin scripts (Chinese, Japanese, Korean, Devanagari)

Runs fully offline

Tight integration with Flutter camera ecosystem

Regular updates and strong community support

Minimal setup overhead

Cons:

Limited to scripts supported by Google ML Kit

Less configurable than Tesseract

Tesseract OCR:

Package: flutter_tesseract_ocr

Pros:

Open-source engine maintained by Google

Supports over 100 languages

Highly configurable

Mature and battle-tested

Cons:

Generally slower than ML Kit

Requires bundling language data files (increases app size)

More complex setup

Recommendation: For most production apps, Google ML Kit is the recommended starting point due to its speed, accuracy, and minimal setup overhead. Consider Tesseract only when you need deep language support or highly customized OCR pipelines.

Setting Up Google ML Kit Text Recognition

Step 1: Add Dependencies:

Add the required dependencies to your pubspec.yaml file:

yaml

dependencies:

flutter:

sdk: flutter

# OCR and ML Kit

google_mlkit_text_recognition: ^0.11.0

# Image handling

image_picker: ^1.0.4

camera: ^0.10.5+5

# Permissions

permission_handler: ^11.0.1

# File paths (optional)

path_provider: ^2.1.1

Run flutter pub get to install the dependencies.

Step 2: Android Configuration

Minimum SDK Version:

Ensure your minSdkVersion is set to at least 21 in android/app/build.gradle.kts:

kotlin:

android {

defaultConfig {

minSdk = 21 // Required for ML Kit

targetSdk = 36

}

}

Gradle Version Requirements:

ML Kit requires specific Gradle versions. Update android/settings.gradle.kts:

kotlin

plugins {

id("dev.flutter.flutter-plugin-loader") version "1.0.0"

id("com.android.application") version "8.9.1" apply false

id("org.jetbrains.kotlin.android") version "1.9.24" apply false

}

Update android/gradle/wrapper/gradle-wrapper.properties:

properties

distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

Permissions:

Add the following to android/app/src/main/AndroidManifest.xml:

xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!– Camera Permission –>

<uses-permission android:name="android.permission.CAMERA" />

<!– Storage Permissions –>

<uses-permission

android:name="android.permission.READ_EXTERNAL_STORAGE" />

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"

android:maxSdkVersion="32" />

<!– Camera Features →

<uses-feature

android:name="android.hardware.camera"

android:required="false" />

<uses-feature

android:name="android.hardware.camera.autofocus"

android:required="false" />

<application>

<!– Your app configuration –>

</application>

</manifest>

Step 3: iOS Configuration:

Minimum iOS Version:

Set minimum iOS version to 12.0 in ios/Podfile:

ruby

platform :ios, '12.0'

Permissions:

Add the following to your ios/Runner/Info.plist:

xml

<key>NSCameraUsageDescription</key>

<string>

Camera access is required for real-time text recognition and capturing images for OCR processing.

</string>

<key>NSPhotoLibraryUsageDescription</key>

<string>

Photo library access is required to select images for text recognition.

</string>

<key>NSPhotoLibraryAddUsageDescription</key>

<string>

Photo library access is required to save processed images.

</string>

Handling Runtime Permissions:

Before accessing the camera or photo library, you must request permissions at runtime. Here’s how to handle this properly:

Permission Request Implementation:

dart:

import 'package:permission_handler/permission_handler.dart';

class PermissionHandler {

Future<bool> requestCameraPermission(BuildContext context) async {

final status = await Permission.camera.request();

if (status.isGranted) {

return true;

} else if (status.isDenied) {

_showPermissionDialog(context, 'Camera');

return false;

} else if (status.isPermanentlyDenied) {

_showSettingsDialog(context, 'Camera');

return false;

}

return false;

}

Future<bool> requestStoragePermission(BuildContext context) async {

final status = await Permission.photos.request();

if (status.isGranted) {

return true;

}

else if (status.isDenied) {

_showPermissionDialog(context, 'Photo Library');

return false;

} else if (status.isPermanentlyDenied) {

_showSettingsDialog(context, 'Photo Library');

return false;

}

return false;

}

void _showPermissionDialog(BuildContext context, String permission) {

showDialog(

context: context,

builder: (context) => AlertDialog(

title: Text('$permission Permission Required'),

content: Text(

'This app needs $permission access to perform OCR.

Please grant permission.',

),

actions: [

TextButton(

onPressed: () => Navigator.of(context).pop(),

child: const Text('Cancel'),

),

TextButton(

onPressed: () {

Navigator.of(context).pop();

// Request permission again

},

child: const Text('Allow'),

),

],

),

);

}

void _showSettingsDialog(BuildContext context, String permission) {

showDialog(

context: context,

builder: (context) => AlertDialog(

title: Text('$permission Permission Denied'),

content: Text(

'Please enable $permission permission in app settings.',),

actions: [

TextButton(

onPressed: () => Navigator.of(context).pop(),

child: const Text('Cancel'),

),

TextButton(

onPressed: () {

Navigator.of(context).pop();

openAppSettings();

},

child: const Text('Open Settings'),

),

],

),

);}

}

This ensures your app handles permissions gracefully on both Android and iOS.

Scanning Text from Camera in Real Time:

Real-time OCR from a camera feed requires setting up a CameraController and processing frames as they arrive. Here's a complete working implementation with proper permission handling and error management.

Complete Live OCR Implementation:

dart

import 'package:flutter/material.dart';

import 'package:camera/camera.dart';

import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';

import 'package:permission_handler/permission_handler.dart';

import 'dart:ui' as ui;

import 'package:flutter/services.dart';

class LiveOCRScreen extends StatefulWidget {

const LiveOCRScreen({super.key});

@override

State<LiveOCRScreen> createState() => _LiveOCRScreenState(); }

class _LiveOCRScreenState extends State<LiveOCRScreen> {

CameraController? _cameraController;

final TextRecognizer _textRecognizer = TextRecognizer();

String _recognizedText = '';

bool _isProcessing = false;

bool _isCameraInitialized = false;

bool _isDetecting = true;

@override

void initState() {

super.initState();

_requestCameraPermission();

}

Future<void> _requestCameraPermission() async {

final status = await Permission.camera.request();

if (status.isGranted) {

_initCamera();

} else {

_showPermissionDeniedDialog();

}

}

void _showPermissionDeniedDialog() {

showDialog(

context: context,

builder: (context) => AlertDialog(

title: const Text('Camera Permission Required'),

content: const Text(

'This app needs camera access to perform real-time OCR.

Please grant camera permission in settings.',

),

actions: [

TextButton(

onPressed: () {

Navigator.of(context).pop();

Navigator.of(context).pop(); },

child: const Text('Cancel'), ),

TextButton(

onPressed: () {

Navigator.of(context).pop();

openAppSettings();

},

child: const Text('Open Settings'),

),

],

),

); }

Future<void> _initCamera() async {

try {

final cameras = await availableCameras();

if (cameras.isEmpty) {

_showError('No cameras found on this device');

return;

}

_cameraController = CameraController(

cameras[0],

ResolutionPreset.high,

enableAudio: false,

imageFormatGroup: ImageFormatGroup.nv21,

);

await _cameraController!.initialize();

if (!mounted) return;

setState(() {

_isCameraInitialized = true;

});

_cameraController!.startImageStream(_processFrame);

} catch (e) {

_showError('Failed to initialize camera: $e');

}

}

void _showError(String message) {

if (!mounted) return;

ScaffoldMessenger.of(context).showSnackBar(

SnackBar(

content: Text(message),

backgroundColor: Colors.red, ),

); }

Future<void> _processFrame(CameraImage image) async {

if (_isProcessing || !_isDetecting) return;

_isProcessing = true;

try {

final inputImage = _convertToInputImage(image);

if (inputImage == null) {

_isProcessing = false;

return; }

final RecognizedText result = await _textRecognizer.processImage(inputImage);

if (mounted) {

setState(() {

_recognizedText = result.text;

}); }

} catch (e) {

debugPrint('Error processing frame: $e');

} finally {

_isProcessing = false; }

}

InputImage? _convertToInputImage(CameraImage image) {

try {

final WriteBuffer allBytes = WriteBuffer();

for (final plane in image.planes) {

allBytes.putUint8List(plane.bytes); }

final bytes = allBytes.done().buffer.asUint8List();

final imageSize = ui.Size(

image.width.toDouble(),

image.height.toDouble(), );

const imageRotation = InputImageRotation.rotation0deg;

final inputImageFormat = InputImageFormatValue.fromRawValue(image.format.raw) ??

InputImageFormat.nv21;

final metadata = InputImageMetadata(

size: imageSize,

rotation: imageRotation,

format: inputImageFormat,

bytesPerRow: image.planes.first.bytesPerRow, );

return InputImage.fromBytes(

bytes: bytes,

metadata: metadata, );

} catch (e) {

debugPrint('Error converting image: $e');

return null;

}

}

void _toggleDetection() {

setState(() {

_isDetecting = !_isDetecting;

if (!_isDetecting) {

_recognizedText = '';

}

});

}

void _copyToClipboard() {

if (_recognizedText.isNotEmpty) {

Clipboard.setData(ClipboardData(text: _recognizedText));

ScaffoldMessenger.of(context).showSnackBar(

const SnackBar(

content: Text('Text copied to clipboard!'),

duration: Duration(seconds: 2),

),

);

}

}

@override

void dispose() {

_cameraController?.dispose();

_textRecognizer.close();

super.dispose();

}

@override

Widget build(BuildContext context) {

if (!_isCameraInitialized) {

return const Scaffold(

body: Center(child: CircularProgressIndicator()),

);

}

return Scaffold(

appBar: AppBar(

title: const Text('Live Camera OCR'),

actions: [

IconButton(

icon: Icon(_isDetecting ? Icons.pause : Icons.play_arrow),

onPressed: _toggleDetection,

tooltip: _isDetecting ? 'Pause Detection' : 'Resume Detection',

),

],

),

body: Stack(

children: [

// Camera Preview

SizedBox(

width: double.infinity,

height: double.infinity,

child: CameraPreview(_cameraController!), ),

// Scanning Guide

Center(

child: Container(

width: MediaQuery.of(context).size.width * 0.8,

height: 200,

decoration: BoxDecoration(

border: Border.all(

color: _isDetecting ? Colors.green : Colors.grey,

width: 2, ),

borderRadius: BorderRadius.circular(12), ), ),),

// Recognized Text Display

Positioned(

bottom: 0,

left: 0,

right: 0,

child: Container(

padding: const EdgeInsets.all(20),

decoration: BoxDecoration(

color: Colors.black.withOpacity(0.85),

borderRadius: const BorderRadius.only(

topLeft: Radius.circular(20),

topRight: Radius.circular(20),

),

),

child: Column(

mainAxisSize: MainAxisSize.min,

crossAxisAlignment: CrossAxisAlignment.start,

children: [

Row(

mainAxisAlignment: MainAxisAlignment.spaceBetween,

children: [

const Text(

'Recognized Text',

style: TextStyle(

color: Colors.white,

fontSize: 18,

fontWeight: FontWeight.bold, ), ),

if (_recognizedText.isNotEmpty)

IconButton(

icon: const Icon(Icons.copy, color: Colors.white),

onPressed: _copyToClipboard,

),

],

),

const SizedBox(height: 10),

Text(

_recognizedText.isEmpty

? 'Point camera at text within the green frame'

: _recognizedText,

style: TextStyle(

color: _recognizedText.isEmpty

? Colors.white54

: Colors.white,

fontSize: 16,

),

),

],

),

),

),

],

),

);

}

}

Key Implementation Details:

Frame Throttling: The _isProcessing flag ensures only one frame is processed at a time, preventing performance issues and battery drain.

Error Handling: Try-catch blocks prevent crashes from camera or ML Kit errors.

Resource Management: Camera and text recognizer are properly disposed in the dispose() method.

User Controls: Pause/resume button allows users to control detection, and copy button enables easy text copying.

Scanning Text from Gallery Images:

For scanning static images selected from the gallery or captured with the camera, the workflow is simpler and more straightforward. This implementation includes both gallery selection and camera capture options.

Complete Gallery OCR Implementation:

dart

import 'package:flutter/material.dart';

import 'package:flutter/services.dart';

import 'package:image_picker/image_picker.dart';

import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';

import 'dart:io';

class GalleryOCRScreen extends StatefulWidget {

const GalleryOCRScreen({super.key});

@override

State<GalleryOCRScreen> createState() => _GalleryOCRScreenState();

}

class _GalleryOCRScreenState extends State<GalleryOCRScreen> {

final ImagePicker _picker = ImagePicker();

final TextRecognizer _textRecognizer = TextRecognizer();

String _extractedText = '';

File? _selectedImage;

bool _isProcessing = false;

List<String> _extractedEmails = [];

List<String> _extractedPhones = [];

List<String> _extractedUrls = [];

Future<void> _pickImageFromGallery() async {

try {

final XFile? pickedFile = await _picker.pickImage(

source: ImageSource.gallery,

imageQuality: 85,

);

if (pickedFile == null) return;

setState(() {

_selectedImage = File(pickedFile.path);

_isProcessing = true;

});

await _processImage(pickedFile.path);

} catch (e) {

_showError('Failed to pick image: $e');

} finally {

setState(() {

_isProcessing = false;

}); }

}

Future<void> _pickImageFromCamera() async {

try {

final XFile? pickedFile = await _picker.pickImage(

source: ImageSource.camera,

imageQuality: 85,

);

if (pickedFile == null) return;

setState(() {

_selectedImage = File(pickedFile.path);

_isProcessing = true;

});

await _processImage(pickedFile.path);

} catch (e) {

_showError('Failed to capture image: $e');

} finally {

setState(() {

_isProcessing = false;

}); } }

Future<void> _processImage(String imagePath) async {

try {

final InputImage inputImage = InputImage.fromFilePath(imagePath);

final RecognizedText result = await _textRecognizer.processImage(inputImage);

final cleanedText = _cleanOCROutput(result.text);

setState(() {

_extractedText = cleanedText;

});

_extractStructuredData(cleanedText);

} catch (e) {

_showError('Failed to process image: $e');

}

}

String _cleanOCROutput(String rawText) {

// Remove extra whitespace and blank lines

String cleaned = rawText

.split('\n')

.map((line) => line.trim())

.where((line) => line.isNotEmpty)

.join('\n');

return cleaned;

}

void _extractStructuredData(String text) {

// Extract emails

final emailRegex = RegExp(

r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',

);

final emails = emailRegex

.allMatches(text)

.map((m) => m.group(0)!)

.toSet()

.toList();

// Extract phone numbers

final phoneRegex = RegExp(

r'[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}',

);

final phones = phoneRegex

.allMatches(text)

.map((m) => m.group(0)!)

.toSet()

.toList();

// Extract URLs

final urlRegex = RegExp( r'https?://[^\s]+|www\.[^\s]+', );

final urls = urlRegex

.allMatches(text)

.map((m) => m.group(0)!)

.toSet()

.toList();

setState(() {

_extractedEmails = emails;

_extractedPhones = phones;

_extractedUrls = urls;

}); }

void _showError(String message) {

ScaffoldMessenger.of(context).showSnackBar(

SnackBar(

content: Text(message),

backgroundColor: Colors.red,

),

);

}

void _copyToClipboard(String text) {

Clipboard.setData(ClipboardData(text: text));

ScaffoldMessenger.of(context).showSnackBar(

const SnackBar(

content: Text('Copied to clipboard!'),

duration: Duration(seconds: 2),

),

);

}

@override

void dispose() {

_textRecognizer.close();

super.dispose();

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: const Text('Gallery OCR'),

),

body: SingleChildScrollView(

child: Padding(

padding: const EdgeInsets.all(16.0),

child: Column(

crossAxisAlignment: CrossAxisAlignment.stretch,

children: [

// Image picker buttons

if (_selectedImage == null) …[

const SizedBox(height: 20),

const Text(

'Select an Image',

textAlign: TextAlign.center,

style: TextStyle(

fontSize: 24,

fontWeight: FontWeight.bold,

),

),

const SizedBox(height: 40),

Row(

children: [

Expanded(

child: ElevatedButton.icon(

onPressed: _pickImageFromGallery,

icon: const Icon(Icons.photo_library),

label: const Text('Gallery'),

),

),

const SizedBox(width: 16),

Expanded(

child: ElevatedButton.icon(

onPressed: _pickImageFromCamera,

icon: const Icon(Icons.camera_alt),

label: const Text('Camera'),

),

),

],

),

],

// Selected image display

if (_selectedImage != null) …[

Image.file(

_selectedImage!,

height: 250,

fit: BoxFit.cover,

),

const SizedBox(height: 20),

],

// Processing indicator

if (_isProcessing)

const Center(

child: CircularProgressIndicator(),

),

// Extracted text display

if (_extractedText.isNotEmpty && !_isProcessing) …[

const Text(

'Extracted Text',

style: TextStyle(

fontSize: 20,

fontWeight: FontWeight.bold,

),

),

const SizedBox(height: 10),

Card(

child: Padding(

padding: const EdgeInsets.all(16.0),

child: Column(

crossAxisAlignment: CrossAxisAlignment.start,

children: [

Row(

mainAxisAlignment: MainAxisAlignment.spaceBetween,

children: [

Text(

'${_extractedText.split('\n').length} lines',

style: const TextStyle(fontSize: 12),

),

IconButton(

icon: const Icon(Icons.copy, size: 20),

onPressed: () => _copyToClipboard(_extractedText), ), ],

),

const Divider(),

SelectableText(_extractedText), ], ),

),

),

const SizedBox(height: 20),

],

// Structured data extraction

if (_extractedEmails.isNotEmpty) …[

const Text(

'Emails Found',

style: TextStyle(fontWeight: FontWeight.bold),

),

…_extractedEmails.map((email) => ListTile(

leading: const Icon(Icons.email),

title: Text(email),

trailing: IconButton(

icon: const Icon(Icons.copy),

onPressed: () => _copyToClipboard(email),

),

)),

],

if (_extractedPhones.isNotEmpty) …[

const Text(

'Phone Numbers Found',

style: TextStyle(fontWeight: FontWeight.bold), ),

…_extractedPhones.map((phone) => ListTile(

leading: const Icon(Icons.phone),

title: Text(phone),

trailing: IconButton(

icon: const Icon(Icons.copy),

onPressed: () => _copyToClipboard(phone), ), )), ],

if (_extractedUrls.isNotEmpty) …[

const Text(

'URLs Found',

style: TextStyle(fontWeight: FontWeight.bold), ),

…_extractedUrls.map((url) => ListTile(

leading: const Icon(Icons.link),

title: Text(url),

trailing: IconButton(

icon: const Icon(Icons.copy),

onPressed: () => _copyToClipboard(url), ), )), ], ], ), ),

),

);

}

}

The InputImage.fromFilePath method handles the conversion automatically, making gallery-based OCR significantly easier to implement than live camera scanning.

Enhanced Features:

This implementation includes several production-ready features:

Dual Input Options: Both gallery selection and camera capture for maximum flexibility.

Structured Data Extraction: Automatically detects and categorizes emails, phone numbers, and URLs using regex patterns.

Copy Functionality: Easy copying of entire text or individual data items.

Image Preview: Shows the selected image before processing.

Error Handling: Graceful error management with user-friendly messages.

Handling Multi-Language OCR:

ML Kit supports script-based recognition. You can specify the script when initializing the TextRecognizer to optimize accuracy for specific languages.

Supported Scripts:

dart

// For Latin scripts (English, French, Spanish, etc.)

final TextRecognizer latinRecognizer =

TextRecognizer(script: TextRecognitionScript.latin);

// For Devanagari (Hindi, Sanskrit, Marathi, etc.)

final TextRecognizer devanagariRecognizer =

TextRecognizer(script: TextRecognitionScript.devanagari);

// For Chinese (Simplified and Traditional)

final TextRecognizer chineseRecognizer =

TextRecognizer(script: TextRecognitionScript.chinese);

// For Japanese

final TextRecognizer japaneseRecognizer =

TextRecognizer(script: TextRecognitionScript.japanese);

// For Korean

final TextRecognizer koreanRecognizer =

TextRecognizer(script: TextRecognitionScript.korean);

Multi-Language Strategy

If your app needs to handle multiple languages simultaneously, consider these approaches:

Run multiple recognizers and merge results

Detect the dominant script before selecting the appropriate recognizer

Allow users to select their preferred language in settings

Use the default recognizer which attempts to handle multiple scripts automatically

Example of language selection:

dart:

class MultiLanguageOCR {

TextRecognizer? _recognizer;

void setLanguage(String scriptCode) {

_recognizer?.close();

switch (scriptCode) {

case 'latin':

_recognizer = TextRecognizer(script: TextRecognitionScript.latin);

break;

case 'chinese':

_recognizer = TextRecognizer(script: TextRecognitionScript.chinese);

break;

case 'japanese':

_recognizer = TextRecognizer(script: TextRecognitionScript.japanese);

break;

case 'korean':

_recognizer = TextRecognizer(script: TextRecognitionScript.korean);

break;

case 'devanagari':

_recognizer = TextRecognizer(script: TextRecognitionScript.devanagari);

break;

default:

_recognizer = TextRecognizer(); // Default multi-script

}

}

Future<String> recognizeText(InputImage image) async {

if (_recognizer == null) {

throw Exception('Recognizer not initialized');

}

final result = await _recognizer!.processImage(image);

return result.text;

}

void dispose() {

_recognizer?.close();

}

}

Tesseract OCR as an Alternative

For apps requiring deep language support or highly customized OCR pipelines, Tesseract remains a solid choice.

Setup:

Add the dependency to pubspec.yaml:

yaml

dependencies:

flutter_tesseract_ocr: ^0.4.13

Basic Usage

dart

import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';

Future<void> extractTextWithTesseract(String imagePath) async {

String result = await FlutterTesseractOcr.extractText(

imagePath,

language: 'eng',

args: {

"preserve_interword_spaces": "1",

"psm": "6", // Assume a single block of text

},

);

print('Extracted text: $result');

}

Advanced Configuration

dart

Future<String> extractTextWithCustomConfig(String imagePath) async {

return await FlutterTesseractOcr.extractText(

imagePath,

language: 'eng+fra', // Multiple languages

args: {

"psm": "3", // Fully automatic page segmentation

"preserve_interword_spaces": "1",

"tessedit_char_whitelist":

"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ",

},

);

}

Important Considerations:

Asset Bundling: Tesseract requires bundling .traineddata language files in your assets, which can significantly increase your app's size.

Performance: Generally slower than ML Kit, especially on older devices.

Language Support: Supports over 100 languages, making it ideal for specialized language requirements.

Configuration: Highly configurable with various page segmentation modes and character whitelisting.

Recommendation: Only include languages that your users genuinely need to minimize app size.

Post-Processing Recognized Text:

Raw OCR output is rarely perfect. Implement post-processing to clean and structure recognized text for practical use.

Basic Text Cleaning:

dart:

String cleanOCROutput(String rawText) {

// Remove extra whitespace and blank lines

String cleaned = rawText

.split('\n')

.map((line) => line.trim())

.where((line) => line.isNotEmpty)

.join('\n');

return cleaned; }

Structured Data Extraction:

dart

class OCRDataExtractor {

// Extract email addresses

List<String> extractEmails(String text) {

final emailRegex = RegExp( r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}');

return emailRegex

.allMatches(text)

.map((m) => m.group(0)!)

.toSet()

.toList();

}

// Extract phone numbers

List<String> extractPhones(String text) {

final phoneRegex = RegExp(

r'[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}'

);

return phoneRegex

.allMatches(text)

.map((m) => m.group(0)!)

.toSet()

.toList();

}

// Extract URLs

List<String> extractUrls(String text) {

final urlRegex = RegExp(

r'https?://[^\s]+|www\.[^\s]+'

);

return urlRegex

.allMatches(text)

.map((m) => m.group(0)!)

.toSet()

.toList();

}

// Extract dates (basic MM/DD/YYYY format)

List<String> extractDates(String text) {

final dateRegex = RegExp(

r'\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b'

);

return dateRegex

.allMatches(text)

.map((m) => m.group(0)!)

.toSet()

.toList();

}

// Extract currency amounts

List<String> extractCurrency(String text) {

final currencyRegex = RegExp(

r'[\$€£¥]\s?\d+(?:,\d{3})*(?:\.\d{2})?'

);

return currencyRegex

.allMatches(text)

.map((m) => m.group(0)!)

.toSet()

.toList();

}

}

Usage Example:

dart

final extractor = OCRDataExtractor();

void processOCRResult(String text) {

final emails = extractor.extractEmails(text);

final phones = extractor.extractPhones(text);

final urls = extractor.extractUrls(text);

final dates = extractor.extractDates(text);

final amounts = extractor.extractCurrency(text);

print('Emails found: $emails');

print('Phones found: $phones');

print('URLs found: $urls');

print('Dates found: $dates');

print('Currency amounts: $amounts');

}

This kind of post-processing makes OCR genuinely useful for specific flows such as contact scanning, invoice extraction, or document parsing.

Real-World Use Cases:

OCR in Flutter finds practical application in many product domains:

1. Document Scanners:

Extract text from PDFs and printed pages for indexing and search functionality. Enable users to digitize physical documents quickly and efficiently.

2. Business Card Readers:

Parse names, phone numbers, emails, and company information directly into contacts. Save networking details with a single photo, eliminating manual data entry.

3. Receipt Scanners:

Extract line items, totals, dates, and merchant information for expense tracking apps. Automate expense reports for businesses and individuals.

4. ID Verification Flows:

Read passport numbers, dates of birth, and names from government documents. Streamline KYC (Know Your Customer) processes for financial applications.

5. Inventory Management:

Scan product labels and barcodes with embedded text. Track inventory levels and product information automatically in warehouses and retail environments.

6. Translation Apps:

Extract text from signs, menus, and documents for real-time translation. Help travelers navigate foreign countries by translating captured text on the fly.

7. Educational Apps:

Convert textbook pages to searchable, highlightable text. Enable students to study more effectively by digitizing their physical study materials.

8. Banking Apps:

Read check information, account numbers, and routing numbers. Simplify check deposits and account setup processes for banking customers.

9. Medical Records:

Digitize handwritten prescriptions and medical notes. Improve healthcare workflows by making handwritten documents searchable and editable.

10. Postal Services:

Automatically read addresses from envelopes and packages. Speed up sorting and routing processes in postal and logistics operations.

Each of these scenarios can be built on the foundation covered in this guide, with customization for specific industry requirements.

Common Mistakes to Avoid:

1. Processing Every Frame Without Throttling:

Problem: Running OCR on every single camera frame causes severe performance degradation and battery drain.

Solution: Always implement frame skipping or processing gates:

dart

bool _isProcessing = false;

Future<void> _processFrame(CameraImage image) async {

if (_isProcessing) return; // Skip this frame

_isProcessing = true;

try {

// Process the frame

final result = await _textRecognizer.processImage(inputImage);

// Handle result

} finally {

_isProcessing = false;

} }

2. Not Disposing Resources:

Problem: Failing to close the TextRecognizer instance causes memory leaks.

Solution: Always call .close() in the dispose method:

dart

@override

void dispose() {

_textRecognizer.close();

_cameraController?.dispose();

super.dispose();

}

3. Displaying Raw OCR Output

Problem: Showing unprocessed OCR output directly to users leads to poor UX with extra spaces, special characters, and formatting issues.

Solution: Always sanitize, trim, and structure the text:

dart

String cleanedText = rawText

.split('\n')

.map((line) => line.trim())

.where((line) => line.isNotEmpty)

.join('\n');

4. Testing Only on Emulator:

Problem: OCR performance on simulators/emulators is unreliable and doesn’t represent real-world usage.

Solution: Always test on physical devices under varied conditions:

Different lighting (bright, dim, mixed)

Various angles and distances

Different text sizes and fonts

Multiple device models

5. Ignoring Permission Handling:

Problem: Not handling permission denials gracefully causes app crashes and poor user experience.

Solution: Implement proper permission request flows:

dart

Future<void> _requestCameraPermission() async {

final status = await Permission.camera.request();

if (status.isDenied) {

// Show explanation dialog

_showPermissionExplanation();

} else if (status.isPermanentlyDenied) {

// Guide user to settings

_showSettingsPrompt();

}

}

6. Using Incorrect Image Formats:

Problem: Not configuring the correct image format group for the platform causes conversion errors.

Solution: Specify the appropriate format:

dart

_cameraController = CameraController(

camera,

ResolutionPreset.high,

imageFormatGroup: ImageFormatGroup.nv21, // For Android

);

7. Not Handling Rotation:

Problem: Text appears upside down or sideways on different device orientations.

Solution: Calculate and apply correct image rotation based on device orientation.

8. Ignoring Gradle Configuration:

Problem: Using outdated Gradle or Android Gradle Plugin versions causes build failures.

Solution: Ensure you have the minimum required versions:

Gradle 8.9+

Android Gradle Plugin 8.7.2+

Kotlin 2.1.0+

Download Complete Source Code:

The complete source code for this Flutter OCR demo application is available on GitHub. This production-ready implementation includes all the features discussed in this tutorial.

GitHub Repository:

Repository: Flutter OCR Demo — Complete Implementation

What’s Included in the Repository:

The GitHub repository contains:

Complete Flutter Application

Live camera OCR with real-time text recognition

Gallery image OCR with photo selection and camera capture

Permission handling for Android and iOS

Copy to clipboard functionality

Pause/resume detection controls

Platform Configurations

Android Gradle setup (Kotlin DSL)

iOS CocoaPods configuration

Proper permission declarations

Gradle version configurations

Complete Documentation

Comprehensive README with setup instructions

Step-by-step installation guide

Troubleshooting section

API documentation

Screenshots and usage examples

Quick Start from GitHub:

bash

# Clone the repository

git clone https://github.com/onlykrishna/ocr_demo.git

# Navigate to project directory

cd flutter-ocr-demo

# Install dependencies

flutter pub get

# For iOS (macOS only)

cd ios && pod install && cd ..

# Run the app (use physical device for best results)

flutter run

Features Demonstrated:

The demo app showcases:

Home Screen — Elegant navigation with feature cards

Live Camera OCR — Real-time text recognition with visual guides

Gallery OCR — Image selection with automatic data extraction

Permission Management — Graceful handling of camera and storage permissions

Structured Data — Automatic detection of emails, phone numbers, and URLs

Copy Functionality — Easy text copying to clipboard

Error Handling — User-friendly error messages and recovery

Use This as a Template:

This repository serves as:

Learning Resource — Study the implementation and architecture

Project Template — Fork and customize for your own apps

Reference Implementation — See best practices in action

Production Baseline — Start with working code and build on top

Repository Structure:

flutter-ocr-demo/

├── lib/

│ ├── main.dart # App entry point

│ └── screens/

│ ├── home_screen.dart # Main navigation

│ ├── live_ocr_screen.dart # Real-time camera OCR

│ └── gallery_ocr_screen.dart # Gallery image OCR

├── android/ # Android configuration

├── ios/ # iOS configuration

├── README.md # Setup instructions

├── CHANGELOG.md # Version history

└── LICENSE # Open source license

Conclusion:

Adding OCR to a Flutter app is one of those capabilities that transforms a standard app into a genuinely intelligent product. With Google ML Kit, the integration is lightweight, accurate, and fully on-device. By combining camera integration, gallery scanning, permission handling, multilingual support, and post-processing, you can build OCR features that solve real problems for your users.

Key Takeaways:

Start with ML Kit for most use cases due to its speed, accuracy, and ease of integration

Handle permissions properly with user-friendly dialogs and settings navigation

Implement frame throttling for real-time camera OCR to prevent performance issues

Always dispose resources to prevent memory leaks

Post-process OCR output before displaying to users

Test on physical devices under various conditions

Extract structured data (emails, phones, URLs) to provide real value

Configure Gradle properly to avoid build failures

Architecture Scalability:

The architecture described here scales well from simple text extraction to full document understanding pipelines:

Start with ML Kit for most cases

Extend with Tesseract when deep language support is needed

Always post-process output before using it in application logic

Build modular components for reusability across features

Next Steps:

To extend this foundation:

Add cloud OCR fallback for complex documents

Implement document edge detection for better scanning

Add text-to-speech for accessibility

Integrate translation services for multilingual apps

Build batch processing for multiple images

Add export functionality (PDF, TXT, CSV)

References:

Google ML Kit Text Recognition : A Flutter plugin to use Google’s ML Kit Text Recognition to recognize text in any Chinese, Devanagari, Japanese scripts. {google_mlkit_text_recognition | Flutter package}

Flutter Tesseract OCR : Tesseract 4 adds a new neural net (LSTM) based OCR engine which is focused on line recognition. It has unicode (UTF-8) support.{ flutter_tesseract_ocr | Flutter package }

ML Kit for Developers : Google’s on-device machine learning kit for mobile developers. { ML Kit | Google for Developers }

Image Picker Plugin : Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera.

{ image_picker | Flutter package }

Camera Plugin : A Flutter plugin for iOS, Android and Web allowing access to the device cameras. { camera | Flutter package }

Permission Handler : Permission plugin for Flutter. This plugin provides a cross-platform API to request and check permissions.

{ permission_handler | Flutter package }

Connect With Us:

Feel free to connect with us:And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire a Flutter developer for your cross-platform Flutter mobile app project hourly or full-time as per your requirement! For any flutter-related queries, you can connect with us on Facebook, GitHub, Twitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


Need help building production-grade Flutter apps? FlutterDevs helps teams ship faster with solid architecture, better UX, and practical AI features. Reach us at support@flutterdevs.com.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments