Showing
14 changed files
with
259 additions
and
102 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 | + | ||
| 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 | ), |
| @@ -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 |
-
Please register or login to post a comment