From dc3ce8b4ac035e6d8588ece2b26186b196e71db2 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Fri, 9 Aug 2024 13:06:21 +0200 Subject: [PATCH 1/3] improve mrz reader --- .../BarcodeScannerController.kt | 99 +++++++++++++++-- .../barcode_scanner/util/MrzUtil.kt | 6 +- example/lib/main.dart | 101 ++++++++++++++---- example/pubspec.lock | 30 +++--- lib/barcode_scanner.widget.dart | 32 ++++-- 5 files changed, 214 insertions(+), 54 deletions(-) diff --git a/android/src/main/kotlin/be/freedelity/barcode_scanner/BarcodeScannerController.kt b/android/src/main/kotlin/be/freedelity/barcode_scanner/BarcodeScannerController.kt index 7d3daed..ded237f 100644 --- a/android/src/main/kotlin/be/freedelity/barcode_scanner/BarcodeScannerController.kt +++ b/android/src/main/kotlin/be/freedelity/barcode_scanner/BarcodeScannerController.kt @@ -7,6 +7,8 @@ import BarcodeFormats import android.annotation.SuppressLint import android.app.Activity import android.content.Context +import android.graphics.Bitmap +import android.graphics.Point import android.graphics.Rect import android.hardware.camera2.CameraAccessException import android.util.Log @@ -33,9 +35,11 @@ import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.runBlocking +import java.io.ByteArrayOutputStream import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit + class BarcodeScannerController(private val activity: Activity, messenger: BinaryMessenger, methodChannelName: String, scanEventChannelName: String) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler { private var methodChannel: MethodChannel = MethodChannel(messenger, methodChannelName) @@ -56,6 +60,7 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary private var scanSucceedTimestamp: Long = System.currentTimeMillis() private var mrzResult: MutableList? = null + private var mrzBitmap: Bitmap? = null init { methodChannel.setMethodCallHandler(this) @@ -192,12 +197,16 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary var textRecognizer: TextRecognizer? = null if (cameraParams?.get("scanner_type") == "mrz" || cameraParams?.get("scanner_type") == "text") { - Log.i("native_scanner", "Start for MRZ scanner") + textRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) + if (cameraParams?.get("scanner_type") == "mrz") { + Log.i("native_scanner", "Start for MRZ scanner") mrzResult = mutableListOf() } + } else { + Log.i("native_scanner", "Start for barcode scanner") val options = BarcodeScannerOptions.Builder().setBarcodeFormats( Barcode.FORMAT_CODE_39, @@ -213,6 +222,7 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary Barcode.FORMAT_QR_CODE ).build() barcodeScanner = BarcodeScanning.getClient(options) + } imageAnalysis.setAnalyzer(executor) { imageProxy -> @@ -316,8 +326,9 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary val convertImageToBitmap = BarcodeScannerUtil.convertToBitmap(mediaImage) val cropRect = Rect(0, 0, imageWidth, imageHeight) - val heightCropPercent = 60 + val heightCropPercent = 50 val widthCropPercent = 1 + val (widthCrop, heightCrop) = when (rotationDegrees) { 90, 270 -> Pair(heightCropPercent / 100f, widthCropPercent / 100f) else -> Pair(widthCropPercent / 100f, heightCropPercent / 100f) @@ -327,9 +338,10 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary (imageWidth * widthCrop / 2).toInt(), (imageHeight * heightCrop / 2).toInt() ) - val croppedBitmap = BarcodeScannerUtil.rotateAndCrop(convertImageToBitmap, rotationDegrees, cropRect) - InputImage.fromBitmap(croppedBitmap, 0) + mrzBitmap = BarcodeScannerUtil.rotateAndCrop(convertImageToBitmap, rotationDegrees, cropRect) + + InputImage.fromBitmap(mrzBitmap!!, 0) } else { InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) } @@ -369,16 +381,83 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary if (cameraParams?.get("scanner_type") == "mrz") { + Log.i("scanner_native", "#####################################################################################################") + val mrz: String? = MrzUtil.extractMRZ(visionText.textBlocks, mrzResult!!) + if (mrz != null) { - eventSink?.success(mapOf( - "mrz" to mrz, - )) + var points: Array? = null + + visionText.textBlocks.forEach { + + if (points == null) { + + var bitmapWithoutMrz = true + + if (it.lines.size >= 3) { + + var countMrzCommonChar = 0 + + it.lines.forEach { line -> + if (line.text.contains("<<")) { + countMrzCommonChar++ + } + } + + if (countMrzCommonChar >= 2) { + bitmapWithoutMrz = false + } + + } + + if (!bitmapWithoutMrz) { + points = it.cornerPoints + } + + } + + } + + if (points?.isNotEmpty() == true) { + + var x = points!![0].x + var y = points!![0].y + var width = points!![1].x - points!![0].x + var height = points!![2].y - points!![0].y + + if (width > mrzBitmap!!.width) { + x = 0 + width = mrzBitmap!!.width + } + if (height > mrzBitmap!!.height) { + y = 0 + height = mrzBitmap!!.height + } + + val croppedBitmap = Bitmap.createBitmap( + mrzBitmap!!, + x, + y, + width, + height, + ) + + val stream = ByteArrayOutputStream() + croppedBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + val mrzByteArray = stream.toByteArray() + + eventSink?.success(mapOf( + "mrz" to mrz, + "img" to mrzByteArray, + )) + + mrzResult!!.clear() + imageProxy.image?.close() + imageProxy.close() + + } - mrzResult!!.clear() - imageProxy.image?.close() - imageProxy.close() } } else { diff --git a/android/src/main/kotlin/be/freedelity/barcode_scanner/util/MrzUtil.kt b/android/src/main/kotlin/be/freedelity/barcode_scanner/util/MrzUtil.kt index bf76b06..ccaf139 100644 --- a/android/src/main/kotlin/be/freedelity/barcode_scanner/util/MrzUtil.kt +++ b/android/src/main/kotlin/be/freedelity/barcode_scanner/util/MrzUtil.kt @@ -105,7 +105,11 @@ object MrzUtil { } if (missed.isNotEmpty()) { - missed.forEach { mrzResult.removeAt(it) } + missed.forEach { + if (it < mrzResult.size) { + mrzResult.removeAt(it) + } + } } else { var result = "${map[0]}\n${map[1]}" if (mrzResult.size == 3) result += "\n${map[2]}" diff --git a/example/lib/main.dart b/example/lib/main.dart index 1e9546a..e575a9d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,9 +3,11 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:native_barcode_scanner/barcode_scanner.dart'; void main() async { @@ -26,7 +28,7 @@ class MyApp extends StatefulWidget { State createState() => _MyAppState(); } -enum CameraActions { flipCamera, toggleFlashlight, stopScanner, startScanner, setOverlay, navigate } +enum CameraActions { flipCamera, toggleFlashlight, stopScanner, startScanner, setOverlay, navigate, mrz, barcode, text } class _MyAppState extends State { @override @@ -42,23 +44,27 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { return MaterialApp(home: Builder(builder: (builderContext) { - return MyDemoApp(); + return const MyDemoApp(); })); } } class MyDemoApp extends StatefulWidget { + + const MyDemoApp({super.key}); + @override State createState() => _MyDemoAppState(); } class _MyDemoAppState extends State { bool withOverlay = true; + ScannerType scannerType = ScannerType.mrz; @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Scanner plugin example app'), actions: [ + appBar: AppBar(title: Text('Scanner ${scannerType.name} example'), actions: [ PopupMenuButton( onSelected: (CameraActions result) { switch (result) { @@ -74,6 +80,15 @@ class _MyDemoAppState extends State { case CameraActions.startScanner: BarcodeScanner.startScanner(); break; + case CameraActions.mrz: + setState(() => scannerType = ScannerType.mrz); + break; + case CameraActions.barcode: + setState(() => scannerType = ScannerType.barcode); + break; + case CameraActions.text: + setState(() => scannerType = ScannerType.text); + break; case CameraActions.setOverlay: setState(() => withOverlay = !withOverlay); break; @@ -84,20 +99,24 @@ class _MyDemoAppState extends State { }, itemBuilder: (BuildContext context) => >[ const PopupMenuItem( - value: CameraActions.flipCamera, - child: Text('Flip camera'), - ), - const PopupMenuItem( - value: CameraActions.toggleFlashlight, - child: Text('Toggle flashlight'), + value: CameraActions.startScanner, + child: Text('Start scanner'), ), const PopupMenuItem( value: CameraActions.stopScanner, child: Text('Stop scanner'), ), + ...List.generate(ScannerType.values.length, (index) => PopupMenuItem( + value: ScannerType.values[index] == ScannerType.mrz ? CameraActions.mrz : ScannerType.values[index] == ScannerType.text ? CameraActions.text: CameraActions.barcode, + child: Text('Type ${ScannerType.values[index].name}'), + )), const PopupMenuItem( - value: CameraActions.startScanner, - child: Text('Start scanner'), + value: CameraActions.flipCamera, + child: Text('Flip camera'), + ), + const PopupMenuItem( + value: CameraActions.toggleFlashlight, + child: Text('Toggle flashlight'), ), PopupMenuItem( value: CameraActions.setOverlay, @@ -112,7 +131,7 @@ class _MyDemoAppState extends State { ]), body: Builder(builder: (builderContext) { Widget child = BarcodeScannerWidget( - scannerType: ScannerType.barcode, + scannerType: scannerType, onBarcodeDetected: (barcode) async { await showDialog( context: builderContext, @@ -128,13 +147,53 @@ class _MyDemoAppState extends State { }, onTextDetected: (String text) async { await showDialog( - context: builderContext, - builder: (dialogContext) { - return Align( - alignment: Alignment.center, - child: Card( - margin: const EdgeInsets.all(24), child: Container(padding: const EdgeInsets.all(16), child: Column(mainAxisSize: MainAxisSize.min, children: [Text('text : \n$text'), ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog'))])))); - }); + context: builderContext, + builder: (dialogContext) { + return Align( + alignment: Alignment.center, + child: Card( + margin: const EdgeInsets.all(24), + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('text : \n$text'), + ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog')), + ], + ), + ), + ), + ); + }, + ); + }, + onMrzDetected: (String text, Uint8List bytes) { + showDialog( + context: builderContext, + builder: (dialogContext) { + return Align( + alignment: Alignment.center, + child: Card( + margin: const EdgeInsets.all(24), + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(text), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Image.memory(bytes), + ), + ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog')), + ], + ), + ), + ), + ); + }, + ); }, onError: (dynamic error) { debugPrint('$error'); @@ -158,7 +217,7 @@ class _MyDemoAppState extends State { top: 16, right: 16, child: ElevatedButton( - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.purple), foregroundColor: MaterialStateProperty.all(Colors.white), shape: MaterialStateProperty.all(const CircleBorder()), padding: MaterialStateProperty.all(const EdgeInsets.all(8))), + style: const ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.purple), foregroundColor: WidgetStatePropertyAll(Colors.white), shape: WidgetStatePropertyAll(CircleBorder()), padding: WidgetStatePropertyAll(EdgeInsets.all(8))), onPressed: () { ScaffoldMessenger.of(builderContext).showSnackBar(const SnackBar(content: Text('Icon button pressed'))); }, @@ -182,7 +241,7 @@ class _MyDemoAppState extends State { const Text('Press back button'), ElevatedButton( onPressed: () { - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MyDemoApp())); + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => const MyDemoApp())); }, child: const Text('Back')) ], diff --git a/example/pubspec.lock b/example/pubspec.lock index 3ecb23d..c686535 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -79,26 +79,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -127,17 +127,17 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" native_barcode_scanner: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.0.9" + version: "1.0.10" path: dependency: transitive description: @@ -195,10 +195,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" vector_math: dependency: transitive description: @@ -211,10 +211,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/barcode_scanner.widget.dart b/lib/barcode_scanner.widget.dart index 0d8dfc2..3703f1f 100644 --- a/lib/barcode_scanner.widget.dart +++ b/lib/barcode_scanner.widget.dart @@ -35,10 +35,21 @@ class BarcodeScannerWidget extends StatefulWidget { /// This function will be called when a bloc of text or a MRZ is detected. final Function(String textResult)? onTextDetected; + final Function(String textResult, Uint8List image)? onMrzDetected; + final Function(dynamic error) onError; const BarcodeScannerWidget( - {Key? key, this.cameraSelector = CameraSelector.back, this.startScanning = true, this.stopScanOnBarcodeDetected = true, this.orientation = CameraOrientation.portrait, this.scannerType = ScannerType.barcode, this.onBarcodeDetected, this.onTextDetected, required this.onError}) + {Key? key, this.cameraSelector = CameraSelector.back, + this.startScanning = true, + this.stopScanOnBarcodeDetected = true, + this.orientation = CameraOrientation.portrait, + this.scannerType = ScannerType.barcode, + this.onBarcodeDetected, + this.onTextDetected, + this.onMrzDetected, + required this.onError, + }) : assert(onBarcodeDetected != null || onTextDetected != null), super(key: key); @@ -67,7 +78,7 @@ class _BarcodeScannerWidgetState extends State { eventSubscription = eventChannel.receiveBroadcastStream().listen((dynamic event) async { if (widget.onBarcodeDetected != null && widget.scannerType == ScannerType.barcode) { final format = BarcodeFormat.unserialize(event['format']); - if (format != null) { + if (format != null && event['barcode'] != null) { await BarcodeScanner.stopScanner(); await widget.onBarcodeDetected!(Barcode(format: format, value: event['barcode'] as String)); @@ -75,13 +86,20 @@ class _BarcodeScannerWidgetState extends State { if (!widget.stopScanOnBarcodeDetected) { BarcodeScanner.startScanner(); } - } - } else if (widget.onTextDetected != null && widget.scannerType != ScannerType.barcode) { - if (widget.scannerType == ScannerType.mrz) { - await BarcodeScanner.stopScanner(); - await widget.onTextDetected!(event['mrz'] as String); } else { + widget.onError(const FormatException('Barcode not found')); + } + } else if (widget.onTextDetected != null && widget.scannerType == ScannerType.text) { + if (event['text'] != null) { await widget.onTextDetected!(event['text'] as String); + } else { + widget.onError(const FormatException('Text not found')); + } + } else if (widget.onMrzDetected != null && widget.scannerType == ScannerType.mrz) { + if (event['mrz'] != null && event["img"] != null) { + await widget.onMrzDetected!(event['mrz'] as String, Uint8List.fromList(event["img"])); + } else { + widget.onError(const FormatException('MRZ not found')); } } }, onError: (dynamic error) { From 8e864356bdbe648d7b7a89ed6800bc3c0b8a57e8 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Fri, 9 Aug 2024 13:33:39 +0200 Subject: [PATCH 2/3] v1.0.11 --- CHANGELOG.md | 3 +++ README.md | 24 +++++++++++++++++++----- example/lib/main.dart | 20 ++++---------------- ios/native_barcode_scanner.podspec | 2 +- lib/barcode_scanner.widget.dart | 2 +- pubspec.yaml | 2 +- 6 files changed, 29 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfea37a..23b162c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.0.11 +- Improve MRZ reader with text and image callback + ## 1.0.10 - Kotlin DSL for example app - Bug fix : Fix Android supported version to 1.8 diff --git a/README.md b/README.md index 7209efe..6a6005e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# native_barcode_scanner +___# native_barcode_scanner A fast flutter plugin to scan barcodes and QR codes using the device camera. This plugin also supports text and MRZ recognition from the camera. @@ -46,7 +46,7 @@ Add this to your package's `pubspec.yaml` file: ```yaml dependencies: - native_barcode_scanner: ^1.0.10 + native_barcode_scanner: ^1.0.11 ``` ## Usage @@ -61,13 +61,27 @@ Then, create a `BarcodeScannerWidget` in your widget tree where you want to show ```dart @override - Widget build(BuildContext context) { - return BarcodeScannerWidget( +Widget build(BuildContext context) { + return BarcodeScannerWidget( onBarcodeDetected: (barcode) { print('Barcode detected: ${barcode.value} (format: ${barcode.format.name})'); } + ); +} +``` + +Depending on what you want to scan, change the `scannerType` which is default to `ScannerType.barcode` and use the associated callback: + +```dart +@override + Widget build(BuildContext context) { + return BarcodeScannerWidget( + scannerType: ScannerType.mrz, + onMrzDetected: (String mrz, Uint8List bytes) { + print('MRZ detected: $mrz'); + } ); } ``` -If you need to manipulate the behaviour of the barcode scanning process, you may use the static methods of the `BarcodeScanner` class. +If you need to manipulate the behaviour of the barcode scanning process, you may use the static methods of the `BarcodeScanner` class.___ diff --git a/example/lib/main.dart b/example/lib/main.dart index e575a9d..20af90f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -28,7 +28,7 @@ class MyApp extends StatefulWidget { State createState() => _MyAppState(); } -enum CameraActions { flipCamera, toggleFlashlight, stopScanner, startScanner, setOverlay, navigate, mrz, barcode, text } +enum CameraActions { flipCamera, toggleFlashlight, stopScanner, startScanner, setOverlay, navigate } class _MyAppState extends State { @override @@ -58,8 +58,9 @@ class MyDemoApp extends StatefulWidget { } class _MyDemoAppState extends State { + bool withOverlay = true; - ScannerType scannerType = ScannerType.mrz; + ScannerType scannerType = ScannerType.barcode; @override Widget build(BuildContext context) { @@ -80,15 +81,6 @@ class _MyDemoAppState extends State { case CameraActions.startScanner: BarcodeScanner.startScanner(); break; - case CameraActions.mrz: - setState(() => scannerType = ScannerType.mrz); - break; - case CameraActions.barcode: - setState(() => scannerType = ScannerType.barcode); - break; - case CameraActions.text: - setState(() => scannerType = ScannerType.text); - break; case CameraActions.setOverlay: setState(() => withOverlay = !withOverlay); break; @@ -106,10 +98,6 @@ class _MyDemoAppState extends State { value: CameraActions.stopScanner, child: Text('Stop scanner'), ), - ...List.generate(ScannerType.values.length, (index) => PopupMenuItem( - value: ScannerType.values[index] == ScannerType.mrz ? CameraActions.mrz : ScannerType.values[index] == ScannerType.text ? CameraActions.text: CameraActions.barcode, - child: Text('Type ${ScannerType.values[index].name}'), - )), const PopupMenuItem( value: CameraActions.flipCamera, child: Text('Flip camera'), @@ -131,7 +119,7 @@ class _MyDemoAppState extends State { ]), body: Builder(builder: (builderContext) { Widget child = BarcodeScannerWidget( - scannerType: scannerType, + scannerType: ScannerType.mrz, onBarcodeDetected: (barcode) async { await showDialog( context: builderContext, diff --git a/ios/native_barcode_scanner.podspec b/ios/native_barcode_scanner.podspec index 4bed3ed..11160d6 100644 --- a/ios/native_barcode_scanner.podspec +++ b/ios/native_barcode_scanner.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'native_barcode_scanner' - s.version = '1.0.10' + s.version = '1.0.11' s.summary = 'Barcode scanner plugin' s.description = <<-DESC Barcode scanner plugin diff --git a/lib/barcode_scanner.widget.dart b/lib/barcode_scanner.widget.dart index 3703f1f..9edc9ea 100644 --- a/lib/barcode_scanner.widget.dart +++ b/lib/barcode_scanner.widget.dart @@ -35,7 +35,7 @@ class BarcodeScannerWidget extends StatefulWidget { /// This function will be called when a bloc of text or a MRZ is detected. final Function(String textResult)? onTextDetected; - final Function(String textResult, Uint8List image)? onMrzDetected; + final Function(String mrz, Uint8List image)? onMrzDetected; final Function(dynamic error) onError; diff --git a/pubspec.yaml b/pubspec.yaml index 079e765..95054fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: native_barcode_scanner description: Fast barcode/QR scanner plugin using PlatformView and processing on native side -version: 1.0.10 +version: 1.0.11 repository: https://github.com/freedelity/flutter_native_barcode_scanner environment: From 3f12eeb14ed48efd75351f04e21dca7e92e3ec30 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Fri, 9 Aug 2024 15:20:35 +0200 Subject: [PATCH 3/3] add progress feedback cause mrz take sometimes to much time --- .../BarcodeScannerController.kt | 27 ++- .../barcode_scanner/util/MrzUtil.kt | 9 +- example/lib/main.dart | 171 ++++++++++-------- lib/barcode_scanner.widget.dart | 26 ++- 4 files changed, 151 insertions(+), 82 deletions(-) diff --git a/android/src/main/kotlin/be/freedelity/barcode_scanner/BarcodeScannerController.kt b/android/src/main/kotlin/be/freedelity/barcode_scanner/BarcodeScannerController.kt index ded237f..7d84eec 100644 --- a/android/src/main/kotlin/be/freedelity/barcode_scanner/BarcodeScannerController.kt +++ b/android/src/main/kotlin/be/freedelity/barcode_scanner/BarcodeScannerController.kt @@ -381,8 +381,6 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary if (cameraParams?.get("scanner_type") == "mrz") { - Log.i("scanner_native", "#####################################################################################################") - val mrz: String? = MrzUtil.extractMRZ(visionText.textBlocks, mrzResult!!) if (mrz != null) { @@ -421,6 +419,8 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary if (points?.isNotEmpty() == true) { + Log.i("native_scanner", "Extract MRZ done with result $mrz") + var x = points!![0].x var y = points!![0].y var width = points!![1].x - points!![0].x @@ -456,8 +456,31 @@ class BarcodeScannerController(private val activity: Activity, messenger: Binary imageProxy.image?.close() imageProxy.close() + } else { + + Log.i("native_scanner", "Extract MRZ done with result $mrz but image is not loaded yet") + + eventSink?.success(mapOf( + "progress" to 90, + )) + + } + + } else { + + var progress = 5 + if (mrzResult!!.size == 1) { + progress = 25 + } else if (mrzResult!!.size == 2) { + progress = 75 } + Log.i("native_scanner", "Extract MRZ progress with current $mrzResult (progress $progress)") + + eventSink?.success(mapOf( + "progress" to progress, + )) + } } else { diff --git a/android/src/main/kotlin/be/freedelity/barcode_scanner/util/MrzUtil.kt b/android/src/main/kotlin/be/freedelity/barcode_scanner/util/MrzUtil.kt index ccaf139..96738b4 100644 --- a/android/src/main/kotlin/be/freedelity/barcode_scanner/util/MrzUtil.kt +++ b/android/src/main/kotlin/be/freedelity/barcode_scanner/util/MrzUtil.kt @@ -48,10 +48,14 @@ object MrzUtil { mrzLines.forEach { line -> - val text = line.text.replace("«", "<").replace(" ", "").uppercase().trim() + var text = line.text.replace("«", "<").replace(" ", "").uppercase().trim() if (text.matches("^[A-Z0-9<]*$".toRegex())) { + while (text.matches("<<<[KC]".toRegex())) { + text = text.replaceFirst("<<<[KC]".toRegex(), "<<<<") + } + if (((mrzResult.size < 3 && text.length == 30) || mrzResult.size < 2 && (text.length == 36 || text.length == 44))) { if (!mrzResult.any { res -> res.substring(0, 20) == text.substring(0, 20) } && (mrzResult.isEmpty() || mrzResult.first().length == text.length)) { @@ -78,8 +82,6 @@ object MrzUtil { val map = mutableMapOf() val missed = mutableListOf() - Log.i("native_scanner_res", "result : $mrzResult") - mrzResult.forEachIndexed{ index, it -> if (mrzResult.size == 3) { @@ -111,6 +113,7 @@ object MrzUtil { } } } else { + var result = "${map[0]}\n${map[1]}" if (mrzResult.size == 3) result += "\n${map[2]}" return result diff --git a/example/lib/main.dart b/example/lib/main.dart index 20af90f..3db43db 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -59,11 +59,13 @@ class MyDemoApp extends StatefulWidget { class _MyDemoAppState extends State { + int? progress; bool withOverlay = true; - ScannerType scannerType = ScannerType.barcode; + ScannerType scannerType = ScannerType.mrz; @override Widget build(BuildContext context) { + return Scaffold( appBar: AppBar(title: Text('Scanner ${scannerType.name} example'), actions: [ PopupMenuButton( @@ -117,83 +119,106 @@ class _MyDemoAppState extends State { ], ), ]), - body: Builder(builder: (builderContext) { - Widget child = BarcodeScannerWidget( - scannerType: ScannerType.mrz, - onBarcodeDetected: (barcode) async { - await showDialog( - context: builderContext, - builder: (dialogContext) { - return Align( - alignment: Alignment.center, - child: Card( + body: Stack( + children: [ + Positioned.fill( + child: Builder(builder: (builderContext) { + Widget child = BarcodeScannerWidget( + scannerType: ScannerType.mrz, + onBarcodeDetected: (barcode) async { + await showDialog( + context: builderContext, + builder: (dialogContext) { + return Align( + alignment: Alignment.center, + child: Card( + margin: const EdgeInsets.all(24), + child: Container( + padding: const EdgeInsets.all(16), + child: Column(mainAxisSize: MainAxisSize.min, children: [Text('barcode : ${barcode.value}'), Text('format : ${barcode.format.name}'), ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog'))])))); + }); + }, + onTextDetected: (String text) async { + await showDialog( + context: builderContext, + builder: (dialogContext) { + return Align( + alignment: Alignment.center, + child: Card( margin: const EdgeInsets.all(24), child: Container( - padding: const EdgeInsets.all(16), - child: Column(mainAxisSize: MainAxisSize.min, children: [Text('barcode : ${barcode.value}'), Text('format : ${barcode.format.name}'), ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog'))])))); - }); - }, - onTextDetected: (String text) async { - await showDialog( - context: builderContext, - builder: (dialogContext) { - return Align( - alignment: Alignment.center, - child: Card( - margin: const EdgeInsets.all(24), - child: Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('text : \n$text'), - ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog')), - ], - ), - ), - ), - ); - }, - ); - }, - onMrzDetected: (String text, Uint8List bytes) { - showDialog( - context: builderContext, - builder: (dialogContext) { - return Align( - alignment: Alignment.center, - child: Card( - margin: const EdgeInsets.all(24), - child: Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(text), - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Image.memory(bytes), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('text : \n$text'), + ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog')), + ], + ), ), - ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog')), - ], - ), - ), - ), - ); - }, - ); - }, - onError: (dynamic error) { - debugPrint('$error'); - }, - ); + ), + ); + }, + ); + }, + onScanProgress: (int? progress) => setState(() => this.progress = progress), + onMrzDetected: (String text, Uint8List bytes) { + setState(() => progress = null); + showDialog( + context: builderContext, + builder: (dialogContext) { + return Align( + alignment: Alignment.center, + child: Card( + margin: const EdgeInsets.all(24), + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(text), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Image.memory(bytes), + ), + ElevatedButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Close dialog')), + ], + ), + ), + ), + ); + }, + ); + }, + onError: (dynamic error) { + debugPrint('$error'); + }, + ); + + if (withOverlay) { + return buildWithOverlay(builderContext, child); + } - if (withOverlay) { - return buildWithOverlay(builderContext, child); - } + return child; - return child; - })); + }), + ), + progress == null ? Container() : Positioned( + bottom: 0, left: 0, right: 0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + color: Colors.white, + child: Row( + children: [ + const Expanded(child: LinearProgressIndicator()), + const SizedBox(width: 16,), + Text('$progress %'), + ], + ), + ), + ), + ], + )); } buildWithOverlay(BuildContext builderContext, Widget scannerWidget) { diff --git a/lib/barcode_scanner.widget.dart b/lib/barcode_scanner.widget.dart index 9edc9ea..9defbf6 100644 --- a/lib/barcode_scanner.widget.dart +++ b/lib/barcode_scanner.widget.dart @@ -32,11 +32,14 @@ class BarcodeScannerWidget extends StatefulWidget { /// This function will be called when a barcode is detected. final Function(Barcode barcode)? onBarcodeDetected; - /// This function will be called when a bloc of text or a MRZ is detected. + /// This function will be called when a bloc of text is detected. final Function(String textResult)? onTextDetected; + /// This function will be called when a bloc MRZ is detected. final Function(String mrz, Uint8List image)? onMrzDetected; + final Function(int? progress)? onScanProgress; + final Function(dynamic error) onError; const BarcodeScannerWidget( @@ -48,6 +51,7 @@ class BarcodeScannerWidget extends StatefulWidget { this.onBarcodeDetected, this.onTextDetected, this.onMrzDetected, + this.onScanProgress, required this.onError, }) : assert(onBarcodeDetected != null || onTextDetected != null), @@ -76,9 +80,17 @@ class _BarcodeScannerWidgetState extends State { }; eventSubscription = eventChannel.receiveBroadcastStream().listen((dynamic event) async { + + if (widget.onScanProgress != null) { + widget.onScanProgress!(event["progress"] as int?); + } + if (widget.onBarcodeDetected != null && widget.scannerType == ScannerType.barcode) { + final format = BarcodeFormat.unserialize(event['format']); + if (format != null && event['barcode'] != null) { + await BarcodeScanner.stopScanner(); await widget.onBarcodeDetected!(Barcode(format: format, value: event['barcode'] as String)); @@ -86,21 +98,27 @@ class _BarcodeScannerWidgetState extends State { if (!widget.stopScanOnBarcodeDetected) { BarcodeScanner.startScanner(); } - } else { + + } else if (event["progress"] == null) { widget.onError(const FormatException('Barcode not found')); } + } else if (widget.onTextDetected != null && widget.scannerType == ScannerType.text) { + if (event['text'] != null) { await widget.onTextDetected!(event['text'] as String); - } else { + } else if (event["progress"] == null) { widget.onError(const FormatException('Text not found')); } + } else if (widget.onMrzDetected != null && widget.scannerType == ScannerType.mrz) { + if (event['mrz'] != null && event["img"] != null) { await widget.onMrzDetected!(event['mrz'] as String, Uint8List.fromList(event["img"])); - } else { + } else if (event["progress"] == null) { widget.onError(const FormatException('MRZ not found')); } + } }, onError: (dynamic error) { widget.onError(error);