simple_ocr_plugin 0.1.1 copy "simple_ocr_plugin: ^0.1.1" to clipboard
simple_ocr_plugin: ^0.1.1 copied to clipboard

Plugin to perform OCR on image / photo, backed by Google ML-Kit.

example/lib/main.dart

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;
  }
  
}
15
likes
30
pub points
66%
popularity

Publisher

unverified uploader

Plugin to perform OCR on image / photo, backed by Google ML-Kit.

Repository (GitLab)
View/report issues

License

Apache-2.0 (LICENSE)

Dependencies

flutter

More

Packages that depend on simple_ocr_plugin