From e56d1fbc79a6cb4c57b5cff3012b402ba1eabe6b Mon Sep 17 00:00:00 2001 From: Khoren Markosyan Date: Sun, 15 Jan 2023 01:58:01 +0400 Subject: [PATCH] implemented multi scan results drawing --- example/lib/main.dart | 274 +++--------------- example/lib/widgets/debug_info_widget.dart | 49 ++++ .../lib/widgets/scan_from_gallery_widget.dart | 43 +++ example/lib/widgets/scan_mode_dropdown.dart | 46 +++ example/lib/widgets/scan_result_widget.dart | 46 +++ .../widgets/unsupported_platform_widget.dart | 15 + lib/flutter_zxing.dart | 12 +- lib/generated_bindings.dart | 4 + lib/src/logic/barcodes_reader.dart | 16 +- lib/src/models/code.dart | 21 ++ lib/src/ui/multi_result_overlay.dart | 123 ++++++++ lib/src/ui/reader_widget.dart | 41 ++- lib/src/ui/ui.dart | 1 + lib/zxing_mobile.dart | 12 +- lib/zxing_web.dart | 10 +- src/native_zxing.cpp | 11 +- src/native_zxing.h | 1 + 17 files changed, 443 insertions(+), 282 deletions(-) create mode 100644 example/lib/widgets/debug_info_widget.dart create mode 100644 example/lib/widgets/scan_from_gallery_widget.dart create mode 100644 example/lib/widgets/scan_mode_dropdown.dart create mode 100644 example/lib/widgets/scan_result_widget.dart create mode 100644 example/lib/widgets/unsupported_platform_widget.dart create mode 100644 lib/src/ui/multi_result_overlay.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 7e24765..6e4dc0c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,12 @@ -import 'dart:ui'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_zxing/flutter_zxing.dart'; -import 'package:image_picker/image_picker.dart'; + +import 'widgets/debug_info_widget.dart'; +import 'widgets/scan_from_gallery_widget.dart'; +import 'widgets/scan_mode_dropdown.dart'; +import 'widgets/scan_result_widget.dart'; +import 'widgets/unsupported_platform_widget.dart'; void main() { zx.setLogEnabled(false); @@ -38,7 +41,10 @@ class _DemoPageState extends State { Uint8List? createdCodeBytes; Code? result; - List multiResult = []; + Codes? multiResult; + + // Currently code positions works incorrect on Android in Portrait mode + bool isMultiScan = false; bool showDebugInfo = true; int successScans = 0; @@ -52,8 +58,7 @@ class _DemoPageState extends State { length: 2, child: Scaffold( appBar: AppBar( - title: const Text('Flutter Zxing Example'), - bottom: const TabBar( + title: const TabBar( tabs: [ Tab(text: 'Scan Code'), Tab(text: 'Create Code'), @@ -82,34 +87,33 @@ class _DemoPageState extends State { onScanFailure: _onScanFailure, onMultiScan: _onMultiScanSuccess, onMultiScanFailure: _onMultiScanFailure, - isMultiScan: false, - // showScannerOverlay: false, - // scanDelay: const Duration(milliseconds: 0), - // tryInverted: true, + isMultiScan: isMultiScan, + scanDelay: isMultiScan + ? Duration.zero + : const Duration(milliseconds: 500), + tryInverted: true, ), - // show multi results as rectangles - // if (multiResult.isNotEmpty) - // Positioned.fill( - // child: CustomPaint( - // painter: MultiScanPainter( - // codes: multiResult, - // size: Size( - // MediaQuery.of(context).size.width, - // MediaQuery.of(context).size.height, - // ), - // ), - // ), - // ), ScanFromGalleryWidget( onScan: _onScanSuccess, onScanFailure: _onScanFailure, ), + // Change single/multi scan mode dropdown button + ScanModeDropdown( + isMultiScan: isMultiScan, + onChanged: (value) { + setState(() { + isMultiScan = value; + }); + }, + ), if (showDebugInfo) DebugInfoWidget( successScans: successScans, failedScans: failedScans, - error: result?.error, - duration: result?.duration ?? 0, + error: isMultiScan ? multiResult?.error : result?.error, + duration: isMultiScan + ? multiResult?.duration ?? 0 + : result?.duration ?? 0, onReset: _onReset, ), ], @@ -133,7 +137,7 @@ class _DemoPageState extends State { }, ), if (createdCodeBytes != null) - Image.memory(createdCodeBytes ?? Uint8List(0), height: 200), + Image.memory(createdCodeBytes ?? Uint8List(0), height: 400), ], ), ], @@ -159,20 +163,20 @@ class _DemoPageState extends State { } } - _onMultiScanSuccess(List codes) { + _onMultiScanSuccess(Codes codes) { setState(() { successScans++; multiResult = codes; }); } - _onMultiScanFailure(List codes) { + _onMultiScanFailure(Codes result) { setState(() { failedScans++; - multiResult = codes; + multiResult = result; }); - if (result?.error?.isNotEmpty == true) { - _showMessage(context, 'Error: ${codes.first.error}'); + if (result.codes.isNotEmpty == true) { + _showMessage(context, 'Error: ${result.codes.first.error}'); } } @@ -190,211 +194,3 @@ class _DemoPageState extends State { }); } } - -class ScanResultWidget extends StatelessWidget { - const ScanResultWidget({ - Key? key, - this.result, - this.onScanAgain, - }) : super(key: key); - - final Code? result; - final Function()? onScanAgain; - - @override - Widget build(BuildContext context) { - return Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - result?.format?.name ?? '', - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 20), - Text( - result?.text ?? '', - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox(height: 20), - Text( - 'Inverted: ${result?.isInverted}\t\tMirrored: ${result?.isMirrored}', - style: Theme.of(context).textTheme.bodyText2, - ), - const SizedBox(height: 40), - ElevatedButton( - onPressed: onScanAgain, - child: const Text('Scan Again'), - ), - ], - ), - ), - ); - } -} - -class ScanFromGalleryWidget extends StatelessWidget { - const ScanFromGalleryWidget({ - Key? key, - this.onScan, - this.onScanFailure, - }) : super(key: key); - - final Function(Code)? onScan; - final Function(Code?)? onScanFailure; - - @override - Widget build(BuildContext context) { - return Positioned( - bottom: 20, - right: 20, - child: FloatingActionButton( - onPressed: _onFromGalleryButtonTapped, - child: const Icon(Icons.image), - ), - ); - } - - void _onFromGalleryButtonTapped() async { - final XFile? file = - await ImagePicker().pickImage(source: ImageSource.gallery); - if (file != null) { - final Code? result = await zx.readBarcodeImagePath( - file, - params: DecodeParams(tryInverted: true), - ); - if (result != null && result.isValid) { - onScan?.call(result); - } else { - result?.error = 'No barcode found'; - onScanFailure?.call(result); - } - } - } -} - -class DebugInfoWidget extends StatelessWidget { - const DebugInfoWidget({ - Key? key, - required this.successScans, - required this.failedScans, - this.error, - this.duration = 0, - this.onReset, - }) : super(key: key); - - final int successScans; - final int failedScans; - final String? error; - final int duration; - - final Function()? onReset; - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Container( - color: Colors.white.withOpacity(0.7), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Success: $successScans\nFailed: $failedScans\nDuration: $duration ms', - style: Theme.of(context).textTheme.bodySmall, - ), - TextButton( - onPressed: onReset, - child: const Text('Reset'), - ), - ], - ), - ), - ), - ), - ); - } -} - -class UnsupportedPlatformWidget extends StatelessWidget { - const UnsupportedPlatformWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - 'This platform is not supported yet.', - style: Theme.of(context).textTheme.headline6, - ), - ); - } -} - -class MultiResultWidget extends StatelessWidget { - const MultiResultWidget({ - super.key, - this.results = const [], - }); - - final List results; - - @override - Widget build(BuildContext context) { - return Container(); - } -} - -class MultiScanPainter extends CustomPainter { - MultiScanPainter({ - required this.codes, - required this.size, - // required this.scale, - // required this.offset, - }); - - final List codes; - final Size size; - final double scale = 1; - final Offset offset = Offset.zero; - - @override - void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = Colors.green - ..strokeWidth = 2 - ..style = PaintingStyle.stroke; - - for (final Code code in codes) { - final position = code.position; - if (position == null) { - continue; - } - // position to points - final List points = positionToPoints(position); - // debugPrint('w: ${position.imageWidth} h: ${position.imageHeight}'); - // print(points); - canvas.drawPoints(PointMode.polygon, points, paint); - } - } - - @override - bool shouldRepaint(MultiScanPainter oldDelegate) { - return true; - } - - List positionToPoints(Position pos) { - return [ - Offset(pos.topLeftX.toDouble(), pos.topLeftY.toDouble()), - Offset(pos.topRightX.toDouble(), pos.topRightY.toDouble()), - Offset(pos.bottomRightX.toDouble(), pos.bottomRightY.toDouble()), - Offset(pos.bottomLeftX.toDouble(), pos.bottomLeftY.toDouble()), - ]; - } -} diff --git a/example/lib/widgets/debug_info_widget.dart b/example/lib/widgets/debug_info_widget.dart new file mode 100644 index 0000000..0996df1 --- /dev/null +++ b/example/lib/widgets/debug_info_widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class DebugInfoWidget extends StatelessWidget { + const DebugInfoWidget({ + Key? key, + required this.successScans, + required this.failedScans, + this.error, + this.duration = 0, + this.onReset, + }) : super(key: key); + + final int successScans; + final int failedScans; + final String? error; + final int duration; + + final Function()? onReset; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + color: Colors.white.withOpacity(0.7), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Success: $successScans\nFailed: $failedScans\nDuration: $duration ms', + style: Theme.of(context).textTheme.bodySmall, + ), + TextButton( + onPressed: onReset, + child: const Text('Reset'), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/widgets/scan_from_gallery_widget.dart b/example/lib/widgets/scan_from_gallery_widget.dart new file mode 100644 index 0000000..e0a2f61 --- /dev/null +++ b/example/lib/widgets/scan_from_gallery_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_zxing/flutter_zxing.dart'; +import 'package:image_picker/image_picker.dart'; + +class ScanFromGalleryWidget extends StatelessWidget { + const ScanFromGalleryWidget({ + Key? key, + this.onScan, + this.onScanFailure, + }) : super(key: key); + + final Function(Code)? onScan; + final Function(Code?)? onScanFailure; + + @override + Widget build(BuildContext context) { + return Positioned( + bottom: 20, + right: 20, + child: FloatingActionButton( + onPressed: _onFromGalleryButtonTapped, + child: const Icon(Icons.image), + ), + ); + } + + void _onFromGalleryButtonTapped() async { + final XFile? file = + await ImagePicker().pickImage(source: ImageSource.gallery); + if (file != null) { + final Code? result = await zx.readBarcodeImagePath( + file, + params: DecodeParams(tryInverted: true), + ); + if (result != null && result.isValid) { + onScan?.call(result); + } else { + result?.error = 'No barcode found'; + onScanFailure?.call(result); + } + } + } +} diff --git a/example/lib/widgets/scan_mode_dropdown.dart b/example/lib/widgets/scan_mode_dropdown.dart new file mode 100644 index 0000000..39315fe --- /dev/null +++ b/example/lib/widgets/scan_mode_dropdown.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class ScanModeDropdown extends StatelessWidget { + const ScanModeDropdown({ + Key? key, + this.isMultiScan = false, + this.onChanged, + }) : super(key: key); + + final bool isMultiScan; + final Function(bool value)? onChanged; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: isMultiScan, + items: const [ + DropdownMenuItem( + value: false, + child: Text('Single Scan'), + ), + DropdownMenuItem( + value: true, + child: Text('Multi Scan'), + ), + ], + onChanged: (value) => onChanged?.call(value ?? false), + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/widgets/scan_result_widget.dart b/example/lib/widgets/scan_result_widget.dart new file mode 100644 index 0000000..4992c62 --- /dev/null +++ b/example/lib/widgets/scan_result_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_zxing/flutter_zxing.dart'; + +class ScanResultWidget extends StatelessWidget { + const ScanResultWidget({ + Key? key, + this.result, + this.onScanAgain, + }) : super(key: key); + + final Code? result; + final Function()? onScanAgain; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + result?.format?.name ?? '', + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: 20), + Text( + result?.text ?? '', + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 20), + Text( + 'Inverted: ${result?.isInverted}\t\tMirrored: ${result?.isMirrored}', + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: onScanAgain, + child: const Text('Scan Again'), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/unsupported_platform_widget.dart b/example/lib/widgets/unsupported_platform_widget.dart new file mode 100644 index 0000000..d7881b5 --- /dev/null +++ b/example/lib/widgets/unsupported_platform_widget.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class UnsupportedPlatformWidget extends StatelessWidget { + const UnsupportedPlatformWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + 'This platform is not supported yet.', + style: Theme.of(context).textTheme.headline6, + ), + ); + } +} diff --git a/lib/flutter_zxing.dart b/lib/flutter_zxing.dart index 2178fe4..4982f12 100644 --- a/lib/flutter_zxing.dart +++ b/lib/flutter_zxing.dart @@ -38,8 +38,8 @@ abstract class Zxing { DecodeParams? params, }); -/// Reads barcodes from the camera - Future> processCameraImageMulti( + /// Reads barcodes from the camera + Future processCameraImageMulti( CameraImage image, { DecodeParams? params, }); @@ -71,25 +71,25 @@ abstract class Zxing { }); /// Reads barcodes from String image path - Future> readBarcodesImagePathString( + Future readBarcodesImagePathString( String path, { DecodeParams? params, }); /// Reads barcodes from XFile image path - Future> readBarcodesImagePath( + Future readBarcodesImagePath( XFile path, { DecodeParams? params, }); /// Reads barcodes from image url - Future> readBarcodesImageUrl( + Future readBarcodesImageUrl( String url, { DecodeParams? params, }); /// Reads barcodes from Uint8List image bytes - List readBarcodes( + Codes readBarcodes( Uint8List bytes, { required int width, required int height, diff --git a/lib/generated_bindings.dart b/lib/generated_bindings.dart index 6fd4a3a..43fe692 100644 --- a/lib/generated_bindings.dart +++ b/lib/generated_bindings.dart @@ -263,6 +263,10 @@ class CodeResults extends ffi.Struct { /// < The results of the barcode decoding external ffi.Pointer results; + + /// < The duration of the decoding in milliseconds + @ffi.Int() + external int duration; } /// @brief EncodeResult encapsulates the result of encoding a barcode. diff --git a/lib/src/logic/barcodes_reader.dart b/lib/src/logic/barcodes_reader.dart index 5cac114..dd6bcea 100644 --- a/lib/src/logic/barcodes_reader.dart +++ b/lib/src/logic/barcodes_reader.dart @@ -1,7 +1,7 @@ part of 'zxing.dart'; /// Reads barcodes from String image path -Future> zxingReadBarcodesImagePathString( +Future zxingReadBarcodesImagePathString( String path, { DecodeParams? params, }) => @@ -11,14 +11,14 @@ Future> zxingReadBarcodesImagePathString( ); /// Reads barcodes from XFile image path -Future> zxingReadBarcodesImagePath( +Future zxingReadBarcodesImagePath( XFile path, { DecodeParams? params, }) async { final Uint8List imageBytes = await path.readAsBytes(); imglib.Image? image = imglib.decodeImage(imageBytes); if (image == null) { - return []; + return Codes([], 0); } image = resizeToMaxSize(image, params?.maxSize); return zxingReadBarcodes( @@ -30,7 +30,7 @@ Future> zxingReadBarcodesImagePath( } /// Reads barcodes from image url -Future> zxingReadBarcodesImageUrl( +Future zxingReadBarcodesImageUrl( String url, { DecodeParams? params, }) async { @@ -38,7 +38,7 @@ Future> zxingReadBarcodesImageUrl( (await NetworkAssetBundle(Uri.parse(url)).load(url)).buffer.asUint8List(); imglib.Image? image = imglib.decodeImage(imageBytes); if (image == null) { - return []; + return Codes([], 0); } image = resizeToMaxSize(image, params?.maxSize); return zxingReadBarcodes( @@ -50,7 +50,7 @@ Future> zxingReadBarcodesImageUrl( } /// Reads barcodes from Uint8List image bytes -List zxingReadBarcodes( +Codes zxingReadBarcodes( Uint8List bytes, { required int width, required int height, @@ -59,7 +59,7 @@ List zxingReadBarcodes( return _readBarcodes(bytes, width, height, params); } -List _readBarcodes( +Codes _readBarcodes( Uint8List bytes, int width, int height, @@ -80,5 +80,5 @@ List _readBarcodes( for (int i = 0; i < result.count; i++) { results.add(result.results.elementAt(i).ref.toCode()); } - return results; + return Codes(results, result.duration); } diff --git a/lib/src/models/code.dart b/lib/src/models/code.dart index 6cb8011..743cb90 100644 --- a/lib/src/models/code.dart +++ b/lib/src/models/code.dart @@ -26,3 +26,24 @@ class Code { bool isMirrored; // Whether the code is mirrored int duration; // The duration of the decoding in milliseconds } + +// Represents a list of barcode codes +class Codes { + Codes( + this.codes, + this.duration, + ); + + List codes; // The list of codes + int duration; // The duration of the decoding in milliseconds + + // Returns the first code error if any + String? get error { + for (final Code code in codes) { + if (code.error != null) { + return code.error; + } + } + return null; + } +} diff --git a/lib/src/ui/multi_result_overlay.dart b/lib/src/ui/multi_result_overlay.dart new file mode 100644 index 0000000..77bd744 --- /dev/null +++ b/lib/src/ui/multi_result_overlay.dart @@ -0,0 +1,123 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import '../../flutter_zxing.dart'; + +class MultiResultOverlay extends StatelessWidget { + const MultiResultOverlay({ + super.key, + this.results = const [], + this.onCodeTap, + }); + + final List results; + final Function(Code code)? onCodeTap; + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: CustomPaint( + painter: MultiScanPainter( + context: context, + codes: results, + color: Theme.of(context).primaryColor, + onCodeTap: onCodeTap, + ), + ), + ); + } +} + +class MultiScanPainter extends CustomPainter { + MultiScanPainter({ + required this.context, + required this.codes, + this.onCodeTap, + this.offset = Offset.zero, + this.color = Colors.blue, + }); + + final BuildContext context; + final List codes; + final Offset offset; + final Color color; + + final Function(Code code)? onCodeTap; + + final Map _codeRects = {}; + + @override + void paint(Canvas canvas, Size size) { + final Paint paint = Paint() + ..color = color + ..strokeWidth = 3 + ..style = PaintingStyle.stroke; + + for (final Code code in codes) { + final Position? position = code.position; + if (position == null) { + continue; + } + final double scale = size.width / position.imageWidth; + + // position to points + final List points = positionToPoints(position); + final List scaledPoints = points + .map((Offset point) => Offset( + point.dx * scale + offset.dx, + point.dy * scale + offset.dy, + )) + .toList(); + canvas.drawPoints(PointMode.polygon, scaledPoints, paint); + + final Rect rect = Rect.fromPoints(scaledPoints[0], scaledPoints[2]); + + final TextPainter textPainter = TextPainter( + text: TextSpan( + text: code.text, + style: TextStyle( + color: color, + fontSize: 12, + ), + ), + maxLines: 2, + textDirection: TextDirection.ltr, + )..layout(); + + textPainter.layout(maxWidth: rect.width); + textPainter.paint(canvas, rect.topLeft.translate(0, -textPainter.height)); + + _codeRects[rect] = code; + } + } + + @override + bool? hitTest(Offset position) { + for (final Rect rect in _codeRects.keys) { + if (rect.contains(position)) { + final Code? code = _codeRects[rect]; + if (code != null) { + onCodeTap?.call(code); + } + return false; + } + } + return super.hitTest(position); + } + + @override + bool shouldRepaint(MultiScanPainter oldDelegate) { + return true; + } + + List positionToPoints(Position pos) { + return [ + Offset(pos.topLeftX.toDouble(), pos.topLeftY.toDouble()), + Offset(pos.topRightX.toDouble(), pos.topRightY.toDouble()), + Offset(pos.bottomRightX.toDouble(), pos.bottomRightY.toDouble()), + Offset(pos.bottomLeftX.toDouble(), pos.bottomLeftY.toDouble()), + Offset(pos.topLeftX.toDouble(), pos.topLeftY.toDouble()), + ]; + } +} diff --git a/lib/src/ui/reader_widget.dart b/lib/src/ui/reader_widget.dart index 6fb4a01..f8f20b1 100644 --- a/lib/src/ui/reader_widget.dart +++ b/lib/src/ui/reader_widget.dart @@ -24,8 +24,8 @@ class ReaderWidget extends StatefulWidget { this.scannerOverlay, this.showFlashlight = true, this.allowPinchZoom = true, - this.scanDelay = const Duration(milliseconds: 1000), // 1000ms delay - this.scanDelaySuccess = const Duration(milliseconds: 1000), // 1000ms delay + this.scanDelay = const Duration(milliseconds: 1000), + this.scanDelaySuccess = const Duration(milliseconds: 1000), this.cropPercent = 0.5, // 50% of the screen this.resolution = ResolutionPreset.high, this.loading = @@ -39,10 +39,10 @@ class ReaderWidget extends StatefulWidget { final Function(Code)? onScanFailure; /// Called when a code is detected - final Function(List)? onMultiScan; + final Function(Codes)? onMultiScan; /// Called when a code is not detected - final Function(List)? onMultiScanFailure; + final Function(Codes)? onMultiScanFailure; /// Called when the camera controller is created final Function(CameraController?)? onControllerCreated; @@ -74,13 +74,13 @@ class ReaderWidget extends StatefulWidget { /// Delay between scans when no code is detected final Duration scanDelay; - /// Crop percent of the screen + /// Crop percent of the screen, will be ignored if isMultiScan is true final double cropPercent; /// Camera resolution final ResolutionPreset resolution; - /// Delay between scans when a code is detected + /// Delay between scans when a code is detected, will be ignored if isMultiScan is true final Duration scanDelaySuccess; /// Loading widget while camera is initializing. Default is a black screen @@ -106,6 +106,8 @@ class _ReaderWidgetState extends State // true when code detecting is ongoing bool _isProcessing = false; + Codes results = Codes([], 0); + @override void initState() { super.initState(); @@ -209,8 +211,7 @@ class _ReaderWidgetState extends State if (!_isProcessing) { _isProcessing = true; try { - final double cropPercent = - widget.showScannerOverlay ? widget.cropPercent : 0; + final double cropPercent = widget.isMultiScan ? 0 : widget.cropPercent; final int cropSize = (min(image.width, image.height) * cropPercent).round(); final DecodeParams params = DecodeParams( @@ -222,16 +223,20 @@ class _ReaderWidgetState extends State isMultiScan: widget.isMultiScan, ); if (widget.isMultiScan) { - final List results = await zx.processCameraImageMulti( + final Codes result = await zx.processCameraImageMulti( image, params: params, ); - if (results.isNotEmpty) { - widget.onMultiScan?.call(results); - await Future.delayed(widget.scanDelaySuccess); + if (result.codes.isNotEmpty) { + results = result; + widget.onMultiScan?.call(result); setState(() {}); + if (!widget.isMultiScan) { + await Future.delayed(widget.scanDelaySuccess); + } } else { - widget.onMultiScanFailure?.call(results); + results = Codes([], 0); + widget.onMultiScanFailure?.call(result); } } else { final Code result = await zx.processCameraImage( @@ -282,13 +287,21 @@ class _ReaderWidgetState extends State width: cameraMaxSize, child: CameraPreview( controller!, + child: widget.showScannerOverlay && + widget.isMultiScan && + results.codes.isNotEmpty + ? MultiResultOverlay( + results: results.codes, + onCodeTap: widget.onScan, + ) + : null, ), ), ), ), ), ), - if (widget.showScannerOverlay) + if (widget.showScannerOverlay && !widget.isMultiScan) Container( decoration: ShapeDecoration( shape: widget.scannerOverlay ?? diff --git a/lib/src/ui/ui.dart b/lib/src/ui/ui.dart index 9b064ea..d9090db 100644 --- a/lib/src/ui/ui.dart +++ b/lib/src/ui/ui.dart @@ -1,5 +1,6 @@ export 'dynamic_scanner_overlay.dart'; export 'fixed_scanner_overlay.dart'; +export 'multi_result_overlay.dart'; export 'reader_widget.dart'; export 'scanner_overlay.dart'; export 'writer_widget.dart'; diff --git a/lib/zxing_mobile.dart b/lib/zxing_mobile.dart index 69fcdbb..7b61a66 100644 --- a/lib/zxing_mobile.dart +++ b/lib/zxing_mobile.dart @@ -46,11 +46,11 @@ class ZxingMobile implements Zxing { await zxingProcessCameraImage(image, params: params) as Code; @override - Future> processCameraImageMulti( + Future processCameraImageMulti( CameraImage image, { DecodeParams? params, }) async => - await zxingProcessCameraImage(image, params: params) as List; + await zxingProcessCameraImage(image, params: params) as Codes; @override Future readBarcodeImagePathString( @@ -83,28 +83,28 @@ class ZxingMobile implements Zxing { zxingReadBarcode(bytes, width: width, height: height, params: params); @override - Future> readBarcodesImagePathString( + Future readBarcodesImagePathString( String path, { DecodeParams? params, }) => zxingReadBarcodesImagePathString(path, params: params); @override - Future> readBarcodesImagePath( + Future readBarcodesImagePath( XFile path, { DecodeParams? params, }) => zxingReadBarcodesImagePath(path, params: params); @override - Future> readBarcodesImageUrl( + Future readBarcodesImageUrl( String url, { DecodeParams? params, }) => zxingReadBarcodesImageUrl(url, params: params); @override - List readBarcodes( + Codes readBarcodes( Uint8List bytes, { required int width, required int height, diff --git a/lib/zxing_web.dart b/lib/zxing_web.dart index edace0f..880d7e2 100644 --- a/lib/zxing_web.dart +++ b/lib/zxing_web.dart @@ -39,7 +39,7 @@ class ZxingWeb implements Zxing { throw UnimplementedError(); @override - Future> processCameraImageMulti( + Future processCameraImageMulti( CameraImage image, { DecodeParams? params, }) => @@ -76,28 +76,28 @@ class ZxingWeb implements Zxing { throw UnimplementedError(); @override - Future> readBarcodesImagePathString( + Future readBarcodesImagePathString( String path, { DecodeParams? params, }) => throw UnimplementedError(); @override - Future> readBarcodesImagePath( + Future readBarcodesImagePath( XFile path, { DecodeParams? params, }) => throw UnimplementedError(); @override - Future> readBarcodesImageUrl( + Future readBarcodesImageUrl( String url, { DecodeParams? params, }) => throw UnimplementedError(); @override - List readBarcodes( + Codes readBarcodes( Uint8List bytes, { required int width, required int height, diff --git a/src/native_zxing.cpp b/src/native_zxing.cpp index 50d9e48..b9263c9 100644 --- a/src/native_zxing.cpp +++ b/src/native_zxing.cpp @@ -40,7 +40,7 @@ extern "C" { image = image.cropped(width / 2 - cropWidth / 2, height / 2 - cropHeight / 2, cropWidth, cropHeight); } - DecodeHints hints = DecodeHints().setTryHarder(tryHarder).setTryRotate(tryRotate).setFormats(BarcodeFormat(format)).setTryInvert(tryInvert); + DecodeHints hints = DecodeHints().setTryHarder(tryHarder).setTryRotate(tryRotate).setFormats(BarcodeFormat(format)).setTryInvert(tryInvert).setReturnErrors(true); Result result = ReadBarcode(image, hints); struct CodeResult code; @@ -71,9 +71,12 @@ extern "C" { image = image.cropped(width / 2 - cropWidth / 2, height / 2 - cropHeight / 2, cropWidth, cropHeight); } - DecodeHints hints = DecodeHints().setTryHarder(tryHarder).setTryRotate(tryRotate).setFormats(BarcodeFormat(format)).setTryInvert(tryInvert); + DecodeHints hints = DecodeHints().setTryHarder(tryHarder).setTryRotate(tryRotate).setFormats(BarcodeFormat(format)).setTryInvert(tryInvert).setReturnErrors(true); Results results = ReadBarcodes(image, hints); + // remove invalid results + results.erase(remove_if(results.begin(), results.end(), [](Result const &result) { return !result.isValid(); }), results.end()); + int evalInMillis = static_cast(get_now() - start); platform_log("Read Barcode in: %d ms\n", evalInMillis); @@ -91,7 +94,7 @@ extern "C" } delete[] data; delete[] bytes; - return {i, codes}; + return {i, codes, evalInMillis}; } FUNCTION_ATTRIBUTE @@ -143,7 +146,7 @@ extern "C" auto tr = p.topRight(); auto bl = p.bottomLeft(); auto br = p.bottomRight(); - code->pos = new Pos{tl.x, tl.y, tr.x, tr.y, bl.x, bl.y, br.x, br.y}; + code->pos = new Pos{0, 0, tl.x, tl.y, tr.x, tr.y, bl.x, bl.y, br.x, br.y}; code->isInverted = result.isInverted(); code->isMirrored = result.isMirrored(); diff --git a/src/native_zxing.h b/src/native_zxing.h index e2d631b..001dd8f 100644 --- a/src/native_zxing.h +++ b/src/native_zxing.h @@ -45,6 +45,7 @@ extern "C" { int count; ///< The number of barcodes detected struct CodeResult *results; ///< The results of the barcode decoding + int duration; ///< The duration of the decoding in milliseconds }; /**