Julian Steenbakker

feat: add local image scanner for Android and iOS

## 0.2.0
You can provide a path to controller.analyzeImage(path) in order to scan a local photo from the gallery!
Check out the example app to see how you can use the image_picker plugin to retrieve a photo from
the gallery. Please keep in mind that this feature is only supported on Android and iOS.
## 0.1.3
* Fixed crash after asking permission. [#29](https://github.com/juliansteenbakker/mobile_scanner/issues/29)
* 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
| :-----: | :-: | :---: | :-: | :---: | :-----: |
| ✔️ | ✔️ | ✔️ | | | |
CameraX for Android requires at least SDK 21.
Android: SDK 21 and newer. Reason: CameraX requires at least SDK 21.
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).
macOS: macOS 10.13. Reason: Apple Vision library.
MLKit for iOS requires at least iOS 11 and a [64bit device](https://developers.google.com/ml-kit/migration/ios).
### iOS
Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:
NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor.
If you want to use the local gallery feature from [image_picker](https://pub.dev/packages/image_picker)
NSPhotoLibraryUsageDescription - describe why your app needs permission for the photo library. This is called Privacy - Photo Library Usage Description in the visual editor.
## Feature Support
| Features | Android | iOS | macOS | Web |
|------------------------|--------------------|--------------------|-------|-----|
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
# Usage
... ...
... ... @@ -213,16 +213,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
val uri = Uri.fromFile( File(call.arguments.toString()))
val inputImage = InputImage.fromFilePath(activity, uri)
var barcodeFound = false
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
val event = mapOf("name" to "barcode", "data" to barcode.data)
sink?.success(event)
barcodeFound = true
sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
}
}
.addOnFailureListener { e -> Log.e(TAG, e.message, e)
result.error(TAG, e.message, e)}
.addOnCompleteListener { result.success(null) }
.addOnCompleteListener { result.success(barcodeFound) }
}
... ...
... ... @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access in order to open photos of barcodes</string>
<key>NSCameraUsageDescription</key>
<string>We use the camera to scan barcodes</string>
<key>CFBundleDevelopmentRegion</key>
... ...
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeScannerWithController extends StatefulWidget {
... ... @@ -85,7 +86,7 @@ class _BarcodeScannerWithControllerState
})),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 160,
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
... ... @@ -117,15 +118,28 @@ class _BarcodeScannerWithControllerState
),
IconButton(
color: Colors.white,
icon: Icon(Icons.browse_gallery),
icon: const Icon(Icons.image),
iconSize: 32.0,
onPressed: () async {
// final ImagePicker _picker = ImagePicker();
// // Pick an image
// final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
// if (image != null) {
// controller.analyzeImage(image.path);
// }
final ImagePicker _picker = ImagePicker();
// Pick an image
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery);
if (image != null) {
if (await controller.analyzeImage(image.path)) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text('Barcode found!'),
backgroundColor: Colors.green,
));
} else {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text('No barcode found!'),
backgroundColor: Colors.red,
));
}
}
},
),
],
... ...
... ... @@ -22,11 +22,12 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
// Image to be sent to the texture
var latestBuffer: CVImageBuffer!
// var analyzeMode: Int = 0
var analyzing: Bool = false
var position = AVCaptureDevice.Position.back
let scanner = BarcodeScanner.barcodeScanner()
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftMobileScannerPlugin(registrar.textures())
... ... @@ -58,6 +59,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
// switchAnalyzeMode(call, result)
case "stop":
stop(result)
case "analyzeImage":
analyzeImage(call, result)
default:
result(FlutterMethodNotImplemented)
}
... ... @@ -102,7 +105,6 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
defaultOrientation: .portrait
)
let scanner = BarcodeScanner.barcodeScanner()
scanner.process(image) { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
... ... @@ -255,6 +257,42 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
// result(nil)
// }
func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let uiImage = UIImage(contentsOfFile: call.arguments as! String)
if (uiImage == nil) {
result(FlutterError(code: "MobileScanner",
message: "No image found in analyzeImage!",
details: nil))
return
}
let image = VisionImage(image: uiImage!)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait
)
var barcodeFound = false
scanner.process(image) { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
barcodeFound = true
let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
sink?(event)
}
} else if error != nil {
result(FlutterError(code: "MobileScanner",
message: error?.localizedDescription,
details: "analyzeImage()"))
}
analyzing = false
result(barcodeFound)
}
}
func stop(_ result: FlutterResult) {
if (device == nil) {
result(FlutterError(code: "MobileScanner",
... ...
... ... @@ -208,11 +208,16 @@ class MobileScannerController {
await start();
}
Future<void> analyzeImage(dynamic path) async {
await methodChannel.invokeMethod('analyzeImage', path);
/// Handles a local image file.
/// Returns true if a barcode or QR code is found.
/// Returns false if nothing is found.
///
/// [path] The path of the image on the devices
Future<bool> analyzeImage(String path) async {
return await methodChannel.invokeMethod('analyzeImage', path);
}
/// Disposes the controller and closes all listeners.
/// Disposes the MobileScannerController and closes all listeners.
void dispose() {
if (hashCode == _controllerHashcode) {
stop();
... ... @@ -223,11 +228,11 @@ class MobileScannerController {
barcodesController.close();
}
/// Checks if the controller is bound to the correct MobileScanner object.
/// Checks if the MobileScannerController is bound to the correct MobileScanner object.
void ensure(String name) {
final message =
'CameraController.$name called after CameraController.dispose\n'
'CameraController methods should not be used after calling dispose.';
'MobileScannerController.$name called after MobileScannerController.dispose\n'
'MobileScannerController methods should not be used after calling dispose.';
assert(hashCode == _controllerHashcode, message);
}
}
... ...
name: mobile_scanner
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.
version: 0.1.3
version: 0.2.0
repository: https://github.com/juliansteenbakker/mobile_scanner
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=2.2.0"
dependencies:
flutter:
... ...