Showing
8 changed files
with
101 additions
and
23 deletions
| 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 | + | ||
| 1 | ## 0.1.3 | 6 | ## 0.1.3 |
| 2 | * Fixed crash after asking permission. [#29](https://github.com/juliansteenbakker/mobile_scanner/issues/29) | 7 | * Fixed crash after asking permission. [#29](https://github.com/juliansteenbakker/mobile_scanner/issues/29) |
| 3 | * Upgraded cameraX from 1.1.0-beta01 to 1.1.0-beta02 | 8 | * Upgraded cameraX from 1.1.0-beta01 to 1.1.0-beta02 |
| @@ -11,9 +11,23 @@ A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX | @@ -11,9 +11,23 @@ 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: SDK 21 and newer. Reason: CameraX requires at least SDK 21. |
| 15 | +iOS: 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). | ||
| 16 | +macOS: macOS 10.13. Reason: Apple Vision library. | ||
| 15 | 17 | ||
| 16 | -MLKit for iOS requires at least iOS 11 and a [64bit device](https://developers.google.com/ml-kit/migration/ios). | 18 | +### iOS |
| 19 | +Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist: | ||
| 20 | + | ||
| 21 | +NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor. | ||
| 22 | + | ||
| 23 | +If you want to use the local gallery feature from [image_picker](https://pub.dev/packages/image_picker) | ||
| 24 | +NSPhotoLibraryUsageDescription - describe why your app needs permission for the photo library. This is called Privacy - Photo Library Usage Description in the visual editor. | ||
| 25 | + | ||
| 26 | +## Feature Support | ||
| 27 | + | ||
| 28 | +| Features | Android | iOS | macOS | Web | | ||
| 29 | +|------------------------|--------------------|--------------------|-------|-----| | ||
| 30 | +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | ||
| 17 | 31 | ||
| 18 | # Usage | 32 | # Usage |
| 19 | 33 |
| @@ -213,16 +213,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -213,16 +213,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 213 | val uri = Uri.fromFile( File(call.arguments.toString())) | 213 | val uri = Uri.fromFile( File(call.arguments.toString())) |
| 214 | val inputImage = InputImage.fromFilePath(activity, uri) | 214 | val inputImage = InputImage.fromFilePath(activity, uri) |
| 215 | 215 | ||
| 216 | + var barcodeFound = false | ||
| 216 | scanner.process(inputImage) | 217 | scanner.process(inputImage) |
| 217 | .addOnSuccessListener { barcodes -> | 218 | .addOnSuccessListener { barcodes -> |
| 218 | for (barcode in barcodes) { | 219 | for (barcode in barcodes) { |
| 219 | - val event = mapOf("name" to "barcode", "data" to barcode.data) | ||
| 220 | - sink?.success(event) | 220 | + barcodeFound = true |
| 221 | + sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) | ||
| 221 | } | 222 | } |
| 222 | } | 223 | } |
| 223 | .addOnFailureListener { e -> Log.e(TAG, e.message, e) | 224 | .addOnFailureListener { e -> Log.e(TAG, e.message, e) |
| 224 | result.error(TAG, e.message, e)} | 225 | result.error(TAG, e.message, e)} |
| 225 | - .addOnCompleteListener { result.success(null) } | 226 | + .addOnCompleteListener { result.success(barcodeFound) } |
| 226 | 227 | ||
| 227 | } | 228 | } |
| 228 | 229 |
| @@ -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 { |
| @@ -85,7 +86,7 @@ class _BarcodeScannerWithControllerState | @@ -85,7 +86,7 @@ class _BarcodeScannerWithControllerState | ||
| 85 | })), | 86 | })), |
| 86 | Center( | 87 | Center( |
| 87 | child: SizedBox( | 88 | child: SizedBox( |
| 88 | - width: MediaQuery.of(context).size.width - 160, | 89 | + width: MediaQuery.of(context).size.width - 200, |
| 89 | height: 50, | 90 | height: 50, |
| 90 | child: FittedBox( | 91 | child: FittedBox( |
| 91 | child: Text( | 92 | child: Text( |
| @@ -117,15 +118,28 @@ class _BarcodeScannerWithControllerState | @@ -117,15 +118,28 @@ class _BarcodeScannerWithControllerState | ||
| 117 | ), | 118 | ), |
| 118 | IconButton( | 119 | IconButton( |
| 119 | color: Colors.white, | 120 | color: Colors.white, |
| 120 | - icon: Icon(Icons.browse_gallery), | 121 | + icon: const Icon(Icons.image), |
| 121 | iconSize: 32.0, | 122 | iconSize: 32.0, |
| 122 | onPressed: () async { | 123 | onPressed: () async { |
| 123 | - // final ImagePicker _picker = ImagePicker(); | ||
| 124 | - // // Pick an image | ||
| 125 | - // final XFile? image = await _picker.pickImage(source: ImageSource.gallery); | ||
| 126 | - // if (image != null) { | ||
| 127 | - // controller.analyzeImage(image.path); | ||
| 128 | - // } | 124 | + final ImagePicker _picker = ImagePicker(); |
| 125 | + // Pick an image | ||
| 126 | + final XFile? image = await _picker.pickImage( | ||
| 127 | + source: ImageSource.gallery); | ||
| 128 | + if (image != null) { | ||
| 129 | + if (await controller.analyzeImage(image.path)) { | ||
| 130 | + ScaffoldMessenger.of(context) | ||
| 131 | + .showSnackBar(const SnackBar( | ||
| 132 | + content: Text('Barcode found!'), | ||
| 133 | + backgroundColor: Colors.green, | ||
| 134 | + )); | ||
| 135 | + } else { | ||
| 136 | + ScaffoldMessenger.of(context) | ||
| 137 | + .showSnackBar(const SnackBar( | ||
| 138 | + content: Text('No barcode found!'), | ||
| 139 | + backgroundColor: Colors.red, | ||
| 140 | + )); | ||
| 141 | + } | ||
| 142 | + } | ||
| 129 | }, | 143 | }, |
| 130 | ), | 144 | ), |
| 131 | ], | 145 | ], |
| @@ -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 | + let 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! { |
| @@ -255,6 +257,42 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -255,6 +257,42 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 255 | // result(nil) | 257 | // result(nil) |
| 256 | // } | 258 | // } |
| 257 | 259 | ||
| 260 | + func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 261 | + let uiImage = UIImage(contentsOfFile: call.arguments as! String) | ||
| 262 | + | ||
| 263 | + if (uiImage == nil) { | ||
| 264 | + result(FlutterError(code: "MobileScanner", | ||
| 265 | + message: "No image found in analyzeImage!", | ||
| 266 | + details: nil)) | ||
| 267 | + return | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + let image = VisionImage(image: uiImage!) | ||
| 271 | + image.orientation = imageOrientation( | ||
| 272 | + deviceOrientation: UIDevice.current.orientation, | ||
| 273 | + defaultOrientation: .portrait | ||
| 274 | + ) | ||
| 275 | + | ||
| 276 | + var barcodeFound = false | ||
| 277 | + | ||
| 278 | + scanner.process(image) { [self] barcodes, error in | ||
| 279 | + if error == nil && barcodes != nil { | ||
| 280 | + for barcode in barcodes! { | ||
| 281 | + barcodeFound = true | ||
| 282 | + let event: [String: Any?] = ["name": "barcode", "data": barcode.data] | ||
| 283 | + sink?(event) | ||
| 284 | + } | ||
| 285 | + } else if error != nil { | ||
| 286 | + result(FlutterError(code: "MobileScanner", | ||
| 287 | + message: error?.localizedDescription, | ||
| 288 | + details: "analyzeImage()")) | ||
| 289 | + } | ||
| 290 | + analyzing = false | ||
| 291 | + result(barcodeFound) | ||
| 292 | + } | ||
| 293 | + | ||
| 294 | + } | ||
| 295 | + | ||
| 258 | func stop(_ result: FlutterResult) { | 296 | func stop(_ result: FlutterResult) { |
| 259 | if (device == nil) { | 297 | if (device == nil) { |
| 260 | result(FlutterError(code: "MobileScanner", | 298 | result(FlutterError(code: "MobileScanner", |
| @@ -208,11 +208,16 @@ class MobileScannerController { | @@ -208,11 +208,16 @@ class MobileScannerController { | ||
| 208 | await start(); | 208 | await start(); |
| 209 | } | 209 | } |
| 210 | 210 | ||
| 211 | - Future<void> analyzeImage(dynamic path) async { | ||
| 212 | - await methodChannel.invokeMethod('analyzeImage', path); | 211 | + /// Handles a local image file. |
| 212 | + /// Returns true if a barcode or QR code is found. | ||
| 213 | + /// Returns false if nothing is found. | ||
| 214 | + /// | ||
| 215 | + /// [path] The path of the image on the devices | ||
| 216 | + Future<bool> analyzeImage(String path) async { | ||
| 217 | + return await methodChannel.invokeMethod('analyzeImage', path); | ||
| 213 | } | 218 | } |
| 214 | 219 | ||
| 215 | - /// Disposes the controller and closes all listeners. | 220 | + /// Disposes the MobileScannerController and closes all listeners. |
| 216 | void dispose() { | 221 | void dispose() { |
| 217 | if (hashCode == _controllerHashcode) { | 222 | if (hashCode == _controllerHashcode) { |
| 218 | stop(); | 223 | stop(); |
| @@ -223,11 +228,11 @@ class MobileScannerController { | @@ -223,11 +228,11 @@ class MobileScannerController { | ||
| 223 | barcodesController.close(); | 228 | barcodesController.close(); |
| 224 | } | 229 | } |
| 225 | 230 | ||
| 226 | - /// Checks if the controller is bound to the correct MobileScanner object. | 231 | + /// Checks if the MobileScannerController is bound to the correct MobileScanner object. |
| 227 | void ensure(String name) { | 232 | void ensure(String name) { |
| 228 | final message = | 233 | final message = |
| 229 | - 'CameraController.$name called after CameraController.dispose\n' | ||
| 230 | - 'CameraController methods should not be used after calling dispose.'; | 234 | + 'MobileScannerController.$name called after MobileScannerController.dispose\n' |
| 235 | + 'MobileScannerController methods should not be used after calling dispose.'; | ||
| 231 | assert(hashCode == _controllerHashcode, message); | 236 | assert(hashCode == _controllerHashcode, message); |
| 232 | } | 237 | } |
| 233 | } | 238 | } |
| 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.3 | 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" | ||
| 9 | 8 | ||
| 10 | dependencies: | 9 | dependencies: |
| 11 | flutter: | 10 | flutter: |
-
Please register or login to post a comment