diff --git a/CHANGELOG.md b/CHANGELOG.md index 412a20625..2165f6e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## NEXT + +* [MacOS] Added the corners and size information to barcode results. +* [MacOS] Added support for `analyzeImage`. +* [web] Added the size information to barcode results. +* Added support for barcode formats to image analysis. + ## 5.2.3 Deprecations: diff --git a/README.md b/README.md index 9626aba27..24099550f 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ See the example app for detailed implementation information. | Features | Android | iOS | macOS | Web | |------------------------|--------------------|--------------------|----------------------|-----| -| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | -| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| returnImage | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | | scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | ## Platform Support @@ -83,8 +83,8 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: ## Web -As of version 5.0.0 adding the library to the `index.html` is no longer required, -as the library is automatically loaded on first use. +As of version 5.0.0 adding the barcode scanning library script to the `index.html` is no longer required, +as the script is automatically loaded on first use. ### Providing a mirror for the barcode scanning library diff --git a/android/build.gradle b/android/build.gradle index b53e3e99b..6b7c31872 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -67,7 +67,7 @@ dependencies { def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false if (useUnbundled.toBoolean()) { // Dynamically downloaded model via Google Play Services - implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0' + implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1' } else { // Bundled model in app implementation 'com.google.mlkit:barcode-scanning:17.2.0' @@ -77,8 +77,8 @@ dependencies { // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) - implementation 'androidx.camera:camera-lifecycle:1.3.3' - implementation 'androidx.camera:camera-camera2:1.3.3' + implementation 'androidx.camera:camera-lifecycle:1.3.4' + implementation 'androidx.camera:camera-camera2:1.3.4' testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.12.0' diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index 1f6897421..15bda1cde 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -151,28 +151,16 @@ class MobileScannerHandler( null } - var barcodeScannerOptions: BarcodeScannerOptions? = null - if (formats != null) { - val formatsList: MutableList = mutableListOf() - for (formatValue in formats) { - formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue) - } - barcodeScannerOptions = if (formatsList.size == 1) { - BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()) - .build() - } else { - BarcodeScannerOptions.Builder().setBarcodeFormats( - formatsList.first(), - *formatsList.subList(1, formatsList.size).toIntArray() - ).build() - } - } + val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats) val position = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA - val detectionSpeed: DetectionSpeed = if (speed == 0) DetectionSpeed.NO_DUPLICATES - else if (speed ==1) DetectionSpeed.NORMAL else DetectionSpeed.UNRESTRICTED + val detectionSpeed: DetectionSpeed = when (speed) { + 0 -> DetectionSpeed.NO_DUPLICATES + 1 -> DetectionSpeed.NORMAL + else -> DetectionSpeed.UNRESTRICTED + } mobileScanner!!.start( barcodeScannerOptions, @@ -243,13 +231,13 @@ class MobileScannerHandler( private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { analyzerResult = result - val uri = Uri.fromFile(File(call.arguments.toString())) - // TODO: parse options from the method call - // See https://github.com/juliansteenbakker/mobile_scanner/issues/1069 + val formats: List? = call.argument>("formats") + val filePath: String = call.argument("filePath")!! + mobileScanner!!.analyzeImage( - uri, - null, + Uri.fromFile(File(filePath)), + buildBarcodeScannerOptions(formats), analyzeImageSuccessCallback, analyzeImageErrorCallback) } @@ -284,4 +272,26 @@ class MobileScannerHandler( result.success(null) } + + private fun buildBarcodeScannerOptions(formats: List?): BarcodeScannerOptions? { + if (formats == null) { + return null + } + + val formatsList: MutableList = mutableListOf() + + for (formatValue in formats) { + formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue) + } + + if (formatsList.size == 1) { + return BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()) + .build() + } + + return BarcodeScannerOptions.Builder().setBarcodeFormats( + formatsList.first(), + *formatsList.subList(1, formatsList.size).toIntArray() + ).build() + } } diff --git a/example/lib/barcode_scanner_analyze_image.dart b/example/lib/barcode_scanner_analyze_image.dart new file mode 100644 index 000000000..de0a7a4a1 --- /dev/null +++ b/example/lib/barcode_scanner_analyze_image.dart @@ -0,0 +1,76 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class BarcodeScannerAnalyzeImage extends StatefulWidget { + const BarcodeScannerAnalyzeImage({super.key}); + + @override + State createState() => + _BarcodeScannerAnalyzeImageState(); +} + +class _BarcodeScannerAnalyzeImageState + extends State { + final MobileScannerController _controller = MobileScannerController(); + + BarcodeCapture? _barcodeCapture; + + Future _analyzeImageFromFile() async { + try { + final XFile? file = + await ImagePicker().pickImage(source: ImageSource.gallery); + + if (!mounted || file == null) { + return; + } + + final BarcodeCapture? barcodeCapture = + await _controller.analyzeImage(file.path); + + if (mounted) { + setState(() { + _barcodeCapture = barcodeCapture; + }); + } + } catch (_) {} + } + + @override + Widget build(BuildContext context) { + Widget label = const Text('Pick a file to detect barcode'); + + if (_barcodeCapture != null) { + label = Text( + _barcodeCapture?.barcodes.firstOrNull?.displayValue ?? + 'No barcode detected', + ); + } + + return Scaffold( + appBar: AppBar(title: const Text('Analyze image from file')), + body: Column( + children: [ + Expanded( + child: Center( + child: ElevatedButton( + onPressed: kIsWeb ? null : _analyzeImageFromFile, + child: kIsWeb + ? const Text('Analyze image is not supported on web') + : const Text('Choose file'), + ), + ), + ), + Expanded(child: Center(child: label)), + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/example/lib/barcode_scanner_window.dart b/example/lib/barcode_scanner_window.dart index 0a13f15ce..4691ac520 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -39,16 +39,16 @@ class _BarcodeScannerWithScanWindowState final scannedBarcode = barcodeCapture.barcodes.first; // No barcode corners, or size, or no camera preview size. - if (scannedBarcode.corners.isEmpty || - value.size.isEmpty || - barcodeCapture.size.isEmpty) { + if (value.size.isEmpty || + scannedBarcode.size.isEmpty || + scannedBarcode.corners.isEmpty) { return const SizedBox(); } return CustomPaint( painter: BarcodeOverlay( barcodeCorners: scannedBarcode.corners, - barcodeSize: barcodeCapture.size, + barcodeSize: scannedBarcode.size, boxFit: BoxFit.contain, cameraPreviewSize: value.size, ), diff --git a/example/lib/main.dart b/example/lib/main.dart index 050d1a70e..bc1ecc1b0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:mobile_scanner_example/barcode_scanner_analyze_image.dart'; import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; @@ -20,95 +21,75 @@ void main() { class MyHome extends StatelessWidget { const MyHome({super.key}); + Widget _buildItem(BuildContext context, String label, Widget page) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => page, + ), + ); + }, + child: Text(label), + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Mobile Scanner Example')), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, + child: ListView( children: [ - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerSimple(), - ), - ); - }, - child: const Text('MobileScanner Simple'), + _buildItem( + context, + 'MobileScanner Simple', + const BarcodeScannerSimple(), ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerListView(), - ), - ); - }, - child: const Text('MobileScanner with ListView'), + _buildItem( + context, + 'MobileScanner with ListView', + const BarcodeScannerListView(), ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithController(), - ), - ); - }, - child: const Text('MobileScanner with Controller'), + _buildItem( + context, + 'MobileScanner with Controller', + const BarcodeScannerWithController(), ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithScanWindow(), - ), - ); - }, - child: const Text('MobileScanner with ScanWindow'), + _buildItem( + context, + 'MobileScanner with ScanWindow', + const BarcodeScannerWithScanWindow(), ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerReturningImage(), - ), - ); - }, - child: const Text( - 'MobileScanner with Controller (returning image)', - ), + _buildItem( + context, + 'MobileScanner with Controller (return image)', + const BarcodeScannerReturningImage(), + ), + _buildItem( + context, + 'MobileScanner with zoom slider', + const BarcodeScannerWithZoom(), ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerWithZoom(), - ), - ); - }, - child: const Text('MobileScanner with zoom slider'), + _buildItem( + context, + 'MobileScanner with PageView', + const BarcodeScannerPageView(), ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BarcodeScannerPageView(), - ), - ); - }, - child: const Text('MobileScanner pageView'), + _buildItem( + context, + 'MobileScanner with Overlay', + const BarcodeScannerWithOverlay(), ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BarcodeScannerWithOverlay(), - ), - ); - }, - child: const Text('MobileScanner with Overlay'), + _buildItem( + context, + 'Analyze image from file', + const BarcodeScannerAnalyzeImage(), ), ], ), diff --git a/example/lib/mobile_scanner_overlay.dart b/example/lib/mobile_scanner_overlay.dart index 85cd8dec6..ae188cb35 100644 --- a/example/lib/mobile_scanner_overlay.dart +++ b/example/lib/mobile_scanner_overlay.dart @@ -5,6 +5,8 @@ import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerWithOverlay extends StatefulWidget { + const BarcodeScannerWithOverlay({super.key}); + @override _BarcodeScannerWithOverlayState createState() => _BarcodeScannerWithOverlayState(); diff --git a/ios/Classes/MobileScanner.swift b/ios/Classes/MobileScanner.swift index e05c338d9..cec1c52ed 100644 --- a/ios/Classes/MobileScanner.swift +++ b/ios/Classes/MobileScanner.swift @@ -22,8 +22,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega /// The selected camera var device: AVCaptureDevice! - /// Barcode scanner for results - var scanner = BarcodeScanner.barcodeScanner() + /// The long lived barcode scanner for scanning barcodes from a camera preview. + var scanner: BarcodeScanner? = nil /// Default position of camera var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back @@ -146,7 +146,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega position: videoPosition ) - scanner.process(image) { [self] barcodes, error in + scanner?.process(image) { [self] barcodes, error in imagesCurrentlyBeingProcessed = false if (detectionSpeed == DetectionSpeed.noDuplicates) { @@ -314,6 +314,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega textureId = nil captureSession = nil device = nil + scanner = nil } /// Toggle the torch. @@ -431,13 +432,16 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega } /// Analyze a single image - func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, + barcodeScannerOptions: BarcodeScannerOptions?, callback: @escaping BarcodeScanningCallback) { let image = VisionImage(image: image) image.orientation = imageOrientation( deviceOrientation: UIDevice.current.orientation, defaultOrientation: .portrait, position: position ) + + let scanner: BarcodeScanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() scanner.process(image, completion: callback) } diff --git a/ios/Classes/MobileScannerPlugin.swift b/ios/Classes/MobileScannerPlugin.swift index 0ee886f51..d0a5f153a 100644 --- a/ios/Classes/MobileScannerPlugin.swift +++ b/ios/Classes/MobileScannerPlugin.swift @@ -134,16 +134,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000) MobileScannerPlugin.returnImage = returnImage - let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} - var barcodeOptions: BarcodeScannerOptions? = nil - - if (formatList.count != 0) { - var barcodeFormats: BarcodeFormat = [] - for index in formats { - barcodeFormats.insert(BarcodeFormat(rawValue: index)) - } - barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats) - } + let barcodeOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats) let position = facing == 0 ? AVCaptureDevice.Position.front : .back let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! @@ -262,7 +253,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { /// Analyzes a single image. private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") + let formats: Array = (call.arguments as! Dictionary)["formats"] as? Array ?? [] + let scannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats) + let uiImage = UIImage(contentsOfFile: (call.arguments as! Dictionary)["filePath"] as? String ?? "") if (uiImage == nil) { result(FlutterError(code: "MobileScanner", @@ -271,7 +264,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { return } - mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { barcodes, error in + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, + barcodeScannerOptions: scannerOptions, callback: { barcodes, error in if error != nil { DispatchQueue.main.async { result(FlutterError(code: "MobileScanner", @@ -297,4 +291,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { } }) } + + private func buildBarcodeScannerOptions(_ formats: [Int]) -> BarcodeScannerOptions? { + guard !formats.isEmpty else { + return nil + } + + var barcodeFormats: BarcodeFormat = [] + + for format in formats { + barcodeFormats.insert(BarcodeFormat(rawValue: format)) + } + + return BarcodeScannerOptions(formats: barcodeFormats) + } } diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 891057b11..7c6900dc1 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:mobile_scanner/src/enums/barcode_format.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; @@ -140,11 +141,22 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { } @override - Future analyzeImage(String path) async { + Future analyzeImage( + String path, { + List formats = const [], + }) async { final Map? result = await methodChannel.invokeMapMethod( 'analyzeImage', - path, + { + 'filePath': path, + 'formats': formats.isEmpty + ? null + : [ + for (final BarcodeFormat format in formats) + if (format != BarcodeFormat.unknown) format.rawValue, + ], + }, ); return _parseBarcode(result); diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 39c11c61e..68efb1cc8 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -174,7 +174,7 @@ class MobileScannerController extends ValueNotifier { /// /// The [path] points to a file on the device. /// - /// This is only supported on Android and iOS. + /// This is only supported on Android, iOS and MacOS. /// /// Returns the [BarcodeCapture] that was found in the image. Future analyzeImage(String path) { diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index 553602160..a283e0c57 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; +import 'package:mobile_scanner/src/enums/barcode_format.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; @@ -46,9 +47,15 @@ abstract class MobileScannerPlatform extends PlatformInterface { /// Analyze a local image file for barcodes. /// /// The [path] is the path to the file on disk. + /// The [formats] specify the barcode formats that should be detected. + /// + /// If [formats] is empty, all barcode formats will be detected. /// /// Returns the barcodes that were found in the image. - Future analyzeImage(String path) { + Future analyzeImage( + String path, { + List formats = const [], + }) { throw UnimplementedError('analyzeImage() has not been implemented.'); } diff --git a/lib/src/objects/barcode.dart b/lib/src/objects/barcode.dart index c03fd9b9b..907756be7 100644 --- a/lib/src/objects/barcode.dart +++ b/lib/src/objects/barcode.dart @@ -152,7 +152,7 @@ class Barcode { /// This is null if the raw value is not available. final String? rawValue; - /// The size of the barcode bounding box. + /// The normalized size of the barcode bounding box. /// /// If the bounding box is unavailable, this will be [Size.zero]. final Size size; diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 963c13d87..32fce5bf4 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -4,6 +4,7 @@ import 'dart:ui_web' as ui_web; import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:mobile_scanner/src/enums/barcode_format.dart'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; @@ -232,7 +233,10 @@ class MobileScannerWeb extends MobileScannerPlatform { } @override - Future analyzeImage(String path) { + Future analyzeImage( + String path, { + List formats = const [], + }) { throw UnsupportedError('analyzeImage() is not supported on the web.'); } diff --git a/lib/src/web/zxing/result.dart b/lib/src/web/zxing/result.dart index 568558384..dd43b09f1 100644 --- a/lib/src/web/zxing/result.dart +++ b/lib/src/web/zxing/result.dart @@ -75,13 +75,33 @@ extension type Result(JSObject _) implements JSObject { /// Convert this result to a [Barcode]. Barcode get toBarcode { + final List corners = resultPoints; + return Barcode( - corners: resultPoints, + corners: corners, format: barcodeFormat, displayValue: text, rawBytes: rawBytes, rawValue: text, + size: _computeSize(corners), type: BarcodeType.text, ); } + + Size _computeSize(List points) { + if (points.length != 4) { + return Size.zero; + } + + final Iterable xCoords = points.map((p) => p.dx); + final Iterable yCoords = points.map((p) => p.dy); + + // Find the minimum and maximum x and y coordinates. + final double xMin = xCoords.reduce((a, b) => a < b ? a : b); + final double xMax = xCoords.reduce((a, b) => a > b ? a : b); + final double yMin = yCoords.reduce((a, b) => a < b ? a : b); + final double yMax = yCoords.reduce((a, b) => a > b ? a : b); + + return Size(xMax - xMin, yMax - yMin); + } } diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index bf828d039..32f23a8ce 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -138,11 +138,10 @@ final class ZXingBarcodeReader extends BarcodeReader { required web.MediaStream videoStream, }) async { final int detectionTimeoutMs = options.detectionTimeoutMs; - final List formats = options.formats; - - if (formats.contains(BarcodeFormat.unknown)) { - formats.removeWhere((element) => element == BarcodeFormat.unknown); - } + final List formats = [ + for (final BarcodeFormat format in options.formats) + if (format != BarcodeFormat.unknown) format, + ]; _reader = ZXingBrowserMultiFormatReader( _createReaderHints(formats), diff --git a/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift b/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift index e5e82f071..b840e5a0e 100644 --- a/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift +++ b/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift @@ -71,6 +71,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, stop(result) case "updateScanWindow": updateScanWindow(call, result) + case "analyzeImage": + analyzeImage(call, result) default: result(FlutterMethodNotImplemented) } @@ -124,7 +126,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, VTCreateCGImageFromCVPixelBuffer(self!.latestBuffer, options: nil, imageOut: &cgImage) let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage!) do { - let barcodeRequest:VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in + let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in self?.imagesCurrentlyBeingProcessed = false if error != nil { @@ -452,6 +454,70 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, result(nil) } + func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + let argReader = MapArgumentReader(call.arguments as? [String: Any]) + let symbologies:[VNBarcodeSymbology] = argReader.toSymbology() + + guard let filePath: String = argReader.string(key: "filePath") else { + // TODO: fix error code + result(FlutterError(code: "MobileScanner", + message: "No image found in analyzeImage!", + details: nil)) + return + } + + let fileUrl = URL(fileURLWithPath: filePath) + + guard let ciImage = CIImage(contentsOf: fileUrl) else { + // TODO: fix error code + result(FlutterError(code: "MobileScanner", + message: "No image found in analyzeImage!", + details: nil)) + return + } + + let imageRequestHandler = VNImageRequestHandler(ciImage: ciImage, orientation: CGImagePropertyOrientation.up, options: [:]) + + do { + let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest( + completionHandler: { [] (request, error) in + + if error != nil { + DispatchQueue.main.async { + // TODO: fix error code + result(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil)) + } + return + } + + guard let barcodes: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else { + return + } + + if barcodes.isEmpty { + return + } + + result([ + "name": "barcode", + "data": barcodes.map({ $0.toMap() }), + ]) + }) + + if !symbologies.isEmpty { + // Add the symbologies the user wishes to support. + barcodeRequest.symbologies = symbologies + } + + try imageRequestHandler.perform([barcodeRequest]) + } catch let e { + // TODO: fix error code + DispatchQueue.main.async { + result(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil)) + } + } + } + // Observer for torch state public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { switch keyPath { @@ -542,10 +608,24 @@ extension CGImage { } extension VNBarcodeObservation { + private func distanceBetween(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat { + return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)) + } + public func toMap() -> [String: Any?] { return [ - "rawValue": self.payloadStringValue ?? "", - "format": self.symbology.toInt ?? -1, + "corners": [ + ["x": topLeft.x, "y": topLeft.y], + ["x": topRight.x, "y": topRight.y], + ["x": bottomRight.x, "y": bottomRight.y], + ["x": bottomLeft.x, "y": bottomLeft.y], + ], + "format": symbology.toInt ?? -1, + "rawValue": payloadStringValue ?? "", + "size": [ + "width": distanceBetween(topLeft, topRight), + "height": distanceBetween(topLeft, bottomLeft), + ], ] } } @@ -585,7 +665,7 @@ extension VNBarcodeSymbology { } } - var toInt:Int? { + var toInt: Int? { if #available(macOS 12.0, *) { if(self == VNBarcodeSymbology.codabar){ return 8