Julian Steenbakker

Merge branch 'master' into web

  1 +## 0.2.0
  2 +You can provide a path to controller.analyzeImage(path) in order to scan a local photo from the gallery!
  3 +Check out the example app to see how you can use the image_picker plugin to retrieve a photo from
  4 +the gallery. Please keep in mind that this feature is only supported on Android and iOS.
  5 +
  6 +Another feature that has been added is a format selector!
  7 +Just keep in mind that iOS for now only supports 1 selected barcode.
  8 +
  9 +## 0.1.3
  10 +* Fixed crash after asking permission. [#29](https://github.com/juliansteenbakker/mobile_scanner/issues/29)
  11 +* Upgraded cameraX from 1.1.0-beta01 to 1.1.0-beta02
  12 +
1 ## 0.1.2 13 ## 0.1.2
2 * MobileScannerArguments is now exported. [#7](https://github.com/juliansteenbakker/mobile_scanner/issues/7) 14 * MobileScannerArguments is now exported. [#7](https://github.com/juliansteenbakker/mobile_scanner/issues/7)
3 15
@@ -11,11 +11,30 @@ A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX @@ -11,11 +11,30 @@ A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX
11 | :-----: | :-: | :---: | :-: | :---: | :-----: | 11 | :-----: | :-: | :---: | :-: | :---: | :-----: |
12 | ✔️ | ✔️ | ✔️ | | | | 12 | ✔️ | ✔️ | ✔️ | | | |
13 13
14 -CameraX for Android requires at least SDK 21. 14 +### Android
  15 +SDK 21 and newer. Reason: CameraX requires at least SDK 21.
15 16
16 -MLKit for iOS requires at least iOS 11 and a [64bit device](https://developers.google.com/ml-kit/migration/ios). 17 +### iOS
  18 +iOS 11 and newer. Reason: MLKit for iOS requires at least iOS 11 and a [64bit device](https://developers.google.com/ml-kit/migration/ios).
17 19
18 -# Usage 20 +**Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:**
  21 +
  22 +NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor.
  23 +
  24 +**If you want to use the local gallery feature from [image_picker](https://pub.dev/packages/image_picker)**
  25 +
  26 +NSPhotoLibraryUsageDescription - describe why your app needs permission for the photo library. This is called Privacy - Photo Library Usage Description in the visual editor.
  27 +
  28 +### macOS
  29 +macOS 10.13 or newer. Reason: Apple Vision library.
  30 +
  31 +## Features Supported
  32 +
  33 +| Features | Android | iOS | macOS | Web |
  34 +|------------------------|--------------------|--------------------|-------|-----|
  35 +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
  36 +
  37 +## Usage
19 38
20 Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller. 39 Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller.
21 40
@@ -9,7 +9,7 @@ buildscript { @@ -9,7 +9,7 @@ buildscript {
9 } 9 }
10 10
11 dependencies { 11 dependencies {
12 - classpath 'com.android.tools.build:gradle:7.1.1' 12 + classpath 'com.android.tools.build:gradle:7.1.2'
13 } 13 }
14 } 14 }
15 15
@@ -47,8 +47,8 @@ android { @@ -47,8 +47,8 @@ android {
47 dependencies { 47 dependencies {
48 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 48 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
49 implementation 'com.google.mlkit:barcode-scanning:17.0.2' 49 implementation 'com.google.mlkit:barcode-scanning:17.0.2'
50 - implementation "androidx.camera:camera-camera2:1.1.0-beta01"  
51 - implementation 'androidx.camera:camera-lifecycle:1.1.0-beta01' 50 + implementation "androidx.camera:camera-camera2:1.1.0-beta02"
  51 + implementation 'androidx.camera:camera-lifecycle:1.1.0-beta02'
52 52
53 // // The following line is optional, as the core library is included indirectly by camera-camera2 53 // // The following line is optional, as the core library is included indirectly by camera-camera2
54 // implementation "androidx.camera:camera-core:1.1.0-alpha11" 54 // implementation "androidx.camera:camera-core:1.1.0-alpha11"
1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 import com.google.mlkit.vision.barcode.BarcodeScannerOptions 3 import com.google.mlkit.vision.barcode.BarcodeScannerOptions
4 -import java.util.ArrayList  
5 4
6 enum class BarcodeFormats(val intValue: Int) { 5 enum class BarcodeFormats(val intValue: Int) {
  6 + UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN),
7 ALL_FORMATS(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_ALL_FORMATS), CODE_128(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_128), CODE_39( 7 ALL_FORMATS(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_ALL_FORMATS), CODE_128(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_128), CODE_39(
8 com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_39 8 com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_39
9 ), 9 ),
@@ -17,76 +17,4 @@ enum class BarcodeFormats(val intValue: Int) { @@ -17,76 +17,4 @@ enum class BarcodeFormats(val intValue: Int) {
17 com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UPC_E 17 com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UPC_E
18 ), 18 ),
19 PDF417(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_PDF417), AZTEC(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_AZTEC); 19 PDF417(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_PDF417), AZTEC(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_AZTEC);
20 -  
21 - companion object {  
22 - private var formatsMap: MutableMap<String, Int>? = null  
23 -  
24 - /**  
25 - * Return the integer value resuling from OR-ing all of the values  
26 - * of the supplied strings.  
27 - *  
28 - *  
29 - * Note that if ALL_FORMATS is defined as well as other values, ALL_FORMATS  
30 - * will be ignored (following how it would work with just OR-ing the ints).  
31 - *  
32 - * @param strings - list of strings representing the various formats  
33 - * @return integer value corresponding to OR of all the values.  
34 - */  
35 - fun intFromStringList(strings: List<String>?): Int {  
36 - if (strings == null) return ALL_FORMATS.intValue  
37 - var `val` = 0  
38 - for (string in strings) {  
39 - val asInt = formatsMap!![string]  
40 - if (asInt != null) {  
41 - `val` = `val` or asInt  
42 - }  
43 - }  
44 - return `val`  
45 - }  
46 -  
47 - fun optionsFromStringList(strings: List<String>?): BarcodeScannerOptions {  
48 - if (strings == null) {  
49 - return BarcodeScannerOptions.Builder().setBarcodeFormats(ALL_FORMATS.intValue)  
50 - .build()  
51 - }  
52 - val ints: MutableList<Int> = ArrayList(strings.size)  
53 - run {  
54 - var i = 0  
55 - val l = strings.size  
56 - while (i < l) {  
57 - val integer =  
58 - formatsMap!![strings[i]]  
59 - if (integer != null) {  
60 - ints.add(integer)  
61 - }  
62 - ++i  
63 - }  
64 - }  
65 - if (ints.size == 0) {  
66 - return BarcodeScannerOptions.Builder().setBarcodeFormats(ALL_FORMATS.intValue)  
67 - .build()  
68 - }  
69 - if (ints.size == 1) {  
70 - return BarcodeScannerOptions.Builder().setBarcodeFormats(ints[0]).build()  
71 - }  
72 - val first = ints[0]  
73 - val rest = IntArray(ints.size - 1)  
74 - var i = 0  
75 - for (e in ints.subList(1, ints.size)) {  
76 - rest[i++] = e  
77 - }  
78 - return BarcodeScannerOptions.Builder()  
79 - .setBarcodeFormats(first, *rest).build()  
80 - }  
81 -  
82 - init {  
83 - val values = values()  
84 - formatsMap =  
85 - HashMap<String, Int>(values.size * 4 / 3)  
86 - for (value in values) {  
87 - formatsMap!![value.name] =  
88 - value.intValue  
89 - }  
90 - }  
91 - }  
92 } 20 }
@@ -4,6 +4,7 @@ import android.Manifest @@ -4,6 +4,7 @@ import android.Manifest
4 import android.app.Activity 4 import android.app.Activity
5 import android.content.pm.PackageManager 5 import android.content.pm.PackageManager
6 import android.graphics.Point 6 import android.graphics.Point
  7 +import android.net.Uri
7 import android.util.Log 8 import android.util.Log
8 import android.util.Size 9 import android.util.Size
9 import android.view.Surface 10 import android.view.Surface
@@ -22,6 +23,8 @@ import io.flutter.plugin.common.MethodCall @@ -22,6 +23,8 @@ import io.flutter.plugin.common.MethodCall
22 import io.flutter.plugin.common.MethodChannel 23 import io.flutter.plugin.common.MethodChannel
23 import io.flutter.plugin.common.PluginRegistry 24 import io.flutter.plugin.common.PluginRegistry
24 import io.flutter.view.TextureRegistry 25 import io.flutter.view.TextureRegistry
  26 +import java.io.File
  27 +import java.net.URI
25 28
26 29
27 class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) 30 class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)
@@ -50,6 +53,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -50,6 +53,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
50 "torch" -> toggleTorch(call, result) 53 "torch" -> toggleTorch(call, result)
51 // "analyze" -> switchAnalyzeMode(call, result) 54 // "analyze" -> switchAnalyzeMode(call, result)
52 "stop" -> stop(result) 55 "stop" -> stop(result)
  56 + "analyzeImage" -> analyzeImage(call, result)
53 else -> result.notImplemented() 57 else -> result.notImplemented()
54 } 58 }
55 } 59 }
@@ -124,11 +128,18 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -124,11 +128,18 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
124 val facing: Int = call.argument<Int>("facing") ?: 0 128 val facing: Int = call.argument<Int>("facing") ?: 0
125 val ratio: Int? = call.argument<Int>("ratio") 129 val ratio: Int? = call.argument<Int>("ratio")
126 val torch: Boolean = call.argument<Boolean>("torch") ?: false 130 val torch: Boolean = call.argument<Boolean>("torch") ?: false
127 - val formatStrings: List<String>? = call.argument<List<String>>("formats") 131 + val formats: List<Int>? = call.argument<List<Int>>("formats")
128 132
129 - if (formatStrings != null) {  
130 - val options: BarcodeScannerOptions = BarcodeFormats.optionsFromStringList(formatStrings)  
131 - scanner = BarcodeScanning.getClient(options) 133 + if (formats != null) {
  134 + val formatsList: MutableList<Int> = mutableListOf()
  135 + for (index in formats) {
  136 + formatsList.add(BarcodeFormats.values()[index].intValue)
  137 + }
  138 + scanner = if (formatsList.size == 1) {
  139 + BarcodeScanning.getClient( BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()).build());
  140 + } else {
  141 + BarcodeScanning.getClient( BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first(), *formatsList.subList(1, formatsList.size).toIntArray() ).build());
  142 + }
132 } 143 }
133 144
134 val future = ProcessCameraProvider.getInstance(activity) 145 val future = ProcessCameraProvider.getInstance(activity)
@@ -136,6 +147,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -136,6 +147,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
136 147
137 future.addListener({ 148 future.addListener({
138 cameraProvider = future.get() 149 cameraProvider = future.get()
  150 + cameraProvider!!.unbindAll()
139 textureEntry = textureRegistry.createSurfaceTexture() 151 textureEntry = textureRegistry.createSurfaceTexture()
140 152
141 // Preview 153 // Preview
@@ -204,6 +216,24 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -204,6 +216,24 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
204 // result.success(null) 216 // result.success(null)
205 // } 217 // }
206 218
  219 + private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
  220 + val uri = Uri.fromFile( File(call.arguments.toString()))
  221 + val inputImage = InputImage.fromFilePath(activity, uri)
  222 +
  223 + var barcodeFound = false
  224 + scanner.process(inputImage)
  225 + .addOnSuccessListener { barcodes ->
  226 + for (barcode in barcodes) {
  227 + barcodeFound = true
  228 + sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
  229 + }
  230 + }
  231 + .addOnFailureListener { e -> Log.e(TAG, e.message, e)
  232 + result.error(TAG, e.message, e)}
  233 + .addOnCompleteListener { result.success(barcodeFound) }
  234 +
  235 + }
  236 +
207 private fun stop(result: MethodChannel.Result) { 237 private fun stop(result: MethodChannel.Result) {
208 if (camera == null) { 238 if (camera == null) {
209 result.error(TAG,"Called stop() while already stopped!", null) 239 result.error(TAG,"Called stop() while already stopped!", null)
@@ -6,7 +6,7 @@ buildscript { @@ -6,7 +6,7 @@ buildscript {
6 } 6 }
7 7
8 dependencies { 8 dependencies {
9 - classpath 'com.android.tools.build:gradle:7.1.1' 9 + classpath 'com.android.tools.build:gradle:7.1.2'
10 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 } 11 }
12 } 12 }
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 <plist version="1.0"> 3 <plist version="1.0">
4 <dict> 4 <dict>
  5 + <key>NSPhotoLibraryUsageDescription</key>
  6 + <string>We need access in order to open photos of barcodes</string>
5 <key>NSCameraUsageDescription</key> 7 <key>NSCameraUsageDescription</key>
6 <string>We use the camera to scan barcodes</string> 8 <string>We use the camera to scan barcodes</string>
7 <key>CFBundleDevelopmentRegion</key> 9 <key>CFBundleDevelopmentRegion</key>
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
  2 +import 'package:image_picker/image_picker.dart';
2 import 'package:mobile_scanner/mobile_scanner.dart'; 3 import 'package:mobile_scanner/mobile_scanner.dart';
3 4
4 class BarcodeScannerWithController extends StatefulWidget { 5 class BarcodeScannerWithController extends StatefulWidget {
@@ -16,9 +17,12 @@ class _BarcodeScannerWithControllerState @@ -16,9 +17,12 @@ class _BarcodeScannerWithControllerState
16 17
17 MobileScannerController controller = MobileScannerController( 18 MobileScannerController controller = MobileScannerController(
18 torchEnabled: true, 19 torchEnabled: true,
  20 + // formats: [BarcodeFormat.qrCode]
19 // facing: CameraFacing.front, 21 // facing: CameraFacing.front,
20 ); 22 );
21 23
  24 + bool isStarted = true;
  25 +
22 @override 26 @override
23 Widget build(BuildContext context) { 27 Widget build(BuildContext context) {
24 return MaterialApp( 28 return MaterialApp(
@@ -69,9 +73,21 @@ class _BarcodeScannerWithControllerState @@ -69,9 +73,21 @@ class _BarcodeScannerWithControllerState
69 iconSize: 32.0, 73 iconSize: 32.0,
70 onPressed: () => controller.toggleTorch(), 74 onPressed: () => controller.toggleTorch(),
71 ), 75 ),
  76 + IconButton(
  77 + color: Colors.white,
  78 + icon: isStarted
  79 + ? const Icon(Icons.stop)
  80 + : const Icon(Icons.play_arrow),
  81 + iconSize: 32.0,
  82 + onPressed: () => setState(() {
  83 + isStarted
  84 + ? controller.stop()
  85 + : controller.start();
  86 + isStarted = !isStarted;
  87 + })),
72 Center( 88 Center(
73 child: SizedBox( 89 child: SizedBox(
74 - width: MediaQuery.of(context).size.width - 120, 90 + width: MediaQuery.of(context).size.width - 200,
75 height: 50, 91 height: 50,
76 child: FittedBox( 92 child: FittedBox(
77 child: Text( 93 child: Text(
@@ -101,6 +117,32 @@ class _BarcodeScannerWithControllerState @@ -101,6 +117,32 @@ class _BarcodeScannerWithControllerState
101 iconSize: 32.0, 117 iconSize: 32.0,
102 onPressed: () => controller.switchCamera(), 118 onPressed: () => controller.switchCamera(),
103 ), 119 ),
  120 + IconButton(
  121 + color: Colors.white,
  122 + icon: const Icon(Icons.image),
  123 + iconSize: 32.0,
  124 + onPressed: () async {
  125 + final ImagePicker _picker = ImagePicker();
  126 + // Pick an image
  127 + final XFile? image = await _picker.pickImage(
  128 + source: ImageSource.gallery);
  129 + if (image != null) {
  130 + if (await controller.analyzeImage(image.path)) {
  131 + ScaffoldMessenger.of(context)
  132 + .showSnackBar(const SnackBar(
  133 + content: Text('Barcode found!'),
  134 + backgroundColor: Colors.green,
  135 + ));
  136 + } else {
  137 + ScaffoldMessenger.of(context)
  138 + .showSnackBar(const SnackBar(
  139 + content: Text('No barcode found!'),
  140 + backgroundColor: Colors.red,
  141 + ));
  142 + }
  143 + }
  144 + },
  145 + ),
104 ], 146 ],
105 ), 147 ),
106 ), 148 ),
@@ -6,6 +6,7 @@ environment: @@ -6,6 +6,7 @@ environment:
6 sdk: ">=2.12.0 <3.0.0" 6 sdk: ">=2.12.0 <3.0.0"
7 7
8 dependencies: 8 dependencies:
  9 + image_picker: ^0.8.4+9
9 flutter: 10 flutter:
10 sdk: flutter 11 sdk: flutter
11 12
@@ -22,11 +22,12 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -22,11 +22,12 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
22 // Image to be sent to the texture 22 // Image to be sent to the texture
23 var latestBuffer: CVImageBuffer! 23 var latestBuffer: CVImageBuffer!
24 24
25 -  
26 // var analyzeMode: Int = 0 25 // var analyzeMode: Int = 0
27 var analyzing: Bool = false 26 var analyzing: Bool = false
28 var position = AVCaptureDevice.Position.back 27 var position = AVCaptureDevice.Position.back
29 28
  29 + var scanner = BarcodeScanner.barcodeScanner()
  30 +
30 public static func register(with registrar: FlutterPluginRegistrar) { 31 public static func register(with registrar: FlutterPluginRegistrar) {
31 let instance = SwiftMobileScannerPlugin(registrar.textures()) 32 let instance = SwiftMobileScannerPlugin(registrar.textures())
32 33
@@ -58,6 +59,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -58,6 +59,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
58 // switchAnalyzeMode(call, result) 59 // switchAnalyzeMode(call, result)
59 case "stop": 60 case "stop":
60 stop(result) 61 stop(result)
  62 + case "analyzeImage":
  63 + analyzeImage(call, result)
61 default: 64 default:
62 result(FlutterMethodNotImplemented) 65 result(FlutterMethodNotImplemented)
63 } 66 }
@@ -102,7 +105,6 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -102,7 +105,6 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
102 defaultOrientation: .portrait 105 defaultOrientation: .portrait
103 ) 106 )
104 107
105 - let scanner = BarcodeScanner.barcodeScanner()  
106 scanner.process(image) { [self] barcodes, error in 108 scanner.process(image) { [self] barcodes, error in
107 if error == nil && barcodes != nil { 109 if error == nil && barcodes != nil {
108 for barcode in barcodes! { 110 for barcode in barcodes! {
@@ -169,6 +171,17 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -169,6 +171,17 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
169 // let ratio: Int = argReader.int(key: "ratio") 171 // let ratio: Int = argReader.int(key: "ratio")
170 let torch: Bool = argReader.bool(key: "torch") ?? false 172 let torch: Bool = argReader.bool(key: "torch") ?? false
171 let facing: Int = argReader.int(key: "facing") ?? 1 173 let facing: Int = argReader.int(key: "facing") ?? 1
  174 + let formats: Array = argReader.intArray(key: "formats") ?? []
  175 +
  176 + let formatList: NSMutableArray = []
  177 + for index in formats {
  178 + formatList.add(BarcodeFormat(rawValue: index))
  179 + }
  180 +
  181 + if (formatList.count != 0) {
  182 + let barcodeOptions = BarcodeScannerOptions(formats: formatList.firstObject as! BarcodeFormat)
  183 + scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions)
  184 + }
172 185
173 // Set the camera to use 186 // Set the camera to use
174 position = facing == 0 ? AVCaptureDevice.Position.front : .back 187 position = facing == 0 ? AVCaptureDevice.Position.front : .back
@@ -255,6 +268,42 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -255,6 +268,42 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
255 // result(nil) 268 // result(nil)
256 // } 269 // }
257 270
  271 + func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  272 + let uiImage = UIImage(contentsOfFile: call.arguments as! String)
  273 +
  274 + if (uiImage == nil) {
  275 + result(FlutterError(code: "MobileScanner",
  276 + message: "No image found in analyzeImage!",
  277 + details: nil))
  278 + return
  279 + }
  280 +
  281 + let image = VisionImage(image: uiImage!)
  282 + image.orientation = imageOrientation(
  283 + deviceOrientation: UIDevice.current.orientation,
  284 + defaultOrientation: .portrait
  285 + )
  286 +
  287 + var barcodeFound = false
  288 +
  289 + scanner.process(image) { [self] barcodes, error in
  290 + if error == nil && barcodes != nil {
  291 + for barcode in barcodes! {
  292 + barcodeFound = true
  293 + let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
  294 + sink?(event)
  295 + }
  296 + } else if error != nil {
  297 + result(FlutterError(code: "MobileScanner",
  298 + message: error?.localizedDescription,
  299 + details: "analyzeImage()"))
  300 + }
  301 + analyzing = false
  302 + result(barcodeFound)
  303 + }
  304 +
  305 + }
  306 +
258 func stop(_ result: FlutterResult) { 307 func stop(_ result: FlutterResult) {
259 if (device == nil) { 308 if (device == nil) {
260 result(FlutterError(code: "MobileScanner", 309 result(FlutterError(code: "MobileScanner",
@@ -319,4 +368,8 @@ class MapArgumentReader { @@ -319,4 +368,8 @@ class MapArgumentReader {
319 return args?[key] as? [String] 368 return args?[key] as? [String]
320 } 369 }
321 370
  371 + func intArray(key: String) -> [Int]? {
  372 + return args?[key] as? [Int]
  373 + }
  374 +
322 } 375 }
@@ -29,12 +29,9 @@ class MobileScanner extends StatefulWidget { @@ -29,12 +29,9 @@ class MobileScanner extends StatefulWidget {
29 final BoxFit fit; 29 final BoxFit fit;
30 30
31 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. 31 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
32 - const MobileScanner({  
33 - Key? key,  
34 - this.onDetect,  
35 - this.controller,  
36 - this.fit = BoxFit.cover,  
37 - }) : super(key: key); 32 + const MobileScanner(
  33 + {Key? key, this.onDetect, this.controller, this.fit = BoxFit.cover})
  34 + : super(key: key);
38 35
39 @override 36 @override
40 State<MobileScanner> createState() => _MobileScannerState(); 37 State<MobileScanner> createState() => _MobileScannerState();
@@ -55,7 +52,7 @@ class _MobileScannerState extends State<MobileScanner> @@ -55,7 +52,7 @@ class _MobileScannerState extends State<MobileScanner>
55 void didChangeAppLifecycleState(AppLifecycleState state) { 52 void didChangeAppLifecycleState(AppLifecycleState state) {
56 switch (state) { 53 switch (state) {
57 case AppLifecycleState.resumed: 54 case AppLifecycleState.resumed:
58 - controller.start(); 55 + if (!controller.isStarting) controller.start();
59 break; 56 break;
60 case AppLifecycleState.inactive: 57 case AppLifecycleState.inactive:
61 case AppLifecycleState.paused: 58 case AppLifecycleState.paused:
1 import 'dart:async'; 1 import 'dart:async';
  2 +import 'dart:io';
2 3
3 import 'package:flutter/cupertino.dart'; 4 import 'package:flutter/cupertino.dart';
4 import 'package:flutter/services.dart'; 5 import 'package:flutter/services.dart';
@@ -44,6 +45,11 @@ class MobileScannerController { @@ -44,6 +45,11 @@ class MobileScannerController {
44 final Ratio? ratio; 45 final Ratio? ratio;
45 final bool? torchEnabled; 46 final bool? torchEnabled;
46 47
  48 + /// If provided, the scanner will only detect those specific formats.
  49 + ///
  50 + /// WARNING: On iOS, only 1 format is supported.
  51 + final List<BarcodeFormat>? formats;
  52 +
47 CameraFacing facing; 53 CameraFacing facing;
48 bool hasTorch = false; 54 bool hasTorch = false;
49 late StreamController<Barcode> barcodesController; 55 late StreamController<Barcode> barcodesController;
@@ -51,7 +57,10 @@ class MobileScannerController { @@ -51,7 +57,10 @@ class MobileScannerController {
51 Stream<Barcode> get barcodes => barcodesController.stream; 57 Stream<Barcode> get barcodes => barcodesController.stream;
52 58
53 MobileScannerController( 59 MobileScannerController(
54 - {this.facing = CameraFacing.back, this.ratio, this.torchEnabled}) { 60 + {this.facing = CameraFacing.back,
  61 + this.ratio,
  62 + this.torchEnabled,
  63 + this.formats}) {
55 // In case a new instance is created before calling dispose() 64 // In case a new instance is created before calling dispose()
56 if (_controllerHashcode != null) { 65 if (_controllerHashcode != null) {
57 stop(); 66 stop();
@@ -103,12 +112,18 @@ class MobileScannerController { @@ -103,12 +112,18 @@ class MobileScannerController {
103 // } 112 // }
104 113
105 // List<BarcodeFormats>? formats = _defaultBarcodeFormats, 114 // List<BarcodeFormats>? formats = _defaultBarcodeFormats,
  115 + bool isStarting = false;
  116 +
106 /// Start barcode scanning. This will first check if the required permissions 117 /// Start barcode scanning. This will first check if the required permissions
107 /// are set. 118 /// are set.
108 Future<void> start() async { 119 Future<void> start() async {
109 ensure('startAsync'); 120 ensure('startAsync');
110 - 121 + if (isStarting) {
  122 + throw Exception('mobile_scanner: Called start() while already starting.');
  123 + }
  124 + isStarting = true;
111 // setAnalyzeMode(AnalyzeMode.barcode.index); 125 // setAnalyzeMode(AnalyzeMode.barcode.index);
  126 +
112 // Check authorization status 127 // Check authorization status
113 MobileScannerState state = 128 MobileScannerState state =
114 MobileScannerState.values[await methodChannel.invokeMethod('state')]; 129 MobileScannerState.values[await methodChannel.invokeMethod('state')];
@@ -119,6 +134,7 @@ class MobileScannerController { @@ -119,6 +134,7 @@ class MobileScannerController {
119 result ? MobileScannerState.authorized : MobileScannerState.denied; 134 result ? MobileScannerState.authorized : MobileScannerState.denied;
120 break; 135 break;
121 case MobileScannerState.denied: 136 case MobileScannerState.denied:
  137 + isStarting = false;
122 throw PlatformException(code: 'NO ACCESS'); 138 throw PlatformException(code: 'NO ACCESS');
123 case MobileScannerState.authorized: 139 case MobileScannerState.authorized:
124 break; 140 break;
@@ -132,6 +148,14 @@ class MobileScannerController { @@ -132,6 +148,14 @@ class MobileScannerController {
132 if (ratio != null) arguments['ratio'] = ratio; 148 if (ratio != null) arguments['ratio'] = ratio;
133 if (torchEnabled != null) arguments['torch'] = torchEnabled; 149 if (torchEnabled != null) arguments['torch'] = torchEnabled;
134 150
  151 + if (formats != null) {
  152 + if (Platform.isAndroid) {
  153 + arguments['formats'] = formats!.map((e) => e.index).toList();
  154 + } else if (Platform.isIOS || Platform.isMacOS) {
  155 + arguments['formats'] = formats!.map((e) => e.rawValue).toList();
  156 + }
  157 + }
  158 +
135 // Start the camera with arguments 159 // Start the camera with arguments
136 Map<String, dynamic>? startResult = {}; 160 Map<String, dynamic>? startResult = {};
137 try { 161 try {
@@ -139,11 +163,13 @@ class MobileScannerController { @@ -139,11 +163,13 @@ class MobileScannerController {
139 'start', arguments); 163 'start', arguments);
140 } on PlatformException catch (error) { 164 } on PlatformException catch (error) {
141 debugPrint('${error.code}: ${error.message}'); 165 debugPrint('${error.code}: ${error.message}');
  166 + isStarting = false;
142 // setAnalyzeMode(AnalyzeMode.none.index); 167 // setAnalyzeMode(AnalyzeMode.none.index);
143 return; 168 return;
144 } 169 }
145 170
146 if (startResult == null) { 171 if (startResult == null) {
  172 + isStarting = false;
147 throw PlatformException(code: 'INITIALIZATION ERROR'); 173 throw PlatformException(code: 'INITIALIZATION ERROR');
148 } 174 }
149 175
@@ -152,6 +178,7 @@ class MobileScannerController { @@ -152,6 +178,7 @@ class MobileScannerController {
152 textureId: startResult['textureId'], 178 textureId: startResult['textureId'],
153 size: toSize(startResult['size']), 179 size: toSize(startResult['size']),
154 hasTorch: hasTorch); 180 hasTorch: hasTorch);
  181 + isStarting = false;
155 } 182 }
156 183
157 Future<void> stop() async { 184 Future<void> stop() async {
@@ -199,7 +226,16 @@ class MobileScannerController { @@ -199,7 +226,16 @@ class MobileScannerController {
199 await start(); 226 await start();
200 } 227 }
201 228
202 - /// Disposes the controller and closes all listeners. 229 + /// Handles a local image file.
  230 + /// Returns true if a barcode or QR code is found.
  231 + /// Returns false if nothing is found.
  232 + ///
  233 + /// [path] The path of the image on the devices
  234 + Future<bool> analyzeImage(String path) async {
  235 + return await methodChannel.invokeMethod('analyzeImage', path);
  236 + }
  237 +
  238 + /// Disposes the MobileScannerController and closes all listeners.
203 void dispose() { 239 void dispose() {
204 if (hashCode == _controllerHashcode) { 240 if (hashCode == _controllerHashcode) {
205 stop(); 241 stop();
@@ -210,11 +246,11 @@ class MobileScannerController { @@ -210,11 +246,11 @@ class MobileScannerController {
210 barcodesController.close(); 246 barcodesController.close();
211 } 247 }
212 248
213 - /// Checks if the controller is bound to the correct MobileScanner object. 249 + /// Checks if the MobileScannerController is bound to the correct MobileScanner object.
214 void ensure(String name) { 250 void ensure(String name) {
215 final message = 251 final message =
216 - 'CameraController.$name called after CameraController.dispose\n'  
217 - 'CameraController methods should not be used after calling dispose.'; 252 + 'MobileScannerController.$name called after MobileScannerController.dispose\n'
  253 + 'MobileScannerController methods should not be used after calling dispose.';
218 assert(hashCode == _controllerHashcode, message); 254 assert(hashCode == _controllerHashcode, message);
219 } 255 }
220 } 256 }
@@ -553,6 +553,43 @@ enum BarcodeFormat { @@ -553,6 +553,43 @@ enum BarcodeFormat {
553 aztec, 553 aztec,
554 } 554 }
555 555
  556 +extension BarcodeValue on BarcodeFormat {
  557 + int get rawValue {
  558 + switch (this) {
  559 + case BarcodeFormat.unknown:
  560 + return -1;
  561 + case BarcodeFormat.all:
  562 + return 0;
  563 + case BarcodeFormat.code128:
  564 + return 1;
  565 + case BarcodeFormat.code39:
  566 + return 2;
  567 + case BarcodeFormat.code93:
  568 + return 4;
  569 + case BarcodeFormat.codebar:
  570 + return 8;
  571 + case BarcodeFormat.dataMatrix:
  572 + return 16;
  573 + case BarcodeFormat.ean13:
  574 + return 32;
  575 + case BarcodeFormat.ean8:
  576 + return 64;
  577 + case BarcodeFormat.itf:
  578 + return 128;
  579 + case BarcodeFormat.qrCode:
  580 + return 256;
  581 + case BarcodeFormat.upcA:
  582 + return 512;
  583 + case BarcodeFormat.upcE:
  584 + return 1024;
  585 + case BarcodeFormat.pdf417:
  586 + return 2048;
  587 + case BarcodeFormat.aztec:
  588 + return 4096;
  589 + }
  590 + }
  591 +}
  592 +
556 /// Address type constants. 593 /// Address type constants.
557 enum AddressType { 594 enum AddressType {
558 /// Unknown address type. 595 /// Unknown address type.
1 name: mobile_scanner 1 name: mobile_scanner
2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. 2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
3 -version: 0.1.2 3 +version: 0.2.0
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 environment: 6 environment:
7 sdk: ">=2.12.0 <3.0.0" 7 sdk: ">=2.12.0 <3.0.0"
8 - flutter: ">=2.2.0" 8 + flutter: ">=1.10.0"
9 9
10 dependencies: 10 dependencies:
11 js: ^0.6.4 11 js: ^0.6.4