Committed by
GitHub
Merge pull request #40 from juliansteenbakker/image-picker
Feature: Image picker
Showing
9 changed files
with
134 additions
and
12 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,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 |
| @@ -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 | } |
| @@ -205,6 +209,24 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -205,6 +209,24 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 205 | // result.success(null) | 209 | // result.success(null) |
| 206 | // } | 210 | // } |
| 207 | 211 | ||
| 212 | + private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { | ||
| 213 | + val uri = Uri.fromFile( File(call.arguments.toString())) | ||
| 214 | + val inputImage = InputImage.fromFilePath(activity, uri) | ||
| 215 | + | ||
| 216 | + var barcodeFound = false | ||
| 217 | + scanner.process(inputImage) | ||
| 218 | + .addOnSuccessListener { barcodes -> | ||
| 219 | + for (barcode in barcodes) { | ||
| 220 | + barcodeFound = true | ||
| 221 | + sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) | ||
| 222 | + } | ||
| 223 | + } | ||
| 224 | + .addOnFailureListener { e -> Log.e(TAG, e.message, e) | ||
| 225 | + result.error(TAG, e.message, e)} | ||
| 226 | + .addOnCompleteListener { result.success(barcodeFound) } | ||
| 227 | + | ||
| 228 | + } | ||
| 229 | + | ||
| 208 | private fun stop(result: MethodChannel.Result) { | 230 | private fun stop(result: MethodChannel.Result) { |
| 209 | if (camera == null) { | 231 | if (camera == null) { |
| 210 | result.error(TAG,"Called stop() while already stopped!", null) | 232 | result.error(TAG,"Called stop() while already stopped!", null) |
| @@ -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( |
| @@ -115,6 +116,32 @@ class _BarcodeScannerWithControllerState | @@ -115,6 +116,32 @@ class _BarcodeScannerWithControllerState | ||
| 115 | iconSize: 32.0, | 116 | iconSize: 32.0, |
| 116 | onPressed: () => controller.switchCamera(), | 117 | onPressed: () => controller.switchCamera(), |
| 117 | ), | 118 | ), |
| 119 | + IconButton( | ||
| 120 | + color: Colors.white, | ||
| 121 | + icon: const Icon(Icons.image), | ||
| 122 | + iconSize: 32.0, | ||
| 123 | + onPressed: () async { | ||
| 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 | + } | ||
| 143 | + }, | ||
| 144 | + ), | ||
| 118 | ], | 145 | ], |
| 119 | ), | 146 | ), |
| 120 | ), | 147 | ), |
| @@ -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,7 +208,16 @@ class MobileScannerController { | @@ -208,7 +208,16 @@ class MobileScannerController { | ||
| 208 | await start(); | 208 | await start(); |
| 209 | } | 209 | } |
| 210 | 210 | ||
| 211 | - /// Disposes the controller and closes all listeners. | 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); | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + /// Disposes the MobileScannerController and closes all listeners. | ||
| 212 | void dispose() { | 221 | void dispose() { |
| 213 | if (hashCode == _controllerHashcode) { | 222 | if (hashCode == _controllerHashcode) { |
| 214 | stop(); | 223 | stop(); |
| @@ -219,11 +228,11 @@ class MobileScannerController { | @@ -219,11 +228,11 @@ class MobileScannerController { | ||
| 219 | barcodesController.close(); | 228 | barcodesController.close(); |
| 220 | } | 229 | } |
| 221 | 230 | ||
| 222 | - /// Checks if the controller is bound to the correct MobileScanner object. | 231 | + /// Checks if the MobileScannerController is bound to the correct MobileScanner object. |
| 223 | void ensure(String name) { | 232 | void ensure(String name) { |
| 224 | final message = | 233 | final message = |
| 225 | - 'CameraController.$name called after CameraController.dispose\n' | ||
| 226 | - '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.'; | ||
| 227 | assert(hashCode == _controllerHashcode, message); | 236 | assert(hashCode == _controllerHashcode, message); |
| 228 | } | 237 | } |
| 229 | } | 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