Text Recognition from Image (OCR) in Flutter

Sushan Shakya
7 min readJun 30, 2023

--

This article only covers text recognition in device without use of any cloud platforms

Flutter is Google’s Solution for building blazing fast UIs.

Since, Flutter is simply only for UIs all the complex stuff like Text Recognition if done in native platforms. So, all we need to do is call the native APIs.

There are packages in Flutter that helps us to call these native APIs.
All we need to do in Flutter is obtain the image and then call the native APIs via the package and pass the image.

This article covers 2 packages to recognize text :
1. google_mlkit_text_recognition
2. flutter_tesseract_ocr

The code for the project can be found here.

Before we deal with these packages, let’s see how we can get the image for processing.

Obtaining Image

Add the dependency for image_picker :

dependencies:
flutter:
sdk: flutter
...
image_picker: ^1.0.0
...

We also need to update the minSdkVersion to 21 or higher inside app/build.gradle :

defaultConfig {
...
minSdkVersion 21
...
}

Now,
We create a page with a floating button to pick image as follows :

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

class Home extends StatefulWidget {
const Home({super.key});

@override
State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Text Recognition'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
body: Container(),
);
}
}

When, the user taps on the button we want to show them options to get image from camera or gallery. For this we can create a separate function to render a Alert Dialog as follows :

import 'package:flutter/material.dart';

Widget imagePickAlert({
void Function()? onCameraPressed,
void Function()? onGalleryPressed,
}) {
return AlertDialog(
title: const Text(
"Pick a source:",
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text(
"Camera",
),
onTap: onCameraPressed,
),
ListTile(
leading: const Icon(Icons.image),
title: const Text(
"Gallery",
),
onTap: onGalleryPressed,
),
],
),
);
}

Then,
When the floating button is tapped, we can show the dialog as :

...
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => imagePickAlert(
onCameraPressed: () {
// Pick image from Camera
},
onGalleryPressed: () {
// Pick image from Gallery
},
),
);
},
child: const Icon(Icons.add),
),
...

We can get image from camera or gallery by using image_picker package as follows :

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

import 'image_widget.dart';

class Home extends StatefulWidget {
...
}

class _HomeState extends State<Home> {
late ImagePicker _picker;

@override
void initState() {
super.initState();
_picker = ImagePicker();
}

Future<String?> obtainImage(ImageSource source) async {
final file = await _picker.pickImage(source: source);
return file?.path;
}

@override
Widget build(BuildContext context) {
return Scaffold(
...
);
}
}

Here,
obtainImage() function can be called to get image.
This can be done as follows :

...
showDialog(
context: context,
builder: (context) => imagePickAlert(
onCameraPressed: () async {
final imgPath = await obtainImage(ImageSource.camera);
...
Navigator.of(context).pop();
},
onGalleryPressed: () {
final imgPath = await obtainImage(ImageSource.gallery);
...
Navigator.of(context).pop();
},
),
);
...

Now,
We can start working on processing the image as we’re able to get image using image_picker .

For processing image to recognize text we’ll create our own abstraction as :

abstract class ITextRecognizer {
Future<String> processImage(String imgPath);
}

Using [google_mlkit_text_recognition]

Add dependency for google_mlkit_text_recognition :

dependencies:
flutter:
sdk: flutter

...
google_mlkit_text_recognition: ^0.8.1
...

Now,
We create a MLKitTextRecognizer for recognizing text as :

import 'dart:io';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'interface/text_recognizer.dart';

class MLKitTextRecognizer extends ITextRecognizer {
late TextRecognizer recognizer;

MLKitTextRecognizer() {
recognizer = TextRecognizer();
}

void dispose() {
recognizer.close();
}

@override
Future<String> processImage(String imgPath) async {
final image = InputImage.fromFile(File(imgPath));
final recognized = await recognizer.processImage(image);
return recognized.text;
}
}

We can create an instance for it as :

...

class _HomeState extends State<Home> {
late ImagePicker _picker;
late ITextRecognizer _recognizer;

@override
void initState() {
super.initState();
_picker = ImagePicker();
_recognizer = MLKitTextRecognizer();
}

@override
void dispose() {
super.dispose();
if(_recognizer is MLKitTextRecognizer) {
(_recognizer as MLKitTextRecognizer).dispose();
}
}

Future<String?> obtainImage(ImageSource source) async {
final file = await _picker.pickImage(source: source);
return file?.path;
}


@override
Widget build(BuildContext context) {
return Scaffold(
...
);
}
}

...

We then create a function to process the image as :

....

class _HomeState extends State<Home> {

...
void processImage(String imgPath) async{
final recognizedText = await _recognizer.processImage(imgPath);
...
}
...

@override
Widget build(BuildContext context) {
return Scaffold(
...
);
}
}

....

Here,
After we have processed the image, we would like to show it in the UI.
For showing it in our UI we need 2 piece of information :
1. The Image File
2. The Recognized Text

So, we’ll create a data class for it as :

class RecognitionResponse {
final String imgPath;
final String recognizedText;

RecognitionResponse({
required this.imgPath,
required this.recognizedText,
});

@override
bool operator ==(covariant RecognitionResponse other) {
if (identical(this, other)) return true;

return other.imgPath == imgPath && other.recognizedText == recognizedText;
}

@override
int get hashCode => imgPath.hashCode ^ recognizedText.hashCode;
}

Then, we create a variable of type RecognitionResponse in HomeState as :

class _HomeState extends State<Home> {

...
RecognitionResponse? _response;
...

@override
Widget build(BuildContext context) {
...
}
}

Now,
We can set this variable from processImage() as :

  ...

void processImage(String imgPath) async{
final recognizedText = await _recognizer.processImage(imgPath);
setState(() {
_response = RecognitionResponse(imgPath: imgPath, recognizedText: recognizedText);
});
}

...

We will call processImage() when image is selected.

...
showDialog(
context: context,
builder: (context) => imagePickAlert(
onCameraPressed: () async {
final imgPath = await obtainImage(ImageSource.camera);
if(imgPath == null) return;
processImage(imgPath);
Navigator.of(context).pop();
},
onGalleryPressed: () {
final imgPath = await obtainImage(ImageSource.gallery);
if(imgPath == null) return;
processImage(imgPath);
Navigator.of(context).pop();
},
),
);
...

Finally,
We need to show the image and recognized text in the UI when _response variable is not null.

class _HomeState extends State<Home> {

...

RecognitionResponse? _response;

...

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Text Recognition'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => imagePickAlert(
onCameraPressed: () async {
final imgPath = await obtainImage(ImageSource.camera);
if (imgPath == null) return;
processImage(imgPath);
Navigator.of(context).pop();
},
onGalleryPressed: () async {
final imgPath = await obtainImage(ImageSource.gallery);
if (imgPath == null) return;
processImage(imgPath);
Navigator.of(context).pop();
},
),
);
},
child: const Icon(Icons.add),
),
body: _response == null
? const Center(
child: Text('Pick image to continue'),
)
: ListView(
children: [
SizedBox(
height: MediaQuery.of(context).size.width,
width: MediaQuery.of(context).size.width,
child: Image.file(File(_response!.imgPath)),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
"Recognized Text",
style: Theme.of(context).textTheme.titleLarge,
),
),
IconButton(
onPressed: () {
Clipboard.setData(
ClipboardData(
text: _response!.recognizedText),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Copied to Clipboard'),
),
);
},
icon: const Icon(Icons.copy),
),
],
),
const SizedBox(height: 10),
Text(_response!.recognizedText),
],
)),
],
),
);
}
}

Ta-da, your app is now able to recognize text from image.

Using [flutter_tesseract_ocr]

Add dependency for flutter_tesseract_ocr as :

dependencies:
flutter:
sdk: flutter
...
flutter_tesseract_ocr: ^0.4.23
...

Then,
Download the trained data for English language from here.
The file is called eng.traineddata

Once downloaded, copy the file to assets/tessdata as :

Now,
We need to create a configuration file in assets/tessdata_config.json as :

Inside tessdata_config.json we add following content:

{
"files": [
"eng.traineddata"
]
}

Then, we have to include the asset file into the flutter project by adding entry to pubspec.yaml :

...
assets:
- assets/
- assets/tessdata/
...

Now,
We create TesseractTextRecognizer for recognizing text as :

import 'package:flutter_tesseract_ocr/android_ios.dart';
import 'package:ml_kit_test/recognition/interface/text_recognizer.dart';

class TesseractTextRecognizer extends ITextRecognizer {
@override
Future<String> processImage(String imgPath) async {
final res = await FlutterTesseractOcr.extractText(imgPath, args: {
"psm": "4",
"preserve_interword_spaces": "1",
});
return res;
}
}

We can use the same UI as we used for google_mlkit_text_recognition .

The only thing we need to change is the initialization of _recognizer as :

  @override
void initState() {
super.initState();
...
_recognizer = TesseractTextRecognizer();
...
}
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:ml_kit_test/model/recognition_response.dart';
import 'package:ml_kit_test/recognition/interface/text_recognizer.dart';
import 'package:ml_kit_test/recognition/text_recognition_service.dart';

import 'image_widget.dart';

class Home extends StatefulWidget {
const Home({super.key});

@override
State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
late ImagePicker _picker;
late ITextRecognizer _recognizer;

RecognitionResponse? _response;

@override
void initState() {
super.initState();
_picker = ImagePicker();
_recognizer = TesseractTextRecognizer();
}

@override
void dispose() {
super.dispose();
if (_recognizer is MLKitTextRecognizer) {
(_recognizer as MLKitTextRecognizer).dispose();
}
}

void processImage(String imgPath) async {
final recognizedText = await _recognizer.processImage(imgPath);
setState(() {
_response =
RecognitionResponse(imgPath: imgPath, recognizedText: recognizedText);
});
}

Future<String?> obtainImage(ImageSource source) async {
final file = await _picker.pickImage(source: source);
return file?.path;
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Text Recognition'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => imagePickAlert(
onCameraPressed: () async {
final imgPath = await obtainImage(ImageSource.camera);
if (imgPath == null) return;
processImage(imgPath);
Navigator.of(context).pop();
},
onGalleryPressed: () async {
final imgPath = await obtainImage(ImageSource.gallery);
if (imgPath == null) return;
processImage(imgPath);
Navigator.of(context).pop();
},
),
);
},
child: const Icon(Icons.add),
),
body: _response == null
? const Center(
child: Text('Pick image to continue'),
)
: ListView(
children: [
SizedBox(
height: MediaQuery.of(context).size.width,
width: MediaQuery.of(context).size.width,
child: Image.file(File(_response!.imgPath)),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
"Recognized Text",
style: Theme.of(context).textTheme.titleLarge,
),
),
IconButton(
onPressed: () {
Clipboard.setData(
ClipboardData(
text: _response!.recognizedText),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Copied to Clipboard'),
),
);
},
icon: const Icon(Icons.copy),
),
],
),
const SizedBox(height: 10),
Text(_response!.recognizedText),
],
)),
],
),
);
}
}

Boom,
We can use the same UI but handle image processing using 2 different packages because we created our own abstraction for text recognition.

--

--