Julian Steenbakker

Merge branch 'master' into pause_function

# Conflicts:
#	ios/Classes/MobileScanner.swift
#	macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift
Showing 69 changed files with 1173 additions and 488 deletions
@@ -12,4 +12,4 @@ jobs: @@ -12,4 +12,4 @@ jobs:
12 assign-author: 12 assign-author:
13 runs-on: ubuntu-latest 13 runs-on: ubuntu-latest
14 steps: 14 steps:
15 - - uses: toshimaru/auto-author-assign@v2.1.0 15 + - uses: toshimaru/auto-author-assign@v2.1.1
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 release-please: 7 release-please:
8 runs-on: ubuntu-latest 8 runs-on: ubuntu-latest
9 steps: 9 steps:
10 - - uses: GoogleCloudPlatform/release-please-action@v4.1.0 10 + - uses: GoogleCloudPlatform/release-please-action@v4.1.3
11 with: 11 with:
12 token: ${{ secrets.GITHUB_TOKEN }} 12 token: ${{ secrets.GITHUB_TOKEN }}
13 release-type: simple 13 release-type: simple
1 -## NEXT  
2 -* This release requires Flutter 3.22.0 and Dart 3.4. 1 +## 6.0.2
  2 +
  3 +Bugs fixed:
  4 +* Fixed a bug that prevented `analyzeImage` from actually accepting the configured formats.
  5 +
  6 +Improvements:
  7 +* [iOS] Excluded the `arm64` architecture for Simulators, which is unsupported by MLKit 7.0.0.
  8 +
  9 +## 6.0.1
  10 +
  11 +Bugs fixed:
  12 +* Fixed a bug that would cause onDetect to not handle errors.
  13 +
  14 +Improvements:
  15 +* [iOS] Excluded the `armv7` architecture, which is unsupported by MLKit 7.0.0.
  16 +* Added a new `onDetectError` error handler to the `MobileScanner` widget, for use with `onDetect`.
  17 +
  18 +## 6.0.0
  19 +
  20 +**BREAKING CHANGES:**
  21 +
  22 +* [iOS] iOS 15.5.0 is now the minimum supported iOS version.
  23 +* [iOS] Updates MLKit to version 7.0.0.
  24 +* [iOS] Updates the minimum supported XCode version to 15.3.0.
  25 +
  26 +Improvements:
  27 +* [MacOS] Added the corners and size information to barcode results.
  28 +* [MacOS] Added support for `analyzeImage`.
  29 +* [MacOS] Added a Privacy Manifest.
  30 +* [web] Added the size information to barcode results.
  31 +* [web] Added the video output size information to barcode capture.
  32 +* Added support for barcode formats to image analysis.
  33 +* Updated the scanner to report any scanning errors that were encountered during processing.
  34 +* Introduced a new getter `hasCameraPermission` for the `MobileScannerState`.
  35 +* Fixed a bug in the lifecycle handling sample. Now instead of checking `isInitialized`,
  36 +the sample recommends using `hasCameraPermission`, which also guards against camera permission errors.
  37 +* Updated the behavior of `returnImage` to only determine if the camera output bytes should be sent.
  38 +* Updated the behavior of `BarcodeCapture.size` to always be provided when available, regardless of `returnImage`.
  39 +
  40 +Bugs fixed:
  41 +* Fixed a bug that would cause the scanner to emit an error when it was already started. Now it ignores any calls to start while it is starting.
  42 +* [MacOS] Fixed a bug that prevented the `anaylzeImage()` sample from working properly.
  43 +
  44 +## 5.2.3
  45 +
  46 +Deprecations:
  47 +* The `EncryptionType.none` constant has been deprecated, as its name was misleading. Use `EncryptionType.unknown` instead.
  48 +
  49 +Bugs fixed:
  50 +* Fixed `EncryptionType` throwing on invalid `SAE` encryption type.
  51 +* [web] Removed the `controls` attribute on the video preview.
  52 +
  53 +Improvements:
  54 +* All enum types for barcode data (i.e. Wifi type or email type) now return `unknown` for unrecognized values.
  55 +
  56 +## 5.2.2
  57 +
  58 +Improvements:
  59 +* [MacOS] Adds Swift Package Manager support.
  60 +* [MacOS] Adds support for `returnImage`.
  61 +* Added a new `size` property to `Barcode`, that denotes the bounding box of the barcode.
  62 +
  63 +Bugs fixed:
  64 +* Fixed some documentation errors for the `size` and `image` of `BarcodeCapture`.
  65 +* [iOS] Fixed a bug with `returnImage`.
  66 +* [Android/iOS] Adjusted the raw barcode scan value to pass the raw event data, like on MacOS.
  67 +
  68 +## 5.2.1
  69 +
  70 +* Updates the `package:web` dependency to use a version range.
  71 +
  72 +## 5.2.0
  73 +
  74 +This release requires Flutter 3.22.0 and Dart 3.4.
3 75
4 * [Android] Fixed a leak of the barcode scanner. 76 * [Android] Fixed a leak of the barcode scanner.
5 * [Android] Fixed a crash when encountering invalid numbers for the scan window. 77 * [Android] Fixed a crash when encountering invalid numbers for the scan window.
@@ -39,7 +111,7 @@ Improvements: @@ -39,7 +111,7 @@ Improvements:
39 This major release contains all the changes from the 5.0.0 beta releases, along with the following changes: 111 This major release contains all the changes from the 5.0.0 beta releases, along with the following changes:
40 112
41 Improvements: 113 Improvements:
42 -- [Android] Remove the Kotlin Standard Library from the dependencies, as it is automatically included in Kotlin 1.4+ 114 +* [Android] Remove the Kotlin Standard Library from the dependencies, as it is automatically included in Kotlin 1.4+
43 115
44 ## 5.0.0-beta.3 116 ## 5.0.0-beta.3
45 **BREAKING CHANGES:** 117 **BREAKING CHANGES:**
@@ -35,8 +35,8 @@ See the example app for detailed implementation information. @@ -35,8 +35,8 @@ See the example app for detailed implementation information.
35 35
36 | Features | Android | iOS | macOS | Web | 36 | Features | Android | iOS | macOS | Web |
37 |------------------------|--------------------|--------------------|----------------------|-----| 37 |------------------------|--------------------|--------------------|----------------------|-----|
38 -| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |  
39 -| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | 38 +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
  39 +| returnImage | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
40 | scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | 40 | scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
41 41
42 ## Platform Support 42 ## Platform Support
@@ -60,6 +60,10 @@ dev.steenbakker.mobile_scanner.useUnbundled=true @@ -60,6 +60,10 @@ dev.steenbakker.mobile_scanner.useUnbundled=true
60 ``` 60 ```
61 61
62 ### iOS 62 ### iOS
  63 +
  64 +_iOS arm64 Simulators are currently not yet supported, until the migration to the Vision API is complete._
  65 +_See_ https://github.com/juliansteenbakker/mobile_scanner/issues/1225
  66 +
63 **Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:** 67 **Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:**
64 NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor. 68 NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor.
65 69
@@ -83,8 +87,8 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: @@ -83,8 +87,8 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities:
83 87
84 ## Web 88 ## Web
85 89
86 -As of version 5.0.0 adding the library to the `index.html` is no longer required,  
87 -as the library is automatically loaded on first use. 90 +As of version 5.0.0 adding the barcode scanning library script to the `index.html` is no longer required,
  91 +as the script is automatically loaded on first use.
88 92
89 ### Providing a mirror for the barcode scanning library 93 ### Providing a mirror for the barcode scanning library
90 94
@@ -127,7 +131,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { @@ -127,7 +131,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
127 void didChangeAppLifecycleState(AppLifecycleState state) { 131 void didChangeAppLifecycleState(AppLifecycleState state) {
128 // If the controller is not ready, do not try to start or stop it. 132 // If the controller is not ready, do not try to start or stop it.
129 // Permission dialogs can trigger lifecycle changes before the controller is ready. 133 // Permission dialogs can trigger lifecycle changes before the controller is ready.
130 - if (!controller.value.isInitialized) { 134 + if (!controller.value.hasCameraPermission) {
131 return; 135 return;
132 } 136 }
133 137
@@ -5,3 +5,4 @@ linter: @@ -5,3 +5,4 @@ linter:
5 - combinators_ordering 5 - combinators_ordering
6 - require_trailing_commas 6 - require_trailing_commas
7 - unnecessary_library_directive 7 - unnecessary_library_directive
  8 + - prefer_single_quotes
@@ -67,7 +67,7 @@ dependencies { @@ -67,7 +67,7 @@ dependencies {
67 def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false 67 def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false
68 if (useUnbundled.toBoolean()) { 68 if (useUnbundled.toBoolean()) {
69 // Dynamically downloaded model via Google Play Services 69 // Dynamically downloaded model via Google Play Services
70 - implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0' 70 + implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1'
71 } else { 71 } else {
72 // Bundled model in app 72 // Bundled model in app
73 implementation 'com.google.mlkit:barcode-scanning:17.2.0' 73 implementation 'com.google.mlkit:barcode-scanning:17.2.0'
@@ -77,8 +77,8 @@ dependencies { @@ -77,8 +77,8 @@ dependencies {
77 // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 77 // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7
78 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) 78 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
79 79
80 - implementation 'androidx.camera:camera-lifecycle:1.3.3'  
81 - implementation 'androidx.camera:camera-camera2:1.3.3' 80 + implementation 'androidx.camera:camera-lifecycle:1.3.4'
  81 + implementation 'androidx.camera:camera-camera2:1.3.4'
82 82
83 testImplementation 'org.jetbrains.kotlin:kotlin-test' 83 testImplementation 'org.jetbrains.kotlin:kotlin-test'
84 testImplementation 'org.mockito:mockito-core:5.12.0' 84 testImplementation 'org.mockito:mockito-core:5.12.0'
@@ -18,6 +18,12 @@ class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHand @@ -18,6 +18,12 @@ class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHand
18 eventChannel.setStreamHandler(this) 18 eventChannel.setStreamHandler(this)
19 } 19 }
20 20
  21 + fun publishError(errorCode: String, errorMessage: String, errorDetails: Any?) {
  22 + Handler(Looper.getMainLooper()).post {
  23 + eventSink?.error(errorCode, errorMessage, errorDetails)
  24 + }
  25 + }
  26 +
21 fun publishEvent(event: Map<String, Any>) { 27 fun publishEvent(event: Map<String, Any>) {
22 Handler(Looper.getMainLooper()).post { 28 Handler(Looper.getMainLooper()).post {
23 eventSink?.success(event) 29 eventSink?.success(event)
@@ -123,9 +123,8 @@ class MobileScanner( @@ -123,9 +123,8 @@ class MobileScanner(
123 mobileScannerCallback( 123 mobileScannerCallback(
124 barcodeMap, 124 barcodeMap,
125 null, 125 null,
126 - null,  
127 - null  
128 - ) 126 + mediaImage.width,
  127 + mediaImage.height)
129 return@addOnSuccessListener 128 return@addOnSuccessListener
130 } 129 }
131 130
@@ -10,6 +10,7 @@ import androidx.camera.core.ExperimentalGetImage @@ -10,6 +10,7 @@ import androidx.camera.core.ExperimentalGetImage
10 import com.google.mlkit.vision.barcode.BarcodeScannerOptions 10 import com.google.mlkit.vision.barcode.BarcodeScannerOptions
11 import dev.steenbakker.mobile_scanner.objects.BarcodeFormats 11 import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
12 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed 12 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
  13 +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
13 import io.flutter.plugin.common.BinaryMessenger 14 import io.flutter.plugin.common.BinaryMessenger
14 import io.flutter.plugin.common.MethodCall 15 import io.flutter.plugin.common.MethodCall
15 import io.flutter.plugin.common.MethodChannel 16 import io.flutter.plugin.common.MethodChannel
@@ -28,7 +29,7 @@ class MobileScannerHandler( @@ -28,7 +29,7 @@ class MobileScannerHandler(
28 29
29 private val analyzeImageErrorCallback: AnalyzerErrorCallback = { 30 private val analyzeImageErrorCallback: AnalyzerErrorCallback = {
30 Handler(Looper.getMainLooper()).post { 31 Handler(Looper.getMainLooper()).post {
31 - analyzerResult?.error("MobileScanner", it, null) 32 + analyzerResult?.error(MobileScannerErrorCodes.BARCODE_ERROR, it, null)
32 analyzerResult = null 33 analyzerResult = null
33 } 34 }
34 } 35 }
@@ -46,27 +47,21 @@ class MobileScannerHandler( @@ -46,27 +47,21 @@ class MobileScannerHandler(
46 private var analyzerResult: MethodChannel.Result? = null 47 private var analyzerResult: MethodChannel.Result? = null
47 48
48 private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? -> 49 private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? ->
49 - if (image != null) {  
50 barcodeHandler.publishEvent(mapOf( 50 barcodeHandler.publishEvent(mapOf(
51 "name" to "barcode", 51 "name" to "barcode",
52 "data" to barcodes, 52 "data" to barcodes,
53 - "image" to image,  
54 - "width" to width!!.toDouble(),  
55 - "height" to height!!.toDouble()  
56 - ))  
57 - } else {  
58 - barcodeHandler.publishEvent(mapOf(  
59 - "name" to "barcode",  
60 - "data" to barcodes 53 + // The image dimensions are always provided.
  54 + // The image bytes are only non-null when `returnImage` is true.
  55 + "image" to mapOf(
  56 + "bytes" to image,
  57 + "width" to width?.toDouble(),
  58 + "height" to height?.toDouble(),
  59 + )
61 )) 60 ))
62 } 61 }
63 - }  
64 62
65 private val errorCallback: MobileScannerErrorCallback = {error: String -> 63 private val errorCallback: MobileScannerErrorCallback = {error: String ->
66 - barcodeHandler.publishEvent(mapOf(  
67 - "name" to "error",  
68 - "data" to error,  
69 - )) 64 + barcodeHandler.publishError(MobileScannerErrorCodes.BARCODE_ERROR, error, null)
70 } 65 }
71 66
72 private var methodChannel: MethodChannel? = null 67 private var methodChannel: MethodChannel? = null
@@ -104,21 +99,21 @@ class MobileScannerHandler( @@ -104,21 +99,21 @@ class MobileScannerHandler(
104 99
105 @ExperimentalGetImage 100 @ExperimentalGetImage
106 override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 101 override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
107 - if (mobileScanner == null) {  
108 - result.error("MobileScanner", "Called ${call.method} before initializing.", null)  
109 - return  
110 - }  
111 when (call.method) { 102 when (call.method) {
112 "state" -> result.success(permissions.hasCameraPermission(activity)) 103 "state" -> result.success(permissions.hasCameraPermission(activity))
113 "request" -> permissions.requestPermission( 104 "request" -> permissions.requestPermission(
114 activity, 105 activity,
115 addPermissionListener, 106 addPermissionListener,
116 object: MobileScannerPermissions.ResultCallback { 107 object: MobileScannerPermissions.ResultCallback {
117 - override fun onResult(errorCode: String?, errorDescription: String?) { 108 + override fun onResult(errorCode: String?) {
118 when(errorCode) { 109 when(errorCode) {
119 null -> result.success(true) 110 null -> result.success(true)
120 - MobileScannerPermissions.CAMERA_ACCESS_DENIED -> result.success(false)  
121 - else -> result.error(errorCode, errorDescription, null) 111 + MobileScannerErrorCodes.CAMERA_ACCESS_DENIED -> result.success(false)
  112 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING -> result.error(
  113 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING,
  114 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE, null)
  115 + else -> result.error(
  116 + MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, null)
122 } 117 }
123 } 118 }
124 }) 119 })
@@ -150,28 +145,16 @@ class MobileScannerHandler( @@ -150,28 +145,16 @@ class MobileScannerHandler(
150 null 145 null
151 } 146 }
152 147
153 - var barcodeScannerOptions: BarcodeScannerOptions? = null  
154 - if (formats != null) {  
155 - val formatsList: MutableList<Int> = mutableListOf()  
156 - for (formatValue in formats) {  
157 - formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue)  
158 - }  
159 - barcodeScannerOptions = if (formatsList.size == 1) {  
160 - BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())  
161 - .build()  
162 - } else {  
163 - BarcodeScannerOptions.Builder().setBarcodeFormats(  
164 - formatsList.first(),  
165 - *formatsList.subList(1, formatsList.size).toIntArray()  
166 - ).build()  
167 - }  
168 - } 148 + val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)
169 149
170 val position = 150 val position =
171 if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA 151 if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
172 152
173 - val detectionSpeed: DetectionSpeed = if (speed == 0) DetectionSpeed.NO_DUPLICATES  
174 - else if (speed ==1) DetectionSpeed.NORMAL else DetectionSpeed.UNRESTRICTED 153 + val detectionSpeed: DetectionSpeed = when (speed) {
  154 + 0 -> DetectionSpeed.NO_DUPLICATES
  155 + 1 -> DetectionSpeed.NORMAL
  156 + else -> DetectionSpeed.UNRESTRICTED
  157 + }
175 158
176 mobileScanner!!.start( 159 mobileScanner!!.start(
177 barcodeScannerOptions, 160 barcodeScannerOptions,
@@ -196,29 +179,29 @@ class MobileScannerHandler( @@ -196,29 +179,29 @@ class MobileScannerHandler(
196 when (it) { 179 when (it) {
197 is AlreadyStarted -> { 180 is AlreadyStarted -> {
198 result.error( 181 result.error(
199 - "MobileScanner",  
200 - "Called start() while already started", 182 + MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  183 + MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
201 null 184 null
202 ) 185 )
203 } 186 }
204 is CameraError -> { 187 is CameraError -> {
205 result.error( 188 result.error(
206 - "MobileScanner",  
207 - "Error occurred when setting up camera!", 189 + MobileScannerErrorCodes.CAMERA_ERROR,
  190 + MobileScannerErrorCodes.CAMERA_ERROR_MESSAGE,
208 null 191 null
209 ) 192 )
210 } 193 }
211 is NoCamera -> { 194 is NoCamera -> {
212 result.error( 195 result.error(
213 - "MobileScanner",  
214 - "No camera found or failed to open camera!", 196 + MobileScannerErrorCodes.NO_CAMERA_ERROR,
  197 + MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
215 null 198 null
216 ) 199 )
217 } 200 }
218 else -> { 201 else -> {
219 result.error( 202 result.error(
220 - "MobileScanner",  
221 - "Unknown error occurred.", 203 + MobileScannerErrorCodes.GENERIC_ERROR,
  204 + MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
222 null 205 null
223 ) 206 )
224 } 207 }
@@ -254,13 +237,13 @@ class MobileScannerHandler( @@ -254,13 +237,13 @@ class MobileScannerHandler(
254 237
255 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { 238 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
256 analyzerResult = result 239 analyzerResult = result
257 - val uri = Uri.fromFile(File(call.arguments.toString()))  
258 240
259 - // TODO: parse options from the method call  
260 - // See https://github.com/juliansteenbakker/mobile_scanner/issues/1069 241 + val formats: List<Int>? = call.argument<List<Int>>("formats")
  242 + val filePath: String = call.argument<String>("filePath")!!
  243 +
261 mobileScanner!!.analyzeImage( 244 mobileScanner!!.analyzeImage(
262 - uri,  
263 - null, 245 + Uri.fromFile(File(filePath)),
  246 + buildBarcodeScannerOptions(formats),
264 analyzeImageSuccessCallback, 247 analyzeImageSuccessCallback,
265 analyzeImageErrorCallback) 248 analyzeImageErrorCallback)
266 } 249 }
@@ -275,9 +258,11 @@ class MobileScannerHandler( @@ -275,9 +258,11 @@ class MobileScannerHandler(
275 mobileScanner!!.setScale(call.arguments as Double) 258 mobileScanner!!.setScale(call.arguments as Double)
276 result.success(null) 259 result.success(null)
277 } catch (e: ZoomWhenStopped) { 260 } catch (e: ZoomWhenStopped) {
278 - result.error("MobileScanner", "Called setScale() while stopped!", null) 261 + result.error(
  262 + MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null)
279 } catch (e: ZoomNotInRange) { 263 } catch (e: ZoomNotInRange) {
280 - result.error("MobileScanner", "Scale should be within 0 and 1", null) 264 + result.error(
  265 + MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE, null)
281 } 266 }
282 } 267 }
283 268
@@ -286,7 +271,8 @@ class MobileScannerHandler( @@ -286,7 +271,8 @@ class MobileScannerHandler(
286 mobileScanner!!.resetScale() 271 mobileScanner!!.resetScale()
287 result.success(null) 272 result.success(null)
288 } catch (e: ZoomWhenStopped) { 273 } catch (e: ZoomWhenStopped) {
289 - result.error("MobileScanner", "Called resetScale() while stopped!", null) 274 + result.error(
  275 + MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null)
290 } 276 }
291 } 277 }
292 278
@@ -295,4 +281,26 @@ class MobileScannerHandler( @@ -295,4 +281,26 @@ class MobileScannerHandler(
295 281
296 result.success(null) 282 result.success(null)
297 } 283 }
  284 +
  285 + private fun buildBarcodeScannerOptions(formats: List<Int>?): BarcodeScannerOptions? {
  286 + if (formats == null) {
  287 + return null
  288 + }
  289 +
  290 + val formatsList: MutableList<Int> = mutableListOf()
  291 +
  292 + for (formatValue in formats) {
  293 + formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue)
  294 + }
  295 +
  296 + if (formatsList.size == 1) {
  297 + return BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
  298 + .build()
  299 + }
  300 +
  301 + return BarcodeScannerOptions.Builder().setBarcodeFormats(
  302 + formatsList.first(),
  303 + *formatsList.subList(1, formatsList.size).toIntArray()
  304 + ).build()
  305 + }
298 } 306 }
@@ -5,6 +5,7 @@ import android.app.Activity @@ -5,6 +5,7 @@ import android.app.Activity
5 import android.content.pm.PackageManager 5 import android.content.pm.PackageManager
6 import androidx.core.app.ActivityCompat 6 import androidx.core.app.ActivityCompat
7 import androidx.core.content.ContextCompat 7 import androidx.core.content.ContextCompat
  8 +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
8 import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener 9 import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
9 10
10 /** 11 /**
@@ -12,11 +13,6 @@ import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener @@ -12,11 +13,6 @@ import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
12 */ 13 */
13 class MobileScannerPermissions { 14 class MobileScannerPermissions {
14 companion object { 15 companion object {
15 - const val CAMERA_ACCESS_DENIED = "CameraAccessDenied"  
16 - const val CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied."  
17 - const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "CameraPermissionsRequestOngoing"  
18 - const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once."  
19 -  
20 /** 16 /**
21 * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. 17 * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
22 * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode 18 * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
@@ -25,7 +21,7 @@ class MobileScannerPermissions { @@ -25,7 +21,7 @@ class MobileScannerPermissions {
25 } 21 }
26 22
27 interface ResultCallback { 23 interface ResultCallback {
28 - fun onResult(errorCode: String?, errorDescription: String?) 24 + fun onResult(errorCode: String?)
29 } 25 }
30 26
31 private var listener: RequestPermissionsResultListener? = null 27 private var listener: RequestPermissionsResultListener? = null
@@ -53,14 +49,13 @@ class MobileScannerPermissions { @@ -53,14 +49,13 @@ class MobileScannerPermissions {
53 addPermissionListener: (RequestPermissionsResultListener) -> Unit, 49 addPermissionListener: (RequestPermissionsResultListener) -> Unit,
54 callback: ResultCallback) { 50 callback: ResultCallback) {
55 if (ongoing) { 51 if (ongoing) {
56 - callback.onResult(  
57 - CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE) 52 + callback.onResult(MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING)
58 return 53 return
59 } 54 }
60 55
61 if(hasCameraPermission(activity) == 1) { 56 if(hasCameraPermission(activity) == 1) {
62 // Permissions already exist. Call the callback with success. 57 // Permissions already exist. Call the callback with success.
63 - callback.onResult(null, null) 58 + callback.onResult(null)
64 return 59 return
65 } 60 }
66 61
@@ -68,10 +63,10 @@ class MobileScannerPermissions { @@ -68,10 +63,10 @@ class MobileScannerPermissions {
68 // Keep track of the listener, so that it can be unregistered later. 63 // Keep track of the listener, so that it can be unregistered later.
69 listener = MobileScannerPermissionsListener( 64 listener = MobileScannerPermissionsListener(
70 object: ResultCallback { 65 object: ResultCallback {
71 - override fun onResult(errorCode: String?, errorDescription: String?) { 66 + override fun onResult(errorCode: String?) {
72 ongoing = false 67 ongoing = false
73 listener = null 68 listener = null
74 - callback.onResult(errorCode, errorDescription) 69 + callback.onResult(errorCode)
75 } 70 }
76 } 71 }
77 ) 72 )
1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 import android.content.pm.PackageManager 3 import android.content.pm.PackageManager
  4 +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
4 import io.flutter.plugin.common.PluginRegistry 5 import io.flutter.plugin.common.PluginRegistry
5 6
6 /** 7 /**
@@ -29,11 +30,9 @@ internal class MobileScannerPermissionsListener( @@ -29,11 +30,9 @@ internal class MobileScannerPermissionsListener(
29 // grantResults could be empty if the permissions request with the user is interrupted 30 // grantResults could be empty if the permissions request with the user is interrupted
30 // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[]) 31 // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[])
31 if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { 32 if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
32 - resultCallback.onResult(  
33 - MobileScannerPermissions.CAMERA_ACCESS_DENIED,  
34 - MobileScannerPermissions.CAMERA_ACCESS_DENIED_MESSAGE) 33 + resultCallback.onResult(MobileScannerErrorCodes.CAMERA_ACCESS_DENIED)
35 } else { 34 } else {
36 - resultCallback.onResult(null, null) 35 + resultCallback.onResult(null)
37 } 36 }
38 37
39 return true 38 return true
@@ -28,12 +28,22 @@ fun Image.toByteArray(): ByteArray { @@ -28,12 +28,22 @@ fun Image.toByteArray(): ByteArray {
28 28
29 val Barcode.data: Map<String, Any?> 29 val Barcode.data: Map<String, Any?>
30 get() = mapOf( 30 get() = mapOf(
31 - "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,  
32 - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,  
33 - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,  
34 - "driverLicense" to driverLicense?.data, "email" to email?.data,  
35 - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,  
36 - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue 31 + "calendarEvent" to calendarEvent?.data,
  32 + "contactInfo" to contactInfo?.data,
  33 + "corners" to cornerPoints?.map { corner -> corner.data },
  34 + "displayValue" to displayValue,
  35 + "driverLicense" to driverLicense?.data,
  36 + "email" to email?.data,
  37 + "format" to format,
  38 + "geoPoint" to geoPoint?.data,
  39 + "phone" to phone?.data,
  40 + "rawBytes" to rawBytes,
  41 + "rawValue" to rawValue,
  42 + "size" to boundingBox?.size,
  43 + "sms" to sms?.data,
  44 + "type" to valueType,
  45 + "url" to url?.data,
  46 + "wifi" to wifi?.data,
37 ) 47 )
38 48
39 private val Point.data: Map<String, Double> 49 private val Point.data: Map<String, Double>
@@ -93,3 +103,13 @@ private val Barcode.UrlBookmark.data: Map<String, Any?> @@ -93,3 +103,13 @@ private val Barcode.UrlBookmark.data: Map<String, Any?>
93 103
94 private val Barcode.WiFi.data: Map<String, Any?> 104 private val Barcode.WiFi.data: Map<String, Any?>
95 get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) 105 get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
  106 +
  107 +private val Rect.size: Map<String, Any?>
  108 + get() {
  109 + // Rect.isValid can't be accessed for some reason, so just do the check manually.
  110 + if (left <= right && top <= bottom) {
  111 + return mapOf("width" to width().toDouble(), "height" to height().toDouble())
  112 + }
  113 +
  114 + return emptyMap()
  115 + }
  1 +package dev.steenbakker.mobile_scanner.objects
  2 +
  3 +class MobileScannerErrorCodes {
  4 + companion object {
  5 + const val ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR"
  6 + const val ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started."
  7 + // The error code 'BARCODE_ERROR' does not have an error message,
  8 + // because it uses the error message from the underlying error.
  9 + const val BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR"
  10 + // The error code 'CAMERA_ACCESS_DENIED' does not have an error message,
  11 + // because it is used for a boolean result.
  12 + const val CAMERA_ACCESS_DENIED = "MOBILE_SCANNER_CAMERA_PERMISSION_DENIED"
  13 + const val CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR"
  14 + const val CAMERA_ERROR_MESSAGE = "An error occurred when opening the camera."
  15 + const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "MOBILE_SCANNER_CAMERA_PERMISSION_REQUEST_PENDING"
  16 + const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once."
  17 + const val GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR"
  18 + const val GENERIC_ERROR_MESSAGE = "An unknown error occurred."
  19 + const val INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)"
  20 + const val NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR"
  21 + const val NO_CAMERA_ERROR_MESSAGE = "No cameras available."
  22 + const val SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR"
  23 + const val SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped."
  24 + }
  25 +}
@@ -5,9 +5,11 @@ @@ -5,9 +5,11 @@
5 *.swp 5 *.swp
6 .DS_Store 6 .DS_Store
7 .atom/ 7 .atom/
  8 +.build/
8 .buildlog/ 9 .buildlog/
9 .history 10 .history
10 .svn/ 11 .svn/
  12 +.swiftpm/
11 migrate_working_dir/ 13 migrate_working_dir/
12 14
13 # IntelliJ related 15 # IntelliJ related
@@ -27,12 +27,12 @@ android { @@ -27,12 +27,12 @@ android {
27 compileSdk 34 27 compileSdk 34
28 28
29 compileOptions { 29 compileOptions {
30 - sourceCompatibility JavaVersion.VERSION_1_8  
31 - targetCompatibility JavaVersion.VERSION_1_8 30 + sourceCompatibility JavaVersion.VERSION_17
  31 + targetCompatibility JavaVersion.VERSION_17
32 } 32 }
33 33
34 kotlinOptions { 34 kotlinOptions {
35 - jvmTarget = '1.8' 35 + jvmTarget = '17'
36 } 36 }
37 37
38 sourceSets { 38 sourceSets {
@@ -42,7 +42,7 @@ android { @@ -42,7 +42,7 @@ android {
42 defaultConfig { 42 defaultConfig {
43 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 43 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
44 applicationId "dev.steenbakker.mobile_scanner_example" 44 applicationId "dev.steenbakker.mobile_scanner_example"
45 - minSdkVersion 21 45 + minSdkVersion 24
46 targetSdkVersion 34 46 targetSdkVersion 34
47 versionCode flutterVersionCode.toInteger() 47 versionCode flutterVersionCode.toInteger()
48 versionName flutterVersionName 48 versionName flutterVersionName
1 #Thu May 02 10:24:49 CEST 2024 1 #Thu May 02 10:24:49 CEST 2024
2 distributionBase=GRADLE_USER_HOME 2 distributionBase=GRADLE_USER_HOME
3 distributionPath=wrapper/dists 3 distributionPath=wrapper/dists
4 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip 4 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
5 zipStoreBase=GRADLE_USER_HOME 5 zipStoreBase=GRADLE_USER_HOME
6 zipStorePath=wrapper/dists 6 zipStorePath=wrapper/dists
@@ -18,8 +18,8 @@ pluginManagement { @@ -18,8 +18,8 @@ pluginManagement {
18 18
19 plugins { 19 plugins {
20 id "dev.flutter.flutter-plugin-loader" version "1.0.0" 20 id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21 - id "com.android.application" version "7.3.0" apply false  
22 - id "org.jetbrains.kotlin.android" version "1.7.22" apply false 21 + id "com.android.application" version "8.3.2" apply false
  22 + id "org.jetbrains.kotlin.android" version "2.0.20" apply false
23 } 23 }
24 24
25 include ":app" 25 include ":app"
@@ -21,6 +21,6 @@ @@ -21,6 +21,6 @@
21 <key>CFBundleVersion</key> 21 <key>CFBundleVersion</key>
22 <string>1.0</string> 22 <string>1.0</string>
23 <key>MinimumOSVersion</key> 23 <key>MinimumOSVersion</key>
24 - <string>12.0</string> 24 + <string>15.5.0</string>
25 </dict> 25 </dict>
26 </plist> 26 </plist>
1 # Uncomment this line to define a global platform for your project 1 # Uncomment this line to define a global platform for your project
2 -# platform :ios, '12.0' 2 +# platform :ios, '15.5.0'
3 3
4 # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -41,7 +41,7 @@ post_install do |installer| @@ -41,7 +41,7 @@ post_install do |installer|
41 installer.pods_project.targets.each do |target| 41 installer.pods_project.targets.each do |target|
42 flutter_additional_ios_build_settings(target) 42 flutter_additional_ios_build_settings(target)
43 target.build_configurations.each do |config| 43 target.build_configurations.each do |config|
44 - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' 44 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5.0'
45 end 45 end
46 end 46 end
47 end 47 end
@@ -470,7 +470,7 @@ @@ -470,7 +470,7 @@
470 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 470 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
471 GCC_WARN_UNUSED_FUNCTION = YES; 471 GCC_WARN_UNUSED_FUNCTION = YES;
472 GCC_WARN_UNUSED_VARIABLE = YES; 472 GCC_WARN_UNUSED_VARIABLE = YES;
473 - IPHONEOS_DEPLOYMENT_TARGET = 12.0; 473 + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
474 MTL_ENABLE_DEBUG_INFO = NO; 474 MTL_ENABLE_DEBUG_INFO = NO;
475 SDKROOT = iphoneos; 475 SDKROOT = iphoneos;
476 SUPPORTED_PLATFORMS = iphoneos; 476 SUPPORTED_PLATFORMS = iphoneos;
@@ -488,14 +488,14 @@ @@ -488,14 +488,14 @@
488 CODE_SIGN_IDENTITY = "Apple Development"; 488 CODE_SIGN_IDENTITY = "Apple Development";
489 CODE_SIGN_STYLE = Automatic; 489 CODE_SIGN_STYLE = Automatic;
490 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 490 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
491 - DEVELOPMENT_TEAM = 75Y2P2WSQQ; 491 + DEVELOPMENT_TEAM = "";
492 ENABLE_BITCODE = NO; 492 ENABLE_BITCODE = NO;
493 INFOPLIST_FILE = Runner/Info.plist; 493 INFOPLIST_FILE = Runner/Info.plist;
494 LD_RUNPATH_SEARCH_PATHS = ( 494 LD_RUNPATH_SEARCH_PATHS = (
495 "$(inherited)", 495 "$(inherited)",
496 "@executable_path/Frameworks", 496 "@executable_path/Frameworks",
497 ); 497 );
498 - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; 498 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
499 PRODUCT_NAME = "$(TARGET_NAME)"; 499 PRODUCT_NAME = "$(TARGET_NAME)";
500 PROVISIONING_PROFILE_SPECIFIER = ""; 500 PROVISIONING_PROFILE_SPECIFIER = "";
501 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 501 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -601,7 +601,7 @@ @@ -601,7 +601,7 @@
601 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 601 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
602 GCC_WARN_UNUSED_FUNCTION = YES; 602 GCC_WARN_UNUSED_FUNCTION = YES;
603 GCC_WARN_UNUSED_VARIABLE = YES; 603 GCC_WARN_UNUSED_VARIABLE = YES;
604 - IPHONEOS_DEPLOYMENT_TARGET = 12.0; 604 + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
605 MTL_ENABLE_DEBUG_INFO = YES; 605 MTL_ENABLE_DEBUG_INFO = YES;
606 ONLY_ACTIVE_ARCH = YES; 606 ONLY_ACTIVE_ARCH = YES;
607 SDKROOT = iphoneos; 607 SDKROOT = iphoneos;
@@ -650,7 +650,7 @@ @@ -650,7 +650,7 @@
650 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 650 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
651 GCC_WARN_UNUSED_FUNCTION = YES; 651 GCC_WARN_UNUSED_FUNCTION = YES;
652 GCC_WARN_UNUSED_VARIABLE = YES; 652 GCC_WARN_UNUSED_VARIABLE = YES;
653 - IPHONEOS_DEPLOYMENT_TARGET = 12.0; 653 + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
654 MTL_ENABLE_DEBUG_INFO = NO; 654 MTL_ENABLE_DEBUG_INFO = NO;
655 SDKROOT = iphoneos; 655 SDKROOT = iphoneos;
656 SUPPORTED_PLATFORMS = iphoneos; 656 SUPPORTED_PLATFORMS = iphoneos;
@@ -670,14 +670,14 @@ @@ -670,14 +670,14 @@
670 CODE_SIGN_IDENTITY = "Apple Development"; 670 CODE_SIGN_IDENTITY = "Apple Development";
671 CODE_SIGN_STYLE = Automatic; 671 CODE_SIGN_STYLE = Automatic;
672 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 672 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
673 - DEVELOPMENT_TEAM = 75Y2P2WSQQ; 673 + DEVELOPMENT_TEAM = "";
674 ENABLE_BITCODE = NO; 674 ENABLE_BITCODE = NO;
675 INFOPLIST_FILE = Runner/Info.plist; 675 INFOPLIST_FILE = Runner/Info.plist;
676 LD_RUNPATH_SEARCH_PATHS = ( 676 LD_RUNPATH_SEARCH_PATHS = (
677 "$(inherited)", 677 "$(inherited)",
678 "@executable_path/Frameworks", 678 "@executable_path/Frameworks",
679 ); 679 );
680 - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; 680 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
681 PRODUCT_NAME = "$(TARGET_NAME)"; 681 PRODUCT_NAME = "$(TARGET_NAME)";
682 PROVISIONING_PROFILE_SPECIFIER = ""; 682 PROVISIONING_PROFILE_SPECIFIER = "";
683 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 683 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -696,14 +696,14 @@ @@ -696,14 +696,14 @@
696 CODE_SIGN_IDENTITY = "Apple Development"; 696 CODE_SIGN_IDENTITY = "Apple Development";
697 CODE_SIGN_STYLE = Automatic; 697 CODE_SIGN_STYLE = Automatic;
698 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 698 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
699 - DEVELOPMENT_TEAM = 75Y2P2WSQQ; 699 + DEVELOPMENT_TEAM = "";
700 ENABLE_BITCODE = NO; 700 ENABLE_BITCODE = NO;
701 INFOPLIST_FILE = Runner/Info.plist; 701 INFOPLIST_FILE = Runner/Info.plist;
702 LD_RUNPATH_SEARCH_PATHS = ( 702 LD_RUNPATH_SEARCH_PATHS = (
703 "$(inherited)", 703 "$(inherited)",
704 "@executable_path/Frameworks", 704 "@executable_path/Frameworks",
705 ); 705 );
706 - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; 706 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
707 PRODUCT_NAME = "$(TARGET_NAME)"; 707 PRODUCT_NAME = "$(TARGET_NAME)";
708 PROVISIONING_PROFILE_SPECIFIER = ""; 708 PROVISIONING_PROFILE_SPECIFIER = "";
709 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 709 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
1 import UIKit 1 import UIKit
2 import Flutter 2 import Flutter
3 3
4 -@UIApplicationMain 4 +@main
5 @objc class AppDelegate: FlutterAppDelegate { 5 @objc class AppDelegate: FlutterAppDelegate {
6 override func application( 6 override func application(
7 _ application: UIApplication, 7 _ application: UIApplication,
  1 +import 'package:flutter/foundation.dart';
  2 +import 'package:flutter/material.dart';
  3 +import 'package:image_picker/image_picker.dart';
  4 +import 'package:mobile_scanner/mobile_scanner.dart';
  5 +
  6 +class BarcodeScannerAnalyzeImage extends StatefulWidget {
  7 + const BarcodeScannerAnalyzeImage({super.key});
  8 +
  9 + @override
  10 + State<BarcodeScannerAnalyzeImage> createState() =>
  11 + _BarcodeScannerAnalyzeImageState();
  12 +}
  13 +
  14 +class _BarcodeScannerAnalyzeImageState
  15 + extends State<BarcodeScannerAnalyzeImage> {
  16 + final MobileScannerController _controller = MobileScannerController();
  17 +
  18 + BarcodeCapture? _barcodeCapture;
  19 +
  20 + Future<void> _analyzeImageFromFile() async {
  21 + try {
  22 + final XFile? file =
  23 + await ImagePicker().pickImage(source: ImageSource.gallery);
  24 +
  25 + if (!mounted) {
  26 + return;
  27 + }
  28 +
  29 + if (file == null) {
  30 + setState(() {
  31 + _barcodeCapture = null;
  32 + });
  33 + return;
  34 + }
  35 +
  36 + final BarcodeCapture? barcodeCapture =
  37 + await _controller.analyzeImage(file.path);
  38 +
  39 + if (mounted) {
  40 + setState(() {
  41 + _barcodeCapture = barcodeCapture;
  42 + });
  43 + }
  44 + } catch (_) {}
  45 + }
  46 +
  47 + @override
  48 + Widget build(BuildContext context) {
  49 + Widget label = const Text('Pick a file to detect barcode');
  50 +
  51 + if (_barcodeCapture != null) {
  52 + label = Text(
  53 + _barcodeCapture?.barcodes.firstOrNull?.rawValue ??
  54 + 'No barcode detected',
  55 + );
  56 + }
  57 +
  58 + return Scaffold(
  59 + appBar: AppBar(title: const Text('Analyze image from file')),
  60 + body: Column(
  61 + children: [
  62 + Expanded(
  63 + child: Center(
  64 + child: ElevatedButton(
  65 + onPressed: kIsWeb ? null : _analyzeImageFromFile,
  66 + child: kIsWeb
  67 + ? const Text('Analyze image is not supported on web')
  68 + : const Text('Choose file'),
  69 + ),
  70 + ),
  71 + ),
  72 + Expanded(child: Center(child: label)),
  73 + ],
  74 + ),
  75 + );
  76 + }
  77 +
  78 + @override
  79 + void dispose() {
  80 + _controller.dispose();
  81 + super.dispose();
  82 + }
  83 +}
@@ -60,7 +60,7 @@ class _BarcodeScannerWithControllerState @@ -60,7 +60,7 @@ class _BarcodeScannerWithControllerState
60 60
61 @override 61 @override
62 void didChangeAppLifecycleState(AppLifecycleState state) { 62 void didChangeAppLifecycleState(AppLifecycleState state) {
63 - if (!controller.value.isInitialized) { 63 + if (!controller.value.hasCameraPermission) {
64 return; 64 return;
65 } 65 }
66 66
@@ -39,16 +39,16 @@ class _BarcodeScannerWithScanWindowState @@ -39,16 +39,16 @@ class _BarcodeScannerWithScanWindowState
39 final scannedBarcode = barcodeCapture.barcodes.first; 39 final scannedBarcode = barcodeCapture.barcodes.first;
40 40
41 // No barcode corners, or size, or no camera preview size. 41 // No barcode corners, or size, or no camera preview size.
42 - if (scannedBarcode.corners.isEmpty ||  
43 - value.size.isEmpty ||  
44 - barcodeCapture.size.isEmpty) { 42 + if (value.size.isEmpty ||
  43 + scannedBarcode.size.isEmpty ||
  44 + scannedBarcode.corners.isEmpty) {
45 return const SizedBox(); 45 return const SizedBox();
46 } 46 }
47 47
48 return CustomPaint( 48 return CustomPaint(
49 painter: BarcodeOverlay( 49 painter: BarcodeOverlay(
50 barcodeCorners: scannedBarcode.corners, 50 barcodeCorners: scannedBarcode.corners,
51 - barcodeSize: barcodeCapture.size, 51 + barcodeSize: scannedBarcode.size,
52 boxFit: BoxFit.contain, 52 boxFit: BoxFit.contain,
53 cameraPreviewSize: value.size, 53 cameraPreviewSize: value.size,
54 ), 54 ),
@@ -131,15 +131,15 @@ class ScannerOverlay extends CustomPainter { @@ -131,15 +131,15 @@ class ScannerOverlay extends CustomPainter {
131 131
132 @override 132 @override
133 void paint(Canvas canvas, Size size) { 133 void paint(Canvas canvas, Size size) {
134 - // TODO: use `Offset.zero & size` instead of Rect.largest  
135 // we need to pass the size to the custom paint widget 134 // we need to pass the size to the custom paint widget
136 - final backgroundPath = Path()..addRect(Rect.largest); 135 + final backgroundPath = Path()
  136 + ..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
137 final cutoutPath = Path()..addRect(scanWindow); 137 final cutoutPath = Path()..addRect(scanWindow);
138 138
139 final backgroundPaint = Paint() 139 final backgroundPaint = Paint()
140 ..color = Colors.black.withOpacity(0.5) 140 ..color = Colors.black.withOpacity(0.5)
141 ..style = PaintingStyle.fill 141 ..style = PaintingStyle.fill
142 - ..blendMode = BlendMode.dstOut; 142 + ..blendMode = BlendMode.dstOver;
143 143
144 final backgroundWithCutout = Path.combine( 144 final backgroundWithCutout = Path.combine(
145 PathOperation.difference, 145 PathOperation.difference,
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
  2 +import 'package:mobile_scanner_example/barcode_scanner_analyze_image.dart';
2 import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; 3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
3 import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; 4 import 'package:mobile_scanner_example/barcode_scanner_listview.dart';
4 import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; 5 import 'package:mobile_scanner_example/barcode_scanner_pageview.dart';
@@ -20,95 +21,75 @@ void main() { @@ -20,95 +21,75 @@ void main() {
20 class MyHome extends StatelessWidget { 21 class MyHome extends StatelessWidget {
21 const MyHome({super.key}); 22 const MyHome({super.key});
22 23
23 - @override  
24 - Widget build(BuildContext context) {  
25 - return Scaffold(  
26 - appBar: AppBar(title: const Text('Mobile Scanner Example')),  
27 - body: Center(  
28 - child: Column(  
29 - mainAxisAlignment: MainAxisAlignment.spaceAround,  
30 - children: [  
31 - ElevatedButton(  
32 - onPressed: () {  
33 - Navigator.of(context).push(  
34 - MaterialPageRoute(  
35 - builder: (context) => const BarcodeScannerSimple(),  
36 - ),  
37 - );  
38 - },  
39 - child: const Text('MobileScanner Simple'),  
40 - ),  
41 - ElevatedButton( 24 + Widget _buildItem(BuildContext context, String label, Widget page) {
  25 + return Padding(
  26 + padding: const EdgeInsets.all(8.0),
  27 + child: Center(
  28 + child: ElevatedButton(
42 onPressed: () { 29 onPressed: () {
43 Navigator.of(context).push( 30 Navigator.of(context).push(
44 MaterialPageRoute( 31 MaterialPageRoute(
45 - builder: (context) => const BarcodeScannerListView(), 32 + builder: (context) => page,
46 ), 33 ),
47 ); 34 );
48 }, 35 },
49 - child: const Text('MobileScanner with ListView'), 36 + child: Text(label),
50 ), 37 ),
51 - ElevatedButton(  
52 - onPressed: () {  
53 - Navigator.of(context).push(  
54 - MaterialPageRoute(  
55 - builder: (context) => const BarcodeScannerWithController(),  
56 ), 38 ),
57 ); 39 );
58 - },  
59 - child: const Text('MobileScanner with Controller'),  
60 - ),  
61 - ElevatedButton(  
62 - onPressed: () {  
63 - Navigator.of(context).push(  
64 - MaterialPageRoute(  
65 - builder: (context) => const BarcodeScannerWithScanWindow(),  
66 - ),  
67 - );  
68 - },  
69 - child: const Text('MobileScanner with ScanWindow'),  
70 - ),  
71 - ElevatedButton(  
72 - onPressed: () {  
73 - Navigator.of(context).push(  
74 - MaterialPageRoute(  
75 - builder: (context) => const BarcodeScannerReturningImage(),  
76 - ),  
77 - );  
78 - },  
79 - child: const Text(  
80 - 'MobileScanner with Controller (returning image)',  
81 - ),  
82 - ),  
83 - ElevatedButton(  
84 - onPressed: () {  
85 - Navigator.of(context).push(  
86 - MaterialPageRoute(  
87 - builder: (context) => const BarcodeScannerWithZoom(),  
88 - ),  
89 - );  
90 - },  
91 - child: const Text('MobileScanner with zoom slider'),  
92 - ),  
93 - ElevatedButton(  
94 - onPressed: () {  
95 - Navigator.of(context).push(  
96 - MaterialPageRoute(  
97 - builder: (context) => const BarcodeScannerPageView(),  
98 - ),  
99 - );  
100 - },  
101 - child: const Text('MobileScanner pageView'),  
102 - ),  
103 - ElevatedButton(  
104 - onPressed: () {  
105 - Navigator.of(context).push(  
106 - MaterialPageRoute(  
107 - builder: (context) => BarcodeScannerWithOverlay(),  
108 - ),  
109 - );  
110 - },  
111 - child: const Text('MobileScanner with Overlay'), 40 + }
  41 +
  42 + @override
  43 + Widget build(BuildContext context) {
  44 + return Scaffold(
  45 + appBar: AppBar(title: const Text('Mobile Scanner Example')),
  46 + body: Center(
  47 + child: ListView(
  48 + children: [
  49 + _buildItem(
  50 + context,
  51 + 'MobileScanner Simple',
  52 + const BarcodeScannerSimple(),
  53 + ),
  54 + _buildItem(
  55 + context,
  56 + 'MobileScanner with ListView',
  57 + const BarcodeScannerListView(),
  58 + ),
  59 + _buildItem(
  60 + context,
  61 + 'MobileScanner with Controller',
  62 + const BarcodeScannerWithController(),
  63 + ),
  64 + _buildItem(
  65 + context,
  66 + 'MobileScanner with ScanWindow',
  67 + const BarcodeScannerWithScanWindow(),
  68 + ),
  69 + _buildItem(
  70 + context,
  71 + 'MobileScanner with Controller (return image)',
  72 + const BarcodeScannerReturningImage(),
  73 + ),
  74 + _buildItem(
  75 + context,
  76 + 'MobileScanner with zoom slider',
  77 + const BarcodeScannerWithZoom(),
  78 + ),
  79 + _buildItem(
  80 + context,
  81 + 'MobileScanner with PageView',
  82 + const BarcodeScannerPageView(),
  83 + ),
  84 + _buildItem(
  85 + context,
  86 + 'MobileScanner with Overlay',
  87 + const BarcodeScannerWithOverlay(),
  88 + ),
  89 + _buildItem(
  90 + context,
  91 + 'Analyze image from file',
  92 + const BarcodeScannerAnalyzeImage(),
112 ), 93 ),
113 ], 94 ],
114 ), 95 ),
@@ -5,6 +5,8 @@ import 'package:mobile_scanner_example/scanner_button_widgets.dart'; @@ -5,6 +5,8 @@ import 'package:mobile_scanner_example/scanner_button_widgets.dart';
5 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 5 import 'package:mobile_scanner_example/scanner_error_widget.dart';
6 6
7 class BarcodeScannerWithOverlay extends StatefulWidget { 7 class BarcodeScannerWithOverlay extends StatefulWidget {
  8 + const BarcodeScannerWithOverlay({super.key});
  9 +
8 @override 10 @override
9 _BarcodeScannerWithOverlayState createState() => 11 _BarcodeScannerWithOverlayState createState() =>
10 _BarcodeScannerWithOverlayState(); 12 _BarcodeScannerWithOverlayState();
@@ -100,9 +102,9 @@ class ScannerOverlay extends CustomPainter { @@ -100,9 +102,9 @@ class ScannerOverlay extends CustomPainter {
100 102
101 @override 103 @override
102 void paint(Canvas canvas, Size size) { 104 void paint(Canvas canvas, Size size) {
103 - // TODO: use `Offset.zero & size` instead of Rect.largest  
104 // we need to pass the size to the custom paint widget 105 // we need to pass the size to the custom paint widget
105 - final backgroundPath = Path()..addRect(Rect.largest); 106 + final backgroundPath = Path()
  107 + ..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
106 108
107 final cutoutPath = Path() 109 final cutoutPath = Path()
108 ..addRRect( 110 ..addRRect(
@@ -118,7 +120,7 @@ class ScannerOverlay extends CustomPainter { @@ -118,7 +120,7 @@ class ScannerOverlay extends CustomPainter {
118 final backgroundPaint = Paint() 120 final backgroundPaint = Paint()
119 ..color = Colors.black.withOpacity(0.5) 121 ..color = Colors.black.withOpacity(0.5)
120 ..style = PaintingStyle.fill 122 ..style = PaintingStyle.fill
121 - ..blendMode = BlendMode.dstOut; 123 + ..blendMode = BlendMode.dstOver;
122 124
123 final backgroundWithCutout = Path.combine( 125 final backgroundWithCutout = Path.combine(
124 PathOperation.difference, 126 PathOperation.difference,
@@ -112,6 +112,7 @@ class SwitchCameraButton extends StatelessWidget { @@ -112,6 +112,7 @@ class SwitchCameraButton extends StatelessWidget {
112 } 112 }
113 113
114 return IconButton( 114 return IconButton(
  115 + color: Colors.white,
115 iconSize: 32.0, 116 iconSize: 32.0,
116 icon: icon, 117 icon: icon,
117 onPressed: () async { 118 onPressed: () async {
@@ -166,9 +167,13 @@ class ToggleFlashlightButton extends StatelessWidget { @@ -166,9 +167,13 @@ class ToggleFlashlightButton extends StatelessWidget {
166 }, 167 },
167 ); 168 );
168 case TorchState.unavailable: 169 case TorchState.unavailable:
169 - return const Icon( 170 + return const SizedBox.square(
  171 + dimension: 48.0,
  172 + child: Icon(
170 Icons.no_flash, 173 Icons.no_flash,
  174 + size: 32.0,
171 color: Colors.grey, 175 color: Colors.grey,
  176 + ),
172 ); 177 );
173 } 178 }
174 }, 179 },
1 import Cocoa 1 import Cocoa
2 import FlutterMacOS 2 import FlutterMacOS
3 3
4 -@NSApplicationMain 4 +@main
5 class AppDelegate: FlutterAppDelegate { 5 class AppDelegate: FlutterAppDelegate {
6 override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 6 override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
7 return true 7 return true
8 } 8 }
  9 +
  10 + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
  11 + return true
  12 + }
9 } 13 }
@@ -10,5 +10,7 @@ @@ -10,5 +10,7 @@
10 <true/> 10 <true/>
11 <key>com.apple.security.network.server</key> 11 <key>com.apple.security.network.server</key>
12 <true/> 12 <true/>
  13 + <key>com.apple.security.files.user-selected.read-only</key>
  14 + <true/>
13 </dict> 15 </dict>
14 </plist> 16 </plist>
@@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
21 <meta name="description" content="Demonstrates how to use the mobile_scanner plugin."> 21 <meta name="description" content="Demonstrates how to use the mobile_scanner plugin.">
22 22
23 <!-- iOS meta tags & icons --> 23 <!-- iOS meta tags & icons -->
24 - <meta name="apple-mobile-web-app-capable" content="yes"> 24 + <meta name="mobile-web-app-capable" content="yes">
25 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 25 <meta name="apple-mobile-web-app-status-bar-style" content="black">
26 <meta name="apple-mobile-web-app-title" content="mobile_scanner_example"> 26 <meta name="apple-mobile-web-app-title" content="mobile_scanner_example">
27 <link rel="apple-touch-icon" href="icons/Icon-192.png"> 27 <link rel="apple-touch-icon" href="icons/Icon-192.png">
@@ -19,6 +19,12 @@ public class BarcodeHandler: NSObject, FlutterStreamHandler { @@ -19,6 +19,12 @@ public class BarcodeHandler: NSObject, FlutterStreamHandler {
19 eventChannel.setStreamHandler(self) 19 eventChannel.setStreamHandler(self)
20 } 20 }
21 21
  22 + func publishError(_ error: FlutterError) {
  23 + DispatchQueue.main.async {
  24 + self.eventSink?(error)
  25 + }
  26 + }
  27 +
22 func publishEvent(_ event: [String: Any?]) { 28 func publishEvent(_ event: [String: Any?]) {
23 DispatchQueue.main.async { 29 DispatchQueue.main.async {
24 self.eventSink?(event) 30 self.eventSink?(event)
@@ -22,11 +22,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -22,11 +22,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
22 /// The selected camera 22 /// The selected camera
23 var device: AVCaptureDevice! 23 var device: AVCaptureDevice!
24 24
25 - /// Barcode scanner for results  
26 - var scanner = BarcodeScanner.barcodeScanner()  
27 -  
28 - /// Return image buffer with the Barcode event  
29 - var returnImage: Bool = false 25 + /// The long lived barcode scanner for scanning barcodes from a camera preview.
  26 + var scanner: BarcodeScanner? = nil
30 27
31 /// Default position of camera 28 /// Default position of camera
32 var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back 29 var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back
@@ -136,7 +133,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -136,7 +133,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
136 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 133 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
137 134
138 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 135 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
139 - print("Failed to get image buffer from sample buffer.")  
140 return 136 return
141 } 137 }
142 latestBuffer = imageBuffer 138 latestBuffer = imageBuffer
@@ -159,7 +155,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -159,7 +155,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
159 position: videoPosition 155 position: videoPosition
160 ) 156 )
161 157
162 - scanner.process(image) { [self] barcodes, error in 158 + scanner?.process(image) { [self] barcodes, error in
163 imagesCurrentlyBeingProcessed = false 159 imagesCurrentlyBeingProcessed = false
164 160
165 if (detectionSpeed == DetectionSpeed.noDuplicates) { 161 if (detectionSpeed == DetectionSpeed.noDuplicates) {
@@ -169,7 +165,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -169,7 +165,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
169 165
170 if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { 166 if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
171 return 167 return
172 - } else if (newScannedBarcodes?.isEmpty == false) { 168 + }
  169 +
  170 + if (newScannedBarcodes?.isEmpty == false) {
173 barcodesString = newScannedBarcodes 171 barcodesString = newScannedBarcodes
174 } 172 }
175 } 173 }
@@ -180,7 +178,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -180,7 +178,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
180 } 178 }
181 179
182 /// Start scanning for barcodes 180 /// Start scanning for barcodes
183 - func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws { 181 + func start(barcodeScannerOptions: BarcodeScannerOptions?, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws {
184 self.detectionSpeed = detectionSpeed 182 self.detectionSpeed = detectionSpeed
185 if (device != nil || captureSession != nil) { 183 if (device != nil || captureSession != nil) {
186 throw MobileScannerError.alreadyStarted 184 throw MobileScannerError.alreadyStarted
@@ -347,6 +345,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -347,6 +345,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
347 private func releaseTexture() { 345 private func releaseTexture() {
348 registry?.unregisterTexture(textureId) 346 registry?.unregisterTexture(textureId)
349 textureId = nil 347 textureId = nil
  348 + scanner = nil
350 } 349 }
351 350
352 /// Toggle the torch. 351 /// Toggle the torch.
@@ -464,7 +463,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -464,7 +463,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
464 } 463 }
465 464
466 /// Analyze a single image 465 /// Analyze a single image
467 - func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { 466 + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position,
  467 + barcodeScannerOptions: BarcodeScannerOptions?, callback: @escaping BarcodeScanningCallback) {
468 let image = VisionImage(image: image) 468 let image = VisionImage(image: image)
469 image.orientation = imageOrientation( 469 image.orientation = imageOrientation(
470 deviceOrientation: UIDevice.current.orientation, 470 deviceOrientation: UIDevice.current.orientation,
@@ -472,22 +472,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -472,22 +472,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
472 position: position 472 position: position
473 ) 473 )
474 474
  475 + let scanner: BarcodeScanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
  476 +
475 scanner.process(image, completion: callback) 477 scanner.process(image, completion: callback)
476 } 478 }
477 479
478 var barcodesString: Array<String?>? 480 var barcodesString: Array<String?>?
479 481
480 - // /// Convert image buffer to jpeg  
481 - // private func ciImageToJpeg(ciImage: CIImage) -> Data {  
482 - //  
483 - // // let ciImage = CIImage(cvPixelBuffer: latestBuffer)  
484 - // let context:CIContext = CIContext.init(options: nil)  
485 - // let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!  
486 - // let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)  
487 - //  
488 - // return uiImage.jpegData(compressionQuality: 0.8)!  
489 - // }  
490 -  
491 /// Rotates images accordingly 482 /// Rotates images accordingly
492 func imageOrientation( 483 func imageOrientation(
493 deviceOrientation: UIDeviceOrientation, 484 deviceOrientation: UIDeviceOrientation,
@@ -6,6 +6,12 @@ @@ -6,6 +6,12 @@
6 // 6 //
7 import Foundation 7 import Foundation
8 8
  9 +// TODO: decide if we should keep or discard this enum
  10 +// When merging the iOS / MacOS implementations we should either keep the enum or remove it
  11 +
  12 +// This enum is a bit of a leftover from older parts of the iOS implementation.
  13 +// It is used by the handler that throws these error codes,
  14 +// while the plugin class intercepts these and converts them to `FlutterError()`s.
9 enum MobileScannerError: Error { 15 enum MobileScannerError: Error {
10 case noCamera 16 case noCamera
11 case alreadyStarted 17 case alreadyStarted
  1 +//
  2 +// MobileScannerErrorCodes.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Navaron Bracke on 28/05/2024.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +/// This struct defines the error codes and error messages for MobileScanner errors.
  11 +///
  12 +/// These are used by `FlutterError` as error code and error message.
  13 +///
  14 +/// This struct should not be confused with `MobileScannerError`,
  15 +/// which is an implementation detail for the iOS implementation.
  16 +struct MobileScannerErrorCodes {
  17 + static let ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR"
  18 + static let ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started."
  19 + // The error code 'BARCODE_ERROR' does not have an error message,
  20 + // because it uses the error message from the undelying error.
  21 + static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR"
  22 + // The error code 'CAMERA_ERROR' does not have an error message,
  23 + // because it uses the error message from the underlying error.
  24 + static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR"
  25 + static let GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR"
  26 + static let GENERIC_ERROR_MESSAGE = "An unknown error occurred."
  27 + // This message is used with the 'GENERIC_ERROR' error code.
  28 + static let INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)"
  29 + static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR"
  30 + static let NO_CAMERA_ERROR_MESSAGE = "No cameras available."
  31 + static let SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR"
  32 + static let SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped."
  33 +}
@@ -12,6 +12,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -12,6 +12,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
12 /// The handler sends all information via an event channel back to Flutter 12 /// The handler sends all information via an event channel back to Flutter
13 private let barcodeHandler: BarcodeHandler 13 private let barcodeHandler: BarcodeHandler
14 14
  15 + /// Whether to return the input image with the barcode event.
  16 + /// This is static to avoid accessing `self` in the callback in the constructor.
  17 + private static var returnImage: Bool = false
  18 +
15 /// The points for the scan window. 19 /// The points for the scan window.
16 static var scanWindow: [CGFloat]? 20 static var scanWindow: [CGFloat]?
17 21
@@ -37,24 +41,47 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -37,24 +41,47 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
37 41
38 init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { 42 init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
39 self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in 43 self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
40 - if barcodes != nil { 44 + if error != nil {
  45 + barcodeHandler.publishError(
  46 + FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR,
  47 + message: error?.localizedDescription,
  48 + details: nil))
  49 + return
  50 + }
  51 +
  52 + if barcodes == nil {
  53 + return
  54 + }
  55 +
41 let barcodesMap: [Any?] = barcodes!.compactMap { barcode in 56 let barcodesMap: [Any?] = barcodes!.compactMap { barcode in
42 - if (MobileScannerPlugin.scanWindow != nil) {  
43 - if (MobileScannerPlugin.isBarcodeInScanWindow(barcode: barcode, imageSize: image.size)) { 57 + if (MobileScannerPlugin.scanWindow == nil) {
44 return barcode.data 58 return barcode.data
45 - } else {  
46 - return nil  
47 } 59 }
48 - } else { 60 +
  61 + if (MobileScannerPlugin.isBarcodeInScanWindow(barcode: barcode, imageSize: image.size)) {
49 return barcode.data 62 return barcode.data
50 } 63 }
  64 +
  65 + return nil
51 } 66 }
52 - if (!barcodesMap.isEmpty) {  
53 - barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!), "width": image.size.width, "height": image.size.height])  
54 - }  
55 - } else if (error != nil){  
56 - barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) 67 +
  68 + if (barcodesMap.isEmpty) {
  69 + return
57 } 70 }
  71 +
  72 + // The image dimensions are always provided.
  73 + // The image bytes are only non-null when `returnImage` is true.
  74 + let imageData: [String: Any?] = [
  75 + "bytes": MobileScannerPlugin.returnImage ? FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!) : nil,
  76 + "width": image.size.width,
  77 + "height": image.size.height,
  78 + ]
  79 +
  80 + barcodeHandler.publishEvent([
  81 + "name": "barcode",
  82 + "data": barcodesMap,
  83 + "image": imageData,
  84 + ])
58 }, torchModeChangeCallback: { torchState in 85 }, torchModeChangeCallback: { torchState in
59 barcodeHandler.publishEvent(["name": "torchState", "data": torchState]) 86 barcodeHandler.publishEvent(["name": "torchState", "data": torchState])
60 }, zoomScaleChangeCallback: { zoomScale in 87 }, zoomScaleChangeCallback: { zoomScale in
@@ -106,23 +133,15 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -106,23 +133,15 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
106 let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0 133 let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0
107 let timeoutMs: Int = (call.arguments as! Dictionary<String, Any?>)["timeout"] as? Int ?? 0 134 let timeoutMs: Int = (call.arguments as! Dictionary<String, Any?>)["timeout"] as? Int ?? 0
108 self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000) 135 self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000)
  136 + MobileScannerPlugin.returnImage = returnImage
109 137
110 - let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}  
111 - var barcodeOptions: BarcodeScannerOptions? = nil  
112 -  
113 - if (formatList.count != 0) {  
114 - var barcodeFormats: BarcodeFormat = []  
115 - for index in formats {  
116 - barcodeFormats.insert(BarcodeFormat(rawValue: index))  
117 - }  
118 - barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)  
119 - } 138 + let barcodeOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)
120 139
121 let position = facing == 0 ? AVCaptureDevice.Position.front : .back 140 let position = facing == 0 ? AVCaptureDevice.Position.front : .back
122 let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! 141 let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
123 142
124 do { 143 do {
125 - try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch, detectionSpeed: detectionSpeed) { parameters in 144 + try mobileScanner.start(barcodeScannerOptions: barcodeOptions, cameraPosition: position, torch: torch, detectionSpeed: detectionSpeed) { parameters in
126 DispatchQueue.main.async { 145 DispatchQueue.main.async {
127 result([ 146 result([
128 "textureId": parameters.textureId, 147 "textureId": parameters.textureId,
@@ -132,20 +151,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -132,20 +151,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
132 } 151 }
133 } 152 }
134 } catch MobileScannerError.alreadyStarted { 153 } catch MobileScannerError.alreadyStarted {
135 - result(FlutterError(code: "MobileScanner",  
136 - message: "Called start() while already started!", 154 + result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  155 + message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
137 details: nil)) 156 details: nil))
138 } catch MobileScannerError.noCamera { 157 } catch MobileScannerError.noCamera {
139 - result(FlutterError(code: "MobileScanner",  
140 - message: "No camera found or failed to open camera!", 158 + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
  159 + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
141 details: nil)) 160 details: nil))
142 } catch MobileScannerError.cameraError(let error) { 161 } catch MobileScannerError.cameraError(let error) {
143 - result(FlutterError(code: "MobileScanner",  
144 - message: "Error occured when setting up camera!",  
145 - details: error)) 162 + result(FlutterError(code: MobileScannerErrorCodes.CAMERA_ERROR,
  163 + message: error.localizedDescription,
  164 + details: nil))
146 } catch { 165 } catch {
147 - result(FlutterError(code: "MobileScanner",  
148 - message: "Unknown error occured.", 166 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  167 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
149 details: nil)) 168 details: nil))
150 } 169 }
151 } 170 }
@@ -176,25 +195,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -176,25 +195,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
176 private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 195 private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
177 let scale = call.arguments as? CGFloat 196 let scale = call.arguments as? CGFloat
178 if (scale == nil) { 197 if (scale == nil) {
179 - result(FlutterError(code: "MobileScanner",  
180 - message: "You must provide a scale when calling setScale!",  
181 - details: nil)) 198 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  199 + message: MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE,
  200 + details: "The invalid zoom scale was nil."))
182 return 201 return
183 } 202 }
184 do { 203 do {
185 try mobileScanner.setScale(scale!) 204 try mobileScanner.setScale(scale!)
186 result(nil) 205 result(nil)
187 } catch MobileScannerError.zoomWhenStopped { 206 } catch MobileScannerError.zoomWhenStopped {
188 - result(FlutterError(code: "MobileScanner",  
189 - message: "Called setScale() while stopped!", 207 + result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR,
  208 + message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE,
190 details: nil)) 209 details: nil))
191 } catch MobileScannerError.zoomError(let error) { 210 } catch MobileScannerError.zoomError(let error) {
192 - result(FlutterError(code: "MobileScanner",  
193 - message: "Error while zooming.",  
194 - details: error)) 211 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  212 + message: error.localizedDescription,
  213 + details: nil))
195 } catch { 214 } catch {
196 - result(FlutterError(code: "MobileScanner",  
197 - message: "Error while zooming.", 215 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  216 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
198 details: nil)) 217 details: nil))
199 } 218 }
200 } 219 }
@@ -205,16 +224,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -205,16 +224,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
205 try mobileScanner.resetScale() 224 try mobileScanner.resetScale()
206 result(nil) 225 result(nil)
207 } catch MobileScannerError.zoomWhenStopped { 226 } catch MobileScannerError.zoomWhenStopped {
208 - result(FlutterError(code: "MobileScanner",  
209 - message: "Called resetScale() while stopped!", 227 + result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR,
  228 + message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE,
210 details: nil)) 229 details: nil))
211 } catch MobileScannerError.zoomError(let error) { 230 } catch MobileScannerError.zoomError(let error) {
212 - result(FlutterError(code: "MobileScanner",  
213 - message: "Error while zooming.",  
214 - details: error)) 231 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  232 + message: error.localizedDescription,
  233 + details: nil))
215 } catch { 234 } catch {
216 - result(FlutterError(code: "MobileScanner",  
217 - message: "Error while zooming.", 235 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  236 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
218 details: nil)) 237 details: nil))
219 } 238 }
220 } 239 }
@@ -243,19 +262,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -243,19 +262,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
243 262
244 /// Analyzes a single image. 263 /// Analyzes a single image.
245 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 264 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
246 - let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") 265 + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
  266 + let scannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)
  267 + let uiImage = UIImage(contentsOfFile: (call.arguments as! Dictionary<String, Any?>)["filePath"] as? String ?? "")
247 268
248 if (uiImage == nil) { 269 if (uiImage == nil) {
249 - result(FlutterError(code: "MobileScanner",  
250 - message: "No image found in analyzeImage!",  
251 - details: nil)) 270 + result(nil)
252 return 271 return
253 } 272 }
254 273
255 - mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { barcodes, error in 274 + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back,
  275 + barcodeScannerOptions: scannerOptions, callback: { barcodes, error in
256 if error != nil { 276 if error != nil {
257 DispatchQueue.main.async { 277 DispatchQueue.main.async {
258 - result(FlutterError(code: "MobileScanner", 278 + result(FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR,
259 message: error?.localizedDescription, 279 message: error?.localizedDescription,
260 details: nil)) 280 details: nil))
261 } 281 }
@@ -267,13 +287,29 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -267,13 +287,29 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
267 DispatchQueue.main.async { 287 DispatchQueue.main.async {
268 result(nil) 288 result(nil)
269 } 289 }
270 - } else { 290 +
  291 + return
  292 + }
  293 +
271 let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data } 294 let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data }
272 295
273 DispatchQueue.main.async { 296 DispatchQueue.main.async {
274 result(["name": "barcode", "data": barcodesMap]) 297 result(["name": "barcode", "data": barcodesMap])
275 } 298 }
276 - }  
277 }) 299 })
278 } 300 }
  301 +
  302 + private func buildBarcodeScannerOptions(_ formats: [Int]) -> BarcodeScannerOptions? {
  303 + guard !formats.isEmpty else {
  304 + return nil
  305 + }
  306 +
  307 + var barcodeFormats: BarcodeFormat = []
  308 +
  309 + for format in formats {
  310 + barcodeFormats.insert(BarcodeFormat(rawValue: format))
  311 + }
  312 +
  313 + return BarcodeScannerOptions(formats: barcodeFormats)
  314 + }
279 } 315 }
@@ -29,8 +29,27 @@ extension UIDeviceOrientation { @@ -29,8 +29,27 @@ extension UIDeviceOrientation {
29 29
30 extension Barcode { 30 extension Barcode {
31 var data: [String: Any?] { 31 var data: [String: Any?] {
32 - let corners = cornerPoints?.map({$0.cgPointValue.data})  
33 - return ["corners": corners, "format": format.rawValue, "rawBytes": rawData, "rawValue": rawValue, "type": valueType.rawValue, "calendarEvent": calendarEvent?.data, "contactInfo": contactInfo?.data, "driverLicense": driverLicense?.data, "email": email?.data, "geoPoint": geoPoint?.data, "phone": phone?.data, "sms": sms?.data, "url": url?.data, "wifi": wifi?.data, "displayValue": displayValue] 32 + return [
  33 + "calendarEvent": calendarEvent?.data,
  34 + "contactInfo": contactInfo?.data,
  35 + "corners": cornerPoints?.map({$0.cgPointValue.data}),
  36 + "displayValue": displayValue,
  37 + "driverLicense": driverLicense?.data,
  38 + "email": email?.data,
  39 + "format": format.rawValue,
  40 + "geoPoint": geoPoint?.data,
  41 + "phone": phone?.data,
  42 + "rawBytes": rawData,
  43 + "rawValue": rawValue,
  44 + "size": frame.isNull ? nil : [
  45 + "width": frame.width,
  46 + "height": frame.height,
  47 + ],
  48 + "sms": sms?.data,
  49 + "type": valueType.rawValue,
  50 + "url": url?.data,
  51 + "wifi": wifi?.data,
  52 + ]
34 } 53 }
35 } 54 }
36 55
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '5.1.1' 7 + s.version = '6.0.2'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
@@ -15,11 +15,16 @@ An universal scanner for Flutter based on MLKit. @@ -15,11 +15,16 @@ An universal scanner for Flutter based on MLKit.
15 s.source = { :path => '.' } 15 s.source = { :path => '.' }
16 s.source_files = 'Classes/**/*' 16 s.source_files = 'Classes/**/*'
17 s.dependency 'Flutter' 17 s.dependency 'Flutter'
18 - s.dependency 'GoogleMLKit/BarcodeScanning', '~> 6.0.0'  
19 - s.platform = :ios, '12.0' 18 + s.dependency 'GoogleMLKit/BarcodeScanning', '~> 7.0.0'
  19 + s.platform = :ios, '15.5.0'
20 s.static_framework = true 20 s.static_framework = true
21 - # Flutter.framework does not contain a i386 slice.  
22 - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 21 + # Flutter.framework does not contain a i386 slice, and MLKit does not support armv7.
  22 + s.pod_target_xcconfig = {
  23 + 'DEFINES_MODULE' => 'YES',
  24 + # TODO: add back arm64 (and armv7?) when switching to the Vision API.
  25 + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386 armv7 arm64',
  26 + 'EXCLUDED_ARCHS[sdk=iphoneos*]' => 'armv7',
  27 + }
23 s.swift_version = '5.0' 28 s.swift_version = '5.0'
24 s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] } 29 s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] }
25 end 30 end
@@ -11,8 +11,7 @@ export 'src/enums/phone_type.dart'; @@ -11,8 +11,7 @@ export 'src/enums/phone_type.dart';
11 export 'src/enums/torch_state.dart'; 11 export 'src/enums/torch_state.dart';
12 export 'src/mobile_scanner.dart'; 12 export 'src/mobile_scanner.dart';
13 export 'src/mobile_scanner_controller.dart'; 13 export 'src/mobile_scanner_controller.dart';
14 -export 'src/mobile_scanner_exception.dart'  
15 - hide PermissionRequestPendingException; 14 +export 'src/mobile_scanner_exception.dart';
16 export 'src/mobile_scanner_platform_interface.dart'; 15 export 'src/mobile_scanner_platform_interface.dart';
17 export 'src/objects/address.dart'; 16 export 'src/objects/address.dart';
18 export 'src/objects/barcode.dart'; 17 export 'src/objects/barcode.dart';
@@ -20,7 +20,7 @@ enum AddressType { @@ -20,7 +20,7 @@ enum AddressType {
20 case 2: 20 case 2:
21 return AddressType.home; 21 return AddressType.home;
22 default: 22 default:
23 - throw ArgumentError.value(value, 'value', 'Invalid raw value.'); 23 + return AddressType.unknown;
24 } 24 }
25 } 25 }
26 26
@@ -70,7 +70,7 @@ enum BarcodeType { @@ -70,7 +70,7 @@ enum BarcodeType {
70 case 12: 70 case 12:
71 return BarcodeType.driverLicense; 71 return BarcodeType.driverLicense;
72 default: 72 default:
73 - throw ArgumentError.value(value, 'value', 'Invalid raw value.'); 73 + return BarcodeType.unknown;
74 } 74 }
75 } 75 }
76 76
@@ -20,7 +20,7 @@ enum EmailType { @@ -20,7 +20,7 @@ enum EmailType {
20 case 2: 20 case 2:
21 return EmailType.home; 21 return EmailType.home;
22 default: 22 default:
23 - throw ArgumentError.value(value, 'value', 'Invalid raw value.'); 23 + return EmailType.unknown;
24 } 24 }
25 } 25 }
26 26
1 /// Wifi encryption type constants. 1 /// Wifi encryption type constants.
2 enum EncryptionType { 2 enum EncryptionType {
3 /// Unknown encryption type. 3 /// Unknown encryption type.
4 - none(0), 4 + unknown(0),
5 5
6 /// Not encrypted. 6 /// Not encrypted.
7 open(1), 7 open(1),
@@ -14,10 +14,15 @@ enum EncryptionType { @@ -14,10 +14,15 @@ enum EncryptionType {
14 14
15 const EncryptionType(this.rawValue); 15 const EncryptionType(this.rawValue);
16 16
  17 + @Deprecated(
  18 + 'EncryptionType.none is deprecated. Use EncryptionType.unknown instead.',
  19 + )
  20 + static const EncryptionType none = EncryptionType.unknown;
  21 +
17 factory EncryptionType.fromRawValue(int value) { 22 factory EncryptionType.fromRawValue(int value) {
18 switch (value) { 23 switch (value) {
19 case 0: 24 case 0:
20 - return EncryptionType.none; 25 + return EncryptionType.unknown;
21 case 1: 26 case 1:
22 return EncryptionType.open; 27 return EncryptionType.open;
23 case 2: 28 case 2:
@@ -25,7 +30,7 @@ enum EncryptionType { @@ -25,7 +30,7 @@ enum EncryptionType {
25 case 3: 30 case 3:
26 return EncryptionType.wep; 31 return EncryptionType.wep;
27 default: 32 default:
28 - throw ArgumentError.value(value, 'value', 'Invalid raw value.'); 33 + return EncryptionType.unknown;
29 } 34 }
30 } 35 }
31 36
  1 +import 'package:flutter/services.dart';
1 import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; 2 import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
2 3
3 /// This enum defines the different error codes for the mobile scanner. 4 /// This enum defines the different error codes for the mobile scanner.
@@ -24,5 +25,25 @@ enum MobileScannerErrorCode { @@ -24,5 +25,25 @@ enum MobileScannerErrorCode {
24 permissionDenied, 25 permissionDenied,
25 26
26 /// Scanning is unsupported on the current device. 27 /// Scanning is unsupported on the current device.
27 - unsupported, 28 + unsupported;
  29 +
  30 + /// Convert the given [PlatformException.code] to a [MobileScannerErrorCode].
  31 + factory MobileScannerErrorCode.fromPlatformException(
  32 + PlatformException exception,
  33 + ) {
  34 + // The following error code mapping should be kept in sync with their native counterparts.
  35 + // These are located in `MobileScannerErrorCodes.kt` and `MobileScannerErrorCodes.swift`.
  36 + return switch (exception.code) {
  37 + // In case the scanner was already started, report the right error code.
  38 + // If the scanner is already starting,
  39 + // this error code is a signal to the controller to just ignore the attempt.
  40 + 'MOBILE_SCANNER_ALREADY_STARTED_ERROR' =>
  41 + MobileScannerErrorCode.controllerAlreadyInitialized,
  42 + // In case no cameras are available, using the scanner is not supported.
  43 + 'MOBILE_SCANNER_NO_CAMERA_ERROR' => MobileScannerErrorCode.unsupported,
  44 + 'MOBILE_SCANNER_CAMERA_PERMISSION_DENIED' =>
  45 + MobileScannerErrorCode.permissionDenied,
  46 + _ => MobileScannerErrorCode.genericError,
  47 + };
  48 + }
28 } 49 }
@@ -30,7 +30,7 @@ enum PhoneType { @@ -30,7 +30,7 @@ enum PhoneType {
30 case 4: 30 case 4:
31 return PhoneType.mobile; 31 return PhoneType.mobile;
32 default: 32 default:
33 - throw ArgumentError.value(value, 'value', 'Invalid raw value.'); 33 + return PhoneType.unknown;
34 } 34 }
35 } 35 }
36 36
@@ -16,6 +16,14 @@ import 'package:mobile_scanner/src/objects/start_options.dart'; @@ -16,6 +16,14 @@ import 'package:mobile_scanner/src/objects/start_options.dart';
16 16
17 /// An implementation of [MobileScannerPlatform] that uses method channels. 17 /// An implementation of [MobileScannerPlatform] that uses method channels.
18 class MethodChannelMobileScanner extends MobileScannerPlatform { 18 class MethodChannelMobileScanner extends MobileScannerPlatform {
  19 + /// The name of the barcode event that is sent when a barcode is scanned.
  20 + @visibleForTesting
  21 + static const String kBarcodeEventName = 'barcode';
  22 +
  23 + /// The name of the error event that is sent when a barcode scan error occurs.
  24 + @visibleForTesting
  25 + static const String kBarcodeErrorEventName = 'MOBILE_SCANNER_BARCODE_ERROR';
  26 +
19 /// The method channel used to interact with the native platform. 27 /// The method channel used to interact with the native platform.
20 @visibleForTesting 28 @visibleForTesting
21 final methodChannel = const MethodChannel( 29 final methodChannel = const MethodChannel(
@@ -55,31 +63,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -55,31 +63,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
55 final List<Map<Object?, Object?>> barcodes = 63 final List<Map<Object?, Object?>> barcodes =
56 data.cast<Map<Object?, Object?>>(); 64 data.cast<Map<Object?, Object?>>();
57 65
58 - if (defaultTargetPlatform == TargetPlatform.macOS) {  
59 - return BarcodeCapture(  
60 - raw: event,  
61 - barcodes: barcodes  
62 - .map(  
63 - (barcode) => Barcode(  
64 - rawValue: barcode['payload'] as String?,  
65 - format: BarcodeFormat.fromRawValue(  
66 - barcode['symbology'] as int? ?? -1,  
67 - ),  
68 - ),  
69 - )  
70 - .toList(),  
71 - );  
72 - }  
73 -  
74 if (defaultTargetPlatform == TargetPlatform.android || 66 if (defaultTargetPlatform == TargetPlatform.android ||
75 - defaultTargetPlatform == TargetPlatform.iOS) {  
76 - final double? width = event['width'] as double?;  
77 - final double? height = event['height'] as double?; 67 + defaultTargetPlatform == TargetPlatform.iOS ||
  68 + defaultTargetPlatform == TargetPlatform.macOS) {
  69 + final Map<Object?, Object?>? imageData =
  70 + event['image'] as Map<Object?, Object?>?;
  71 + final Uint8List? image = imageData?['bytes'] as Uint8List?;
  72 + final double? width = imageData?['width'] as double?;
  73 + final double? height = imageData?['height'] as double?;
78 74
79 return BarcodeCapture( 75 return BarcodeCapture(
80 - raw: data, 76 + raw: event,
81 barcodes: barcodes.map(Barcode.fromNative).toList(), 77 barcodes: barcodes.map(Barcode.fromNative).toList(),
82 - image: event['image'] as Uint8List?, 78 + image: image,
83 size: width == null || height == null ? Size.zero : Size(width, height), 79 size: width == null || height == null ? Size.zero : Size(width, height),
84 ); 80 );
85 } 81 }
@@ -92,6 +88,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -92,6 +88,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
92 ); 88 );
93 } 89 }
94 90
  91 + /// Parse a [MobileScannerBarcodeException] from the given [error] and [stackTrace], and throw it.
  92 + ///
  93 + /// If the error is not a [PlatformException],
  94 + /// with [kBarcodeErrorEventName] as [PlatformException.code], the error is rethrown as-is.
  95 + Never _parseBarcodeError(Object error, StackTrace stackTrace) {
  96 + if (error case PlatformException(:final String code, :final String? message)
  97 + when code == kBarcodeErrorEventName) {
  98 + throw MobileScannerBarcodeException(message);
  99 + }
  100 +
  101 + Error.throwWithStackTrace(error, stackTrace);
  102 + }
  103 +
95 /// Request permission to access the camera. 104 /// Request permission to access the camera.
96 /// 105 ///
97 /// Throws a [MobileScannerException] if the permission is not granted. 106 /// Throws a [MobileScannerException] if the permission is not granted.
@@ -134,9 +143,12 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -134,9 +143,12 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
134 143
135 @override 144 @override
136 Stream<BarcodeCapture?> get barcodesStream { 145 Stream<BarcodeCapture?> get barcodesStream {
  146 + // Handle incoming barcode events.
  147 + // The error events are transformed to `MobileScannerBarcodeException` where possible.
137 return eventsStream 148 return eventsStream
138 - .where((event) => event['name'] == 'barcode')  
139 - .map((event) => _parseBarcode(event)); 149 + .where((e) => e['name'] == kBarcodeEventName)
  150 + .map((event) => _parseBarcode(event))
  151 + .handleError(_parseBarcodeError);
140 } 152 }
141 153
142 @override 154 @override
@@ -154,14 +166,34 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -154,14 +166,34 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
154 } 166 }
155 167
156 @override 168 @override
157 - Future<BarcodeCapture?> analyzeImage(String path) async {  
158 - final Map<String, Object?>? result =  
159 - await methodChannel.invokeMapMethod<String, Object?>( 169 + Future<BarcodeCapture?> analyzeImage(
  170 + String path, {
  171 + List<BarcodeFormat> formats = const <BarcodeFormat>[],
  172 + }) async {
  173 + try {
  174 + final Map<Object?, Object?>? result =
  175 + await methodChannel.invokeMapMethod<Object?, Object?>(
160 'analyzeImage', 176 'analyzeImage',
161 - path, 177 + {
  178 + 'filePath': path,
  179 + 'formats': formats.isEmpty
  180 + ? null
  181 + : [
  182 + for (final BarcodeFormat format in formats)
  183 + if (format != BarcodeFormat.unknown) format.rawValue,
  184 + ],
  185 + },
162 ); 186 );
163 187
164 return _parseBarcode(result); 188 return _parseBarcode(result);
  189 + } on PlatformException catch (error) {
  190 + // Handle any errors from analyze image requests.
  191 + if (error.code == kBarcodeErrorEventName) {
  192 + throw MobileScannerBarcodeException(error.message);
  193 + }
  194 +
  195 + return null;
  196 + }
165 } 197 }
166 198
167 @override 199 @override
@@ -189,8 +221,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -189,8 +221,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
189 throw const MobileScannerException( 221 throw const MobileScannerException(
190 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, 222 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
191 errorDetails: MobileScannerErrorDetails( 223 errorDetails: MobileScannerErrorDetails(
192 - message:  
193 - 'The scanner was already started. Call stop() before calling start() again.', 224 + message: 'The scanner was already started.',
194 ), 225 ),
195 ); 226 );
196 } 227 }
@@ -206,7 +237,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -206,7 +237,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
206 ); 237 );
207 } on PlatformException catch (error) { 238 } on PlatformException catch (error) {
208 throw MobileScannerException( 239 throw MobileScannerException(
209 - errorCode: MobileScannerErrorCode.genericError, 240 + errorCode: MobileScannerErrorCode.fromPlatformException(error),
210 errorDetails: MobileScannerErrorDetails( 241 errorDetails: MobileScannerErrorDetails(
211 code: error.code, 242 code: error.code,
212 details: error.details as Object?, 243 details: error.details as Object?,
@@ -242,17 +273,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -242,17 +273,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
242 startResult['currentTorchState'] as int? ?? -1, 273 startResult['currentTorchState'] as int? ?? -1,
243 ); 274 );
244 275
245 - final Map<Object?, Object?>? sizeInfo =  
246 - startResult['size'] as Map<Object?, Object?>?;  
247 - final double? width = sizeInfo?['width'] as double?;  
248 - final double? height = sizeInfo?['height'] as double?;  
249 -  
250 final Size size; 276 final Size size;
251 277
252 - if (width == null || height == null) {  
253 - size = Size.zero;  
254 - } else { 278 + if (startResult['size']
  279 + case {'width': final double width, 'height': final double height}) {
255 size = Size(width, height); 280 size = Size(width, height);
  281 + } else {
  282 + size = Size.zero;
256 } 283 }
257 284
258 _pausing = false; 285 _pausing = false;
@@ -21,6 +21,7 @@ class MobileScanner extends StatefulWidget { @@ -21,6 +21,7 @@ class MobileScanner extends StatefulWidget {
21 const MobileScanner({ 21 const MobileScanner({
22 this.controller, 22 this.controller,
23 this.onDetect, 23 this.onDetect,
  24 + this.onDetectError = _onDetectErrorHandler,
24 this.fit = BoxFit.cover, 25 this.fit = BoxFit.cover,
25 this.errorBuilder, 26 this.errorBuilder,
26 this.overlayBuilder, 27 this.overlayBuilder,
@@ -34,9 +35,17 @@ class MobileScanner extends StatefulWidget { @@ -34,9 +35,17 @@ class MobileScanner extends StatefulWidget {
34 final MobileScannerController? controller; 35 final MobileScannerController? controller;
35 36
36 /// The function that signals when new codes were detected by the [controller]. 37 /// The function that signals when new codes were detected by the [controller].
37 - /// If null, use the controller.barcodes stream directly to capture barcodes. 38 + ///
  39 + /// To handle both [BarcodeCapture]s and [MobileScannerBarcodeException]s,
  40 + /// use the [MobileScannerController.barcodes] stream directly (recommended),
  41 + /// or provide a function to [onDetectError].
38 final void Function(BarcodeCapture barcodes)? onDetect; 42 final void Function(BarcodeCapture barcodes)? onDetect;
39 43
  44 + /// The error handler equivalent for the [onDetect] function.
  45 + ///
  46 + /// If [onDetect] is not null, and this is null, errors are silently ignored.
  47 + final void Function(Object error, StackTrace stackTrace) onDetectError;
  48 +
40 /// The error builder for the camera preview. 49 /// The error builder for the camera preview.
41 /// 50 ///
42 /// If this is null, a black [ColoredBox], 51 /// If this is null, a black [ColoredBox],
@@ -116,6 +125,11 @@ class MobileScanner extends StatefulWidget { @@ -116,6 +125,11 @@ class MobileScanner extends StatefulWidget {
116 125
117 @override 126 @override
118 State<MobileScanner> createState() => _MobileScannerState(); 127 State<MobileScanner> createState() => _MobileScannerState();
  128 +
  129 + /// This empty function is used as the default error handler for [onDetect].
  130 + static void _onDetectErrorHandler(Object error, StackTrace stackTrace) {
  131 + // Do nothing.
  132 + }
119 } 133 }
120 134
121 class _MobileScannerState extends State<MobileScanner> 135 class _MobileScannerState extends State<MobileScanner>
@@ -249,7 +263,11 @@ class _MobileScannerState extends State<MobileScanner> @@ -249,7 +263,11 @@ class _MobileScannerState extends State<MobileScanner>
249 void initState() { 263 void initState() {
250 if (widget.onDetect != null) { 264 if (widget.onDetect != null) {
251 WidgetsBinding.instance.addObserver(this); 265 WidgetsBinding.instance.addObserver(this);
252 - _subscription = controller.barcodes.listen(widget.onDetect); 266 + _subscription = controller.barcodes.listen(
  267 + widget.onDetect,
  268 + onError: widget.onDetectError,
  269 + cancelOnError: false,
  270 + );
253 } 271 }
254 if (controller.autoStart) { 272 if (controller.autoStart) {
255 controller.start(); 273 controller.start();
@@ -281,8 +299,7 @@ class _MobileScannerState extends State<MobileScanner> @@ -281,8 +299,7 @@ class _MobileScannerState extends State<MobileScanner>
281 299
282 @override 300 @override
283 void didChangeAppLifecycleState(AppLifecycleState state) { 301 void didChangeAppLifecycleState(AppLifecycleState state) {
284 - if (widget.controller != null) return;  
285 - if (!controller.value.isInitialized) { 302 + if (widget.controller != null || !controller.value.hasCameraPermission) {
286 return; 303 return;
287 } 304 }
288 305
@@ -292,7 +309,11 @@ class _MobileScannerState extends State<MobileScanner> @@ -292,7 +309,11 @@ class _MobileScannerState extends State<MobileScanner>
292 case AppLifecycleState.paused: 309 case AppLifecycleState.paused:
293 return; 310 return;
294 case AppLifecycleState.resumed: 311 case AppLifecycleState.resumed:
295 - _subscription = controller.barcodes.listen(widget.onDetect); 312 + _subscription = controller.barcodes.listen(
  313 + widget.onDetect,
  314 + onError: widget.onDetectError,
  315 + cancelOnError: false,
  316 + );
296 317
297 unawaited(controller.start()); 318 unawaited(controller.start());
298 case AppLifecycleState.inactive: 319 case AppLifecycleState.inactive:
@@ -75,12 +75,11 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -75,12 +75,11 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
75 /// If this is empty, all supported formats are detected. 75 /// If this is empty, all supported formats are detected.
76 final List<BarcodeFormat> formats; 76 final List<BarcodeFormat> formats;
77 77
78 - /// Whether scanned barcodes should contain the image  
79 - /// that is embedded into the barcode. 78 + /// Whether the [BarcodeCapture.image] bytes should be provided.
80 /// 79 ///
81 /// If this is false, [BarcodeCapture.image] will always be null. 80 /// If this is false, [BarcodeCapture.image] will always be null.
82 /// 81 ///
83 - /// Defaults to false, and is only supported on iOS and Android. 82 + /// Defaults to false, and is only supported on iOS, MacOS and Android.
84 final bool returnImage; 83 final bool returnImage;
85 84
86 /// Whether the flashlight should be turned on when the camera is started. 85 /// Whether the flashlight should be turned on when the camera is started.
@@ -102,6 +101,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -102,6 +101,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
102 StreamController.broadcast(); 101 StreamController.broadcast();
103 102
104 /// Get the stream of scanned barcodes. 103 /// Get the stream of scanned barcodes.
  104 + ///
  105 + /// If an error occurred during the detection of a barcode,
  106 + /// a [MobileScannerBarcodeException] error is emitted to the stream.
105 Stream<BarcodeCapture> get barcodes => _barcodesController.stream; 107 Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
106 108
107 StreamSubscription<BarcodeCapture?>? _barcodesSubscription; 109 StreamSubscription<BarcodeCapture?>? _barcodesSubscription;
@@ -121,14 +123,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -121,14 +123,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
121 } 123 }
122 124
123 void _setupListeners() { 125 void _setupListeners() {
124 - _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream  
125 - .listen((BarcodeCapture? barcode) { 126 + _barcodesSubscription =
  127 + MobileScannerPlatform.instance.barcodesStream.listen(
  128 + (BarcodeCapture? barcode) {
126 if (_barcodesController.isClosed || barcode == null) { 129 if (_barcodesController.isClosed || barcode == null) {
127 return; 130 return;
128 } 131 }
129 132
130 _barcodesController.add(barcode); 133 _barcodesController.add(barcode);
131 - }); 134 + },
  135 + onError: (Object error) {
  136 + if (_barcodesController.isClosed) {
  137 + return;
  138 + }
  139 +
  140 + _barcodesController.addError(error);
  141 + },
  142 + // Errors are handled gracefully by forwarding them.
  143 + cancelOnError: false,
  144 + );
132 145
133 _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream 146 _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream
134 .listen((TorchState torchState) { 147 .listen((TorchState torchState) {
@@ -197,12 +210,20 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -197,12 +210,20 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
197 /// Analyze an image file. 210 /// Analyze an image file.
198 /// 211 ///
199 /// The [path] points to a file on the device. 212 /// The [path] points to a file on the device.
  213 + /// The [formats] specify the barcode formats that should be detected in the image.
  214 + /// If the [formats] are omitted or empty, all formats are detected.
200 /// 215 ///
201 - /// This is only supported on Android and iOS. 216 + /// This is only supported on Android, iOS and MacOS.
202 /// 217 ///
203 /// Returns the [BarcodeCapture] that was found in the image. 218 /// Returns the [BarcodeCapture] that was found in the image.
204 - Future<BarcodeCapture?> analyzeImage(String path) {  
205 - return MobileScannerPlatform.instance.analyzeImage(path); 219 + ///
  220 + /// If an error occurred during the analysis of the image,
  221 + /// a [MobileScannerBarcodeException] error is thrown.
  222 + Future<BarcodeCapture?> analyzeImage(
  223 + String path, {
  224 + List<BarcodeFormat> formats = const <BarcodeFormat>[],
  225 + }) {
  226 + return MobileScannerPlatform.instance.analyzeImage(path, formats: formats);
206 } 227 }
207 228
208 /// Build a camera preview widget. 229 /// Build a camera preview widget.
@@ -270,13 +291,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -270,13 +291,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
270 ); 291 );
271 } 292 }
272 293
273 - // Permission was denied, do nothing.  
274 - // When the controller is stopped,  
275 - // the error is reset so the permission can be requested again if possible.  
276 - if (value.error?.errorCode == MobileScannerErrorCode.permissionDenied) {  
277 - return;  
278 - }  
279 -  
280 // Do nothing if the camera is already running. 294 // Do nothing if the camera is already running.
281 if (value.isRunning) { 295 if (value.isRunning) {
282 return; 296 return;
@@ -316,6 +330,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -316,6 +330,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
316 ); 330 );
317 } 331 }
318 } on MobileScannerException catch (error) { 332 } on MobileScannerException catch (error) {
  333 + // If the controller is already initialized, ignore the error.
  334 + // Starting the controller while it is already started, or in the process of starting, is redundant.
  335 + if (error.errorCode ==
  336 + MobileScannerErrorCode.controllerAlreadyInitialized) {
  337 + return;
  338 + }
  339 +
319 // The initialization finished with an error. 340 // The initialization finished with an error.
320 // To avoid stale values, reset the output size, 341 // To avoid stale values, reset the output size,
321 // torch state and zoom scale to the defaults. 342 // torch state and zoom scale to the defaults.
@@ -330,8 +351,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -330,8 +351,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
330 zoomScale: 1.0, 351 zoomScale: 1.0,
331 ); 352 );
332 } 353 }
333 - } on PermissionRequestPendingException catch (_) {  
334 - // If a permission request was already pending, do nothing.  
335 } 354 }
336 } 355 }
337 356
1 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; 1 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
2 2
3 -/// This class represents an exception thrown by the mobile scanner. 3 +/// This class represents an exception thrown by the [MobileScannerController].
4 class MobileScannerException implements Exception { 4 class MobileScannerException implements Exception {
5 const MobileScannerException({ 5 const MobileScannerException({
6 required this.errorCode, 6 required this.errorCode,
@@ -16,9 +16,9 @@ class MobileScannerException implements Exception { @@ -16,9 +16,9 @@ class MobileScannerException implements Exception {
16 @override 16 @override
17 String toString() { 17 String toString() {
18 if (errorDetails != null && errorDetails?.message != null) { 18 if (errorDetails != null && errorDetails?.message != null) {
19 - return "MobileScannerException: code ${errorCode.name}, message: ${errorDetails?.message}"; 19 + return 'MobileScannerException(${errorCode.name}, ${errorDetails?.message})';
20 } 20 }
21 - return "MobileScannerException: ${errorCode.name}"; 21 + return 'MobileScannerException(${errorCode.name})';
22 } 22 }
23 } 23 }
24 24
@@ -40,9 +40,21 @@ class MobileScannerErrorDetails { @@ -40,9 +40,21 @@ class MobileScannerErrorDetails {
40 final String? message; 40 final String? message;
41 } 41 }
42 42
43 -/// This class represents an exception that is thrown  
44 -/// when the scanner was (re)started while a permission request was pending.  
45 -///  
46 -/// This exception type is only used internally,  
47 -/// and is not part of the public API.  
48 -class PermissionRequestPendingException implements Exception {} 43 +/// This class represents an exception thrown by the [MobileScannerController]
  44 +/// when a barcode scanning error occurs when processing an input frame.
  45 +class MobileScannerBarcodeException implements Exception {
  46 + /// Creates a new [MobileScannerBarcodeException] with the given error message.
  47 + const MobileScannerBarcodeException(this.message);
  48 +
  49 + /// The error message of the exception.
  50 + final String? message;
  51 +
  52 + @override
  53 + String toString() {
  54 + if (message?.isNotEmpty ?? false) {
  55 + return 'MobileScannerBarcodeException($message)';
  56 + }
  57 +
  58 + return 'MobileScannerBarcodeException(Could not detect a barcode in the input image.)';
  59 + }
  60 +}
1 import 'package:flutter/widgets.dart'; 1 import 'package:flutter/widgets.dart';
  2 +import 'package:mobile_scanner/src/enums/barcode_format.dart';
2 import 'package:mobile_scanner/src/enums/torch_state.dart'; 3 import 'package:mobile_scanner/src/enums/torch_state.dart';
3 import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; 4 import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart';
4 import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; 5 import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
@@ -46,9 +47,15 @@ abstract class MobileScannerPlatform extends PlatformInterface { @@ -46,9 +47,15 @@ abstract class MobileScannerPlatform extends PlatformInterface {
46 /// Analyze a local image file for barcodes. 47 /// Analyze a local image file for barcodes.
47 /// 48 ///
48 /// The [path] is the path to the file on disk. 49 /// The [path] is the path to the file on disk.
  50 + /// The [formats] specify the barcode formats that should be detected.
  51 + ///
  52 + /// If [formats] is empty, all barcode formats will be detected.
49 /// 53 ///
50 /// Returns the barcodes that were found in the image. 54 /// Returns the barcodes that were found in the image.
51 - Future<BarcodeCapture?> analyzeImage(String path) { 55 + Future<BarcodeCapture?> analyzeImage(
  56 + String path, {
  57 + List<BarcodeFormat> formats = const <BarcodeFormat>[],
  58 + }) {
52 throw UnimplementedError('analyzeImage() has not been implemented.'); 59 throw UnimplementedError('analyzeImage() has not been implemented.');
53 } 60 }
54 61
@@ -28,6 +28,7 @@ class Barcode { @@ -28,6 +28,7 @@ class Barcode {
28 this.phone, 28 this.phone,
29 this.rawBytes, 29 this.rawBytes,
30 this.rawValue, 30 this.rawValue,
  31 + this.size = Size.zero,
31 this.sms, 32 this.sms,
32 this.type = BarcodeType.unknown, 33 this.type = BarcodeType.unknown,
33 this.url, 34 this.url,
@@ -38,9 +39,9 @@ class Barcode { @@ -38,9 +39,9 @@ class Barcode {
38 factory Barcode.fromNative(Map<Object?, Object?> data) { 39 factory Barcode.fromNative(Map<Object?, Object?> data) {
39 final Map<Object?, Object?>? calendarEvent = 40 final Map<Object?, Object?>? calendarEvent =
40 data['calendarEvent'] as Map<Object?, Object?>?; 41 data['calendarEvent'] as Map<Object?, Object?>?;
41 - final List<Object?>? corners = data['corners'] as List<Object?>?;  
42 final Map<Object?, Object?>? contactInfo = 42 final Map<Object?, Object?>? contactInfo =
43 data['contactInfo'] as Map<Object?, Object?>?; 43 data['contactInfo'] as Map<Object?, Object?>?;
  44 + final List<Object?>? corners = data['corners'] as List<Object?>?;
44 final Map<Object?, Object?>? driverLicense = 45 final Map<Object?, Object?>? driverLicense =
45 data['driverLicense'] as Map<Object?, Object?>?; 46 data['driverLicense'] as Map<Object?, Object?>?;
46 final Map<Object?, Object?>? email = 47 final Map<Object?, Object?>? email =
@@ -50,9 +51,13 @@ class Barcode { @@ -50,9 +51,13 @@ class Barcode {
50 final Map<Object?, Object?>? phone = 51 final Map<Object?, Object?>? phone =
51 data['phone'] as Map<Object?, Object?>?; 52 data['phone'] as Map<Object?, Object?>?;
52 final Map<Object?, Object?>? sms = data['sms'] as Map<Object?, Object?>?; 53 final Map<Object?, Object?>? sms = data['sms'] as Map<Object?, Object?>?;
  54 + final Map<Object?, Object?>? size = data['size'] as Map<Object?, Object?>?;
53 final Map<Object?, Object?>? url = data['url'] as Map<Object?, Object?>?; 55 final Map<Object?, Object?>? url = data['url'] as Map<Object?, Object?>?;
54 final Map<Object?, Object?>? wifi = data['wifi'] as Map<Object?, Object?>?; 56 final Map<Object?, Object?>? wifi = data['wifi'] as Map<Object?, Object?>?;
55 57
  58 + final double? barcodeWidth = size?['width'] as double?;
  59 + final double? barcodeHeight = size?['height'] as double?;
  60 +
56 return Barcode( 61 return Barcode(
57 calendarEvent: calendarEvent == null 62 calendarEvent: calendarEvent == null
58 ? null 63 ? null
@@ -81,6 +86,9 @@ class Barcode { @@ -81,6 +86,9 @@ class Barcode {
81 phone: phone == null ? null : Phone.fromNative(phone), 86 phone: phone == null ? null : Phone.fromNative(phone),
82 rawBytes: data['rawBytes'] as Uint8List?, 87 rawBytes: data['rawBytes'] as Uint8List?,
83 rawValue: data['rawValue'] as String?, 88 rawValue: data['rawValue'] as String?,
  89 + size: barcodeWidth == null || barcodeHeight == null
  90 + ? Size.zero
  91 + : Size(barcodeWidth, barcodeHeight),
84 sms: sms == null ? null : SMS.fromNative(sms), 92 sms: sms == null ? null : SMS.fromNative(sms),
85 type: BarcodeType.fromRawValue(data['type'] as int? ?? 0), 93 type: BarcodeType.fromRawValue(data['type'] as int? ?? 0),
86 url: url == null ? null : UrlBookmark.fromNative(url), 94 url: url == null ? null : UrlBookmark.fromNative(url),
@@ -144,6 +152,11 @@ class Barcode { @@ -144,6 +152,11 @@ class Barcode {
144 /// This is null if the raw value is not available. 152 /// This is null if the raw value is not available.
145 final String? rawValue; 153 final String? rawValue;
146 154
  155 + /// The normalized size of the barcode bounding box.
  156 + ///
  157 + /// If the bounding box is unavailable, this will be [Size.zero].
  158 + final Size size;
  159 +
147 /// The SMS message that is embedded in the barcode. 160 /// The SMS message that is embedded in the barcode.
148 final SMS? sms; 161 final SMS? sms;
149 162
  1 +/// @docImport 'package:mobile_scanner/src/mobile_scanner_controller.dart';
  2 +library;
  3 +
1 import 'dart:typed_data'; 4 import 'dart:typed_data';
2 import 'dart:ui'; 5 import 'dart:ui';
3 6
@@ -16,15 +19,19 @@ class BarcodeCapture { @@ -16,15 +19,19 @@ class BarcodeCapture {
16 /// The list of scanned barcodes. 19 /// The list of scanned barcodes.
17 final List<Barcode> barcodes; 20 final List<Barcode> barcodes;
18 21
19 - /// The bytes of the image that is embedded in the barcode. 22 + /// The input image of the barcode capture.
  23 + ///
  24 + /// This is the image that was used to detect the available [barcodes],
  25 + /// not the image from a specific barcode.
20 /// 26 ///
21 - /// This null if [MobileScannerController.returnImage] is false,  
22 - /// or if there is no available image. 27 + /// This is always null if [MobileScannerController.returnImage] is false.
23 final Uint8List? image; 28 final Uint8List? image;
24 29
25 - /// The raw data of the scanned barcode. 30 + /// The raw data of the barcode scan.
  31 + ///
  32 + /// This is the data that was used to detect the available [barcodes], the input [image] and the [size].
26 final Object? raw; 33 final Object? raw;
27 34
28 - /// The size of the scanned barcode. 35 + /// The size of the camera input [image].
29 final Size size; 36 final Size size;
30 } 37 }
1 import 'dart:ui'; 1 import 'dart:ui';
2 2
3 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 3 import 'package:mobile_scanner/src/enums/camera_facing.dart';
  4 +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
4 import 'package:mobile_scanner/src/enums/torch_state.dart'; 5 import 'package:mobile_scanner/src/enums/torch_state.dart';
5 import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; 6 import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
6 7
@@ -43,7 +44,8 @@ class MobileScannerState { @@ -43,7 +44,8 @@ class MobileScannerState {
43 44
44 /// Whether the mobile scanner has initialized successfully. 45 /// Whether the mobile scanner has initialized successfully.
45 /// 46 ///
46 - /// This is `true` if the camera is ready to be used. 47 + /// This does not indicate that the camera permission was granted.
  48 + /// To check if the camera permission was granted, use [hasCameraPermission].
47 final bool isInitialized; 49 final bool isInitialized;
48 50
49 /// Whether the mobile scanner is currently running. 51 /// Whether the mobile scanner is currently running.
@@ -60,6 +62,12 @@ class MobileScannerState { @@ -60,6 +62,12 @@ class MobileScannerState {
60 /// The current zoom scale of the camera. 62 /// The current zoom scale of the camera.
61 final double zoomScale; 63 final double zoomScale;
62 64
  65 + /// Whether permission to access the camera was granted.
  66 + bool get hasCameraPermission {
  67 + return isInitialized &&
  68 + error?.errorCode != MobileScannerErrorCode.permissionDenied;
  69 + }
  70 +
63 /// Create a copy of this state with the given parameters. 71 /// Create a copy of this state with the given parameters.
64 MobileScannerState copyWith({ 72 MobileScannerState copyWith({
65 int? availableCameras, 73 int? availableCameras,
@@ -5,7 +5,7 @@ import 'package:mobile_scanner/src/enums/encryption_type.dart'; @@ -5,7 +5,7 @@ import 'package:mobile_scanner/src/enums/encryption_type.dart';
5 class WiFi { 5 class WiFi {
6 /// Construct a new [WiFi] instance. 6 /// Construct a new [WiFi] instance.
7 const WiFi({ 7 const WiFi({
8 - this.encryptionType = EncryptionType.none, 8 + this.encryptionType = EncryptionType.unknown,
9 this.ssid, 9 this.ssid,
10 this.password, 10 this.password,
11 }); 11 });
@@ -4,6 +4,7 @@ import 'dart:ui_web' as ui_web; @@ -4,6 +4,7 @@ import 'dart:ui_web' as ui_web;
4 4
5 import 'package:flutter/widgets.dart'; 5 import 'package:flutter/widgets.dart';
6 import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 6 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
  7 +import 'package:mobile_scanner/src/enums/barcode_format.dart';
7 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 8 import 'package:mobile_scanner/src/enums/camera_facing.dart';
8 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; 9 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
9 import 'package:mobile_scanner/src/enums/torch_state.dart'; 10 import 'package:mobile_scanner/src/enums/torch_state.dart';
@@ -38,12 +39,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -38,12 +39,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
38 /// The container div element for the camera view. 39 /// The container div element for the camera view.
39 late HTMLDivElement _divElement; 40 late HTMLDivElement _divElement;
40 41
41 - /// The flag that keeps track of whether a permission request is in progress.  
42 - ///  
43 - /// On the web, a permission request triggers a dialog, that in turn triggers a lifecycle change.  
44 - /// While the permission request is in progress, any attempts at (re)starting the camera should be ignored.  
45 - bool _permissionRequestInProgress = false;  
46 -  
47 /// The stream controller for the media track settings stream. 42 /// The stream controller for the media track settings stream.
48 /// 43 ///
49 /// Currently, only the facing mode setting can be supported, 44 /// Currently, only the facing mode setting can be supported,
@@ -88,6 +83,18 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -88,6 +83,18 @@ class MobileScannerWeb extends MobileScannerPlatform {
88 ..transformOrigin = 'center' 83 ..transformOrigin = 'center'
89 ..pointerEvents = 'none'; 84 ..pointerEvents = 'none';
90 85
  86 + // Do not show the media controls, as this is a preview element.
  87 + // Also prevent play/pause events from changing the media controls.
  88 + videoElement.controls = false;
  89 +
  90 + videoElement.onplay = (JSAny _) {
  91 + videoElement.controls = false;
  92 + }.toJS;
  93 +
  94 + videoElement.onpause = (JSAny _) {
  95 + videoElement.controls = false;
  96 + }.toJS;
  97 +
91 // Attach the video element to its parent container 98 // Attach the video element to its parent container
92 // and setup the PlatformView factory for this `textureId`. 99 // and setup the PlatformView factory for this `textureId`.
93 _divElement = HTMLDivElement() 100 _divElement = HTMLDivElement()
@@ -136,6 +143,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -136,6 +143,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
136 final JSArray<JSString>? facingModes = capabilities.facingModeNullable; 143 final JSArray<JSString>? facingModes = capabilities.facingModeNullable;
137 144
138 // TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera. 145 // TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera.
  146 + // We might be able to add a workaround, using the label of the video track.
139 // Facing mode is not supported by this track, do nothing. 147 // Facing mode is not supported by this track, do nothing.
140 if (facingModes == null || facingModes.toDart.isEmpty) { 148 if (facingModes == null || facingModes.toDart.isEmpty) {
141 return; 149 return;
@@ -186,17 +194,12 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -186,17 +194,12 @@ class MobileScannerWeb extends MobileScannerPlatform {
186 } 194 }
187 195
188 try { 196 try {
189 - _permissionRequestInProgress = true;  
190 -  
191 // Retrieving the media devices requests the camera permission. 197 // Retrieving the media devices requests the camera permission.
192 final MediaStream videoStream = 198 final MediaStream videoStream =
193 await window.navigator.mediaDevices.getUserMedia(constraints).toDart; 199 await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
194 200
195 - _permissionRequestInProgress = false;  
196 -  
197 return videoStream; 201 return videoStream;
198 } on DOMException catch (error, stackTrace) { 202 } on DOMException catch (error, stackTrace) {
199 - _permissionRequestInProgress = false;  
200 final String errorMessage = error.toString(); 203 final String errorMessage = error.toString();
201 204
202 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; 205 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
@@ -220,7 +223,10 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -220,7 +223,10 @@ class MobileScannerWeb extends MobileScannerPlatform {
220 } 223 }
221 224
222 @override 225 @override
223 - Future<BarcodeCapture?> analyzeImage(String path) { 226 + Future<BarcodeCapture?> analyzeImage(
  227 + String path, {
  228 + List<BarcodeFormat> formats = const <BarcodeFormat>[],
  229 + }) {
224 throw UnsupportedError('analyzeImage() is not supported on the web.'); 230 throw UnsupportedError('analyzeImage() is not supported on the web.');
225 } 231 }
226 232
@@ -256,11 +262,13 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -256,11 +262,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
256 262
257 @override 263 @override
258 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { 264 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
259 - // If the permission request has not yet completed,  
260 - // the camera view is not ready yet.  
261 - // Prevent the permission popup from triggering a restart of the scanner.  
262 - if (_permissionRequestInProgress) {  
263 - throw PermissionRequestPendingException(); 265 + if (_barcodeReader != null) {
  266 + throw const MobileScannerException(
  267 + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
  268 + errorDetails: MobileScannerErrorDetails(
  269 + message: 'The scanner was already started.',
  270 + ),
  271 + );
264 } 272 }
265 273
266 // If the previous state is a pause, reset scanner. 274 // If the previous state is a pause, reset scanner.
@@ -274,16 +282,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -274,16 +282,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
274 alternateScriptUrl: _alternateScriptUrl, 282 alternateScriptUrl: _alternateScriptUrl,
275 ); 283 );
276 284
277 - if (_barcodeReader?.isScanning ?? false) {  
278 - throw const MobileScannerException(  
279 - errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,  
280 - errorDetails: MobileScannerErrorDetails(  
281 - message:  
282 - 'The scanner was already started. Call stop() before calling start() again.',  
283 - ),  
284 - );  
285 - }  
286 -  
287 // Request camera permissions and prepare the video stream. 285 // Request camera permissions and prepare the video stream.
288 final MediaStream videoStream = await _prepareVideoStream( 286 final MediaStream videoStream = await _prepareVideoStream(
289 startOptions.cameraDirection, 287 startOptions.cameraDirection,
@@ -330,6 +328,15 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -330,6 +328,15 @@ class MobileScannerWeb extends MobileScannerPlatform {
330 328
331 _barcodesController.add(barcode); 329 _barcodesController.add(barcode);
332 }, 330 },
  331 + onError: (Object error) {
  332 + if (_barcodesController.isClosed) {
  333 + return;
  334 + }
  335 +
  336 + _barcodesController.addError(error);
  337 + },
  338 + // Errors are handled gracefully by forwarding them.
  339 + cancelOnError: false,
333 ); 340 );
334 341
335 final bool hasTorch = await _barcodeReader?.hasTorch() ?? false; 342 final bool hasTorch = await _barcodeReader?.hasTorch() ?? false;
@@ -75,13 +75,33 @@ extension type Result(JSObject _) implements JSObject { @@ -75,13 +75,33 @@ extension type Result(JSObject _) implements JSObject {
75 75
76 /// Convert this result to a [Barcode]. 76 /// Convert this result to a [Barcode].
77 Barcode get toBarcode { 77 Barcode get toBarcode {
  78 + final List<Offset> corners = resultPoints;
  79 +
78 return Barcode( 80 return Barcode(
79 - corners: resultPoints, 81 + corners: corners,
80 format: barcodeFormat, 82 format: barcodeFormat,
81 displayValue: text, 83 displayValue: text,
82 rawBytes: rawBytes, 84 rawBytes: rawBytes,
83 rawValue: text, 85 rawValue: text,
  86 + size: _computeSize(corners),
84 type: BarcodeType.text, 87 type: BarcodeType.text,
85 ); 88 );
86 } 89 }
  90 +
  91 + Size _computeSize(List<Offset> points) {
  92 + if (points.length != 4) {
  93 + return Size.zero;
  94 + }
  95 +
  96 + final Iterable<double> xCoords = points.map((p) => p.dx);
  97 + final Iterable<double> yCoords = points.map((p) => p.dy);
  98 +
  99 + // Find the minimum and maximum x and y coordinates.
  100 + final double xMin = xCoords.reduce((a, b) => a < b ? a : b);
  101 + final double xMax = xCoords.reduce((a, b) => a > b ? a : b);
  102 + final double yMin = yCoords.reduce((a, b) => a < b ? a : b);
  103 + final double yMax = yCoords.reduce((a, b) => a > b ? a : b);
  104 +
  105 + return Size(xMax - xMin, yMax - yMin);
  106 + }
87 } 107 }
@@ -2,7 +2,9 @@ import 'dart:async'; @@ -2,7 +2,9 @@ import 'dart:async';
2 import 'dart:js_interop'; 2 import 'dart:js_interop';
3 import 'dart:ui'; 3 import 'dart:ui';
4 4
  5 +import 'package:flutter/foundation.dart';
5 import 'package:mobile_scanner/src/enums/barcode_format.dart'; 6 import 'package:mobile_scanner/src/enums/barcode_format.dart';
  7 +import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
6 import 'package:mobile_scanner/src/objects/barcode_capture.dart'; 8 import 'package:mobile_scanner/src/objects/barcode_capture.dart';
7 import 'package:mobile_scanner/src/objects/start_options.dart'; 9 import 'package:mobile_scanner/src/objects/start_options.dart';
8 import 'package:mobile_scanner/src/web/barcode_reader.dart'; 10 import 'package:mobile_scanner/src/web/barcode_reader.dart';
@@ -10,12 +12,18 @@ import 'package:mobile_scanner/src/web/javascript_map.dart'; @@ -10,12 +12,18 @@ import 'package:mobile_scanner/src/web/javascript_map.dart';
10 import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart'; 12 import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart';
11 import 'package:mobile_scanner/src/web/zxing/result.dart'; 13 import 'package:mobile_scanner/src/web/zxing/result.dart';
12 import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; 14 import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart';
  15 +import 'package:mobile_scanner/src/web/zxing/zxing_exception.dart';
13 import 'package:web/web.dart' as web; 16 import 'package:web/web.dart' as web;
14 17
15 /// A barcode reader implementation that uses the ZXing library. 18 /// A barcode reader implementation that uses the ZXing library.
16 final class ZXingBarcodeReader extends BarcodeReader { 19 final class ZXingBarcodeReader extends BarcodeReader {
17 ZXingBarcodeReader(); 20 ZXingBarcodeReader();
18 21
  22 + /// ZXing reports an error with this message if the code could not be detected.
  23 + @visibleForTesting
  24 + static const String kNoCodeDetectedErrorMessage =
  25 + 'No MultiFormat Readers were able to detect the code.';
  26 +
19 /// The listener for media track settings changes. 27 /// The listener for media track settings changes.
20 void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged; 28 void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged;
21 29
@@ -98,16 +106,25 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -98,16 +106,25 @@ final class ZXingBarcodeReader extends BarcodeReader {
98 _reader?.decodeContinuously.callAsFunction( 106 _reader?.decodeContinuously.callAsFunction(
99 _reader, 107 _reader,
100 _reader?.videoElement, 108 _reader?.videoElement,
101 - (Result? result, JSAny? error) {  
102 - if (controller.isClosed || result == null) { 109 + (Result? result, ZXingException? error) {
  110 + if (controller.isClosed) {
  111 + return;
  112 + }
  113 +
  114 + // Skip the event if no code was detected.
  115 + if (error != null && error.message != kNoCodeDetectedErrorMessage) {
  116 + controller.addError(MobileScannerBarcodeException(error.message));
103 return; 117 return;
104 } 118 }
105 119
  120 + if (result != null) {
106 controller.add( 121 controller.add(
107 BarcodeCapture( 122 BarcodeCapture(
108 barcodes: [result.toBarcode], 123 barcodes: [result.toBarcode],
  124 + size: videoSize,
109 ), 125 ),
110 ); 126 );
  127 + }
111 }.toJS, 128 }.toJS,
112 ); 129 );
113 }; 130 };
@@ -138,11 +155,10 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -138,11 +155,10 @@ final class ZXingBarcodeReader extends BarcodeReader {
138 required web.MediaStream videoStream, 155 required web.MediaStream videoStream,
139 }) async { 156 }) async {
140 final int detectionTimeoutMs = options.detectionTimeoutMs; 157 final int detectionTimeoutMs = options.detectionTimeoutMs;
141 - final List<BarcodeFormat> formats = options.formats;  
142 -  
143 - if (formats.contains(BarcodeFormat.unknown)) {  
144 - formats.removeWhere((element) => element == BarcodeFormat.unknown);  
145 - } 158 + final List<BarcodeFormat> formats = [
  159 + for (final BarcodeFormat format in options.formats)
  160 + if (format != BarcodeFormat.unknown) format,
  161 + ];
146 162
147 _reader = ZXingBrowserMultiFormatReader( 163 _reader = ZXingBrowserMultiFormatReader(
148 _createReaderHints(formats), 164 _createReaderHints(formats),
  1 +import 'dart:js_interop';
  2 +
  3 +/// The JS static interop class for the Result class in the ZXing library.
  4 +///
  5 +/// See also: https://github.com/zxing-js/library/blob/master/src/core/Exception.ts
  6 +@JS('ZXing.Exception')
  7 +extension type ZXingException._(JSObject _) implements JSObject {
  8 + /// The error message of the exception, if any.
  9 + external String? get message;
  10 +}
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '5.1.1' 7 + s.version = '6.0.2'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
@@ -13,9 +13,10 @@ An universal scanner for Flutter based on MLKit. @@ -13,9 +13,10 @@ An universal scanner for Flutter based on MLKit.
13 s.license = { :file => '../LICENSE' } 13 s.license = { :file => '../LICENSE' }
14 s.author = { 'Julian Steenbakker' => 'juliansteenbakker@outlook.com' } 14 s.author = { 'Julian Steenbakker' => 'juliansteenbakker@outlook.com' }
15 s.source = { :path => '.' } 15 s.source = { :path => '.' }
16 - s.source_files = 'Classes/**/*' 16 + s.source_files = 'mobile_scanner/Sources/mobile_scanner/**/*.swift'
17 s.dependency 'FlutterMacOS' 17 s.dependency 'FlutterMacOS'
18 s.platform = :osx, '10.14' 18 s.platform = :osx, '10.14'
19 s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } 19 s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
20 s.swift_version = '5.0' 20 s.swift_version = '5.0'
  21 + s.resource_bundles = {'mobile_scanner_macos_privacy' => ['mobile_scanner/Sources/mobile_scanner/Resources/PrivacyInfo.xcprivacy']}
21 end 22 end
  1 +// swift-tools-version: 5.9
  2 +// The swift-tools-version declares the minimum version of Swift required to build this package.
  3 +
  4 +import PackageDescription
  5 +
  6 +let package = Package(
  7 + name: "mobile_scanner",
  8 + platforms: [
  9 + .macOS("10.14")
  10 + ],
  11 + products: [
  12 + .library(name: "mobile-scanner", targets: ["mobile_scanner"])
  13 + ],
  14 + dependencies: [],
  15 + targets: [
  16 + .target(
  17 + name: "mobile_scanner",
  18 + dependencies: [],
  19 + resources: [
  20 + .process("Resources"),
  21 + ]
  22 + )
  23 + ]
  24 +)
  1 +//
  2 +// MobileScannerErrorCodes.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Navaron Bracke on 27/05/2024.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +struct MobileScannerErrorCodes {
  11 + static let ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR"
  12 + static let ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started."
  13 + // The error code 'BARCODE_ERROR' does not have an error message,
  14 + // because it uses the error message from the undelying error.
  15 + static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR"
  16 + // The error code 'CAMERA_ERROR' does not have an error message,
  17 + // because it uses the error message from the underlying error.
  18 + static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR"
  19 + static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR"
  20 + static let NO_CAMERA_ERROR_MESSAGE = "No cameras available."
  21 +}
@@ -26,14 +26,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -26,14 +26,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
26 // optional window to limit scan search 26 // optional window to limit scan search
27 var scanWindow: CGRect? 27 var scanWindow: CGRect?
28 28
  29 + /// Whether to return the input image with the barcode event.
  30 + /// This is static to avoid accessing `self` in the `VNDetectBarcodesRequest` callback.
  31 + private static var returnImage: Bool = false
  32 +
29 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates 33 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
30 34
31 var timeoutSeconds: Double = 0 35 var timeoutSeconds: Double = 0
32 36
33 var symbologies:[VNBarcodeSymbology] = [] 37 var symbologies:[VNBarcodeSymbology] = []
34 38
35 - // var analyzeMode: Int = 0  
36 - var analyzing: Bool = false  
37 var position = AVCaptureDevice.Position.back 39 var position = AVCaptureDevice.Position.back
38 40
39 private var stopped: Bool { 41 private var stopped: Bool {
@@ -73,14 +75,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -73,14 +75,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
73 setScale(call, result) 75 setScale(call, result)
74 case "resetScale": 76 case "resetScale":
75 resetScale(call, result) 77 resetScale(call, result)
76 - // case "analyze":  
77 - // switchAnalyzeMode(call, result)  
78 case "pause": 78 case "pause":
79 pause(result) 79 pause(result)
80 case "stop": 80 case "stop":
81 stop(result) 81 stop(result)
82 case "updateScanWindow": 82 case "updateScanWindow":
83 updateScanWindow(call, result) 83 updateScanWindow(call, result)
  84 + case "analyzeImage":
  85 + analyzeImage(call, result)
84 default: 86 default:
85 result(FlutterMethodNotImplemented) 87 result(FlutterMethodNotImplemented)
86 } 88 }
@@ -111,12 +113,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -111,12 +113,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
111 113
112 // Gets called when a new image is added to the buffer 114 // Gets called when a new image is added to the buffer
113 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 115 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
114 - // Ignore invalid textureId 116 + // Ignore invalid texture id.
115 if textureId == nil { 117 if textureId == nil {
116 return 118 return
117 } 119 }
118 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 120 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
119 - print("Failed to get image buffer from sample buffer.")  
120 return 121 return
121 } 122 }
122 latestBuffer = imageBuffer 123 latestBuffer = imageBuffer
@@ -128,57 +129,78 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -128,57 +129,78 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
128 nextScanTime = currentTime + timeoutSeconds 129 nextScanTime = currentTime + timeoutSeconds
129 imagesCurrentlyBeingProcessed = true 130 imagesCurrentlyBeingProcessed = true
130 DispatchQueue.global(qos: .userInitiated).async { [weak self] in 131 DispatchQueue.global(qos: .userInitiated).async { [weak self] in
131 - if(self!.latestBuffer == nil){ 132 + if self!.latestBuffer == nil {
132 return 133 return
133 } 134 }
134 var cgImage: CGImage? 135 var cgImage: CGImage?
135 VTCreateCGImageFromCVPixelBuffer(self!.latestBuffer, options: nil, imageOut: &cgImage) 136 VTCreateCGImageFromCVPixelBuffer(self!.latestBuffer, options: nil, imageOut: &cgImage)
136 let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage!) 137 let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage!)
137 do { 138 do {
138 - let barcodeRequest:VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in 139 + let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in
139 self?.imagesCurrentlyBeingProcessed = false 140 self?.imagesCurrentlyBeingProcessed = false
140 - if error == nil {  
141 - if let results = request.results as? [VNBarcodeObservation] {  
142 - for barcode in results {  
143 - if self?.scanWindow != nil && cgImage != nil {  
144 - let match = self?.isBarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!) ?? false  
145 - if (!match) {  
146 - continue 141 +
  142 + if error != nil {
  143 + DispatchQueue.main.async {
  144 + self?.sink?(FlutterError(
  145 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  146 + message: error?.localizedDescription, details: nil))
147 } 147 }
  148 + return
148 } 149 }
149 150
150 - DispatchQueue.main.async {  
151 - self?.sink?([  
152 - "name": "barcode",  
153 - "data": [  
154 - [  
155 - "payload": barcode.payloadStringValue ?? "",  
156 - "symbology": barcode.symbology.toInt ?? -1,  
157 - ],  
158 - ],  
159 - ]) 151 + guard let results: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else {
  152 + return
160 } 153 }
161 - // if barcodeType == "QR" {  
162 - // let image = CIImage(image: source)  
163 - // image?.cropping(to: barcode.boundingBox)  
164 - // self.qrCodeDescriptor(qrCode: barcode, qrCodeImage: image!)  
165 - // } 154 +
  155 + if results.isEmpty {
  156 + return
166 } 157 }
  158 +
  159 + let barcodes: [VNBarcodeObservation] = results.compactMap({ barcode in
  160 + // If there is a scan window, check if the barcode is within said scan window.
  161 + if self?.scanWindow != nil && cgImage != nil && !(self?.isBarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!) ?? false) {
  162 + return nil
167 } 163 }
168 - } else { 164 +
  165 + return barcode
  166 + })
  167 +
169 DispatchQueue.main.async { 168 DispatchQueue.main.async {
170 - self?.sink?(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil)) 169 + guard let image = cgImage else {
  170 + self?.sink?([
  171 + "name": "barcode",
  172 + "data": barcodes.map({ $0.toMap() }),
  173 + ])
  174 + return
171 } 175 }
  176 +
  177 + // The image dimensions are always provided.
  178 + // The image bytes are only non-null when `returnImage` is true.
  179 + let imageData: [String: Any?] = [
  180 + "bytes": MobileScannerPlugin.returnImage ? FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!) : nil,
  181 + "width": Double(image.width),
  182 + "height": Double(image.height),
  183 + ]
  184 +
  185 + self?.sink?([
  186 + "name": "barcode",
  187 + "data": barcodes.map({ $0.toMap() }),
  188 + "image": imageData,
  189 + ])
172 } 190 }
173 }) 191 })
174 - if(self?.symbologies.isEmpty == false){  
175 - // add the symbologies the user wishes to support 192 +
  193 + if self?.symbologies.isEmpty == false {
  194 + // Add the symbologies the user wishes to support.
176 barcodeRequest.symbologies = self!.symbologies 195 barcodeRequest.symbologies = self!.symbologies
177 } 196 }
  197 +
178 try imageRequestHandler.perform([barcodeRequest]) 198 try imageRequestHandler.perform([barcodeRequest])
179 - } catch let e { 199 + } catch let error {
180 DispatchQueue.main.async { 200 DispatchQueue.main.async {
181 - self?.sink?(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil)) 201 + self?.sink?(FlutterError(
  202 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  203 + message: error.localizedDescription, details: nil))
182 } 204 }
183 } 205 }
184 } 206 }
@@ -258,8 +280,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -258,8 +280,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
258 280
259 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 281 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
260 if (device != nil || captureSession != nil) { 282 if (device != nil || captureSession != nil) {
261 - result(FlutterError(code: "MobileScanner",  
262 - message: "Called start() while already started!", 283 + result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  284 + message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
263 details: nil)) 285 details: nil))
264 return 286 return
265 } 287 }
@@ -269,12 +291,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -269,12 +291,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
269 291
270 let argReader = MapArgumentReader(call.arguments as? [String: Any]) 292 let argReader = MapArgumentReader(call.arguments as? [String: Any])
271 293
272 - // let ratio: Int = argReader.int(key: "ratio")  
273 let torch:Bool = argReader.bool(key: "torch") ?? false 294 let torch:Bool = argReader.bool(key: "torch") ?? false
274 let facing:Int = argReader.int(key: "facing") ?? 1 295 let facing:Int = argReader.int(key: "facing") ?? 1
275 let speed:Int = argReader.int(key: "speed") ?? 0 296 let speed:Int = argReader.int(key: "speed") ?? 0
276 let timeoutMs:Int = argReader.int(key: "timeout") ?? 0 297 let timeoutMs:Int = argReader.int(key: "timeout") ?? 0
277 symbologies = argReader.toSymbology() 298 symbologies = argReader.toSymbology()
  299 + MobileScannerPlugin.returnImage = argReader.bool(key: "returnImage") ?? false
278 300
279 timeoutSeconds = Double(timeoutMs) / 1000.0 301 timeoutSeconds = Double(timeoutMs) / 1000.0
280 detectionSpeed = DetectionSpeed(rawValue: speed)! 302 detectionSpeed = DetectionSpeed(rawValue: speed)!
@@ -290,8 +312,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -290,8 +312,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
290 } 312 }
291 313
292 if (device == nil) { 314 if (device == nil) {
293 - result(FlutterError(code: "MobileScanner",  
294 - message: "No camera found or failed to open camera!", 315 + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
  316 + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
295 details: nil)) 317 details: nil))
296 return 318 return
297 } 319 }
@@ -309,7 +331,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -309,7 +331,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
309 let input = try AVCaptureDeviceInput(device: device) 331 let input = try AVCaptureDeviceInput(device: device)
310 captureSession!.addInput(input) 332 captureSession!.addInput(input)
311 } catch { 333 } catch {
312 - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) 334 + result(FlutterError(
  335 + code: MobileScannerErrorCodes.CAMERA_ERROR,
  336 + message: error.localizedDescription, details: nil))
313 return 337 return
314 } 338 }
315 captureSession!.sessionPreset = AVCaptureSession.Preset.photo 339 captureSession!.sessionPreset = AVCaptureSession.Preset.photo
@@ -322,7 +346,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -322,7 +346,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
322 videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) 346 videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
323 captureSession!.addOutput(videoOutput) 347 captureSession!.addOutput(videoOutput)
324 for connection in videoOutput.connections { 348 for connection in videoOutput.connections {
325 - // connection.videoOrientation = .portrait  
326 if position == .front && connection.isVideoMirroringSupported { 349 if position == .front && connection.isVideoMirroringSupported {
327 connection.isVideoMirrored = true 350 connection.isVideoMirrored = true
328 } 351 }
@@ -425,11 +448,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -425,11 +448,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
425 result(nil) 448 result(nil)
426 } 449 }
427 450
428 - // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
429 - // analyzeMode = call.arguments as! Int  
430 - // result(nil)  
431 - // }  
432 -  
433 func pause(_ result: FlutterResult) { 451 func pause(_ result: FlutterResult) {
434 if (paused || stopped) { 452 if (paused || stopped) {
435 result(nil) 453 result(nil)
@@ -464,6 +482,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -464,6 +482,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
464 captureSession.removeOutput(output) 482 captureSession.removeOutput(output)
465 } 483 }
466 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) 484 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
  485 +// registry.unregisterTexture(textureId) TODO
  486 +
467 latestBuffer = nil 487 latestBuffer = nil
468 self.captureSession = nil 488 self.captureSession = nil
469 device = nil 489 device = nil
@@ -474,6 +494,66 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -474,6 +494,66 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
474 textureId = nil 494 textureId = nil
475 } 495 }
476 496
  497 + func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  498 + let argReader = MapArgumentReader(call.arguments as? [String: Any])
  499 + let symbologies:[VNBarcodeSymbology] = argReader.toSymbology()
  500 +
  501 + guard let filePath: String = argReader.string(key: "filePath") else {
  502 + result(nil)
  503 + return
  504 + }
  505 +
  506 + let fileUrl = URL(fileURLWithPath: filePath)
  507 +
  508 + guard let ciImage = CIImage(contentsOf: fileUrl) else {
  509 + result(nil)
  510 + return
  511 + }
  512 +
  513 + let imageRequestHandler = VNImageRequestHandler(ciImage: ciImage, orientation: CGImagePropertyOrientation.up, options: [:])
  514 +
  515 + do {
  516 + let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest(
  517 + completionHandler: { [] (request, error) in
  518 +
  519 + if error != nil {
  520 + DispatchQueue.main.async {
  521 + result(FlutterError(
  522 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  523 + message: error?.localizedDescription, details: nil))
  524 + }
  525 + return
  526 + }
  527 +
  528 + guard let barcodes: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else {
  529 + return
  530 + }
  531 +
  532 + if barcodes.isEmpty {
  533 + return
  534 + }
  535 +
  536 + result([
  537 + "name": "barcode",
  538 + "data": barcodes.map({ $0.toMap() }),
  539 + ])
  540 + })
  541 +
  542 + if !symbologies.isEmpty {
  543 + // Add the symbologies the user wishes to support.
  544 + barcodeRequest.symbologies = symbologies
  545 + }
  546 +
  547 + try imageRequestHandler.perform([barcodeRequest])
  548 + } catch let error {
  549 + DispatchQueue.main.async {
  550 + result(FlutterError(
  551 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  552 + message: error.localizedDescription, details: nil))
  553 + }
  554 + }
  555 + }
  556 +
477 // Observer for torch state 557 // Observer for torch state
478 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 558 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
479 switch keyPath { 559 switch keyPath {
@@ -533,6 +613,59 @@ class MapArgumentReader { @@ -533,6 +613,59 @@ class MapArgumentReader {
533 613
534 } 614 }
535 615
  616 +extension CGImage {
  617 + public func jpegData(compressionQuality: CGFloat) -> Data? {
  618 + let mutableData = CFDataCreateMutable(nil, 0)
  619 +
  620 + let formatHint: CFString
  621 +
  622 + if #available(macOS 11.0, *) {
  623 + formatHint = UTType.jpeg.identifier as CFString
  624 + } else {
  625 + formatHint = kUTTypeJPEG
  626 + }
  627 +
  628 + guard let destination = CGImageDestinationCreateWithData(mutableData!, formatHint, 1, nil) else {
  629 + return nil
  630 + }
  631 +
  632 + let options: NSDictionary = [
  633 + kCGImageDestinationLossyCompressionQuality: compressionQuality,
  634 + ]
  635 +
  636 + CGImageDestinationAddImage(destination, self, options)
  637 +
  638 + if !CGImageDestinationFinalize(destination) {
  639 + return nil
  640 + }
  641 +
  642 + return mutableData as Data?
  643 + }
  644 +}
  645 +
  646 +extension VNBarcodeObservation {
  647 + private func distanceBetween(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat {
  648 + return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2))
  649 + }
  650 +
  651 + public func toMap() -> [String: Any?] {
  652 + return [
  653 + "corners": [
  654 + ["x": topLeft.x, "y": topLeft.y],
  655 + ["x": topRight.x, "y": topRight.y],
  656 + ["x": bottomRight.x, "y": bottomRight.y],
  657 + ["x": bottomLeft.x, "y": bottomLeft.y],
  658 + ],
  659 + "format": symbology.toInt ?? -1,
  660 + "rawValue": payloadStringValue ?? "",
  661 + "size": [
  662 + "width": distanceBetween(topLeft, topRight),
  663 + "height": distanceBetween(topLeft, bottomLeft),
  664 + ],
  665 + ]
  666 + }
  667 +}
  668 +
536 extension VNBarcodeSymbology { 669 extension VNBarcodeSymbology {
537 static func fromInt(_ mapValue:Int) -> VNBarcodeSymbology? { 670 static func fromInt(_ mapValue:Int) -> VNBarcodeSymbology? {
538 if #available(macOS 12.0, *) { 671 if #available(macOS 12.0, *) {
@@ -568,7 +701,7 @@ extension VNBarcodeSymbology { @@ -568,7 +701,7 @@ extension VNBarcodeSymbology {
568 } 701 }
569 } 702 }
570 703
571 - var toInt:Int? { 704 + var toInt: Int? {
572 if #available(macOS 12.0, *) { 705 if #available(macOS 12.0, *) {
573 if(self == VNBarcodeSymbology.codabar){ 706 if(self == VNBarcodeSymbology.codabar){
574 return 8 707 return 8
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  3 +<plist version="1.0">
  4 +<dict>
  5 + <!-- The key NSPrivacyAccessedAPITypes is not required for MacOS -->
  6 + <key>NSPrivacyTrackingDomains</key>
  7 + <array/>
  8 + <key>NSPrivacyCollectedDataTypes</key>
  9 + <array/>
  10 + <key>NSPrivacyTracking</key>
  11 + <false/>
  12 +</dict>
  13 +</plist>
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: 5.1.1 3 +version: 6.0.2
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 screenshots: 6 screenshots:
@@ -25,7 +25,7 @@ dependencies: @@ -25,7 +25,7 @@ dependencies:
25 flutter_web_plugins: 25 flutter_web_plugins:
26 sdk: flutter 26 sdk: flutter
27 plugin_platform_interface: ^2.0.2 27 plugin_platform_interface: ^2.0.2
28 - web: ^1.0.0 28 + web: ">=0.5.1 <2.0.0"
29 29
30 dev_dependencies: 30 dev_dependencies:
31 flutter_test: 31 flutter_test:
@@ -17,12 +17,12 @@ void main() { @@ -17,12 +17,12 @@ void main() {
17 } 17 }
18 }); 18 });
19 19
20 - test('invalid raw value throws argument error', () { 20 + test('invalid raw value returns AddressType.unknown', () {
21 const int negative = -1; 21 const int negative = -1;
22 const int outOfRange = 3; 22 const int outOfRange = 3;
23 23
24 - expect(() => AddressType.fromRawValue(negative), throwsArgumentError);  
25 - expect(() => AddressType.fromRawValue(outOfRange), throwsArgumentError); 24 + expect(AddressType.fromRawValue(negative), AddressType.unknown);
  25 + expect(AddressType.fromRawValue(outOfRange), AddressType.unknown);
26 }); 26 });
27 27
28 test('can be converted to raw value', () { 28 test('can be converted to raw value', () {
@@ -27,12 +27,12 @@ void main() { @@ -27,12 +27,12 @@ void main() {
27 } 27 }
28 }); 28 });
29 29
30 - test('invalid raw value throws argument error', () { 30 + test('invalid raw value returns BarcodeType.unknown', () {
31 const int negative = -1; 31 const int negative = -1;
32 const int outOfRange = 13; 32 const int outOfRange = 13;
33 33
34 - expect(() => BarcodeType.fromRawValue(negative), throwsArgumentError);  
35 - expect(() => BarcodeType.fromRawValue(outOfRange), throwsArgumentError); 34 + expect(BarcodeType.fromRawValue(negative), BarcodeType.unknown);
  35 + expect(BarcodeType.fromRawValue(outOfRange), BarcodeType.unknown);
36 }); 36 });
37 37
38 test('can be converted to raw value', () { 38 test('can be converted to raw value', () {
@@ -17,12 +17,12 @@ void main() { @@ -17,12 +17,12 @@ void main() {
17 } 17 }
18 }); 18 });
19 19
20 - test('invalid raw value throws argument error', () { 20 + test('invalid raw value returns EmailType.unknown', () {
21 const int negative = -1; 21 const int negative = -1;
22 const int outOfRange = 3; 22 const int outOfRange = 3;
23 23
24 - expect(() => EmailType.fromRawValue(negative), throwsArgumentError);  
25 - expect(() => EmailType.fromRawValue(outOfRange), throwsArgumentError); 24 + expect(EmailType.fromRawValue(negative), EmailType.unknown);
  25 + expect(EmailType.fromRawValue(outOfRange), EmailType.unknown);
26 }); 26 });
27 27
28 test('can be converted to raw value', () { 28 test('can be converted to raw value', () {
@@ -5,7 +5,7 @@ void main() { @@ -5,7 +5,7 @@ void main() {
5 group('$EncryptionType tests', () { 5 group('$EncryptionType tests', () {
6 test('can be created from raw value', () { 6 test('can be created from raw value', () {
7 const values = <int, EncryptionType>{ 7 const values = <int, EncryptionType>{
8 - 0: EncryptionType.none, 8 + 0: EncryptionType.unknown,
9 1: EncryptionType.open, 9 1: EncryptionType.open,
10 2: EncryptionType.wpa, 10 2: EncryptionType.wpa,
11 3: EncryptionType.wep, 11 3: EncryptionType.wep,
@@ -18,20 +18,17 @@ void main() { @@ -18,20 +18,17 @@ void main() {
18 } 18 }
19 }); 19 });
20 20
21 - test('invalid raw value throws argument error', () { 21 + test('invalid raw value returns EncryptionType.unknown', () {
22 const int negative = -1; 22 const int negative = -1;
23 const int outOfRange = 4; 23 const int outOfRange = 4;
24 24
25 - expect(() => EncryptionType.fromRawValue(negative), throwsArgumentError);  
26 - expect(  
27 - () => EncryptionType.fromRawValue(outOfRange),  
28 - throwsArgumentError,  
29 - ); 25 + expect(EncryptionType.fromRawValue(negative), EncryptionType.unknown);
  26 + expect(EncryptionType.fromRawValue(outOfRange), EncryptionType.unknown);
30 }); 27 });
31 28
32 test('can be converted to raw value', () { 29 test('can be converted to raw value', () {
33 const values = <EncryptionType, int>{ 30 const values = <EncryptionType, int>{
34 - EncryptionType.none: 0, 31 + EncryptionType.unknown: 0,
35 EncryptionType.open: 1, 32 EncryptionType.open: 1,
36 EncryptionType.wpa: 2, 33 EncryptionType.wpa: 2,
37 EncryptionType.wep: 3, 34 EncryptionType.wep: 3,
@@ -19,12 +19,12 @@ void main() { @@ -19,12 +19,12 @@ void main() {
19 } 19 }
20 }); 20 });
21 21
22 - test('invalid raw value throws argument error', () { 22 + test('invalid raw value returns PhoneType.unknown', () {
23 const int negative = -1; 23 const int negative = -1;
24 const int outOfRange = 5; 24 const int outOfRange = 5;
25 25
26 - expect(() => PhoneType.fromRawValue(negative), throwsArgumentError);  
27 - expect(() => PhoneType.fromRawValue(outOfRange), throwsArgumentError); 26 + expect(PhoneType.fromRawValue(negative), PhoneType.unknown);
  27 + expect(PhoneType.fromRawValue(outOfRange), PhoneType.unknown);
28 }); 28 });
29 29
30 test('can be converted to raw value', () { 30 test('can be converted to raw value', () {