Since Dynamsoft Label Recognizer supports Windows, Linux, Android, iOS and Web, we can create a 5 in 1 Flutter plugin for it to cover desktop, mobile and web platform development. In this article, you will see how to write an MRZ Flutter plugin using different platform-specific code, such as Java, Swift, Dart, C++ and JavaScript.
Download flutter_ocr_sdk from Pub.dev
https://pub.dev/packages/flutter_ocr_sdk
Initialize Flutter MRZ Detection Plugin for Multiple Platforms
We scaffold a Flutter plugin project for multiple platforms by running the following command:
flutter create --org com.dynamsoft --template=plugin --platforms=android,ios,windows,linux,web -a java flutter_ocr_sdk
MRZ Model
To implement the package, the first step is to download the Dynamsoft Label Recognizer SDK and add it to the project. From https://www.dynamsoft.com/label-recognition/downloads, you can get shared libraries and model files.
The MRZ model consists of four files: MRZ.caffemodel
, MRZ.json
, MRZ.prototxt
, and MRZ.txt
. We put them in the lib/model
folder of the Flutter project and add the asset path to pubspec.yaml
:
assets:
- lib/model/
Linking MRZ OCR Libraries
The ways of linking third-party libraries in Flutter plugin project are different for multiple platforms.
Windows and Linux
Both Windows and Linux use CMake to build the project. We copy header files and shared libraries to the target platform folder and then configure the CMakeLists.txt
file.
Windows
cmake_minimum_required(VERSION 3.14)
set(PROJECT_NAME "flutter_ocr_sdk")
project(${PROJECT_NAME} LANGUAGES CXX)
set(PLUGIN_NAME "flutter_ocr_sdk_plugin")
link_directories("${PROJECT_SOURCE_DIR}/lib/")
list(APPEND PLUGIN_SOURCES
"flutter_ocr_sdk_plugin.cpp"
"flutter_ocr_sdk_plugin.h"
)
add_library(${PLUGIN_NAME} SHARED
"include/flutter_ocr_sdk/flutter_ocr_sdk_plugin_c_api.h"
"flutter_ocr_sdk_plugin_c_api.cpp"
${PLUGIN_SOURCES}
)
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin "DynamsoftLabelRecognizerx64")
set(flutter_ocr_sdk_bundled_libraries
"${PROJECT_SOURCE_DIR}/bin/"
PARENT_SCOPE
)
Linux
cmake_minimum_required(VERSION 3.10)
set(PROJECT_NAME "flutter_ocr_sdk")
project(${PROJECT_NAME} LANGUAGES CXX)
set(PLUGIN_NAME "flutter_ocr_sdk_plugin")
link_directories("${PROJECT_SOURCE_DIR}/lib/")
add_library(${PLUGIN_NAME} SHARED
"flutter_ocr_sdk_plugin.cc"
)
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter "DynamsoftLabelRecognizer")
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
set(flutter_ocr_sdk_bundled_libraries
""
PARENT_SCOPE
)
Android and iOS
Gradle and CocoaPods are used to build Android and iOS projects. The minimum supported version of Android is 21 and the minimum supported version of iOS is 9.
Configure build.gradle for Android
rootProject.allprojects {
repositories {
maven {
url "https://download2.dynamsoft.com/maven/aar"
}
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 21
}
}
dependencies {
implementation "com.dynamsoft:dynamsoftlabelrecognizer:2.2.20"
}
Configure flutter_ocr_sdk.podspec for iOS
Pod::Spec.new do |s|
s.name = 'flutter_ocr_sdk'
s.version = '1.0.0'
s.summary = 'A wrapper for Dynamsoft OCR SDK, detecting MRZ in passports, travel documents, and ID cards.'
s.description = <<-DESC
A wrapper for Dynamsoft OCR SDK, detecting MRZ in passports, travel documents, and ID cards.
DESC
s.homepage = 'https://github.com/yushulx/flutter_ocr_sdk'
s.license = { :file => '../LICENSE' }
s.author = { 'yushulx' => 'lingxiao1002@gmail.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '9.0'
s.dependency 'DynamsoftLabelRecognizer', '2.2.20'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end
Web
When using the plugin for web, developers need to include the JavaScript library of Dynamsoft Label Recognizer in their index.html
file:
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-label-recognizer@2.2.11/dist/dlr.js"></script>
In the next section, we will write the shared Dart code for all platforms.
Dart Code for MRZ Recognition
The flutter_ocr_sdk_platform_interface.dart
defines the methods to be implemented by each platform.
abstract class FlutterOcrSdkPlatform extends PlatformInterface {
FlutterOcrSdkPlatform() : super(token: _token);
static final Object _token = Object();
static FlutterOcrSdkPlatform _instance = MethodChannelFlutterOcrSdk();
static FlutterOcrSdkPlatform get instance => _instance;
static set instance(FlutterOcrSdkPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<int?> init(String path, String key) {
throw UnimplementedError('init() has not been implemented.');
}
Future<List<List<MrzLine>>?> recognizeByBuffer(
Uint8List bytes, int width, int height, int stride, int format) {
throw UnimplementedError('recognizeByBuffer() has not been implemented.');
}
Future<List<List<MrzLine>>?> recognizeByFile(String filename) {
throw UnimplementedError('recognizeByFile() has not been implemented.');
}
Future<int?> loadModel() async {
throw UnimplementedError('loadModel() has not been implemented.');
}
}
-
init()
: initialize the OCR SDK with the SDK path (Web Only) and the license key of Dynamsoft Label Recognizer. -
loadModel()
: load the MRZ recognition model. -
recognizeByBuffer()
: recognize the MRZ from the image buffer. -
recognizeByFile()
: recognize the MRZ from the image file.
The flutter_ocr_sdk_web.dart
file implements the methods for web, and the flutter_ocr_sdk_method_channel.dart
file implements the methods for Android, iOS, Windows and Linux.
MRZ Detection API for Web
-
In the
web_dlr_manager.dart
file, declare the JavaScript properties and functions to be called:
@JS('Dynamsoft') library dynamsoft; import 'dart:convert'; import 'dart:typed_data'; import 'package:js/js.dart'; import 'mrz_line.dart'; import 'utils.dart'; /// DocumentNormalizer class. @JS('DLR.LabelRecognizer') class LabelRecognizer { external static set license(String license); external static set engineResourcePath(String resourcePath); external static PromiseJsImpl<LabelRecognizer> createInstance(); external PromiseJsImpl<void> updateRuntimeSettingsFromString(String settings); external PromiseJsImpl<List<dynamic>> recognize(dynamic source); external PromiseJsImpl<List<dynamic>> recognizeBuffer( Uint8List buffer, int width, int height, int stride, int format); }
-
Create a
DLRManager
class to interoperate with the JavaScript library:
class DLRManager { LabelRecognizer? _recognizer; Future<int> init(String path, String key) async { LabelRecognizer.engineResourcePath = path; LabelRecognizer.license = key; _recognizer = await handleThenable(LabelRecognizer.createInstance()); return 0; } Future<List<List<MrzLine>>?> recognizeByFile(String file) async { if (_recognizer != null) { List<dynamic> results = await handleThenable(_recognizer!.recognize(file)); return _resultWrapper(results); } return []; } Future<List<List<MrzLine>>?> recognizeByBuffer( Uint8List bytes, int width, int height, int stride, int format) async { if (_recognizer != null) { List<dynamic> results = await handleThenable( _recognizer!.recognizeBuffer(bytes, width, height, stride, format)); return _resultWrapper(results); } return []; } Future<int?> loadModel() async { if (_recognizer != null) { await handleThenable(_recognizer!.updateRuntimeSettingsFromString("MRZ")); } return 0; } List<List<MrzLine>> _resultWrapper(List<dynamic> results) { List<List<MrzLine>> output = []; for (dynamic result in results) { Map value = json.decode(stringify(result)); List<dynamic> area = value['lineResults']; List<MrzLine> lines = []; if (area.length == 2 || area.length == 3) { for (int i = 0; i < area.length; i++) { MrzLine line = MrzLine(); line.text = area[i]['text']; line.x1 = area[i]['location']['points'][0]['x']; line.y1 = area[i]['location']['points'][0]['y']; line.x2 = area[i]['location']['points'][1]['x']; line.y2 = area[i]['location']['points'][1]['y']; line.x3 = area[i]['location']['points'][2]['x']; line.y3 = area[i]['location']['points'][2]['y']; line.x4 = area[i]['location']['points'][3]['x']; line.y4 = area[i]['location']['points'][3]['y']; lines.add(line); } } output.add(lines); } return output; } }
MRZ Detection API for Mobile and Desktop
Comparing to the web, the implementation of loading MRZ model for mobile and desktop is a little bit complicated, because the native APIs are different between mobile and desktop. Thus, the platform check is required in the flutter_ocr_sdk_method_channel.dart
file.
Future<int?> loadModel() async {
final directory = await getApplicationDocumentsDirectory();
String modelPath = 'packages/flutter_ocr_sdk/lib/model/';
bool isDesktop = false;
if (Platform.isWindows || Platform.isLinux) {
isDesktop = true;
}
int? ret = 0;
var fileNames = ["MRZ"];
for (var i = 0; i < fileNames.length; i++) {
var fileName = fileNames[i];
var prototxtName = '$fileName.prototxt';
var prototxtBufferPath = join(modelPath, prototxtName);
ByteData prototxtBuffer = await loadAssetBytes(prototxtBufferPath);
var txtBufferName = '$fileName.txt';
var txtBufferPath = join(modelPath, txtBufferName);
ByteData txtBuffer = await loadAssetBytes(txtBufferPath);
var characterModelName = '$fileName.caffemodel';
var characterModelBufferPath = join(modelPath, characterModelName);
ByteData characterModelBuffer =
await loadAssetBytes(characterModelBufferPath);
if (isDesktop) {
List<int> bytes = prototxtBuffer.buffer.asUint8List();
await File(join(directory.path, prototxtName)).writeAsBytes(bytes);
bytes = txtBuffer.buffer.asUint8List();
await File(join(directory.path, txtBufferName)).writeAsBytes(bytes);
bytes = characterModelBuffer.buffer.asUint8List();
await File(join(directory.path, characterModelName))
.writeAsBytes(bytes);
} else {
loadModelFiles(
fileName,
prototxtBuffer.buffer.asUint8List(),
txtBuffer.buffer.asUint8List(),
characterModelBuffer.buffer.asUint8List());
}
}
var templateName = 'MRZ.json';
var templatePath = join(modelPath, templateName);
String template = await loadAssetString(templatePath);
if (isDesktop) {
var templateMap = json.decode(template);
templateMap['CharacterModelArray'][0]['DirectoryPath'] = directory.path;
ByteData templateBuffer = await loadAssetBytes(templatePath);
List<int> bytes = templateBuffer.buffer.asUint8List();
await File(join(directory.path, templateName)).writeAsBytes(bytes);
await methodChannel.invokeMethod('loadModel',
{'path': directory.path, 'template': json.encode(templateMap)});
} else {
ret = await loadTemplate(template);
}
return ret;
}
On mobile, the model files are loaded directly from the assets folder. Whereas on desktop, the model files are copied to the application documents directory and then loaded from the directory.
The other three methods are similar to the web implementation.
Future<int?> init(String path, String key) async {
return await methodChannel
.invokeMethod<int>('init', {'path': path, 'key': key});
}
Future<List<List<MrzLine>>?> recognizeByBuffer(
Uint8List bytes, int width, int height, int stride, int format) async {
List<dynamic>? results =
await methodChannel.invokeMethod('recognizeByBuffer', {
'bytes': bytes,
'width': width,
'height': height,
'stride': stride,
'format': format,
});
if (results == null || results.isEmpty) return [];
return _resultWrapper(results);
}
Future<List<List<MrzLine>>?> recognizeByFile(String filename) async {
List<dynamic>? results =
await methodChannel.invokeMethod('recognizeByFile', {
'filename': filename,
});
if (results == null || results.isEmpty) return [];
return _resultWrapper(results);
}
List<List<MrzLine>> _resultWrapper(List<dynamic> data) {
List<List<MrzLine>> results = [];
for (List<dynamic> area in data) {
List<MrzLine> lines = [];
if (area.length == 2 || area.length == 3) {
for (int i = 0; i < area.length; i++) {
MrzLine line = MrzLine();
Map<dynamic, dynamic> map = area[i];
line.confidence = map['confidence'];
line.text = map['text'];
line.x1 = map['x1'];
line.y1 = map['y1'];
line.x2 = map['x2'];
line.y2 = map['y2'];
line.x3 = map['x3'];
line.y3 = map['y3'];
line.x4 = map['x4'];
line.y4 = map['y4'];
lines.add(line);
}
}
results.add(lines);
}
return results;
}
MRZ Parser
MRZ parser is used to parse the MRZ text and get the information of the passport. The MRZ parser is implemented in the mrz_parser.dart
file.
class MRZ {
/// Parse two lines of MRZ string.
static MrzResult parseTwoLines(String line1, String line2) {
MrzResult mrzInfo = MrzResult();
String type = line1.substring(0, 1);
RegExp exp = RegExp(r'[I|P|V]');
RegExpMatch? match = exp.firstMatch(type);
if (match == null) {
return mrzInfo;
}
if (type == 'P') {
mrzInfo.type = 'PASSPORT (TD-3)';
} else if (type == 'V') {
if (line1.length == 44) {
mrzInfo.type = 'VISA (MRV-A)';
} else if (line1.length == 36) {
mrzInfo.type = 'VISA (MRV-B)';
}
} else if (type == 'I') {
mrzInfo.type = 'ID CARD (TD-2)';
}
// Get issuing State information
String nation = line1.substring(2, 5);
exp = RegExp(r'[0-9]');
match = exp.firstMatch(nation);
if (match != null) return mrzInfo;
if (nation[nation.length - 1] == '<') {
nation = nation.substring(0, 2);
}
mrzInfo.nationality = nation;
// Get surname information
line1 = line1.substring(5);
int pos = line1.indexOf('<<');
String surName = line1.substring(0, pos);
exp = RegExp(r'[0-9]');
match = exp.firstMatch(surName);
if (match != null) return mrzInfo;
surName = surName.replaceAll('<', ' ');
mrzInfo.surname = surName;
// Get givenname information
String givenName = line1.substring(surName.length + 2);
exp = RegExp(r'[0-9]');
match = exp.firstMatch(givenName);
if (match != null) return mrzInfo;
givenName = givenName.replaceAll('<', ' ');
givenName = givenName.trim();
mrzInfo.givenName = givenName;
// Get passport number information
String passportNumber = '';
passportNumber = line2.substring(0, 9);
passportNumber = passportNumber.replaceAll('<', ' ');
mrzInfo.passportNumber = passportNumber;
// Get Nationality information
String issueCountry = line2.substring(10, 13);
exp = RegExp(r'[0-9]');
match = exp.firstMatch(issueCountry);
if (match != null) return mrzInfo;
if (issueCountry[issueCountry.length - 1] == '<') {
issueCountry = issueCountry.substring(0, 2);
}
mrzInfo.issuingCountry = issueCountry;
// Get date of birth information
String birth = line2.substring(13, 19);
DateTime now = DateTime.now();
int currentYear = now.year;
if (int.parse(birth.substring(0, 2)) > (currentYear % 100)) {
birth = '19$birth';
} else {
birth = '20$birth';
}
birth =
'${birth.substring(0, 4)}/${birth.substring(4, 6)}/${birth.substring(6, 8)}';
mrzInfo.birthDate = birth;
// Get gender information
String gender = line2[20];
exp = RegExp(r'[M|F|x|<]');
match = exp.firstMatch(gender);
if (match == null) return mrzInfo;
mrzInfo.gender = gender;
// Get date of expiry information
String expiry = line2.substring(21, 27);
exp = RegExp(r'[A-Za-z]');
match = exp.firstMatch(expiry);
if (match != null) return mrzInfo;
if (int.parse(expiry.substring(0, 2)) >= 60) {
expiry = '19$expiry';
} else {
expiry = '20$expiry';
}
expiry =
'${expiry.substring(0, 4)}/${expiry.substring(4, 6)}/${expiry.substring(6)}';
mrzInfo.expiration = expiry;
return mrzInfo;
}
/// Parse three lines of MRZ string.
static MrzResult parseThreeLines(String line1, String line2, String line3) {
MrzResult mrzInfo = MrzResult();
String type = line1.substring(0, 1);
RegExp exp = RegExp(r'[I|P|V]');
RegExpMatch? match = exp.firstMatch(type);
if (match == null) {
return mrzInfo;
}
mrzInfo.type = 'ID CARD (TD-1)';
// Get nationality information
String nation = line2.substring(15, 18);
exp = RegExp(r'[0-9]');
match = exp.firstMatch(nation);
if (match != null) return mrzInfo;
nation = nation.replaceAll('<', '');
mrzInfo.nationality = nation;
// Get surname information
int pos = line3.indexOf('<<');
String surName = line3.substring(0, pos);
exp = RegExp(r'[0-9]');
match = exp.firstMatch(surName);
if (match != null) return mrzInfo;
surName = surName.replaceAll('<', ' ');
surName.trim();
mrzInfo.surname = surName;
// Get givenname information
String givenName = line3.substring(surName.length + 2);
exp = RegExp(r'[0-9]');
match = exp.firstMatch(givenName);
if (match != null) return mrzInfo;
givenName = givenName.replaceAll('<', ' ');
givenName = givenName.trim();
mrzInfo.givenName = givenName;
// Get passport number information
String passportNumber = '';
passportNumber = line1.substring(5, 14);
passportNumber = passportNumber.replaceAll('<', ' ');
mrzInfo.passportNumber = passportNumber;
// Get issuing country or organization information
String issueCountry = line1.substring(2, 5);
exp = RegExp(r'[0-9]');
match = exp.firstMatch(issueCountry);
if (match != null) return mrzInfo;
issueCountry = issueCountry.replaceAll('<', '');
mrzInfo.issuingCountry = issueCountry;
// Get date of birth information
String birth = line2.substring(0, 6);
exp = RegExp(r'[A-Za-z]');
match = exp.firstMatch(birth);
if (match != null) return mrzInfo;
DateTime now = DateTime.now();
int currentYear = now.year;
if (int.parse(birth.substring(0, 2)) > (currentYear % 100)) {
birth = '19$birth';
} else {
birth = '20$birth';
}
birth =
'${birth.substring(0, 4)}/${birth.substring(4, 6)}/${birth.substring(6, 8)}';
mrzInfo.birthDate = birth;
// Get gender information
String gender = line2[7];
exp = RegExp(r'[M|F|x|<]');
match = exp.firstMatch(gender);
if (match == null) return mrzInfo;
gender = gender.replaceAll('<', 'X');
mrzInfo.gender = gender;
// Get date of expiry information
String expiry = '20$line2.substring(8, 14)';
exp = RegExp(r'[A-Za-z]');
match = exp.firstMatch(expiry);
if (match != null) return mrzInfo;
expiry =
'${expiry.substring(0, 4)}/${expiry.substring(4, 6)}/${expiry.substring(6)}';
mrzInfo.expiration = expiry;
return mrzInfo;
}
}
Platform-specific Code of MRZ Recognition for Android, iOS, Windows and Linux
In the above section, we have implemented the MRZ recognition logic in Dart. For web, the Dart code can be directly used. Now, we will write Java, Swift and C++ code for Android, iOS, Windows, and Linux.
Get Function Names and Arguments via Method Channel
Android
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "init": {
final String license = call.argument("key");
break;
}
case "recognizeByFile": {
final String filename = call.argument("filename");
final Result r = result;
}
break;
case "recognizeByBuffer": {
final byte[] bytes = call.argument("bytes");
final int width = call.argument("width");
final int height = call.argument("height");
final int stride = call.argument("stride");
final int format = call.argument("format");
final Result r = result;
}
break;
case "loadModelFiles": {
final String name = call.argument("name");
final byte[] prototxtBuffer = call.argument("prototxtBuffer");
final byte[] txtBuffer = call.argument("txtBuffer");
final byte[] characterModelBuffer = call.argument("characterModelBuffer");
}
break;
case "loadTemplate": {
final String template = call.argument("template");
}
break;
default:
result.notImplemented();
}
}
iOS
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let arguments: NSDictionary = call.arguments as! NSDictionary
switch call.method {
case "init":
completionHandlers.append(result)
let license: String = arguments.value(forKey: "key") as! String
case "loadModelFiles":
let name: String = arguments.value(forKey: "name") as! String
let prototxtBuffer: FlutterStandardTypedData = arguments.value(forKey: "prototxtBuffer") as! FlutterStandardTypedData
let txtBuffer: FlutterStandardTypedData = arguments.value(forKey: "txtBuffer") as! FlutterStandardTypedData
let characterModelBuffer: FlutterStandardTypedData = arguments.value(forKey: "characterModelBuffer") as! FlutterStandardTypedData
case "loadTemplate":
if self.recognizer == nil {
result(.none)
return
}
let params: String = arguments.value(forKey: "template") as! String
case "recognizeByFile":
if recognizer == nil {
result(.none)
return
}
DispatchQueue.global().async {
let filename: String = arguments.value(forKey: "filename") as! String
}
case "recognizeByBuffer":
if self.recognizer == nil {
result(.none)
return
}
DispatchQueue.global().async {
let buffer: FlutterStandardTypedData = arguments.value(forKey: "bytes") as! FlutterStandardTypedData
let width: Int = arguments.value(forKey: "width") as! Int
let height: Int = arguments.value(forKey: "height") as! Int
let stride: Int = arguments.value(forKey: "stride") as! Int
let format: Int = arguments.value(forKey: "format") as! Int
let enumImagePixelFormat = EnumImagePixelFormat(rawValue: format)
}
default:
result(.none)
}
}
Windows
void FlutterOcrSdkPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
const auto *arguments = std::get_if<EncodableMap>(method_call.arguments());
if (method_call.method_name().compare("init") == 0)
{
std::string license;
int ret = 0;
if (arguments)
{
auto license_it = arguments->find(EncodableValue("key"));
if (license_it != arguments->end())
{
license = std::get<std::string>(license_it->second);
}
}
}
else if (method_call.method_name().compare("loadModel") == 0)
{
std::string path, params;
int ret = 0;
if (arguments)
{
auto path_it = arguments->find(EncodableValue("path"));
if (path_it != arguments->end())
{
path = std::get<std::string>(path_it->second);
}
auto params_it = arguments->find(EncodableValue("template"));
if (params_it != arguments->end())
{
params = std::get<std::string>(params_it->second);
}
}
}
else if (method_call.method_name().compare("recognizeByFile") == 0)
{
std::string filename;
EncodableList results;
if (arguments)
{
auto filename_it = arguments->find(EncodableValue("filename"));
if (filename_it != arguments->end())
{
filename = std::get<std::string>(filename_it->second);
}
}
}
else if (method_call.method_name().compare("recognizeByBuffer") == 0)
{
EncodableList results;
std::vector<unsigned char> bytes;
int width = 0, height = 0, stride = 0, format = 0;
if (arguments)
{
auto bytes_it = arguments->find(EncodableValue("bytes"));
if (bytes_it != arguments->end())
{
bytes = std::get<vector<unsigned char>>(bytes_it->second);
}
auto width_it = arguments->find(EncodableValue("width"));
if (width_it != arguments->end())
{
width = std::get<int>(width_it->second);
}
auto height_it = arguments->find(EncodableValue("height"));
if (height_it != arguments->end())
{
height = std::get<int>(height_it->second);
}
auto stride_it = arguments->find(EncodableValue("stride"));
if (stride_it != arguments->end())
{
stride = std::get<int>(stride_it->second);
}
auto format_it = arguments->find(EncodableValue("format"));
if (format_it != arguments->end())
{
format = std::get<int>(format_it->second);
}
}
}
else
{
result->NotImplemented();
}
}
Linux
static void flutter_ocr_sdk_plugin_handle_method_call(
FlutterOcrSdkPlugin* self,
FlMethodCall* method_call) {
g_autoptr(FlMethodResponse) response = nullptr;
const gchar* method = fl_method_call_get_name(method_call);
FlValue* args = fl_method_call_get_args(method_call);
if (strcmp(method, "init") == 0)
{
if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP)
{
return;
}
FlValue *value = fl_value_lookup_string(args, "key");
if (value == nullptr)
{
return;
}
const char *license = fl_value_get_string(value);
}
else if (strcmp(method, "loadModel") == 0)
{
if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP)
{
return;
}
FlValue *value = fl_value_lookup_string(args, "path");
if (value == nullptr)
{
return;
}
const char *path = fl_value_get_string(value);
value = fl_value_lookup_string(args, "template");
if (value == nullptr)
{
return;
}
const char * params = fl_value_get_string(value);
int ret = self->manager->LoadModel(path, params);
}
else if (strcmp(method, "recognizeByFile") == 0)
{
if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP)
{
return;
}
FlValue *value = fl_value_lookup_string(args, "filename");
if (value == nullptr)
{
return;
}
const char *filename = fl_value_get_string(value);
}
else if (strcmp(method, "recognizeByBuffer") == 0)
{
if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) {
return;
}
FlValue* value = fl_value_lookup_string(args, "bytes");
if (value == nullptr) {
return;
}
unsigned char* bytes = (unsigned char*)fl_value_get_uint8_list(value);
value = fl_value_lookup_string(args, "width");
if (value == nullptr) {
return;
}
int width = fl_value_get_int(value);
value = fl_value_lookup_string(args, "height");
if (value == nullptr) {
return;
}
int height = fl_value_get_int(value);
value = fl_value_lookup_string(args, "stride");
if (value == nullptr) {
return;
}
int stride = fl_value_get_int(value);
value = fl_value_lookup_string(args, "format");
if (value == nullptr) {
return;
}
int format = fl_value_get_int(value);
}
else {
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
fl_method_call_respond(method_call, response, nullptr);
}
The Init() Method
Android
LicenseManager.initLicense(
license, context,
new LicenseVerificationListener() {
@Override
public void licenseVerificationCallback(boolean isSuccessful, CoreException e) {
if (isSuccessful)
{
result.success(0);
}
else {
result.success(-1);
}
}
});
iOS
DynamsoftLicenseManager.initLicense(license, verificationDelegate: self)
public func licenseVerificationCallback(_ isSuccess: Bool, error: Error?) {
if isSuccess {
completionHandlers.first?(0)
} else{
completionHandlers.first?(-1)
}
}
Windows
char errorMsgBuffer[512];
int ret = DLR_InitLicense(license, errorMsgBuffer, 512);
recognizer = DLR_CreateInstance();
Linux
char errorMsgBuffer[512];
int ret = DLR_InitLicense(license, errorMsgBuffer, 512);
recognizer = DLR_CreateInstance();
The LoadModel() Method
Android
try {
mLabelRecognizer.appendCharacterModelBuffer(name, prototxtBuffer, txtBuffer, characterModelBuffer);
mLabelRecognizer.initRuntimeSettings(content);
} catch (Exception e) {
e.printStackTrace();
}
iOS
DynamsoftLabelRecognizer.appendCharacterModel(name, prototxtBuffer: prototxtBuffer.data, txtBuffer: txtBuffer.data, characterModelBuffer: characterModelBuffer.data)
try? self.recognizer!.initRuntimeSettings(params)
Windows
char errorMessage[256];
int ret = DLR_AppendSettingsFromString(recognizer, params, errorMessage, 256);
Linux
char errorMessage[256];
int ret = DLR_AppendSettingsFromString(recognizer, params, errorMessage, 256);
The RecognizeByFile() Method
Android
DLRResult[] results = null;
try {
results = mLabelRecognizer.recognizeFile(fileName);
} catch (Exception e) {
Log.e(TAG, e.toString());
}
iOS
let res = try? self.recognizer!.recognizeFile(filename)
Windows
int ret = DLR_RecognizeByFile(recognizer, filename, "locr");
Linux
int ret = DLR_RecognizeByFile(recognizer, filename, "locr");
The RecognizeByBuffer() Method
Android
DLRResult[] results = null;
ImageData data = new ImageData();
data.bytes = bytes;
data.width = width;
data.height = height;
data.stride = stride;
data.format = format;
try {
results = mLabelRecognizer.recognizeBuffer(data);
} catch (Exception e) {
Log.e(TAG, e.toString());
}
iOS
let imageData = iImageData.init()
imageData.bytes = buffer.data
imageData.width = width
imageData.height = height
imageData.stride = stride
imageData.format = enumImagePixelFormat!
let res = try? self.recognizer!.recognizeBuffer(imageData)
Windows
ImageData data;
data.bytes = buffer;
data.width = width;
data.height = height;
data.stride = stride;
data.format = pixelFormat;
data.bytesLength = length;
int ret = DLR_RecognizeByBuffer(recognizer, &data, "locr");
Linux
ImageData data;
data.bytes = buffer;
data.width = width;
data.height = height;
data.stride = stride;
data.format = pixelFormat;
data.bytesLength = length;
int ret = DLR_RecognizeByBuffer(recognizer, &data, "locr");
Building a Flutter App to Recognize Passport MRZ
-
Add
image_picker
,file_selector
andflutter_ocr_sdk
topubspec.yaml
. Bothimage_picker
andfile_selector
are used to select an image file from the local file system, but they are implemented for different platforms.
dependencies: ... flutter_ocr_sdk: image_picker: file_selector:
-
Create a stateful widget:
import 'dart:typed_data'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'dart:async'; import 'dart:io'; import 'package:flutter_ocr_sdk/flutter_ocr_sdk.dart'; import 'package:flutter_ocr_sdk/flutter_ocr_sdk_platform_interface.dart'; import 'package:flutter_ocr_sdk/mrz_line.dart'; import 'package:flutter_ocr_sdk/mrz_parser.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'dart:ui' as ui; Future<void> main() async { runApp( MaterialApp( title: 'MRZ OCR', home: Scaffold( appBar: AppBar( title: const Text("MRZ OCR"), ), body: MRZApp(), ), ), ); } class MRZApp extends StatefulWidget { @override MobileState createState() => MobileState(); }
-
Initialize the MRZ detection SDK:
class MobileState extends State<MRZApp> { late FlutterOcrSdk _mrzDetector; final picker = ImagePicker(); @override void initState() { super.initState(); initSDK(); } Future<void> initSDK() async { _mrzDetector = FlutterOcrSdk(); int? ret = await _mrzDetector.init( "https://cdn.jsdelivr.net/npm/dynamsoft-label-recognizer@2.2.11/dist/", "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="); await _mrzDetector.loadModel(); } }
-
Build the UI:
Widget build(BuildContext context) { double width = MediaQuery.of(context).size.width; double height = MediaQuery.of(context).size.height; double left = 5; double mrzHeight = 50; double mrzWidth = width - left * 2; return Scaffold( body: Stack(children: [ Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ MaterialButton( textColor: Colors.white, color: Colors.blue, onPressed: () async { showDialog( context: context, builder: (BuildContext context) { return const Center( child: CircularProgressIndicator(), ); }); pictureScan('gallery'); }, child: const Text('Pick gallery image'), ), MaterialButton( textColor: Colors.white, color: Colors.blue, onPressed: () async { showDialog( context: context, builder: (BuildContext context) { return const Center( child: CircularProgressIndicator(), ); }); pictureScan('camera'); }, child: const Text('Pick camera image'), ), ]), ) ]), ); }
-
Press the button to load an image file from the local file system and then recognize MRZ from the image:
void pictureScan(String source) async { XFile? photo; if (kIsWeb || Platform.isAndroid || Platform.isIOS) { if (source == 'camera') { photo = await picker.pickImage(source: ImageSource.camera); } else { photo = await picker.pickImage(source: ImageSource.gallery); } } else if (Platform.isWindows || Platform.isLinux) { const XTypeGroup typeGroup = XTypeGroup( label: 'images', extensions: <String>['jpg', 'png', 'bmp', 'tiff', 'pdf', 'gif'], ); photo = await openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]); } if (photo == null) { if (!mounted) return; Navigator.pop(context); return; } String information = 'No results'; List<List<MrzLine>>? results = await _mrzDetector.recognizeByFile(photo.path); if (results != null && results.isNotEmpty) { for (List<MrzLine> area in results) { if (area.length == 2) { information = MRZ.parseTwoLines(area[0].text, area[1].text).toString(); } else if (area.length == 3) { information = MRZ .parseThreeLines(area[0].text, area[1].text, area[2].text) .toString(); } } } if (!mounted) return; Navigator.pop(context); Navigator.push( context, MaterialPageRoute( builder: (context) => DisplayPictureScreen( imagePath: photo!.path, mrzInformation: information), ), ); }
-
Display the MRZ information on a stateless widget:
Image getImage(String imagePath) { if (kIsWeb) { return Image.network(imagePath); } else { return Image.file( File(imagePath), fit: BoxFit.contain, height: double.infinity, width: double.infinity, alignment: Alignment.center, ); } } class DisplayPictureScreen extends StatelessWidget { final String imagePath; final String mrzInformation; const DisplayPictureScreen( {Key? key, required this.imagePath, required this.mrzInformation}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('MRZ OCR')), body: Stack( alignment: const Alignment(0.0, 0.0), children: [ getImage(imagePath), Container( decoration: const BoxDecoration( color: Colors.black45, ), child: Text( mrzInformation, style: const TextStyle( fontSize: 14, color: Colors.white, ), ), ), ], ), ); } }
-
Run the app on web, Windows, Linux, Android and iOS platforms.
Web
Windows and Linux
Android and iOS