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