Navaron Bracke
Committed by GitHub

Merge pull request #1177 from navaronbracke/macos_extras

fix: support formats for analyze image
  1 +## NEXT
  2 +
  3 +* [MacOS] Added the corners and size information to barcode results.
  4 +* [MacOS] Added support for `analyzeImage`.
  5 +* [web] Added the size information to barcode results.
  6 +* Added support for barcode formats to image analysis.
  7 +
1 ## 5.2.3 8 ## 5.2.3
2 9
3 Deprecations: 10 Deprecations:
@@ -35,8 +35,8 @@ See the example app for detailed implementation information. @@ -35,8 +35,8 @@ See the example app for detailed implementation information.
35 35
36 | Features | Android | iOS | macOS | Web | 36 | Features | Android | iOS | macOS | Web |
37 |------------------------|--------------------|--------------------|----------------------|-----| 37 |------------------------|--------------------|--------------------|----------------------|-----|
38 -| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |  
39 -| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | 38 +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
  39 +| returnImage | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
40 | scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | 40 | scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
41 41
42 ## Platform Support 42 ## Platform Support
@@ -83,8 +83,8 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: @@ -83,8 +83,8 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities:
83 83
84 ## Web 84 ## Web
85 85
86 -As of version 5.0.0 adding the library to the `index.html` is no longer required,  
87 -as the library is automatically loaded on first use. 86 +As of version 5.0.0 adding the barcode scanning library script to the `index.html` is no longer required,
  87 +as the script is automatically loaded on first use.
88 88
89 ### Providing a mirror for the barcode scanning library 89 ### Providing a mirror for the barcode scanning library
90 90
@@ -67,7 +67,7 @@ dependencies { @@ -67,7 +67,7 @@ dependencies {
67 def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false 67 def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false
68 if (useUnbundled.toBoolean()) { 68 if (useUnbundled.toBoolean()) {
69 // Dynamically downloaded model via Google Play Services 69 // Dynamically downloaded model via Google Play Services
70 - implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0' 70 + implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1'
71 } else { 71 } else {
72 // Bundled model in app 72 // Bundled model in app
73 implementation 'com.google.mlkit:barcode-scanning:17.2.0' 73 implementation 'com.google.mlkit:barcode-scanning:17.2.0'
@@ -77,8 +77,8 @@ dependencies { @@ -77,8 +77,8 @@ dependencies {
77 // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 77 // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7
78 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) 78 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
79 79
80 - implementation 'androidx.camera:camera-lifecycle:1.3.3'  
81 - implementation 'androidx.camera:camera-camera2:1.3.3' 80 + implementation 'androidx.camera:camera-lifecycle:1.3.4'
  81 + implementation 'androidx.camera:camera-camera2:1.3.4'
82 82
83 testImplementation 'org.jetbrains.kotlin:kotlin-test' 83 testImplementation 'org.jetbrains.kotlin:kotlin-test'
84 testImplementation 'org.mockito:mockito-core:5.12.0' 84 testImplementation 'org.mockito:mockito-core:5.12.0'
@@ -151,28 +151,16 @@ class MobileScannerHandler( @@ -151,28 +151,16 @@ class MobileScannerHandler(
151 null 151 null
152 } 152 }
153 153
154 - var barcodeScannerOptions: BarcodeScannerOptions? = null  
155 - if (formats != null) {  
156 - val formatsList: MutableList<Int> = mutableListOf()  
157 - for (formatValue in formats) {  
158 - formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue)  
159 - }  
160 - barcodeScannerOptions = if (formatsList.size == 1) {  
161 - BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())  
162 - .build()  
163 - } else {  
164 - BarcodeScannerOptions.Builder().setBarcodeFormats(  
165 - formatsList.first(),  
166 - *formatsList.subList(1, formatsList.size).toIntArray()  
167 - ).build()  
168 - }  
169 - } 154 + val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)
170 155
171 val position = 156 val position =
172 if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA 157 if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
173 158
174 - val detectionSpeed: DetectionSpeed = if (speed == 0) DetectionSpeed.NO_DUPLICATES  
175 - else if (speed ==1) DetectionSpeed.NORMAL else DetectionSpeed.UNRESTRICTED 159 + val detectionSpeed: DetectionSpeed = when (speed) {
  160 + 0 -> DetectionSpeed.NO_DUPLICATES
  161 + 1 -> DetectionSpeed.NORMAL
  162 + else -> DetectionSpeed.UNRESTRICTED
  163 + }
176 164
177 mobileScanner!!.start( 165 mobileScanner!!.start(
178 barcodeScannerOptions, 166 barcodeScannerOptions,
@@ -243,13 +231,13 @@ class MobileScannerHandler( @@ -243,13 +231,13 @@ class MobileScannerHandler(
243 231
244 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { 232 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
245 analyzerResult = result 233 analyzerResult = result
246 - val uri = Uri.fromFile(File(call.arguments.toString()))  
247 234
248 - // TODO: parse options from the method call  
249 - // See https://github.com/juliansteenbakker/mobile_scanner/issues/1069 235 + val formats: List<Int>? = call.argument<List<Int>>("formats")
  236 + val filePath: String = call.argument<String>("filePath")!!
  237 +
250 mobileScanner!!.analyzeImage( 238 mobileScanner!!.analyzeImage(
251 - uri,  
252 - null, 239 + Uri.fromFile(File(filePath)),
  240 + buildBarcodeScannerOptions(formats),
253 analyzeImageSuccessCallback, 241 analyzeImageSuccessCallback,
254 analyzeImageErrorCallback) 242 analyzeImageErrorCallback)
255 } 243 }
@@ -284,4 +272,26 @@ class MobileScannerHandler( @@ -284,4 +272,26 @@ class MobileScannerHandler(
284 272
285 result.success(null) 273 result.success(null)
286 } 274 }
  275 +
  276 + private fun buildBarcodeScannerOptions(formats: List<Int>?): BarcodeScannerOptions? {
  277 + if (formats == null) {
  278 + return null
  279 + }
  280 +
  281 + val formatsList: MutableList<Int> = mutableListOf()
  282 +
  283 + for (formatValue in formats) {
  284 + formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue)
  285 + }
  286 +
  287 + if (formatsList.size == 1) {
  288 + return BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
  289 + .build()
  290 + }
  291 +
  292 + return BarcodeScannerOptions.Builder().setBarcodeFormats(
  293 + formatsList.first(),
  294 + *formatsList.subList(1, formatsList.size).toIntArray()
  295 + ).build()
  296 + }
287 } 297 }
  1 +import 'package:flutter/foundation.dart';
  2 +import 'package:flutter/material.dart';
  3 +import 'package:image_picker/image_picker.dart';
  4 +import 'package:mobile_scanner/mobile_scanner.dart';
  5 +
  6 +class BarcodeScannerAnalyzeImage extends StatefulWidget {
  7 + const BarcodeScannerAnalyzeImage({super.key});
  8 +
  9 + @override
  10 + State<BarcodeScannerAnalyzeImage> createState() =>
  11 + _BarcodeScannerAnalyzeImageState();
  12 +}
  13 +
  14 +class _BarcodeScannerAnalyzeImageState
  15 + extends State<BarcodeScannerAnalyzeImage> {
  16 + final MobileScannerController _controller = MobileScannerController();
  17 +
  18 + BarcodeCapture? _barcodeCapture;
  19 +
  20 + Future<void> _analyzeImageFromFile() async {
  21 + try {
  22 + final XFile? file =
  23 + await ImagePicker().pickImage(source: ImageSource.gallery);
  24 +
  25 + if (!mounted || file == null) {
  26 + return;
  27 + }
  28 +
  29 + final BarcodeCapture? barcodeCapture =
  30 + await _controller.analyzeImage(file.path);
  31 +
  32 + if (mounted) {
  33 + setState(() {
  34 + _barcodeCapture = barcodeCapture;
  35 + });
  36 + }
  37 + } catch (_) {}
  38 + }
  39 +
  40 + @override
  41 + Widget build(BuildContext context) {
  42 + Widget label = const Text('Pick a file to detect barcode');
  43 +
  44 + if (_barcodeCapture != null) {
  45 + label = Text(
  46 + _barcodeCapture?.barcodes.firstOrNull?.displayValue ??
  47 + 'No barcode detected',
  48 + );
  49 + }
  50 +
  51 + return Scaffold(
  52 + appBar: AppBar(title: const Text('Analyze image from file')),
  53 + body: Column(
  54 + children: [
  55 + Expanded(
  56 + child: Center(
  57 + child: ElevatedButton(
  58 + onPressed: kIsWeb ? null : _analyzeImageFromFile,
  59 + child: kIsWeb
  60 + ? const Text('Analyze image is not supported on web')
  61 + : const Text('Choose file'),
  62 + ),
  63 + ),
  64 + ),
  65 + Expanded(child: Center(child: label)),
  66 + ],
  67 + ),
  68 + );
  69 + }
  70 +
  71 + @override
  72 + void dispose() {
  73 + _controller.dispose();
  74 + super.dispose();
  75 + }
  76 +}
@@ -39,16 +39,16 @@ class _BarcodeScannerWithScanWindowState @@ -39,16 +39,16 @@ class _BarcodeScannerWithScanWindowState
39 final scannedBarcode = barcodeCapture.barcodes.first; 39 final scannedBarcode = barcodeCapture.barcodes.first;
40 40
41 // No barcode corners, or size, or no camera preview size. 41 // No barcode corners, or size, or no camera preview size.
42 - if (scannedBarcode.corners.isEmpty ||  
43 - value.size.isEmpty ||  
44 - barcodeCapture.size.isEmpty) { 42 + if (value.size.isEmpty ||
  43 + scannedBarcode.size.isEmpty ||
  44 + scannedBarcode.corners.isEmpty) {
45 return const SizedBox(); 45 return const SizedBox();
46 } 46 }
47 47
48 return CustomPaint( 48 return CustomPaint(
49 painter: BarcodeOverlay( 49 painter: BarcodeOverlay(
50 barcodeCorners: scannedBarcode.corners, 50 barcodeCorners: scannedBarcode.corners,
51 - barcodeSize: barcodeCapture.size, 51 + barcodeSize: scannedBarcode.size,
52 boxFit: BoxFit.contain, 52 boxFit: BoxFit.contain,
53 cameraPreviewSize: value.size, 53 cameraPreviewSize: value.size,
54 ), 54 ),
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
  2 +import 'package:mobile_scanner_example/barcode_scanner_analyze_image.dart';
2 import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; 3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
3 import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; 4 import 'package:mobile_scanner_example/barcode_scanner_listview.dart';
4 import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; 5 import 'package:mobile_scanner_example/barcode_scanner_pageview.dart';
@@ -20,95 +21,75 @@ void main() { @@ -20,95 +21,75 @@ void main() {
20 class MyHome extends StatelessWidget { 21 class MyHome extends StatelessWidget {
21 const MyHome({super.key}); 22 const MyHome({super.key});
22 23
  24 + Widget _buildItem(BuildContext context, String label, Widget page) {
  25 + return Padding(
  26 + padding: const EdgeInsets.all(8.0),
  27 + child: Center(
  28 + child: ElevatedButton(
  29 + onPressed: () {
  30 + Navigator.of(context).push(
  31 + MaterialPageRoute(
  32 + builder: (context) => page,
  33 + ),
  34 + );
  35 + },
  36 + child: Text(label),
  37 + ),
  38 + ),
  39 + );
  40 + }
  41 +
23 @override 42 @override
24 Widget build(BuildContext context) { 43 Widget build(BuildContext context) {
25 return Scaffold( 44 return Scaffold(
26 appBar: AppBar(title: const Text('Mobile Scanner Example')), 45 appBar: AppBar(title: const Text('Mobile Scanner Example')),
27 body: Center( 46 body: Center(
28 - child: Column(  
29 - mainAxisAlignment: MainAxisAlignment.spaceAround, 47 + child: ListView(
30 children: [ 48 children: [
31 - ElevatedButton(  
32 - onPressed: () {  
33 - Navigator.of(context).push(  
34 - MaterialPageRoute(  
35 - builder: (context) => const BarcodeScannerSimple(),  
36 - ),  
37 - );  
38 - },  
39 - child: const Text('MobileScanner Simple'), 49 + _buildItem(
  50 + context,
  51 + 'MobileScanner Simple',
  52 + const BarcodeScannerSimple(),
40 ), 53 ),
41 - ElevatedButton(  
42 - onPressed: () {  
43 - Navigator.of(context).push(  
44 - MaterialPageRoute(  
45 - builder: (context) => const BarcodeScannerListView(),  
46 - ),  
47 - );  
48 - },  
49 - child: const Text('MobileScanner with ListView'), 54 + _buildItem(
  55 + context,
  56 + 'MobileScanner with ListView',
  57 + const BarcodeScannerListView(),
50 ), 58 ),
51 - ElevatedButton(  
52 - onPressed: () {  
53 - Navigator.of(context).push(  
54 - MaterialPageRoute(  
55 - builder: (context) => const BarcodeScannerWithController(),  
56 - ),  
57 - );  
58 - },  
59 - child: const Text('MobileScanner with Controller'), 59 + _buildItem(
  60 + context,
  61 + 'MobileScanner with Controller',
  62 + const BarcodeScannerWithController(),
60 ), 63 ),
61 - ElevatedButton(  
62 - onPressed: () {  
63 - Navigator.of(context).push(  
64 - MaterialPageRoute(  
65 - builder: (context) => const BarcodeScannerWithScanWindow(),  
66 - ),  
67 - );  
68 - },  
69 - child: const Text('MobileScanner with ScanWindow'), 64 + _buildItem(
  65 + context,
  66 + 'MobileScanner with ScanWindow',
  67 + const BarcodeScannerWithScanWindow(),
70 ), 68 ),
71 - ElevatedButton(  
72 - onPressed: () {  
73 - Navigator.of(context).push(  
74 - MaterialPageRoute(  
75 - builder: (context) => const BarcodeScannerReturningImage(),  
76 - ),  
77 - );  
78 - },  
79 - child: const Text(  
80 - 'MobileScanner with Controller (returning image)',  
81 - ), 69 + _buildItem(
  70 + context,
  71 + 'MobileScanner with Controller (return image)',
  72 + const BarcodeScannerReturningImage(),
  73 + ),
  74 + _buildItem(
  75 + context,
  76 + 'MobileScanner with zoom slider',
  77 + const BarcodeScannerWithZoom(),
82 ), 78 ),
83 - ElevatedButton(  
84 - onPressed: () {  
85 - Navigator.of(context).push(  
86 - MaterialPageRoute(  
87 - builder: (context) => const BarcodeScannerWithZoom(),  
88 - ),  
89 - );  
90 - },  
91 - child: const Text('MobileScanner with zoom slider'), 79 + _buildItem(
  80 + context,
  81 + 'MobileScanner with PageView',
  82 + const BarcodeScannerPageView(),
92 ), 83 ),
93 - ElevatedButton(  
94 - onPressed: () {  
95 - Navigator.of(context).push(  
96 - MaterialPageRoute(  
97 - builder: (context) => const BarcodeScannerPageView(),  
98 - ),  
99 - );  
100 - },  
101 - child: const Text('MobileScanner pageView'), 84 + _buildItem(
  85 + context,
  86 + 'MobileScanner with Overlay',
  87 + const BarcodeScannerWithOverlay(),
102 ), 88 ),
103 - ElevatedButton(  
104 - onPressed: () {  
105 - Navigator.of(context).push(  
106 - MaterialPageRoute(  
107 - builder: (context) => BarcodeScannerWithOverlay(),  
108 - ),  
109 - );  
110 - },  
111 - child: const Text('MobileScanner with Overlay'), 89 + _buildItem(
  90 + context,
  91 + 'Analyze image from file',
  92 + const BarcodeScannerAnalyzeImage(),
112 ), 93 ),
113 ], 94 ],
114 ), 95 ),
@@ -5,6 +5,8 @@ import 'package:mobile_scanner_example/scanner_button_widgets.dart'; @@ -5,6 +5,8 @@ import 'package:mobile_scanner_example/scanner_button_widgets.dart';
5 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 5 import 'package:mobile_scanner_example/scanner_error_widget.dart';
6 6
7 class BarcodeScannerWithOverlay extends StatefulWidget { 7 class BarcodeScannerWithOverlay extends StatefulWidget {
  8 + const BarcodeScannerWithOverlay({super.key});
  9 +
8 @override 10 @override
9 _BarcodeScannerWithOverlayState createState() => 11 _BarcodeScannerWithOverlayState createState() =>
10 _BarcodeScannerWithOverlayState(); 12 _BarcodeScannerWithOverlayState();
@@ -22,8 +22,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -22,8 +22,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
22 /// The selected camera 22 /// The selected camera
23 var device: AVCaptureDevice! 23 var device: AVCaptureDevice!
24 24
25 - /// Barcode scanner for results  
26 - var scanner = BarcodeScanner.barcodeScanner() 25 + /// The long lived barcode scanner for scanning barcodes from a camera preview.
  26 + var scanner: BarcodeScanner? = nil
27 27
28 /// Default position of camera 28 /// Default position of camera
29 var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back 29 var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back
@@ -146,7 +146,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -146,7 +146,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
146 position: videoPosition 146 position: videoPosition
147 ) 147 )
148 148
149 - scanner.process(image) { [self] barcodes, error in 149 + scanner?.process(image) { [self] barcodes, error in
150 imagesCurrentlyBeingProcessed = false 150 imagesCurrentlyBeingProcessed = false
151 151
152 if (detectionSpeed == DetectionSpeed.noDuplicates) { 152 if (detectionSpeed == DetectionSpeed.noDuplicates) {
@@ -314,6 +314,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -314,6 +314,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
314 textureId = nil 314 textureId = nil
315 captureSession = nil 315 captureSession = nil
316 device = nil 316 device = nil
  317 + scanner = nil
317 } 318 }
318 319
319 /// Toggle the torch. 320 /// Toggle the torch.
@@ -431,13 +432,16 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -431,13 +432,16 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
431 } 432 }
432 433
433 /// Analyze a single image 434 /// Analyze a single image
434 - func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { 435 + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position,
  436 + barcodeScannerOptions: BarcodeScannerOptions?, callback: @escaping BarcodeScanningCallback) {
435 let image = VisionImage(image: image) 437 let image = VisionImage(image: image)
436 image.orientation = imageOrientation( 438 image.orientation = imageOrientation(
437 deviceOrientation: UIDevice.current.orientation, 439 deviceOrientation: UIDevice.current.orientation,
438 defaultOrientation: .portrait, 440 defaultOrientation: .portrait,
439 position: position 441 position: position
440 ) 442 )
  443 +
  444 + let scanner: BarcodeScanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
441 445
442 scanner.process(image, completion: callback) 446 scanner.process(image, completion: callback)
443 } 447 }
@@ -134,16 +134,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -134,16 +134,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
134 self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000) 134 self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000)
135 MobileScannerPlugin.returnImage = returnImage 135 MobileScannerPlugin.returnImage = returnImage
136 136
137 - let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}  
138 - var barcodeOptions: BarcodeScannerOptions? = nil  
139 -  
140 - if (formatList.count != 0) {  
141 - var barcodeFormats: BarcodeFormat = []  
142 - for index in formats {  
143 - barcodeFormats.insert(BarcodeFormat(rawValue: index))  
144 - }  
145 - barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)  
146 - } 137 + let barcodeOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)
147 138
148 let position = facing == 0 ? AVCaptureDevice.Position.front : .back 139 let position = facing == 0 ? AVCaptureDevice.Position.front : .back
149 let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! 140 let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
@@ -262,7 +253,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -262,7 +253,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
262 253
263 /// Analyzes a single image. 254 /// Analyzes a single image.
264 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 255 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
265 - let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") 256 + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
  257 + let scannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)
  258 + let uiImage = UIImage(contentsOfFile: (call.arguments as! Dictionary<String, Any?>)["filePath"] as? String ?? "")
266 259
267 if (uiImage == nil) { 260 if (uiImage == nil) {
268 result(FlutterError(code: "MobileScanner", 261 result(FlutterError(code: "MobileScanner",
@@ -271,7 +264,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -271,7 +264,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
271 return 264 return
272 } 265 }
273 266
274 - mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { barcodes, error in 267 + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back,
  268 + barcodeScannerOptions: scannerOptions, callback: { barcodes, error in
275 if error != nil { 269 if error != nil {
276 DispatchQueue.main.async { 270 DispatchQueue.main.async {
277 result(FlutterError(code: "MobileScanner", 271 result(FlutterError(code: "MobileScanner",
@@ -297,4 +291,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -297,4 +291,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
297 } 291 }
298 }) 292 })
299 } 293 }
  294 +
  295 + private func buildBarcodeScannerOptions(_ formats: [Int]) -> BarcodeScannerOptions? {
  296 + guard !formats.isEmpty else {
  297 + return nil
  298 + }
  299 +
  300 + var barcodeFormats: BarcodeFormat = []
  301 +
  302 + for format in formats {
  303 + barcodeFormats.insert(BarcodeFormat(rawValue: format))
  304 + }
  305 +
  306 + return BarcodeScannerOptions(formats: barcodeFormats)
  307 + }
300 } 308 }
@@ -3,6 +3,7 @@ import 'dart:async'; @@ -3,6 +3,7 @@ import 'dart:async';
3 import 'package:flutter/foundation.dart'; 3 import 'package:flutter/foundation.dart';
4 import 'package:flutter/services.dart'; 4 import 'package:flutter/services.dart';
5 import 'package:flutter/widgets.dart'; 5 import 'package:flutter/widgets.dart';
  6 +import 'package:mobile_scanner/src/enums/barcode_format.dart';
6 import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart'; 7 import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart';
7 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; 8 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
8 import 'package:mobile_scanner/src/enums/torch_state.dart'; 9 import 'package:mobile_scanner/src/enums/torch_state.dart';
@@ -140,11 +141,22 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -140,11 +141,22 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
140 } 141 }
141 142
142 @override 143 @override
143 - Future<BarcodeCapture?> analyzeImage(String path) async { 144 + Future<BarcodeCapture?> analyzeImage(
  145 + String path, {
  146 + List<BarcodeFormat> formats = const <BarcodeFormat>[],
  147 + }) async {
144 final Map<Object?, Object?>? result = 148 final Map<Object?, Object?>? result =
145 await methodChannel.invokeMapMethod<Object?, Object?>( 149 await methodChannel.invokeMapMethod<Object?, Object?>(
146 'analyzeImage', 150 'analyzeImage',
147 - path, 151 + {
  152 + 'filePath': path,
  153 + 'formats': formats.isEmpty
  154 + ? null
  155 + : [
  156 + for (final BarcodeFormat format in formats)
  157 + if (format != BarcodeFormat.unknown) format.rawValue,
  158 + ],
  159 + },
148 ); 160 );
149 161
150 return _parseBarcode(result); 162 return _parseBarcode(result);
@@ -174,7 +174,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -174,7 +174,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
174 /// 174 ///
175 /// The [path] points to a file on the device. 175 /// The [path] points to a file on the device.
176 /// 176 ///
177 - /// This is only supported on Android and iOS. 177 + /// This is only supported on Android, iOS and MacOS.
178 /// 178 ///
179 /// Returns the [BarcodeCapture] that was found in the image. 179 /// Returns the [BarcodeCapture] that was found in the image.
180 Future<BarcodeCapture?> analyzeImage(String path) { 180 Future<BarcodeCapture?> analyzeImage(String path) {
1 import 'package:flutter/widgets.dart'; 1 import 'package:flutter/widgets.dart';
  2 +import 'package:mobile_scanner/src/enums/barcode_format.dart';
2 import 'package:mobile_scanner/src/enums/torch_state.dart'; 3 import 'package:mobile_scanner/src/enums/torch_state.dart';
3 import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; 4 import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart';
4 import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; 5 import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
@@ -46,9 +47,15 @@ abstract class MobileScannerPlatform extends PlatformInterface { @@ -46,9 +47,15 @@ abstract class MobileScannerPlatform extends PlatformInterface {
46 /// Analyze a local image file for barcodes. 47 /// Analyze a local image file for barcodes.
47 /// 48 ///
48 /// The [path] is the path to the file on disk. 49 /// The [path] is the path to the file on disk.
  50 + /// The [formats] specify the barcode formats that should be detected.
  51 + ///
  52 + /// If [formats] is empty, all barcode formats will be detected.
49 /// 53 ///
50 /// Returns the barcodes that were found in the image. 54 /// Returns the barcodes that were found in the image.
51 - Future<BarcodeCapture?> analyzeImage(String path) { 55 + Future<BarcodeCapture?> analyzeImage(
  56 + String path, {
  57 + List<BarcodeFormat> formats = const <BarcodeFormat>[],
  58 + }) {
52 throw UnimplementedError('analyzeImage() has not been implemented.'); 59 throw UnimplementedError('analyzeImage() has not been implemented.');
53 } 60 }
54 61
@@ -152,7 +152,7 @@ class Barcode { @@ -152,7 +152,7 @@ class Barcode {
152 /// This is null if the raw value is not available. 152 /// This is null if the raw value is not available.
153 final String? rawValue; 153 final String? rawValue;
154 154
155 - /// The size of the barcode bounding box. 155 + /// The normalized size of the barcode bounding box.
156 /// 156 ///
157 /// If the bounding box is unavailable, this will be [Size.zero]. 157 /// If the bounding box is unavailable, this will be [Size.zero].
158 final Size size; 158 final Size size;
@@ -4,6 +4,7 @@ import 'dart:ui_web' as ui_web; @@ -4,6 +4,7 @@ import 'dart:ui_web' as ui_web;
4 4
5 import 'package:flutter/widgets.dart'; 5 import 'package:flutter/widgets.dart';
6 import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 6 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
  7 +import 'package:mobile_scanner/src/enums/barcode_format.dart';
7 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 8 import 'package:mobile_scanner/src/enums/camera_facing.dart';
8 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; 9 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
9 import 'package:mobile_scanner/src/enums/torch_state.dart'; 10 import 'package:mobile_scanner/src/enums/torch_state.dart';
@@ -232,7 +233,10 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -232,7 +233,10 @@ class MobileScannerWeb extends MobileScannerPlatform {
232 } 233 }
233 234
234 @override 235 @override
235 - Future<BarcodeCapture?> analyzeImage(String path) { 236 + Future<BarcodeCapture?> analyzeImage(
  237 + String path, {
  238 + List<BarcodeFormat> formats = const <BarcodeFormat>[],
  239 + }) {
236 throw UnsupportedError('analyzeImage() is not supported on the web.'); 240 throw UnsupportedError('analyzeImage() is not supported on the web.');
237 } 241 }
238 242
@@ -75,13 +75,33 @@ extension type Result(JSObject _) implements JSObject { @@ -75,13 +75,33 @@ extension type Result(JSObject _) implements JSObject {
75 75
76 /// Convert this result to a [Barcode]. 76 /// Convert this result to a [Barcode].
77 Barcode get toBarcode { 77 Barcode get toBarcode {
  78 + final List<Offset> corners = resultPoints;
  79 +
78 return Barcode( 80 return Barcode(
79 - corners: resultPoints, 81 + corners: corners,
80 format: barcodeFormat, 82 format: barcodeFormat,
81 displayValue: text, 83 displayValue: text,
82 rawBytes: rawBytes, 84 rawBytes: rawBytes,
83 rawValue: text, 85 rawValue: text,
  86 + size: _computeSize(corners),
84 type: BarcodeType.text, 87 type: BarcodeType.text,
85 ); 88 );
86 } 89 }
  90 +
  91 + Size _computeSize(List<Offset> points) {
  92 + if (points.length != 4) {
  93 + return Size.zero;
  94 + }
  95 +
  96 + final Iterable<double> xCoords = points.map((p) => p.dx);
  97 + final Iterable<double> yCoords = points.map((p) => p.dy);
  98 +
  99 + // Find the minimum and maximum x and y coordinates.
  100 + final double xMin = xCoords.reduce((a, b) => a < b ? a : b);
  101 + final double xMax = xCoords.reduce((a, b) => a > b ? a : b);
  102 + final double yMin = yCoords.reduce((a, b) => a < b ? a : b);
  103 + final double yMax = yCoords.reduce((a, b) => a > b ? a : b);
  104 +
  105 + return Size(xMax - xMin, yMax - yMin);
  106 + }
87 } 107 }
@@ -138,11 +138,10 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -138,11 +138,10 @@ final class ZXingBarcodeReader extends BarcodeReader {
138 required web.MediaStream videoStream, 138 required web.MediaStream videoStream,
139 }) async { 139 }) async {
140 final int detectionTimeoutMs = options.detectionTimeoutMs; 140 final int detectionTimeoutMs = options.detectionTimeoutMs;
141 - final List<BarcodeFormat> formats = options.formats;  
142 -  
143 - if (formats.contains(BarcodeFormat.unknown)) {  
144 - formats.removeWhere((element) => element == BarcodeFormat.unknown);  
145 - } 141 + final List<BarcodeFormat> formats = [
  142 + for (final BarcodeFormat format in options.formats)
  143 + if (format != BarcodeFormat.unknown) format,
  144 + ];
146 145
147 _reader = ZXingBrowserMultiFormatReader( 146 _reader = ZXingBrowserMultiFormatReader(
148 _createReaderHints(formats), 147 _createReaderHints(formats),
@@ -71,6 +71,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -71,6 +71,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
71 stop(result) 71 stop(result)
72 case "updateScanWindow": 72 case "updateScanWindow":
73 updateScanWindow(call, result) 73 updateScanWindow(call, result)
  74 + case "analyzeImage":
  75 + analyzeImage(call, result)
74 default: 76 default:
75 result(FlutterMethodNotImplemented) 77 result(FlutterMethodNotImplemented)
76 } 78 }
@@ -124,7 +126,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -124,7 +126,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
124 VTCreateCGImageFromCVPixelBuffer(self!.latestBuffer, options: nil, imageOut: &cgImage) 126 VTCreateCGImageFromCVPixelBuffer(self!.latestBuffer, options: nil, imageOut: &cgImage)
125 let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage!) 127 let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage!)
126 do { 128 do {
127 - let barcodeRequest:VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in 129 + let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in
128 self?.imagesCurrentlyBeingProcessed = false 130 self?.imagesCurrentlyBeingProcessed = false
129 131
130 if error != nil { 132 if error != nil {
@@ -452,6 +454,70 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -452,6 +454,70 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
452 result(nil) 454 result(nil)
453 } 455 }
454 456
  457 + func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  458 + let argReader = MapArgumentReader(call.arguments as? [String: Any])
  459 + let symbologies:[VNBarcodeSymbology] = argReader.toSymbology()
  460 +
  461 + guard let filePath: String = argReader.string(key: "filePath") else {
  462 + // TODO: fix error code
  463 + result(FlutterError(code: "MobileScanner",
  464 + message: "No image found in analyzeImage!",
  465 + details: nil))
  466 + return
  467 + }
  468 +
  469 + let fileUrl = URL(fileURLWithPath: filePath)
  470 +
  471 + guard let ciImage = CIImage(contentsOf: fileUrl) else {
  472 + // TODO: fix error code
  473 + result(FlutterError(code: "MobileScanner",
  474 + message: "No image found in analyzeImage!",
  475 + details: nil))
  476 + return
  477 + }
  478 +
  479 + let imageRequestHandler = VNImageRequestHandler(ciImage: ciImage, orientation: CGImagePropertyOrientation.up, options: [:])
  480 +
  481 + do {
  482 + let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest(
  483 + completionHandler: { [] (request, error) in
  484 +
  485 + if error != nil {
  486 + DispatchQueue.main.async {
  487 + // TODO: fix error code
  488 + result(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil))
  489 + }
  490 + return
  491 + }
  492 +
  493 + guard let barcodes: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else {
  494 + return
  495 + }
  496 +
  497 + if barcodes.isEmpty {
  498 + return
  499 + }
  500 +
  501 + result([
  502 + "name": "barcode",
  503 + "data": barcodes.map({ $0.toMap() }),
  504 + ])
  505 + })
  506 +
  507 + if !symbologies.isEmpty {
  508 + // Add the symbologies the user wishes to support.
  509 + barcodeRequest.symbologies = symbologies
  510 + }
  511 +
  512 + try imageRequestHandler.perform([barcodeRequest])
  513 + } catch let e {
  514 + // TODO: fix error code
  515 + DispatchQueue.main.async {
  516 + result(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil))
  517 + }
  518 + }
  519 + }
  520 +
455 // Observer for torch state 521 // Observer for torch state
456 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 522 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
457 switch keyPath { 523 switch keyPath {
@@ -542,10 +608,24 @@ extension CGImage { @@ -542,10 +608,24 @@ extension CGImage {
542 } 608 }
543 609
544 extension VNBarcodeObservation { 610 extension VNBarcodeObservation {
  611 + private func distanceBetween(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat {
  612 + return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2))
  613 + }
  614 +
545 public func toMap() -> [String: Any?] { 615 public func toMap() -> [String: Any?] {
546 return [ 616 return [
547 - "rawValue": self.payloadStringValue ?? "",  
548 - "format": self.symbology.toInt ?? -1, 617 + "corners": [
  618 + ["x": topLeft.x, "y": topLeft.y],
  619 + ["x": topRight.x, "y": topRight.y],
  620 + ["x": bottomRight.x, "y": bottomRight.y],
  621 + ["x": bottomLeft.x, "y": bottomLeft.y],
  622 + ],
  623 + "format": symbology.toInt ?? -1,
  624 + "rawValue": payloadStringValue ?? "",
  625 + "size": [
  626 + "width": distanceBetween(topLeft, topRight),
  627 + "height": distanceBetween(topLeft, bottomLeft),
  628 + ],
549 ] 629 ]
550 } 630 }
551 } 631 }
@@ -585,7 +665,7 @@ extension VNBarcodeSymbology { @@ -585,7 +665,7 @@ extension VNBarcodeSymbology {
585 } 665 }
586 } 666 }
587 667
588 - var toInt:Int? { 668 + var toInt: Int? {
589 if #available(macOS 12.0, *) { 669 if #available(macOS 12.0, *) {
590 if(self == VNBarcodeSymbology.codabar){ 670 if(self == VNBarcodeSymbology.codabar){
591 return 8 671 return 8