simple_ocr_plugin 0.1.1 simple_ocr_plugin: ^0.1.1 copied to clipboard
Plugin to perform OCR on image / photo, backed by Google ML-Kit.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:simple_ocr_plugin/simple_ocr_plugin.dart';
import 'package:image_picker/image_picker.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:exif/exif.dart';
import 'package:image/image.dart' as im;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext c) {
return MaterialApp(
title: "Simple OCR plugin example",
theme: ThemeData(
primarySwatch: Colors.blue
),
debugShowCheckedModeBanner: false,
home: MainPage(),
);
}
}
/// The app's main page with all features listed.
///
/// This is a single page app; hence all features would be available at here.
class MainPage extends StatefulWidget {
State createState() => _MainPageState();
}
/// The state(s) controller of the main page.
class _MainPageState extends State<MainPage> {
File _imageFile;
PickedFile _pickedImageFile;
TextEditingController _regTextCtrl = TextEditingController();
final _navbarHeight = 42.0;
@override
void initState() {
super.initState();
_regTextCtrl.text = "here would display the results recognized by OCR";
}
@override
Widget build(BuildContext c) {
Size _s = MediaQuery.of(c).size;
return Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
bottomNavigationBar: _buildBottomNavBar(c),
body: Column(
children: [
SizedBox(
width: _s.width,
height: _s.height*0.4,
child: Container(
color: Colors.grey[300],
child: (_imageFile!=null)?Image.file(_imageFile, fit:BoxFit.contain):Image.asset("assets/empty_foto.png"),
),
),
Padding(
padding: EdgeInsets.fromLTRB(4, 8, 4, 12),
child: Text("OCR results"),
),
SizedBox(
width: _s.width,
height: _s.height*0.4 - _navbarHeight -5,
child: ListView(
shrinkWrap: true,
children: [
TextField(
controller: _regTextCtrl,
enabled: false,
minLines: 5,
maxLines: 1000,
),
],
)
),
],
),
);
}
/// Build the bottomNavBar widget.
///
/// Corresponding button widgets on the bottomNavBar would be generated by [_buildBottomNavBarButton()].
Widget _buildBottomNavBar(BuildContext c) {
return BottomAppBar(
elevation: 4.0,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildBottomNavBarButton(c, 0, Icons.camera, "camera"),
_buildBottomNavBarButton(c, 1, Icons.photo_album, "gallery"),
_buildBottomNavBarButton(c, 3, Icons.search, "OCR"),
],
),
);
}
/// Build the button widget for the bottomNavBar.
///
/// The [idx] identifies which button it is during an onTap event.
/// The [icon] refers to the chosen icon for display.
/// The [title] indicates the description of this button.
Widget _buildBottomNavBarButton(BuildContext c, int idx, IconData icon, String title) {
return GestureDetector(
onTap: () => _onBottomNavBarTap(idx),
child: Container(
height: _navbarHeight,
child: Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon),
Text(title),
],
),
),
),
);
}
/// The event-handler for the bottomNavBar button(s).
///
/// The [idx] indicates which button is tapped.
void _onBottomNavBarTap(int idx) {
switch (idx) {
case 0:
_showImagePicker(ImageSource.camera);
break;
case 1:
_showImagePicker(ImageSource.gallery);
break;
case 3:
_performOCR();
break;
}
}
/// Decides and displays the correct UI for either taking a photo from camera __OR__ picking from the photo album.
///
/// UI option decided by [choice]. If a new photo has been taken by camera,
/// the corresponding photo would be saved to the photo album.
Future<void> _showImagePicker(ImageSource choice) async {
_pickedImageFile = await ImagePicker().getImage(source: choice);
if (_pickedImageFile == null) {
// Cancelled by user.
return;
}
_imageFile = File(_pickedImageFile.path);
setState(() {}); // Update main UI thread.
if (choice == ImageSource.camera) {
var _result = await GallerySaver.saveImage(_pickedImageFile.path);
print("saved at ${_pickedImageFile.path} with result: $_result");
}
}
/// The helper method to perform OCR.
///
/// Image / photo optimization is optional but sometimes necessary. For this method's implementation, a __resize__ optimization
/// would be performed before running OCR. Exceptions might occur during the OCR operation and the corresponding error message
/// would be shown on the UI. If everything is good, the json result would be displayed instead.
_performOCR() async {
// Approach: optimization based on resizing the photo.
await _PhotoOptimizerForOCR.optimizeByResize(_pickedImageFile.path);
if (_pickedImageFile != null && _pickedImageFile.path != "") {
// " " = \n delimiter
// To use a dedicated delimiter instead of " ",
// provide the delimiter parameter => delimiter: " *** " now the blocks recognized would be separated by " *** " instead
try {
String _resultString = await SimpleOcrPlugin.performOCR(_pickedImageFile.path);
setState(() {
_regTextCtrl.text = _resultString;
});
} catch(e) {
setState(() {
_regTextCtrl.text = "error in recognizing the image / photo => ${e.toString()}";
});
} // End -- try
}
}
}
/// A helper class to provide support for Photo optimizations.
///
/// All methods provided are static (stateless) and available as follows:
/// * getPhotoFileMeta - returning the exif metadata on the provided image / photo file.
/// * getPhotoFileMetaInString - returning the exif metadata in String format.
/// * optimizeByResize - optimization based on resizing the given photo by a certain dimension value.
class _PhotoOptimizerForOCR {
/// The exif metadata key representing a photo's length (corresponding to width of an [ui.Image])
static const exifTagImageLength = "EXIF ExifImageLength";
/// The exif metadata key representing a photo's width (corresponding to height of an [ui.Image])
static const exifTagImageWidth = "EXIF ExifImageWidth";
/// Returns the raw Map of exif metadata on the [path].
///
/// __PS__. Not every photo would have exif metadata; hence it is normal to return an empty [Map].
static Future<Map<String, IfdTag>> getPhotoFileMeta(String path) async {
Future<Map<String, IfdTag>> _meta = readExifFromBytes(File(path).readAsBytesSync());
return _meta;
}
/// Returns the String description of the exif metadata on [path].
///
/// __PS__. Not every photo would have exif metadata;
/// hence if no metadata available a message "oops, no exif data available for this photo!!!" would be returned
static Future<String> getPhotoFileMetaInString(String path) async {
Map<String, IfdTag> _meta = await readExifFromBytes(File(path).readAsBytesSync());
StringBuffer _s = StringBuffer();
if (_meta == null || _meta.isEmpty) {
_s.writeln("oops, no exif data available for this photo!!!");
return _s.toString();
}
// Iterate all keys and its value.
_meta.keys.forEach((_k) {
_s.writeln("[$_k]: (${_meta[_k].tagType} - ${_meta[_k]})");
});
return _s.toString();
}
/// Optimizes the photo at [path] by a constraint of [maxWidthOrLength].
///
/// Resize logic is based on comparing the width and height of the image on [path] with the [maxWidthOrLength];
/// if either dimension is larger than [maxWidthOrLength], a corresponding resizing would be implemented.
/// Aspect ratio would be maintained to prevent image distortion. Finally the resized image would replace the original one.
static Future<bool> optimizeByResize(String path, {int maxWidthOrLength = 1500}) async {
int _w = 0;
int _h = 0;
Map<String, IfdTag> _meta = await _PhotoOptimizerForOCR.getPhotoFileMeta(path);
// Note that not every photo might have exif information~~~
if (_meta == null || _meta.isEmpty ||
_meta[_PhotoOptimizerForOCR.exifTagImageWidth] == null ||
_meta[_PhotoOptimizerForOCR.exifTagImageLength] == null)
{
// Use the old fashion ImageProvider to resolve the photo's dimensions.
Completer _completer = Completer();
FileImage(File(path)).
resolve(ImageConfiguration()).
addListener(ImageStreamListener((imgInfo, _) {
_completer.complete(imgInfo.image);
}));
var _img = await _completer.future as ui.Image;
_w = _img.height;
_h = _img.width;
} else {
_w = _meta[_PhotoOptimizerForOCR.exifTagImageWidth].values[0] as int;
_h = _meta[_PhotoOptimizerForOCR.exifTagImageLength].values[0] as int;
}
double _factor = 1.0;
// Update the resized w and h after resizing.
if (_w >= _h) {
_factor = maxWidthOrLength / _w;
_w = (_w * _factor).round();
_h = (_h * _factor).round();
} else {
_factor = maxWidthOrLength / _h;
_w = (_w * _factor).round();
_h = (_h * _factor).round();
}
// [DOC] note the exif width = height of the image !! whilst exif length = width of the image !!
im.Image _resizedImage = im.copyResize(
im.decodeImage(File(path).readAsBytesSync()),
width: _h,
height: _w);
// Overwrite existing file with the resized one.
File(path)..writeAsBytesSync(im.encodeJpg(_resizedImage));
return true;
}
}