Committed by
GitHub
Merge branch 'master' into fix_android_permission_bug
Showing
20 changed files
with
1216 additions
and
145 deletions
| @@ -21,6 +21,7 @@ Breaking changes: | @@ -21,6 +21,7 @@ Breaking changes: | ||
| 21 | * The `autoResume` attribute has been removed from the `MobileScanner` widget. | 21 | * The `autoResume` attribute has been removed from the `MobileScanner` widget. |
| 22 | The controller already automatically resumes, so it had no effect. | 22 | The controller already automatically resumes, so it had no effect. |
| 23 | * Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef. | 23 | * Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef. |
| 24 | +* [Web] Replaced `jsqr` library with `zxing-js` for full barcode support. | ||
| 24 | 25 | ||
| 25 | Improvements: | 26 | Improvements: |
| 26 | * Toggling the device torch now does nothing if the device has no torch, rather than throwing an error. | 27 | * Toggling the device torch now does nothing if the device has no torch, rather than throwing an error. |
| @@ -30,6 +31,9 @@ Features: | @@ -30,6 +31,9 @@ Features: | ||
| 30 | * Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder. | 31 | * Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder. |
| 31 | * Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically. | 32 | * Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically. |
| 32 | * Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch. | 33 | * Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch. |
| 34 | +* [iOS] Support `torchEnabled` parameter from MobileScannerController() on iOS | ||
| 35 | +* [Web] Added ability to use custom barcode scanning js libraries | ||
| 36 | + by extending `WebBarcodeReaderBase` class and changing `barCodeReader` property in `MobileScannerWebPlugin` | ||
| 33 | 37 | ||
| 34 | Fixes: | 38 | Fixes: |
| 35 | * Fixes the missing gradle setup for the Android project, which prevented gradle sync from working. | 39 | * Fixes the missing gradle setup for the Android project, which prevented gradle sync from working. |
| @@ -40,6 +44,7 @@ Fixes: | @@ -40,6 +44,7 @@ Fixes: | ||
| 40 | * Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`. | 44 | * Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`. |
| 41 | Now it only depends on its layout constraints. | 45 | Now it only depends on its layout constraints. |
| 42 | * Fixed a potential crash when the scanner is restarted due to the app being resumed. | 46 | * Fixed a potential crash when the scanner is restarted due to the app being resumed. |
| 47 | +* [iOS] Fix crash when changing torch state | ||
| 43 | 48 | ||
| 44 | ## 3.0.0-beta.2 | 49 | ## 3.0.0-beta.2 |
| 45 | Breaking changes: | 50 | Breaking changes: |
| 1 | package dev.steenbakker.mobile_scanner | 1 | package dev.steenbakker.mobile_scanner |
| 2 | 2 | ||
| 3 | import android.app.Activity | 3 | import android.app.Activity |
| 4 | +import android.content.pm.PackageManager | ||
| 5 | +import android.graphics.Rect | ||
| 4 | import android.net.Uri | 6 | import android.net.Uri |
| 5 | import android.os.Handler | 7 | import android.os.Handler |
| 6 | import android.os.Looper | 8 | import android.os.Looper |
| 9 | +import android.util.Log | ||
| 7 | import android.view.Surface | 10 | import android.view.Surface |
| 8 | import androidx.camera.core.* | 11 | import androidx.camera.core.* |
| 9 | import androidx.camera.lifecycle.ProcessCameraProvider | 12 | import androidx.camera.lifecycle.ProcessCameraProvider |
| @@ -11,22 +14,29 @@ import androidx.core.content.ContextCompat | @@ -11,22 +14,29 @@ import androidx.core.content.ContextCompat | ||
| 11 | import androidx.lifecycle.LifecycleOwner | 14 | import androidx.lifecycle.LifecycleOwner |
| 12 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 15 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 13 | import com.google.mlkit.vision.barcode.BarcodeScanning | 16 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 17 | +import com.google.mlkit.vision.barcode.common.Barcode | ||
| 14 | import com.google.mlkit.vision.common.InputImage | 18 | import com.google.mlkit.vision.common.InputImage |
| 15 | import dev.steenbakker.mobile_scanner.objects.DetectionSpeed | 19 | import dev.steenbakker.mobile_scanner.objects.DetectionSpeed |
| 16 | import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters | 20 | import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters |
| 17 | import io.flutter.view.TextureRegistry | 21 | import io.flutter.view.TextureRegistry |
| 18 | -typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit | 22 | +import kotlin.math.roundToInt |
| 23 | + | ||
| 24 | + | ||
| 25 | +typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit | ||
| 19 | typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit | 26 | typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit |
| 20 | typealias MobileScannerErrorCallback = (error: String) -> Unit | 27 | typealias MobileScannerErrorCallback = (error: String) -> Unit |
| 21 | typealias TorchStateCallback = (state: Int) -> Unit | 28 | typealias TorchStateCallback = (state: Int) -> Unit |
| 22 | typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit | 29 | typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit |
| 23 | 30 | ||
| 31 | + | ||
| 24 | class NoCamera : Exception() | 32 | class NoCamera : Exception() |
| 25 | class AlreadyStarted : Exception() | 33 | class AlreadyStarted : Exception() |
| 26 | class AlreadyStopped : Exception() | 34 | class AlreadyStopped : Exception() |
| 27 | class TorchError : Exception() | 35 | class TorchError : Exception() |
| 28 | class CameraError : Exception() | 36 | class CameraError : Exception() |
| 29 | class TorchWhenStopped : Exception() | 37 | class TorchWhenStopped : Exception() |
| 38 | +class ZoomWhenStopped : Exception() | ||
| 39 | +class ZoomNotInRange : Exception() | ||
| 30 | 40 | ||
| 31 | class MobileScanner( | 41 | class MobileScanner( |
| 32 | private val activity: Activity, | 42 | private val activity: Activity, |
| @@ -39,6 +49,7 @@ class MobileScanner( | @@ -39,6 +49,7 @@ class MobileScanner( | ||
| 39 | private var camera: Camera? = null | 49 | private var camera: Camera? = null |
| 40 | private var preview: Preview? = null | 50 | private var preview: Preview? = null |
| 41 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null | 51 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null |
| 52 | + var scanWindow: List<Float>? = null | ||
| 42 | 53 | ||
| 43 | private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES | 54 | private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES |
| 44 | private var detectionTimeout: Long = 250 | 55 | private var detectionTimeout: Long = 250 |
| @@ -76,12 +87,27 @@ class MobileScanner( | @@ -76,12 +87,27 @@ class MobileScanner( | ||
| 76 | lastScanned = newScannedBarcodes | 87 | lastScanned = newScannedBarcodes |
| 77 | } | 88 | } |
| 78 | 89 | ||
| 79 | - val barcodeMap = barcodes.map { barcode -> barcode.data } | 90 | + val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf() |
| 91 | + | ||
| 92 | + for ( barcode in barcodes) { | ||
| 93 | + if(scanWindow != null) { | ||
| 94 | + val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy) | ||
| 95 | + if(!match) { | ||
| 96 | + continue | ||
| 97 | + } else { | ||
| 98 | + barcodeMap.add(barcode.data) | ||
| 99 | + } | ||
| 100 | + } else { | ||
| 101 | + barcodeMap.add(barcode.data) | ||
| 102 | + } | ||
| 103 | + } | ||
| 80 | 104 | ||
| 81 | if (barcodeMap.isNotEmpty()) { | 105 | if (barcodeMap.isNotEmpty()) { |
| 82 | mobileScannerCallback( | 106 | mobileScannerCallback( |
| 83 | barcodeMap, | 107 | barcodeMap, |
| 84 | - if (returnImage) mediaImage.toByteArray() else null | 108 | + if (returnImage) mediaImage.toByteArray() else null, |
| 109 | + if (returnImage) mediaImage.width else null, | ||
| 110 | + if (returnImage) mediaImage.height else null | ||
| 85 | ) | 111 | ) |
| 86 | } | 112 | } |
| 87 | } | 113 | } |
| @@ -100,6 +126,23 @@ class MobileScanner( | @@ -100,6 +126,23 @@ class MobileScanner( | ||
| 100 | } | 126 | } |
| 101 | } | 127 | } |
| 102 | 128 | ||
| 129 | + // scales the scanWindow to the provided inputImage and checks if that scaled | ||
| 130 | + // scanWindow contains the barcode | ||
| 131 | + private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: ImageProxy): Boolean { | ||
| 132 | + val barcodeBoundingBox = barcode.boundingBox ?: return false | ||
| 133 | + | ||
| 134 | + val imageWidth = inputImage.height | ||
| 135 | + val imageHeight = inputImage.width | ||
| 136 | + | ||
| 137 | + val left = (scanWindow[0] * imageWidth).roundToInt() | ||
| 138 | + val top = (scanWindow[1] * imageHeight).roundToInt() | ||
| 139 | + val right = (scanWindow[2] * imageWidth).roundToInt() | ||
| 140 | + val bottom = (scanWindow[3] * imageHeight).roundToInt() | ||
| 141 | + | ||
| 142 | + val scaledScanWindow = Rect(left, top, right, bottom) | ||
| 143 | + return scaledScanWindow.contains(barcodeBoundingBox) | ||
| 144 | + } | ||
| 145 | + | ||
| 103 | /** | 146 | /** |
| 104 | * Start barcode scanning by initializing the camera and barcode scanner. | 147 | * Start barcode scanning by initializing the camera and barcode scanner. |
| 105 | */ | 148 | */ |
| @@ -182,7 +225,7 @@ class MobileScanner( | @@ -182,7 +225,7 @@ class MobileScanner( | ||
| 182 | // Enable torch if provided | 225 | // Enable torch if provided |
| 183 | camera!!.cameraControl.enableTorch(torch) | 226 | camera!!.cameraControl.enableTorch(torch) |
| 184 | 227 | ||
| 185 | - val resolution = preview!!.resolutionInfo!!.resolution | 228 | + val resolution = analysis.resolutionInfo!!.resolution |
| 186 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | 229 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 |
| 187 | val width = resolution.width.toDouble() | 230 | val width = resolution.width.toDouble() |
| 188 | val height = resolution.height.toDouble() | 231 | val height = resolution.height.toDouble() |
| @@ -251,4 +294,13 @@ class MobileScanner( | @@ -251,4 +294,13 @@ class MobileScanner( | ||
| 251 | } | 294 | } |
| 252 | } | 295 | } |
| 253 | 296 | ||
| 297 | + /** | ||
| 298 | + * Set the zoom rate of the camera. | ||
| 299 | + */ | ||
| 300 | + fun setScale(scale: Double) { | ||
| 301 | + if (camera == null) throw ZoomWhenStopped() | ||
| 302 | + if (scale > 1.0 || scale < 0) throw ZoomNotInRange() | ||
| 303 | + camera!!.cameraControl.setLinearZoom(scale.toFloat()) | ||
| 304 | + } | ||
| 305 | + | ||
| 254 | } | 306 | } |
| @@ -9,6 +9,72 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { | @@ -9,6 +9,72 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { | ||
| 9 | private var activityPluginBinding: ActivityPluginBinding? = null | 9 | private var activityPluginBinding: ActivityPluginBinding? = null |
| 10 | private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null | 10 | private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null |
| 11 | private var methodCallHandler: MethodCallHandlerImpl? = null | 11 | private var methodCallHandler: MethodCallHandlerImpl? = null |
| 12 | + private var handler: MobileScanner? = null | ||
| 13 | + private var method: MethodChannel? = null | ||
| 14 | + | ||
| 15 | + private lateinit var barcodeHandler: BarcodeHandler | ||
| 16 | + | ||
| 17 | + private var analyzerResult: MethodChannel.Result? = null | ||
| 18 | + | ||
| 19 | + private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? -> | ||
| 20 | + if (image != null) { | ||
| 21 | + barcodeHandler.publishEvent(mapOf( | ||
| 22 | + "name" to "barcode", | ||
| 23 | + "data" to barcodes, | ||
| 24 | + "image" to image, | ||
| 25 | + "width" to width!!.toDouble(), | ||
| 26 | + "height" to height!!.toDouble() | ||
| 27 | + )) | ||
| 28 | + } else { | ||
| 29 | + barcodeHandler.publishEvent(mapOf( | ||
| 30 | + "name" to "barcode", | ||
| 31 | + "data" to barcodes | ||
| 32 | + )) | ||
| 33 | + } | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?-> | ||
| 37 | + if (barcodes != null) { | ||
| 38 | + barcodeHandler.publishEvent(mapOf( | ||
| 39 | + "name" to "barcode", | ||
| 40 | + "data" to barcodes | ||
| 41 | + )) | ||
| 42 | + analyzerResult?.success(true) | ||
| 43 | + } else { | ||
| 44 | + analyzerResult?.success(false) | ||
| 45 | + } | ||
| 46 | + analyzerResult = null | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + private val errorCallback: MobileScannerErrorCallback = {error: String -> | ||
| 50 | + barcodeHandler.publishEvent(mapOf( | ||
| 51 | + "name" to "error", | ||
| 52 | + "data" to error, | ||
| 53 | + )) | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + private val torchStateCallback: TorchStateCallback = {state: Int -> | ||
| 57 | + barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + @ExperimentalGetImage | ||
| 61 | + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { | ||
| 62 | + if (handler == null) { | ||
| 63 | + result.error("MobileScanner", "Called ${call.method} before initializing.", null) | ||
| 64 | + return | ||
| 65 | + } | ||
| 66 | + when (call.method) { | ||
| 67 | + "state" -> result.success(handler!!.hasCameraPermission()) | ||
| 68 | + "request" -> handler!!.requestPermission(result) | ||
| 69 | + "start" -> start(call, result) | ||
| 70 | + "torch" -> toggleTorch(call, result) | ||
| 71 | + "stop" -> stop(result) | ||
| 72 | + "analyzeImage" -> analyzeImage(call, result) | ||
| 73 | + "setScale" -> setScale(call, result) | ||
| 74 | + "updateScanWindow" -> updateScanWindow(call) | ||
| 75 | + else -> result.notImplemented() | ||
| 76 | + } | ||
| 77 | + } | ||
| 12 | 78 | ||
| 13 | override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { | 79 | override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { |
| 14 | this.flutterPluginBinding = binding | 80 | this.flutterPluginBinding = binding |
| @@ -46,4 +112,19 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { | @@ -46,4 +112,19 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { | ||
| 46 | override fun onDetachedFromActivityForConfigChanges() { | 112 | override fun onDetachedFromActivityForConfigChanges() { |
| 47 | onDetachedFromActivity() | 113 | onDetachedFromActivity() |
| 48 | } | 114 | } |
| 115 | + | ||
| 116 | + private fun setScale(call: MethodCall, result: MethodChannel.Result) { | ||
| 117 | + try { | ||
| 118 | + handler!!.setScale(call.arguments as Double) | ||
| 119 | + result.success(null) | ||
| 120 | + } catch (e: ZoomWhenStopped) { | ||
| 121 | + result.error("MobileScanner", "Called setScale() while stopped!", null) | ||
| 122 | + } catch (e: ZoomNotInRange) { | ||
| 123 | + result.error("MobileScanner", "Scale should be within 0 and 1", null) | ||
| 124 | + } | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + private fun updateScanWindow(call: MethodCall) { | ||
| 128 | + handler!!.scanWindow = call.argument<List<Float>>("rect") | ||
| 129 | + } | ||
| 49 | } | 130 | } |
| @@ -354,6 +354,8 @@ | @@ -354,6 +354,8 @@ | ||
| 354 | buildSettings = { | 354 | buildSettings = { |
| 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
| 356 | CLANG_ENABLE_MODULES = YES; | 356 | CLANG_ENABLE_MODULES = YES; |
| 357 | + CODE_SIGN_IDENTITY = "Apple Development"; | ||
| 358 | + CODE_SIGN_STYLE = Automatic; | ||
| 357 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 359 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 358 | DEVELOPMENT_TEAM = 75Y2P2WSQQ; | 360 | DEVELOPMENT_TEAM = 75Y2P2WSQQ; |
| 359 | ENABLE_BITCODE = NO; | 361 | ENABLE_BITCODE = NO; |
| @@ -364,6 +366,7 @@ | @@ -364,6 +366,7 @@ | ||
| 364 | ); | 366 | ); |
| 365 | PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; | 367 | PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; |
| 366 | PRODUCT_NAME = "$(TARGET_NAME)"; | 368 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 369 | + PROVISIONING_PROFILE_SPECIFIER = ""; | ||
| 367 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 370 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| 368 | SWIFT_VERSION = 5.0; | 371 | SWIFT_VERSION = 5.0; |
| 369 | VERSIONING_SYSTEM = "apple-generic"; | 372 | VERSIONING_SYSTEM = "apple-generic"; |
| @@ -483,6 +486,8 @@ | @@ -483,6 +486,8 @@ | ||
| 483 | buildSettings = { | 486 | buildSettings = { |
| 484 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 487 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
| 485 | CLANG_ENABLE_MODULES = YES; | 488 | CLANG_ENABLE_MODULES = YES; |
| 489 | + CODE_SIGN_IDENTITY = "Apple Development"; | ||
| 490 | + CODE_SIGN_STYLE = Automatic; | ||
| 486 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 491 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 487 | DEVELOPMENT_TEAM = 75Y2P2WSQQ; | 492 | DEVELOPMENT_TEAM = 75Y2P2WSQQ; |
| 488 | ENABLE_BITCODE = NO; | 493 | ENABLE_BITCODE = NO; |
| @@ -493,6 +498,7 @@ | @@ -493,6 +498,7 @@ | ||
| 493 | ); | 498 | ); |
| 494 | PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; | 499 | PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; |
| 495 | PRODUCT_NAME = "$(TARGET_NAME)"; | 500 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 501 | + PROVISIONING_PROFILE_SPECIFIER = ""; | ||
| 496 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 502 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| 497 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | 503 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; |
| 498 | SWIFT_VERSION = 5.0; | 504 | SWIFT_VERSION = 5.0; |
| @@ -506,6 +512,8 @@ | @@ -506,6 +512,8 @@ | ||
| 506 | buildSettings = { | 512 | buildSettings = { |
| 507 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 513 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
| 508 | CLANG_ENABLE_MODULES = YES; | 514 | CLANG_ENABLE_MODULES = YES; |
| 515 | + CODE_SIGN_IDENTITY = "Apple Development"; | ||
| 516 | + CODE_SIGN_STYLE = Automatic; | ||
| 509 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 517 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 510 | DEVELOPMENT_TEAM = 75Y2P2WSQQ; | 518 | DEVELOPMENT_TEAM = 75Y2P2WSQQ; |
| 511 | ENABLE_BITCODE = NO; | 519 | ENABLE_BITCODE = NO; |
| @@ -516,6 +524,7 @@ | @@ -516,6 +524,7 @@ | ||
| 516 | ); | 524 | ); |
| 517 | PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; | 525 | PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; |
| 518 | PRODUCT_NAME = "$(TARGET_NAME)"; | 526 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 527 | + PROVISIONING_PROFILE_SPECIFIER = ""; | ||
| 519 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 528 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| 520 | SWIFT_VERSION = 5.0; | 529 | SWIFT_VERSION = 5.0; |
| 521 | VERSIONING_SYSTEM = "apple-generic"; | 530 | VERSIONING_SYSTEM = "apple-generic"; |
| @@ -72,33 +72,41 @@ class _BarcodeScannerWithControllerState | @@ -72,33 +72,41 @@ class _BarcodeScannerWithControllerState | ||
| 72 | child: Row( | 72 | child: Row( |
| 73 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, | 73 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| 74 | children: [ | 74 | children: [ |
| 75 | - IconButton( | ||
| 76 | - color: Colors.white, | ||
| 77 | - icon: ValueListenableBuilder( | ||
| 78 | - valueListenable: controller.torchState, | ||
| 79 | - builder: (context, state, child) { | ||
| 80 | - if (state == null) { | ||
| 81 | - return const Icon( | ||
| 82 | - Icons.flash_off, | ||
| 83 | - color: Colors.grey, | ||
| 84 | - ); | ||
| 85 | - } | ||
| 86 | - switch (state as TorchState) { | ||
| 87 | - case TorchState.off: | ||
| 88 | - return const Icon( | ||
| 89 | - Icons.flash_off, | ||
| 90 | - color: Colors.grey, | ||
| 91 | - ); | ||
| 92 | - case TorchState.on: | ||
| 93 | - return const Icon( | ||
| 94 | - Icons.flash_on, | ||
| 95 | - color: Colors.yellow, | ||
| 96 | - ); | ||
| 97 | - } | ||
| 98 | - }, | ||
| 99 | - ), | ||
| 100 | - iconSize: 32.0, | ||
| 101 | - onPressed: () => controller.toggleTorch(), | 75 | + ValueListenableBuilder( |
| 76 | + valueListenable: controller.hasTorchState, | ||
| 77 | + builder: (context, state, child) { | ||
| 78 | + if (state != true) { | ||
| 79 | + return const SizedBox.shrink(); | ||
| 80 | + } | ||
| 81 | + return IconButton( | ||
| 82 | + color: Colors.white, | ||
| 83 | + icon: ValueListenableBuilder( | ||
| 84 | + valueListenable: controller.torchState, | ||
| 85 | + builder: (context, state, child) { | ||
| 86 | + if (state == null) { | ||
| 87 | + return const Icon( | ||
| 88 | + Icons.flash_off, | ||
| 89 | + color: Colors.grey, | ||
| 90 | + ); | ||
| 91 | + } | ||
| 92 | + switch (state as TorchState) { | ||
| 93 | + case TorchState.off: | ||
| 94 | + return const Icon( | ||
| 95 | + Icons.flash_off, | ||
| 96 | + color: Colors.grey, | ||
| 97 | + ); | ||
| 98 | + case TorchState.on: | ||
| 99 | + return const Icon( | ||
| 100 | + Icons.flash_on, | ||
| 101 | + color: Colors.yellow, | ||
| 102 | + ); | ||
| 103 | + } | ||
| 104 | + }, | ||
| 105 | + ), | ||
| 106 | + iconSize: 32.0, | ||
| 107 | + onPressed: () => controller.toggleTorch(), | ||
| 108 | + ); | ||
| 109 | + }, | ||
| 102 | ), | 110 | ), |
| 103 | IconButton( | 111 | IconButton( |
| 104 | color: Colors.white, | 112 | color: Colors.white, |
example/lib/barcode_scanner_window.dart
0 → 100644
| 1 | +import 'dart:io'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 5 | + | ||
| 6 | +class BarcodeScannerWithScanWindow extends StatefulWidget { | ||
| 7 | + const BarcodeScannerWithScanWindow({Key? key}) : super(key: key); | ||
| 8 | + | ||
| 9 | + @override | ||
| 10 | + _BarcodeScannerWithScanWindowState createState() => | ||
| 11 | + _BarcodeScannerWithScanWindowState(); | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +class _BarcodeScannerWithScanWindowState | ||
| 15 | + extends State<BarcodeScannerWithScanWindow> { | ||
| 16 | + late MobileScannerController controller = MobileScannerController(); | ||
| 17 | + Barcode? barcode; | ||
| 18 | + BarcodeCapture? capture; | ||
| 19 | + | ||
| 20 | + Future<void> onDetect(BarcodeCapture barcode) async { | ||
| 21 | + capture = barcode; | ||
| 22 | + setState(() => this.barcode = barcode.barcodes.first); | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + MobileScannerArguments? arguments; | ||
| 26 | + | ||
| 27 | + @override | ||
| 28 | + Widget build(BuildContext context) { | ||
| 29 | + final scanWindow = Rect.fromCenter( | ||
| 30 | + center: MediaQuery.of(context).size.center(Offset.zero), | ||
| 31 | + width: 200, | ||
| 32 | + height: 200, | ||
| 33 | + ); | ||
| 34 | + return Scaffold( | ||
| 35 | + backgroundColor: Colors.black, | ||
| 36 | + body: Builder( | ||
| 37 | + builder: (context) { | ||
| 38 | + return Stack( | ||
| 39 | + fit: StackFit.expand, | ||
| 40 | + children: [ | ||
| 41 | + MobileScanner( | ||
| 42 | + fit: BoxFit.contain, | ||
| 43 | + scanWindow: scanWindow, | ||
| 44 | + controller: controller, | ||
| 45 | + onScannerStarted: (arguments) { | ||
| 46 | + setState(() { | ||
| 47 | + this.arguments = arguments; | ||
| 48 | + }); | ||
| 49 | + }, | ||
| 50 | + onDetect: onDetect, | ||
| 51 | + ), | ||
| 52 | + if (barcode != null && | ||
| 53 | + barcode?.corners != null && | ||
| 54 | + arguments != null) | ||
| 55 | + CustomPaint( | ||
| 56 | + painter: BarcodeOverlay( | ||
| 57 | + barcode!, | ||
| 58 | + arguments!, | ||
| 59 | + BoxFit.contain, | ||
| 60 | + MediaQuery.of(context).devicePixelRatio, | ||
| 61 | + capture!, | ||
| 62 | + ), | ||
| 63 | + ), | ||
| 64 | + CustomPaint( | ||
| 65 | + painter: ScannerOverlay(scanWindow), | ||
| 66 | + ), | ||
| 67 | + Align( | ||
| 68 | + alignment: Alignment.bottomCenter, | ||
| 69 | + child: Container( | ||
| 70 | + alignment: Alignment.bottomCenter, | ||
| 71 | + height: 100, | ||
| 72 | + color: Colors.black.withOpacity(0.4), | ||
| 73 | + child: Row( | ||
| 74 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 75 | + children: [ | ||
| 76 | + Center( | ||
| 77 | + child: SizedBox( | ||
| 78 | + width: MediaQuery.of(context).size.width - 120, | ||
| 79 | + height: 50, | ||
| 80 | + child: FittedBox( | ||
| 81 | + child: Text( | ||
| 82 | + barcode?.displayValue ?? 'Scan something!', | ||
| 83 | + overflow: TextOverflow.fade, | ||
| 84 | + style: Theme.of(context) | ||
| 85 | + .textTheme | ||
| 86 | + .headline4! | ||
| 87 | + .copyWith(color: Colors.white), | ||
| 88 | + ), | ||
| 89 | + ), | ||
| 90 | + ), | ||
| 91 | + ), | ||
| 92 | + ], | ||
| 93 | + ), | ||
| 94 | + ), | ||
| 95 | + ), | ||
| 96 | + ], | ||
| 97 | + ); | ||
| 98 | + }, | ||
| 99 | + ), | ||
| 100 | + ); | ||
| 101 | + } | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +class ScannerOverlay extends CustomPainter { | ||
| 105 | + ScannerOverlay(this.scanWindow); | ||
| 106 | + | ||
| 107 | + final Rect scanWindow; | ||
| 108 | + | ||
| 109 | + @override | ||
| 110 | + void paint(Canvas canvas, Size size) { | ||
| 111 | + final backgroundPath = Path()..addRect(Rect.largest); | ||
| 112 | + final cutoutPath = Path()..addRect(scanWindow); | ||
| 113 | + | ||
| 114 | + final backgroundPaint = Paint() | ||
| 115 | + ..color = Colors.black.withOpacity(0.5) | ||
| 116 | + ..style = PaintingStyle.fill | ||
| 117 | + ..blendMode = BlendMode.dstOut; | ||
| 118 | + | ||
| 119 | + final backgroundWithCutout = Path.combine( | ||
| 120 | + PathOperation.difference, | ||
| 121 | + backgroundPath, | ||
| 122 | + cutoutPath, | ||
| 123 | + ); | ||
| 124 | + canvas.drawPath(backgroundWithCutout, backgroundPaint); | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + @override | ||
| 128 | + bool shouldRepaint(covariant CustomPainter oldDelegate) { | ||
| 129 | + return false; | ||
| 130 | + } | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +class BarcodeOverlay extends CustomPainter { | ||
| 134 | + BarcodeOverlay( | ||
| 135 | + this.barcode, | ||
| 136 | + this.arguments, | ||
| 137 | + this.boxFit, | ||
| 138 | + this.devicePixelRatio, | ||
| 139 | + this.capture, | ||
| 140 | + ); | ||
| 141 | + | ||
| 142 | + final BarcodeCapture capture; | ||
| 143 | + final Barcode barcode; | ||
| 144 | + final MobileScannerArguments arguments; | ||
| 145 | + final BoxFit boxFit; | ||
| 146 | + final double devicePixelRatio; | ||
| 147 | + | ||
| 148 | + @override | ||
| 149 | + void paint(Canvas canvas, Size size) { | ||
| 150 | + if (barcode.corners == null) return; | ||
| 151 | + final adjustedSize = applyBoxFit(boxFit, arguments.size, size); | ||
| 152 | + | ||
| 153 | + double verticalPadding = size.height - adjustedSize.destination.height; | ||
| 154 | + double horizontalPadding = size.width - adjustedSize.destination.width; | ||
| 155 | + if (verticalPadding > 0) { | ||
| 156 | + verticalPadding = verticalPadding / 2; | ||
| 157 | + } else { | ||
| 158 | + verticalPadding = 0; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + if (horizontalPadding > 0) { | ||
| 162 | + horizontalPadding = horizontalPadding / 2; | ||
| 163 | + } else { | ||
| 164 | + horizontalPadding = 0; | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + final ratioWidth = | ||
| 168 | + (Platform.isIOS ? capture.width! : arguments.size.width) / | ||
| 169 | + adjustedSize.destination.width; | ||
| 170 | + final ratioHeight = | ||
| 171 | + (Platform.isIOS ? capture.height! : arguments.size.height) / | ||
| 172 | + adjustedSize.destination.height; | ||
| 173 | + | ||
| 174 | + final List<Offset> adjustedOffset = []; | ||
| 175 | + for (final offset in barcode.corners!) { | ||
| 176 | + adjustedOffset.add( | ||
| 177 | + Offset( | ||
| 178 | + offset.dx / ratioWidth + horizontalPadding, | ||
| 179 | + offset.dy / ratioHeight + verticalPadding, | ||
| 180 | + ), | ||
| 181 | + ); | ||
| 182 | + } | ||
| 183 | + final cutoutPath = Path()..addPolygon(adjustedOffset, true); | ||
| 184 | + | ||
| 185 | + final backgroundPaint = Paint() | ||
| 186 | + ..color = Colors.red.withOpacity(0.3) | ||
| 187 | + ..style = PaintingStyle.fill | ||
| 188 | + ..blendMode = BlendMode.dstOut; | ||
| 189 | + | ||
| 190 | + canvas.drawPath(cutoutPath, backgroundPaint); | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + @override | ||
| 194 | + bool shouldRepaint(covariant CustomPainter oldDelegate) { | ||
| 195 | + return false; | ||
| 196 | + } | ||
| 197 | +} |
example/lib/barcode_scanner_zoom.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:image_picker/image_picker.dart'; | ||
| 3 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 4 | + | ||
| 5 | +class BarcodeScannerWithZoom extends StatefulWidget { | ||
| 6 | + const BarcodeScannerWithZoom({Key? key}) : super(key: key); | ||
| 7 | + | ||
| 8 | + @override | ||
| 9 | + _BarcodeScannerWithZoomState createState() => _BarcodeScannerWithZoomState(); | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> | ||
| 13 | + with SingleTickerProviderStateMixin { | ||
| 14 | + BarcodeCapture? barcode; | ||
| 15 | + | ||
| 16 | + MobileScannerController controller = MobileScannerController( | ||
| 17 | + torchEnabled: true, | ||
| 18 | + ); | ||
| 19 | + | ||
| 20 | + bool isStarted = true; | ||
| 21 | + double _zoomFactor = 0.0; | ||
| 22 | + | ||
| 23 | + @override | ||
| 24 | + Widget build(BuildContext context) { | ||
| 25 | + return Scaffold( | ||
| 26 | + backgroundColor: Colors.black, | ||
| 27 | + body: Builder( | ||
| 28 | + builder: (context) { | ||
| 29 | + return Stack( | ||
| 30 | + children: [ | ||
| 31 | + MobileScanner( | ||
| 32 | + controller: controller, | ||
| 33 | + fit: BoxFit.contain, | ||
| 34 | + onDetect: (barcode) { | ||
| 35 | + setState(() { | ||
| 36 | + this.barcode = barcode; | ||
| 37 | + }); | ||
| 38 | + }, | ||
| 39 | + ), | ||
| 40 | + Align( | ||
| 41 | + alignment: Alignment.bottomCenter, | ||
| 42 | + child: Container( | ||
| 43 | + alignment: Alignment.bottomCenter, | ||
| 44 | + height: 100, | ||
| 45 | + color: Colors.black.withOpacity(0.4), | ||
| 46 | + child: Column( | ||
| 47 | + children: [ | ||
| 48 | + Slider( | ||
| 49 | + value: _zoomFactor, | ||
| 50 | + onChanged: (value) { | ||
| 51 | + setState(() { | ||
| 52 | + _zoomFactor = value; | ||
| 53 | + controller.setZoomScale(value); | ||
| 54 | + }); | ||
| 55 | + }, | ||
| 56 | + ), | ||
| 57 | + Row( | ||
| 58 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 59 | + children: [ | ||
| 60 | + IconButton( | ||
| 61 | + color: Colors.white, | ||
| 62 | + icon: ValueListenableBuilder( | ||
| 63 | + valueListenable: controller.torchState, | ||
| 64 | + builder: (context, state, child) { | ||
| 65 | + if (state == null) { | ||
| 66 | + return const Icon( | ||
| 67 | + Icons.flash_off, | ||
| 68 | + color: Colors.grey, | ||
| 69 | + ); | ||
| 70 | + } | ||
| 71 | + switch (state as TorchState) { | ||
| 72 | + case TorchState.off: | ||
| 73 | + return const Icon( | ||
| 74 | + Icons.flash_off, | ||
| 75 | + color: Colors.grey, | ||
| 76 | + ); | ||
| 77 | + case TorchState.on: | ||
| 78 | + return const Icon( | ||
| 79 | + Icons.flash_on, | ||
| 80 | + color: Colors.yellow, | ||
| 81 | + ); | ||
| 82 | + } | ||
| 83 | + }, | ||
| 84 | + ), | ||
| 85 | + iconSize: 32.0, | ||
| 86 | + onPressed: () => controller.toggleTorch(), | ||
| 87 | + ), | ||
| 88 | + IconButton( | ||
| 89 | + color: Colors.white, | ||
| 90 | + icon: isStarted | ||
| 91 | + ? const Icon(Icons.stop) | ||
| 92 | + : const Icon(Icons.play_arrow), | ||
| 93 | + iconSize: 32.0, | ||
| 94 | + onPressed: () => setState(() { | ||
| 95 | + isStarted | ||
| 96 | + ? controller.stop() | ||
| 97 | + : controller.start(); | ||
| 98 | + isStarted = !isStarted; | ||
| 99 | + }), | ||
| 100 | + ), | ||
| 101 | + Center( | ||
| 102 | + child: SizedBox( | ||
| 103 | + width: MediaQuery.of(context).size.width - 200, | ||
| 104 | + height: 50, | ||
| 105 | + child: FittedBox( | ||
| 106 | + child: Text( | ||
| 107 | + barcode?.barcodes.first.rawValue ?? | ||
| 108 | + 'Scan something!', | ||
| 109 | + overflow: TextOverflow.fade, | ||
| 110 | + style: Theme.of(context) | ||
| 111 | + .textTheme | ||
| 112 | + .headline4! | ||
| 113 | + .copyWith(color: Colors.white), | ||
| 114 | + ), | ||
| 115 | + ), | ||
| 116 | + ), | ||
| 117 | + ), | ||
| 118 | + IconButton( | ||
| 119 | + color: Colors.white, | ||
| 120 | + icon: ValueListenableBuilder( | ||
| 121 | + valueListenable: controller.cameraFacingState, | ||
| 122 | + builder: (context, state, child) { | ||
| 123 | + if (state == null) { | ||
| 124 | + return const Icon(Icons.camera_front); | ||
| 125 | + } | ||
| 126 | + switch (state as CameraFacing) { | ||
| 127 | + case CameraFacing.front: | ||
| 128 | + return const Icon(Icons.camera_front); | ||
| 129 | + case CameraFacing.back: | ||
| 130 | + return const Icon(Icons.camera_rear); | ||
| 131 | + } | ||
| 132 | + }, | ||
| 133 | + ), | ||
| 134 | + iconSize: 32.0, | ||
| 135 | + onPressed: () => controller.switchCamera(), | ||
| 136 | + ), | ||
| 137 | + IconButton( | ||
| 138 | + color: Colors.white, | ||
| 139 | + icon: const Icon(Icons.image), | ||
| 140 | + iconSize: 32.0, | ||
| 141 | + onPressed: () async { | ||
| 142 | + final ImagePicker picker = ImagePicker(); | ||
| 143 | + // Pick an image | ||
| 144 | + final XFile? image = await picker.pickImage( | ||
| 145 | + source: ImageSource.gallery, | ||
| 146 | + ); | ||
| 147 | + if (image != null) { | ||
| 148 | + if (await controller.analyzeImage(image.path)) { | ||
| 149 | + if (!mounted) return; | ||
| 150 | + ScaffoldMessenger.of(context).showSnackBar( | ||
| 151 | + const SnackBar( | ||
| 152 | + content: Text('Barcode found!'), | ||
| 153 | + backgroundColor: Colors.green, | ||
| 154 | + ), | ||
| 155 | + ); | ||
| 156 | + } else { | ||
| 157 | + if (!mounted) return; | ||
| 158 | + ScaffoldMessenger.of(context).showSnackBar( | ||
| 159 | + const SnackBar( | ||
| 160 | + content: Text('No barcode found!'), | ||
| 161 | + backgroundColor: Colors.red, | ||
| 162 | + ), | ||
| 163 | + ); | ||
| 164 | + } | ||
| 165 | + } | ||
| 166 | + }, | ||
| 167 | + ), | ||
| 168 | + ], | ||
| 169 | + ), | ||
| 170 | + ], | ||
| 171 | + ), | ||
| 172 | + ), | ||
| 173 | + ), | ||
| 174 | + ], | ||
| 175 | + ); | ||
| 176 | + }, | ||
| 177 | + ), | ||
| 178 | + ); | ||
| 179 | + } | ||
| 180 | +} |
| @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; | @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; | ||
| 2 | import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; | 2 | import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; |
| 3 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; | 3 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; |
| 4 | import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; | 4 | import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; |
| 5 | +import 'package:mobile_scanner_example/barcode_scanner_window.dart'; | ||
| 5 | import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; | 6 | import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; |
| 7 | +import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; | ||
| 6 | 8 | ||
| 7 | void main() => runApp(const MaterialApp(home: MyHome())); | 9 | void main() => runApp(const MaterialApp(home: MyHome())); |
| 8 | 10 | ||
| @@ -44,6 +46,16 @@ class MyHome extends StatelessWidget { | @@ -44,6 +46,16 @@ class MyHome extends StatelessWidget { | ||
| 44 | onPressed: () { | 46 | onPressed: () { |
| 45 | Navigator.of(context).push( | 47 | Navigator.of(context).push( |
| 46 | MaterialPageRoute( | 48 | MaterialPageRoute( |
| 49 | + builder: (context) => const BarcodeScannerWithScanWindow(), | ||
| 50 | + ), | ||
| 51 | + ); | ||
| 52 | + }, | ||
| 53 | + child: const Text('MobileScanner with ScanWindow'), | ||
| 54 | + ), | ||
| 55 | + ElevatedButton( | ||
| 56 | + onPressed: () { | ||
| 57 | + Navigator.of(context).push( | ||
| 58 | + MaterialPageRoute( | ||
| 47 | builder: (context) => const BarcodeScannerReturningImage(), | 59 | builder: (context) => const BarcodeScannerReturningImage(), |
| 48 | ), | 60 | ), |
| 49 | ); | 61 | ); |
| @@ -62,6 +74,16 @@ class MyHome extends StatelessWidget { | @@ -62,6 +74,16 @@ class MyHome extends StatelessWidget { | ||
| 62 | }, | 74 | }, |
| 63 | child: const Text('MobileScanner without Controller'), | 75 | child: const Text('MobileScanner without Controller'), |
| 64 | ), | 76 | ), |
| 77 | + ElevatedButton( | ||
| 78 | + onPressed: () { | ||
| 79 | + Navigator.of(context).push( | ||
| 80 | + MaterialPageRoute( | ||
| 81 | + builder: (context) => const BarcodeScannerWithZoom(), | ||
| 82 | + ), | ||
| 83 | + ); | ||
| 84 | + }, | ||
| 85 | + child: const Text('MobileScanner with zoom slider'), | ||
| 86 | + ), | ||
| 65 | ], | 87 | ], |
| 66 | ), | 88 | ), |
| 67 | ), | 89 | ), |
| @@ -12,6 +12,7 @@ import MLKitVision | @@ -12,6 +12,7 @@ import MLKitVision | ||
| 12 | import MLKitBarcodeScanning | 12 | import MLKitBarcodeScanning |
| 13 | 13 | ||
| 14 | typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ()) | 14 | typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ()) |
| 15 | +typealias TorchModeChangeCallback = ((Int?) -> ()) | ||
| 15 | 16 | ||
| 16 | public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture { | 17 | public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture { |
| 17 | /// Capture session of the camera | 18 | /// Capture session of the camera |
| @@ -32,6 +33,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -32,6 +33,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 32 | /// When results are found, this callback will be called | 33 | /// When results are found, this callback will be called |
| 33 | let mobileScannerCallback: MobileScannerCallback | 34 | let mobileScannerCallback: MobileScannerCallback |
| 34 | 35 | ||
| 36 | + /// When torch mode is changes, this callback will be called | ||
| 37 | + let torchModeChangeCallback: TorchModeChangeCallback | ||
| 38 | + | ||
| 35 | /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture. | 39 | /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture. |
| 36 | private let registry: FlutterTextureRegistry? | 40 | private let registry: FlutterTextureRegistry? |
| 37 | 41 | ||
| @@ -43,9 +47,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -43,9 +47,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 43 | 47 | ||
| 44 | var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates | 48 | var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates |
| 45 | 49 | ||
| 46 | - init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) { | 50 | + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback) { |
| 47 | self.registry = registry | 51 | self.registry = registry |
| 48 | self.mobileScannerCallback = mobileScannerCallback | 52 | self.mobileScannerCallback = mobileScannerCallback |
| 53 | + self.torchModeChangeCallback = torchModeChangeCallback | ||
| 49 | super.init() | 54 | super.init() |
| 50 | } | 55 | } |
| 51 | 56 | ||
| @@ -127,17 +132,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -127,17 +132,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 127 | throw MobileScannerError.noCamera | 132 | throw MobileScannerError.noCamera |
| 128 | } | 133 | } |
| 129 | 134 | ||
| 130 | - // Enable the torch if parameter is set and torch is available | ||
| 131 | - if (device.hasTorch && device.isTorchAvailable) { | ||
| 132 | - do { | ||
| 133 | - try device.lockForConfiguration() | ||
| 134 | - device.torchMode = torch | ||
| 135 | - device.unlockForConfiguration() | ||
| 136 | - } catch { | ||
| 137 | - throw MobileScannerError.torchError(error) | ||
| 138 | - } | ||
| 139 | - } | ||
| 140 | - | ||
| 141 | device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) | 135 | device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) |
| 142 | captureSession.beginConfiguration() | 136 | captureSession.beginConfiguration() |
| 143 | 137 | ||
| @@ -169,6 +163,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -169,6 +163,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 169 | } | 163 | } |
| 170 | captureSession.commitConfiguration() | 164 | captureSession.commitConfiguration() |
| 171 | captureSession.startRunning() | 165 | captureSession.startRunning() |
| 166 | + // Enable the torch if parameter is set and torch is available | ||
| 167 | + // torch should be set after 'startRunning' is called | ||
| 168 | + do { | ||
| 169 | + try toggleTorch(torch) | ||
| 170 | + } catch { | ||
| 171 | + print("Failed to set initial torch state.") | ||
| 172 | + } | ||
| 172 | let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | 173 | let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) |
| 173 | 174 | ||
| 174 | return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) | 175 | return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) |
| @@ -198,13 +199,54 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -198,13 +199,54 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 198 | if (device == nil) { | 199 | if (device == nil) { |
| 199 | throw MobileScannerError.torchWhenStopped | 200 | throw MobileScannerError.torchWhenStopped |
| 200 | } | 201 | } |
| 202 | + if (device.hasTorch && device.isTorchAvailable) { | ||
| 203 | + do { | ||
| 204 | + try device.lockForConfiguration() | ||
| 205 | + device.torchMode = torch | ||
| 206 | + device.unlockForConfiguration() | ||
| 207 | + } catch { | ||
| 208 | + throw MobileScannerError.torchError(error) | ||
| 209 | + } | ||
| 210 | + } | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + // Observer for torch state | ||
| 214 | + public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | ||
| 215 | + switch keyPath { | ||
| 216 | + case "torchMode": | ||
| 217 | + // off = 0; on = 1; auto = 2; | ||
| 218 | + let state = change?[.newKey] as? Int | ||
| 219 | + torchModeChangeCallback(state) | ||
| 220 | + default: | ||
| 221 | + break | ||
| 222 | + } | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + /// Set the zoom factor of the camera | ||
| 226 | + func setScale(_ scale: CGFloat) throws { | ||
| 227 | + if (device == nil) { | ||
| 228 | + throw MobileScannerError.torchWhenStopped | ||
| 229 | + } | ||
| 230 | + | ||
| 201 | do { | 231 | do { |
| 202 | try device.lockForConfiguration() | 232 | try device.lockForConfiguration() |
| 203 | - device.torchMode = torch | 233 | + var maxZoomFactor = device.activeFormat.videoMaxZoomFactor |
| 234 | + | ||
| 235 | + var actualScale = (scale * 4) + 1 | ||
| 236 | + | ||
| 237 | + // Set maximum zoomrate of 5x | ||
| 238 | + actualScale = min(5.0, actualScale) | ||
| 239 | + | ||
| 240 | + // Limit to max rate of camera | ||
| 241 | + actualScale = min(maxZoomFactor, actualScale) | ||
| 242 | + | ||
| 243 | + // Limit to 1.0 scale | ||
| 244 | + device.ramp(toVideoZoomFactor: actualScale, withRate: 5) | ||
| 204 | device.unlockForConfiguration() | 245 | device.unlockForConfiguration() |
| 205 | } catch { | 246 | } catch { |
| 206 | - throw MobileScannerError.torchError(error) | 247 | + throw MobileScannerError.zoomError(error) |
| 207 | } | 248 | } |
| 249 | + | ||
| 208 | } | 250 | } |
| 209 | 251 | ||
| 210 | /// Analyze a single image | 252 | /// Analyze a single image |
| @@ -13,5 +13,7 @@ enum MobileScannerError: Error { | @@ -13,5 +13,7 @@ enum MobileScannerError: Error { | ||
| 13 | case torchError(_ error: Error) | 13 | case torchError(_ error: Error) |
| 14 | case cameraError(_ error: Error) | 14 | case cameraError(_ error: Error) |
| 15 | case torchWhenStopped | 15 | case torchWhenStopped |
| 16 | + case zoomWhenStopped | ||
| 17 | + case zoomError(_ error: Error) | ||
| 16 | case analyzerError(_ error: Error) | 18 | case analyzerError(_ error: Error) |
| 17 | } | 19 | } |
| @@ -2,6 +2,7 @@ import Flutter | @@ -2,6 +2,7 @@ import Flutter | ||
| 2 | import MLKitVision | 2 | import MLKitVision |
| 3 | import MLKitBarcodeScanning | 3 | import MLKitBarcodeScanning |
| 4 | import AVFoundation | 4 | import AVFoundation |
| 5 | +import UIKit | ||
| 5 | 6 | ||
| 6 | public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | 7 | public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { |
| 7 | 8 | ||
| @@ -10,19 +11,52 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -10,19 +11,52 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 10 | 11 | ||
| 11 | /// The handler sends all information via an event channel back to Flutter | 12 | /// The handler sends all information via an event channel back to Flutter |
| 12 | private let barcodeHandler: BarcodeHandler | 13 | private let barcodeHandler: BarcodeHandler |
| 14 | + | ||
| 15 | + static var scanWindow: [CGFloat]? | ||
| 16 | + | ||
| 17 | + private static func isBarcodeInScanWindow(barcode: Barcode, imageSize: CGSize) -> Bool { | ||
| 18 | + let scanwindow = SwiftMobileScannerPlugin.scanWindow! | ||
| 19 | + let barcodeminX = barcode.cornerPoints![0].cgPointValue.x | ||
| 20 | + let barcodeminY = barcode.cornerPoints![1].cgPointValue.y | ||
| 21 | + | ||
| 22 | + let barcodewidth = barcode.cornerPoints![2].cgPointValue.x - barcodeminX | ||
| 23 | + let barcodeheight = barcode.cornerPoints![3].cgPointValue.y - barcodeminY | ||
| 24 | + let barcodeBox = CGRect(x: barcodeminX, y: barcodeminY, width: barcodewidth, height: barcodeheight) | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + let minX = scanwindow[0] * imageSize.width | ||
| 28 | + let minY = scanwindow[1] * imageSize.height | ||
| 29 | + | ||
| 30 | + let width = (scanwindow[2] * imageSize.width) - minX | ||
| 31 | + let height = (scanwindow[3] * imageSize.height) - minY | ||
| 32 | + | ||
| 33 | + let scaledWindow = CGRect(x: minX, y: minY, width: width, height: height) | ||
| 34 | + | ||
| 35 | + return scaledWindow.contains(barcodeBox) | ||
| 36 | + } | ||
| 13 | 37 | ||
| 14 | init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { | 38 | init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { |
| 15 | self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in | 39 | self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in |
| 16 | if barcodes != nil { | 40 | if barcodes != nil { |
| 17 | - let barcodesMap = barcodes!.map { barcode in | ||
| 18 | - return barcode.data | 41 | + let barcodesMap = barcodes!.compactMap { barcode in |
| 42 | + if (SwiftMobileScannerPlugin.scanWindow != nil) { | ||
| 43 | + if (SwiftMobileScannerPlugin.isBarcodeInScanWindow(barcode: barcode, imageSize: image.size)) { | ||
| 44 | + return barcode.data | ||
| 45 | + } else { | ||
| 46 | + return nil | ||
| 47 | + } | ||
| 48 | + } else { | ||
| 49 | + return barcode.data | ||
| 50 | + } | ||
| 19 | } | 51 | } |
| 20 | if (!barcodesMap.isEmpty) { | 52 | if (!barcodesMap.isEmpty) { |
| 21 | - barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)]) | 53 | + barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!), "width": image.size.width, "height": image.size.height]) |
| 22 | } | 54 | } |
| 23 | } else if (error != nil){ | 55 | } else if (error != nil){ |
| 24 | barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) | 56 | barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) |
| 25 | } | 57 | } |
| 58 | + }, torchModeChangeCallback: { torchState in | ||
| 59 | + barcodeHandler.publishEvent(["name": "torchState", "data": torchState]) | ||
| 26 | }) | 60 | }) |
| 27 | self.barcodeHandler = barcodeHandler | 61 | self.barcodeHandler = barcodeHandler |
| 28 | super.init() | 62 | super.init() |
| @@ -49,6 +83,10 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -49,6 +83,10 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 49 | toggleTorch(call, result) | 83 | toggleTorch(call, result) |
| 50 | case "analyzeImage": | 84 | case "analyzeImage": |
| 51 | analyzeImage(call, result) | 85 | analyzeImage(call, result) |
| 86 | + case "setScale": | ||
| 87 | + setScale(call, result) | ||
| 88 | + case "updateScanWindow": | ||
| 89 | + updateScanWindow(call, result) | ||
| 52 | default: | 90 | default: |
| 53 | result(FlutterMethodNotImplemented) | 91 | result(FlutterMethodNotImplemented) |
| 54 | } | 92 | } |
| @@ -64,7 +102,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -64,7 +102,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 64 | 102 | ||
| 65 | let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} | 103 | let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} |
| 66 | var barcodeOptions: BarcodeScannerOptions? = nil | 104 | var barcodeOptions: BarcodeScannerOptions? = nil |
| 67 | - | 105 | + |
| 68 | if (formatList.count != 0) { | 106 | if (formatList.count != 0) { |
| 69 | var barcodeFormats: BarcodeFormat = [] | 107 | var barcodeFormats: BarcodeFormat = [] |
| 70 | for index in formats { | 108 | for index in formats { |
| @@ -123,6 +161,55 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -123,6 +161,55 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 123 | result(nil) | 161 | result(nil) |
| 124 | } | 162 | } |
| 125 | 163 | ||
| 164 | + /// Toggles the zoomScale | ||
| 165 | + private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 166 | + var scale = call.arguments as? CGFloat | ||
| 167 | + if (scale == nil) { | ||
| 168 | + result(FlutterError(code: "MobileScanner", | ||
| 169 | + message: "You must provide a scale when calling setScale!", | ||
| 170 | + details: nil)) | ||
| 171 | + return | ||
| 172 | + } | ||
| 173 | + do { | ||
| 174 | + try mobileScanner.setScale(scale!) | ||
| 175 | + } catch MobileScannerError.zoomWhenStopped { | ||
| 176 | + result(FlutterError(code: "MobileScanner", | ||
| 177 | + message: "Called setScale() while stopped!", | ||
| 178 | + details: nil)) | ||
| 179 | + } catch MobileScannerError.zoomError(let error) { | ||
| 180 | + result(FlutterError(code: "MobileScanner", | ||
| 181 | + message: "Error while zooming.", | ||
| 182 | + details: error)) | ||
| 183 | + } catch { | ||
| 184 | + result(FlutterError(code: "MobileScanner", | ||
| 185 | + message: "Error while zooming.", | ||
| 186 | + details: nil)) | ||
| 187 | + } | ||
| 188 | + result(nil) | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + /// Toggles the torch | ||
| 192 | + func updateScanWindow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 193 | + let scanWindowData: Array? = (call.arguments as? [String: Any])?["rect"] as? [CGFloat] | ||
| 194 | + SwiftMobileScannerPlugin.scanWindow = scanWindowData | ||
| 195 | + | ||
| 196 | + result(nil) | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + static func arrayToRect(scanWindowData: [CGFloat]?) -> CGRect? { | ||
| 200 | + if (scanWindowData == nil) { | ||
| 201 | + return nil | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + let minX = scanWindowData![0] | ||
| 205 | + let minY = scanWindowData![1] | ||
| 206 | + | ||
| 207 | + let width = scanWindowData![2] - minX | ||
| 208 | + let height = scanWindowData![3] - minY | ||
| 209 | + | ||
| 210 | + return CGRect(x: minX, y: minY, width: width, height: height) | ||
| 211 | + } | ||
| 212 | + | ||
| 126 | /// Analyzes a single image | 213 | /// Analyzes a single image |
| 127 | private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 214 | private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 128 | let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") | 215 | let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") |
| @@ -145,16 +232,4 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -145,16 +232,4 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 145 | }) | 232 | }) |
| 146 | result(nil) | 233 | result(nil) |
| 147 | } | 234 | } |
| 148 | - | ||
| 149 | - /// Observer for torch state | ||
| 150 | - public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | ||
| 151 | - switch keyPath { | ||
| 152 | - case "torchMode": | ||
| 153 | - // off = 0; on = 1; auto = 2; | ||
| 154 | - let state = change?[.newKey] as? Int | ||
| 155 | - barcodeHandler.publishEvent(["name": "torchState", "data": state]) | ||
| 156 | - default: | ||
| 157 | - break | ||
| 158 | - } | ||
| 159 | - } | ||
| 160 | } | 235 | } |
| @@ -5,7 +5,9 @@ import 'dart:ui' as ui; | @@ -5,7 +5,9 @@ import 'dart:ui' as ui; | ||
| 5 | import 'package:flutter/services.dart'; | 5 | import 'package:flutter/services.dart'; |
| 6 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | 6 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; |
| 7 | import 'package:mobile_scanner/mobile_scanner_web.dart'; | 7 | import 'package:mobile_scanner/mobile_scanner_web.dart'; |
| 8 | +import 'package:mobile_scanner/src/barcode_utility.dart'; | ||
| 8 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; | 9 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; |
| 10 | +import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 9 | 11 | ||
| 10 | /// This plugin is the web implementation of mobile_scanner. | 12 | /// This plugin is the web implementation of mobile_scanner. |
| 11 | /// It only supports QR codes. | 13 | /// It only supports QR codes. |
| @@ -35,6 +37,17 @@ class MobileScannerWebPlugin { | @@ -35,6 +37,17 @@ class MobileScannerWebPlugin { | ||
| 35 | 37 | ||
| 36 | static final html.DivElement vidDiv = html.DivElement(); | 38 | static final html.DivElement vidDiv = html.DivElement(); |
| 37 | 39 | ||
| 40 | + /// Represents barcode reader library. | ||
| 41 | + /// Change this property if you want to use a custom implementation. | ||
| 42 | + /// | ||
| 43 | + /// Example of using the jsQR library: | ||
| 44 | + /// void main() { | ||
| 45 | + /// if (kIsWeb) { | ||
| 46 | + /// MobileScannerWebPlugin.barCodeReader = | ||
| 47 | + /// JsQrCodeReader(videoContainer: MobileScannerWebPlugin.vidDiv); | ||
| 48 | + /// } | ||
| 49 | + /// runApp(const MaterialApp(home: MyHome())); | ||
| 50 | + /// } | ||
| 38 | static WebBarcodeReaderBase barCodeReader = | 51 | static WebBarcodeReaderBase barCodeReader = |
| 39 | ZXingBarcodeReader(videoContainer: vidDiv); | 52 | ZXingBarcodeReader(videoContainer: vidDiv); |
| 40 | StreamSubscription? _barCodeStreamSubscription; | 53 | StreamSubscription? _barCodeStreamSubscription; |
| @@ -82,16 +95,32 @@ class MobileScannerWebPlugin { | @@ -82,16 +95,32 @@ class MobileScannerWebPlugin { | ||
| 82 | 95 | ||
| 83 | // Check if stream is running | 96 | // Check if stream is running |
| 84 | if (barCodeReader.isStarted) { | 97 | if (barCodeReader.isStarted) { |
| 98 | + final hasTorch = await barCodeReader.hasTorch(); | ||
| 85 | return { | 99 | return { |
| 86 | 'ViewID': viewID, | 100 | 'ViewID': viewID, |
| 87 | 'videoWidth': barCodeReader.videoWidth, | 101 | 'videoWidth': barCodeReader.videoWidth, |
| 88 | 'videoHeight': barCodeReader.videoHeight, | 102 | 'videoHeight': barCodeReader.videoHeight, |
| 89 | - 'torchable': barCodeReader.hasTorch, | 103 | + 'torchable': hasTorch, |
| 90 | }; | 104 | }; |
| 91 | } | 105 | } |
| 92 | try { | 106 | try { |
| 107 | + List<BarcodeFormat>? formats; | ||
| 108 | + if (arguments.containsKey('formats')) { | ||
| 109 | + formats = (arguments['formats'] as List) | ||
| 110 | + .cast<int>() | ||
| 111 | + .map((e) => toFormat(e)) | ||
| 112 | + .toList(); | ||
| 113 | + } | ||
| 114 | + final Duration? detectionTimeout; | ||
| 115 | + if (arguments.containsKey('timeout')) { | ||
| 116 | + detectionTimeout = Duration(milliseconds: arguments['timeout'] as int); | ||
| 117 | + } else { | ||
| 118 | + detectionTimeout = null; | ||
| 119 | + } | ||
| 93 | await barCodeReader.start( | 120 | await barCodeReader.start( |
| 94 | cameraFacing: cameraFacing, | 121 | cameraFacing: cameraFacing, |
| 122 | + formats: formats, | ||
| 123 | + detectionTimeout: detectionTimeout, | ||
| 95 | ); | 124 | ); |
| 96 | 125 | ||
| 97 | _barCodeStreamSubscription = | 126 | _barCodeStreamSubscription = |
| @@ -102,16 +131,22 @@ class MobileScannerWebPlugin { | @@ -102,16 +131,22 @@ class MobileScannerWebPlugin { | ||
| 102 | 'data': { | 131 | 'data': { |
| 103 | 'rawValue': code.rawValue, | 132 | 'rawValue': code.rawValue, |
| 104 | 'rawBytes': code.rawBytes, | 133 | 'rawBytes': code.rawBytes, |
| 134 | + 'format': code.format.rawValue, | ||
| 105 | }, | 135 | }, |
| 106 | }); | 136 | }); |
| 107 | } | 137 | } |
| 108 | }); | 138 | }); |
| 139 | + final hasTorch = await barCodeReader.hasTorch(); | ||
| 140 | + | ||
| 141 | + if (hasTorch && arguments.containsKey('torch')) { | ||
| 142 | + barCodeReader.toggleTorch(enabled: arguments['torch'] as bool); | ||
| 143 | + } | ||
| 109 | 144 | ||
| 110 | return { | 145 | return { |
| 111 | 'ViewID': viewID, | 146 | 'ViewID': viewID, |
| 112 | 'videoWidth': barCodeReader.videoWidth, | 147 | 'videoWidth': barCodeReader.videoWidth, |
| 113 | 'videoHeight': barCodeReader.videoHeight, | 148 | 'videoHeight': barCodeReader.videoHeight, |
| 114 | - 'torchable': barCodeReader.hasTorch, | 149 | + 'torchable': hasTorch, |
| 115 | }; | 150 | }; |
| 116 | } catch (e) { | 151 | } catch (e) { |
| 117 | throw PlatformException(code: 'MobileScannerWeb', message: '$e'); | 152 | throw PlatformException(code: 'MobileScannerWeb', message: '$e'); |
| 1 | +import 'dart:math' as math; | ||
| 2 | + | ||
| 1 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 2 | import 'package:mobile_scanner/mobile_scanner.dart'; | 4 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 3 | 5 | ||
| @@ -147,3 +149,79 @@ WiFi? toWiFi(Map? data) { | @@ -147,3 +149,79 @@ WiFi? toWiFi(Map? data) { | ||
| 147 | return null; | 149 | return null; |
| 148 | } | 150 | } |
| 149 | } | 151 | } |
| 152 | + | ||
| 153 | +Size applyBoxFit(BoxFit fit, Size input, Size output) { | ||
| 154 | + if (input.height <= 0.0 || | ||
| 155 | + input.width <= 0.0 || | ||
| 156 | + output.height <= 0.0 || | ||
| 157 | + output.width <= 0.0) { | ||
| 158 | + return Size.zero; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + Size destination; | ||
| 162 | + | ||
| 163 | + final inputAspectRatio = input.width / input.height; | ||
| 164 | + final outputAspectRatio = output.width / output.height; | ||
| 165 | + | ||
| 166 | + switch (fit) { | ||
| 167 | + case BoxFit.fill: | ||
| 168 | + destination = output; | ||
| 169 | + break; | ||
| 170 | + case BoxFit.contain: | ||
| 171 | + if (outputAspectRatio > inputAspectRatio) { | ||
| 172 | + destination = Size( | ||
| 173 | + input.width * output.height / input.height, | ||
| 174 | + output.height, | ||
| 175 | + ); | ||
| 176 | + } else { | ||
| 177 | + destination = Size( | ||
| 178 | + output.width, | ||
| 179 | + input.height * output.width / input.width, | ||
| 180 | + ); | ||
| 181 | + } | ||
| 182 | + break; | ||
| 183 | + | ||
| 184 | + case BoxFit.cover: | ||
| 185 | + if (outputAspectRatio > inputAspectRatio) { | ||
| 186 | + destination = Size( | ||
| 187 | + output.width, | ||
| 188 | + input.height * (output.width / input.width), | ||
| 189 | + ); | ||
| 190 | + } else { | ||
| 191 | + destination = Size( | ||
| 192 | + input.width * (output.height / input.height), | ||
| 193 | + output.height, | ||
| 194 | + ); | ||
| 195 | + } | ||
| 196 | + break; | ||
| 197 | + case BoxFit.fitWidth: | ||
| 198 | + destination = Size( | ||
| 199 | + output.width, | ||
| 200 | + input.height * (output.width / input.width), | ||
| 201 | + ); | ||
| 202 | + break; | ||
| 203 | + case BoxFit.fitHeight: | ||
| 204 | + destination = Size( | ||
| 205 | + input.width * (output.height / input.height), | ||
| 206 | + output.height, | ||
| 207 | + ); | ||
| 208 | + break; | ||
| 209 | + case BoxFit.none: | ||
| 210 | + destination = Size( | ||
| 211 | + math.min(input.width, output.width), | ||
| 212 | + math.min(input.height, output.height), | ||
| 213 | + ); | ||
| 214 | + break; | ||
| 215 | + case BoxFit.scaleDown: | ||
| 216 | + destination = input; | ||
| 217 | + if (destination.height > output.height) { | ||
| 218 | + destination = Size(output.height * inputAspectRatio, output.height); | ||
| 219 | + } | ||
| 220 | + if (destination.width > output.width) { | ||
| 221 | + destination = Size(output.width, output.width / inputAspectRatio); | ||
| 222 | + } | ||
| 223 | + break; | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + return destination; | ||
| 227 | +} |
| @@ -49,6 +49,13 @@ class MobileScanner extends StatefulWidget { | @@ -49,6 +49,13 @@ class MobileScanner extends StatefulWidget { | ||
| 49 | /// If this is null, a black [ColoredBox] is used as placeholder. | 49 | /// If this is null, a black [ColoredBox] is used as placeholder. |
| 50 | final Widget Function(BuildContext, Widget?)? placeholderBuilder; | 50 | final Widget Function(BuildContext, Widget?)? placeholderBuilder; |
| 51 | 51 | ||
| 52 | + /// if set barcodes will only be scanned if they fall within this [Rect] | ||
| 53 | + /// useful for having a cut-out overlay for example. these [Rect] | ||
| 54 | + /// coordinates are relative to the widget size, so by how much your | ||
| 55 | + /// rectangle overlays the actual image can depend on things like the | ||
| 56 | + /// [BoxFit] | ||
| 57 | + final Rect? scanWindow; | ||
| 58 | + | ||
| 52 | /// Create a new [MobileScanner] using the provided [controller] | 59 | /// Create a new [MobileScanner] using the provided [controller] |
| 53 | /// and [onBarcodeDetected] callback. | 60 | /// and [onBarcodeDetected] callback. |
| 54 | const MobileScanner({ | 61 | const MobileScanner({ |
| @@ -59,6 +66,7 @@ class MobileScanner extends StatefulWidget { | @@ -59,6 +66,7 @@ class MobileScanner extends StatefulWidget { | ||
| 59 | @Deprecated('Use onScannerStarted() instead.') this.onStart, | 66 | @Deprecated('Use onScannerStarted() instead.') this.onStart, |
| 60 | this.onScannerStarted, | 67 | this.onScannerStarted, |
| 61 | this.placeholderBuilder, | 68 | this.placeholderBuilder, |
| 69 | + this.scanWindow, | ||
| 62 | super.key, | 70 | super.key, |
| 63 | }); | 71 | }); |
| 64 | 72 | ||
| @@ -156,33 +164,101 @@ class _MobileScannerState extends State<MobileScanner> | @@ -156,33 +164,101 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 156 | } | 164 | } |
| 157 | } | 165 | } |
| 158 | 166 | ||
| 167 | + /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible, | ||
| 168 | + /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize] | ||
| 169 | + /// | ||
| 170 | + /// since when using a [BoxFit] the content will always be centered on its parent. we can convert the rect | ||
| 171 | + /// to be relative to the texture. | ||
| 172 | + /// | ||
| 173 | + /// since the textures size and the actuall image (on the texture size) might not be the same, we also need to | ||
| 174 | + /// calculate the scanWindow in terms of percentages of the texture, not pixels. | ||
| 175 | + Rect calculateScanWindowRelativeToTextureInPercentage( | ||
| 176 | + BoxFit fit, | ||
| 177 | + Rect scanWindow, | ||
| 178 | + Size textureSize, | ||
| 179 | + Size widgetSize, | ||
| 180 | + ) { | ||
| 181 | + /// map the texture size to get its new size after fitted to screen | ||
| 182 | + final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize); | ||
| 183 | + | ||
| 184 | + /// create a new rectangle that represents the texture on the screen | ||
| 185 | + final minX = widgetSize.width / 2 - fittedTextureSize.destination.width / 2; | ||
| 186 | + final minY = | ||
| 187 | + widgetSize.height / 2 - fittedTextureSize.destination.height / 2; | ||
| 188 | + final textureWindow = Offset(minX, minY) & fittedTextureSize.destination; | ||
| 189 | + | ||
| 190 | + /// create a new scan window and with only the area of the rect intersecting the texture window | ||
| 191 | + final scanWindowInTexture = scanWindow.intersect(textureWindow); | ||
| 192 | + | ||
| 193 | + /// update the scanWindow left and top to be relative to the texture not the widget | ||
| 194 | + final newLeft = scanWindowInTexture.left - textureWindow.left; | ||
| 195 | + final newTop = scanWindowInTexture.top - textureWindow.top; | ||
| 196 | + final newWidth = scanWindowInTexture.width; | ||
| 197 | + final newHeight = scanWindowInTexture.height; | ||
| 198 | + | ||
| 199 | + /// new scanWindow that is adapted to the boxfit and relative to the texture | ||
| 200 | + final windowInTexture = Rect.fromLTWH(newLeft, newTop, newWidth, newHeight); | ||
| 201 | + | ||
| 202 | + /// get the scanWindow as a percentage of the texture | ||
| 203 | + final percentageLeft = | ||
| 204 | + windowInTexture.left / fittedTextureSize.destination.width; | ||
| 205 | + final percentageTop = | ||
| 206 | + windowInTexture.top / fittedTextureSize.destination.height; | ||
| 207 | + final percentageRight = | ||
| 208 | + windowInTexture.right / fittedTextureSize.destination.width; | ||
| 209 | + final percentagebottom = | ||
| 210 | + windowInTexture.bottom / fittedTextureSize.destination.height; | ||
| 211 | + | ||
| 212 | + /// this rectangle can be send to native code and used to cut out a rectangle of the scan image | ||
| 213 | + return Rect.fromLTRB( | ||
| 214 | + percentageLeft, | ||
| 215 | + percentageTop, | ||
| 216 | + percentageRight, | ||
| 217 | + percentagebottom, | ||
| 218 | + ); | ||
| 219 | + } | ||
| 220 | + | ||
| 159 | @override | 221 | @override |
| 160 | Widget build(BuildContext context) { | 222 | Widget build(BuildContext context) { |
| 161 | - return ValueListenableBuilder<MobileScannerArguments?>( | ||
| 162 | - valueListenable: _controller.startArguments, | ||
| 163 | - builder: (context, value, child) { | ||
| 164 | - if (value == null) { | ||
| 165 | - return __buildPlaceholderOrError(context, child); | ||
| 166 | - } | 223 | + return LayoutBuilder( |
| 224 | + builder: (context, constraints) { | ||
| 225 | + return ValueListenableBuilder<MobileScannerArguments?>( | ||
| 226 | + valueListenable: _controller.startArguments, | ||
| 227 | + builder: (context, value, child) { | ||
| 228 | + if (value == null) { | ||
| 229 | + return __buildPlaceholderOrError(context, child); | ||
| 230 | + } | ||
| 167 | 231 | ||
| 168 | - return ClipRect( | ||
| 169 | - child: LayoutBuilder( | ||
| 170 | - builder: (_, constraints) { | ||
| 171 | - return SizedBox.fromSize( | ||
| 172 | - size: constraints.biggest, | ||
| 173 | - child: FittedBox( | ||
| 174 | - fit: widget.fit, | ||
| 175 | - child: SizedBox( | ||
| 176 | - width: value.size.width, | ||
| 177 | - height: value.size.height, | ||
| 178 | - child: kIsWeb | ||
| 179 | - ? HtmlElementView(viewType: value.webId!) | ||
| 180 | - : Texture(textureId: value.textureId!), | ||
| 181 | - ), | ||
| 182 | - ), | 232 | + if (widget.scanWindow != null) { |
| 233 | + final window = calculateScanWindowRelativeToTextureInPercentage( | ||
| 234 | + widget.fit, | ||
| 235 | + widget.scanWindow!, | ||
| 236 | + value.size, | ||
| 237 | + Size(constraints.maxWidth, constraints.maxHeight), | ||
| 183 | ); | 238 | ); |
| 184 | - }, | ||
| 185 | - ), | 239 | + _controller.updateScanWindow(window); |
| 240 | + } | ||
| 241 | + | ||
| 242 | + return ClipRect( | ||
| 243 | + child: LayoutBuilder( | ||
| 244 | + builder: (_, constraints) { | ||
| 245 | + return SizedBox.fromSize( | ||
| 246 | + size: constraints.biggest, | ||
| 247 | + child: FittedBox( | ||
| 248 | + fit: widget.fit, | ||
| 249 | + child: SizedBox( | ||
| 250 | + width: value.size.width, | ||
| 251 | + height: value.size.height, | ||
| 252 | + child: kIsWeb | ||
| 253 | + ? HtmlElementView(viewType: value.webId!) | ||
| 254 | + : Texture(textureId: value.textureId!), | ||
| 255 | + ), | ||
| 256 | + ), | ||
| 257 | + ); | ||
| 258 | + }, | ||
| 259 | + ), | ||
| 260 | + ); | ||
| 261 | + }, | ||
| 186 | ); | 262 | ); |
| 187 | }, | 263 | }, |
| 188 | ); | 264 | ); |
| @@ -99,19 +99,21 @@ class MobileScannerController { | @@ -99,19 +99,21 @@ class MobileScannerController { | ||
| 99 | 99 | ||
| 100 | bool isStarting = false; | 100 | bool isStarting = false; |
| 101 | 101 | ||
| 102 | - bool? _hasTorch; | 102 | + /// A notifier that provides availability of the Torch (Flash) |
| 103 | + final ValueNotifier<bool?> hasTorchState = ValueNotifier(false); | ||
| 103 | 104 | ||
| 104 | /// Returns whether the device has a torch. | 105 | /// Returns whether the device has a torch. |
| 105 | /// | 106 | /// |
| 106 | /// Throws an error if the controller is not initialized. | 107 | /// Throws an error if the controller is not initialized. |
| 107 | bool get hasTorch { | 108 | bool get hasTorch { |
| 108 | - if (_hasTorch == null) { | 109 | + final hasTorch = hasTorchState.value; |
| 110 | + if (hasTorch == null) { | ||
| 109 | throw const MobileScannerException( | 111 | throw const MobileScannerException( |
| 110 | errorCode: MobileScannerErrorCode.controllerUninitialized, | 112 | errorCode: MobileScannerErrorCode.controllerUninitialized, |
| 111 | ); | 113 | ); |
| 112 | } | 114 | } |
| 113 | 115 | ||
| 114 | - return _hasTorch!; | 116 | + return hasTorch; |
| 115 | } | 117 | } |
| 116 | 118 | ||
| 117 | /// Set the starting arguments for the camera | 119 | /// Set the starting arguments for the camera |
| @@ -124,11 +126,20 @@ class MobileScannerController { | @@ -124,11 +126,20 @@ class MobileScannerController { | ||
| 124 | arguments['speed'] = detectionSpeed.index; | 126 | arguments['speed'] = detectionSpeed.index; |
| 125 | arguments['timeout'] = detectionTimeoutMs; | 127 | arguments['timeout'] = detectionTimeoutMs; |
| 126 | 128 | ||
| 129 | + /* if (scanWindow != null) { | ||
| 130 | + arguments['scanWindow'] = [ | ||
| 131 | + scanWindow!.left, | ||
| 132 | + scanWindow!.top, | ||
| 133 | + scanWindow!.right, | ||
| 134 | + scanWindow!.bottom, | ||
| 135 | + ]; | ||
| 136 | + } */ | ||
| 137 | + | ||
| 127 | if (formats != null) { | 138 | if (formats != null) { |
| 128 | - if (Platform.isAndroid) { | ||
| 129 | - arguments['formats'] = formats!.map((e) => e.index).toList(); | ||
| 130 | - } else if (Platform.isIOS || Platform.isMacOS) { | 139 | + if (kIsWeb || Platform.isIOS || Platform.isMacOS) { |
| 131 | arguments['formats'] = formats!.map((e) => e.rawValue).toList(); | 140 | arguments['formats'] = formats!.map((e) => e.rawValue).toList(); |
| 141 | + } else if (Platform.isAndroid) { | ||
| 142 | + arguments['formats'] = formats!.map((e) => e.index).toList(); | ||
| 132 | } | 143 | } |
| 133 | } | 144 | } |
| 134 | arguments['returnImage'] = true; | 145 | arguments['returnImage'] = true; |
| @@ -221,8 +232,9 @@ class MobileScannerController { | @@ -221,8 +232,9 @@ class MobileScannerController { | ||
| 221 | ); | 232 | ); |
| 222 | } | 233 | } |
| 223 | 234 | ||
| 224 | - _hasTorch = startResult['torchable'] as bool? ?? false; | ||
| 225 | - if (_hasTorch! && torchEnabled) { | 235 | + final hasTorch = startResult['torchable'] as bool? ?? false; |
| 236 | + hasTorchState.value = hasTorch; | ||
| 237 | + if (hasTorch && torchEnabled) { | ||
| 226 | torchState.value = TorchState.on; | 238 | torchState.value = TorchState.on; |
| 227 | } | 239 | } |
| 228 | 240 | ||
| @@ -234,7 +246,7 @@ class MobileScannerController { | @@ -234,7 +246,7 @@ class MobileScannerController { | ||
| 234 | startResult['videoHeight'] as double? ?? 0, | 246 | startResult['videoHeight'] as double? ?? 0, |
| 235 | ) | 247 | ) |
| 236 | : toSize(startResult['size'] as Map? ?? {}), | 248 | : toSize(startResult['size'] as Map? ?? {}), |
| 237 | - hasTorch: _hasTorch!, | 249 | + hasTorch: hasTorch, |
| 238 | textureId: kIsWeb ? null : startResult['textureId'] as int?, | 250 | textureId: kIsWeb ? null : startResult['textureId'] as int?, |
| 239 | webId: kIsWeb ? startResult['ViewID'] as String? : null, | 251 | webId: kIsWeb ? startResult['ViewID'] as String? : null, |
| 240 | ); | 252 | ); |
| @@ -255,7 +267,7 @@ class MobileScannerController { | @@ -255,7 +267,7 @@ class MobileScannerController { | ||
| 255 | /// | 267 | /// |
| 256 | /// Throws if the controller was not initialized. | 268 | /// Throws if the controller was not initialized. |
| 257 | Future<void> toggleTorch() async { | 269 | Future<void> toggleTorch() async { |
| 258 | - final hasTorch = _hasTorch; | 270 | + final hasTorch = hasTorchState.value; |
| 259 | 271 | ||
| 260 | if (hasTorch == null) { | 272 | if (hasTorch == null) { |
| 261 | throw const MobileScannerException( | 273 | throw const MobileScannerException( |
| @@ -294,6 +306,22 @@ class MobileScannerController { | @@ -294,6 +306,22 @@ class MobileScannerController { | ||
| 294 | .then<bool>((bool? value) => value ?? false); | 306 | .then<bool>((bool? value) => value ?? false); |
| 295 | } | 307 | } |
| 296 | 308 | ||
| 309 | + /// Set the zoomScale of the camera. | ||
| 310 | + /// | ||
| 311 | + /// [zoomScale] must be within 0.0 and 1.0, where 1.0 is the max zoom, and 0.0 | ||
| 312 | + /// is zoomed out. | ||
| 313 | + Future<void> setZoomScale(double zoomScale) async { | ||
| 314 | + if (zoomScale < 0 || zoomScale > 1) { | ||
| 315 | + throw const MobileScannerException( | ||
| 316 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 317 | + errorDetails: MobileScannerErrorDetails( | ||
| 318 | + message: 'The zoomScale must be between 0 and 1.', | ||
| 319 | + ), | ||
| 320 | + ); | ||
| 321 | + } | ||
| 322 | + await _methodChannel.invokeMethod('setScale', zoomScale); | ||
| 323 | + } | ||
| 324 | + | ||
| 297 | /// Disposes the MobileScannerController and closes all listeners. | 325 | /// Disposes the MobileScannerController and closes all listeners. |
| 298 | /// | 326 | /// |
| 299 | /// If you call this, you cannot use this controller object anymore. | 327 | /// If you call this, you cannot use this controller object anymore. |
| @@ -325,6 +353,8 @@ class MobileScannerController { | @@ -325,6 +353,8 @@ class MobileScannerController { | ||
| 325 | BarcodeCapture( | 353 | BarcodeCapture( |
| 326 | barcodes: parsed, | 354 | barcodes: parsed, |
| 327 | image: event['image'] as Uint8List?, | 355 | image: event['image'] as Uint8List?, |
| 356 | + width: event['width'] as double?, | ||
| 357 | + height: event['height'] as double?, | ||
| 328 | ), | 358 | ), |
| 329 | ); | 359 | ); |
| 330 | break; | 360 | break; |
| @@ -344,10 +374,12 @@ class MobileScannerController { | @@ -344,10 +374,12 @@ class MobileScannerController { | ||
| 344 | _barcodesController.add( | 374 | _barcodesController.add( |
| 345 | BarcodeCapture( | 375 | BarcodeCapture( |
| 346 | barcodes: [ | 376 | barcodes: [ |
| 347 | - Barcode( | ||
| 348 | - rawValue: barcode?['rawValue'] as String?, | ||
| 349 | - rawBytes: barcode?['rawBytes'] as Uint8List?, | ||
| 350 | - ) | 377 | + if (barcode != null) |
| 378 | + Barcode( | ||
| 379 | + rawValue: barcode['rawValue'] as String?, | ||
| 380 | + rawBytes: barcode['rawBytes'] as Uint8List?, | ||
| 381 | + format: toFormat(barcode['format'] as int), | ||
| 382 | + ), | ||
| 351 | ], | 383 | ], |
| 352 | ), | 384 | ), |
| 353 | ); | 385 | ); |
| @@ -361,4 +393,10 @@ class MobileScannerController { | @@ -361,4 +393,10 @@ class MobileScannerController { | ||
| 361 | throw UnimplementedError(name as String?); | 393 | throw UnimplementedError(name as String?); |
| 362 | } | 394 | } |
| 363 | } | 395 | } |
| 396 | + | ||
| 397 | + /// updates the native scanwindow | ||
| 398 | + Future<void> updateScanWindow(Rect window) async { | ||
| 399 | + final data = [window.left, window.top, window.right, window.bottom]; | ||
| 400 | + await _methodChannel.invokeMethod('updateScanWindow', {'rect': data}); | ||
| 401 | + } | ||
| 364 | } | 402 | } |
| @@ -12,8 +12,14 @@ class BarcodeCapture { | @@ -12,8 +12,14 @@ class BarcodeCapture { | ||
| 12 | 12 | ||
| 13 | final Uint8List? image; | 13 | final Uint8List? image; |
| 14 | 14 | ||
| 15 | + final double? width; | ||
| 16 | + | ||
| 17 | + final double? height; | ||
| 18 | + | ||
| 15 | BarcodeCapture({ | 19 | BarcodeCapture({ |
| 16 | required this.barcodes, | 20 | required this.barcodes, |
| 17 | this.image, | 21 | this.image, |
| 22 | + this.width, | ||
| 23 | + this.height, | ||
| 18 | }); | 24 | }); |
| 19 | } | 25 | } |
| 1 | -import 'dart:html'; | 1 | +import 'dart:html' as html; |
| 2 | 2 | ||
| 3 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 4 | +import 'package:js/js.dart'; | ||
| 5 | +import 'package:js/js_util.dart'; | ||
| 4 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; | 6 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; |
| 5 | import 'package:mobile_scanner/src/objects/barcode.dart'; | 7 | import 'package:mobile_scanner/src/objects/barcode.dart'; |
| 6 | import 'package:mobile_scanner/src/web/media.dart'; | 8 | import 'package:mobile_scanner/src/web/media.dart'; |
| 7 | 9 | ||
| 8 | abstract class WebBarcodeReaderBase { | 10 | abstract class WebBarcodeReaderBase { |
| 9 | /// Timer used to capture frames to be analyzed | 11 | /// Timer used to capture frames to be analyzed |
| 10 | - final Duration frameInterval; | ||
| 11 | - final DivElement videoContainer; | 12 | + Duration frameInterval = const Duration(milliseconds: 200); |
| 13 | + final html.DivElement videoContainer; | ||
| 12 | 14 | ||
| 13 | - const WebBarcodeReaderBase({ | 15 | + WebBarcodeReaderBase({ |
| 14 | required this.videoContainer, | 16 | required this.videoContainer, |
| 15 | - this.frameInterval = const Duration(milliseconds: 200), | ||
| 16 | }); | 17 | }); |
| 17 | 18 | ||
| 18 | bool get isStarted; | 19 | bool get isStarted; |
| @@ -23,6 +24,8 @@ abstract class WebBarcodeReaderBase { | @@ -23,6 +24,8 @@ abstract class WebBarcodeReaderBase { | ||
| 23 | /// Starts streaming video | 24 | /// Starts streaming video |
| 24 | Future<void> start({ | 25 | Future<void> start({ |
| 25 | required CameraFacing cameraFacing, | 26 | required CameraFacing cameraFacing, |
| 27 | + List<BarcodeFormat>? formats, | ||
| 28 | + Duration? detectionTimeout, | ||
| 26 | }); | 29 | }); |
| 27 | 30 | ||
| 28 | /// Starts scanning QR codes or barcodes | 31 | /// Starts scanning QR codes or barcodes |
| @@ -35,24 +38,24 @@ abstract class WebBarcodeReaderBase { | @@ -35,24 +38,24 @@ abstract class WebBarcodeReaderBase { | ||
| 35 | Future<void> toggleTorch({required bool enabled}); | 38 | Future<void> toggleTorch({required bool enabled}); |
| 36 | 39 | ||
| 37 | /// Determine whether device has flash | 40 | /// Determine whether device has flash |
| 38 | - bool get hasTorch; | 41 | + Future<bool> hasTorch(); |
| 39 | } | 42 | } |
| 40 | 43 | ||
| 41 | mixin InternalStreamCreation on WebBarcodeReaderBase { | 44 | mixin InternalStreamCreation on WebBarcodeReaderBase { |
| 42 | /// The video stream. | 45 | /// The video stream. |
| 43 | /// Will be initialized later to see which camera needs to be used. | 46 | /// Will be initialized later to see which camera needs to be used. |
| 44 | - MediaStream? localMediaStream; | ||
| 45 | - final VideoElement video = VideoElement(); | 47 | + html.MediaStream? localMediaStream; |
| 48 | + final html.VideoElement video = html.VideoElement(); | ||
| 46 | 49 | ||
| 47 | @override | 50 | @override |
| 48 | int get videoWidth => video.videoWidth; | 51 | int get videoWidth => video.videoWidth; |
| 49 | @override | 52 | @override |
| 50 | int get videoHeight => video.videoHeight; | 53 | int get videoHeight => video.videoHeight; |
| 51 | 54 | ||
| 52 | - Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async { | 55 | + Future<html.MediaStream?> initMediaStream(CameraFacing cameraFacing) async { |
| 53 | // Check if browser supports multiple camera's and set if supported | 56 | // Check if browser supports multiple camera's and set if supported |
| 54 | final Map? capabilities = | 57 | final Map? capabilities = |
| 55 | - window.navigator.mediaDevices?.getSupportedConstraints(); | 58 | + html.window.navigator.mediaDevices?.getSupportedConstraints(); |
| 56 | final Map<String, dynamic> constraints; | 59 | final Map<String, dynamic> constraints; |
| 57 | if (capabilities != null && capabilities['facingMode'] as bool) { | 60 | if (capabilities != null && capabilities['facingMode'] as bool) { |
| 58 | constraints = { | 61 | constraints = { |
| @@ -65,15 +68,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { | @@ -65,15 +68,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { | ||
| 65 | constraints = {'video': true}; | 68 | constraints = {'video': true}; |
| 66 | } | 69 | } |
| 67 | final stream = | 70 | final stream = |
| 68 | - await window.navigator.mediaDevices?.getUserMedia(constraints); | 71 | + await html.window.navigator.mediaDevices?.getUserMedia(constraints); |
| 69 | return stream; | 72 | return stream; |
| 70 | } | 73 | } |
| 71 | 74 | ||
| 72 | - void prepareVideoElement(VideoElement videoSource); | 75 | + void prepareVideoElement(html.VideoElement videoSource); |
| 73 | 76 | ||
| 74 | Future<void> attachStreamToVideo( | 77 | Future<void> attachStreamToVideo( |
| 75 | - MediaStream stream, | ||
| 76 | - VideoElement videoSource, | 78 | + html.MediaStream stream, |
| 79 | + html.VideoElement videoSource, | ||
| 77 | ); | 80 | ); |
| 78 | 81 | ||
| 79 | @override | 82 | @override |
| @@ -96,19 +99,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { | @@ -96,19 +99,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { | ||
| 96 | 99 | ||
| 97 | /// Mixin for libraries that don't have built-in torch support | 100 | /// Mixin for libraries that don't have built-in torch support |
| 98 | mixin InternalTorchDetection on InternalStreamCreation { | 101 | mixin InternalTorchDetection on InternalStreamCreation { |
| 102 | + Future<List<String>> getSupportedTorchStates() async { | ||
| 103 | + try { | ||
| 104 | + final track = localMediaStream?.getVideoTracks(); | ||
| 105 | + if (track != null) { | ||
| 106 | + final imageCapture = ImageCapture(track.first); | ||
| 107 | + final photoCapabilities = await promiseToFuture<PhotoCapabilities>( | ||
| 108 | + imageCapture.getPhotoCapabilities(), | ||
| 109 | + ); | ||
| 110 | + final fillLightMode = photoCapabilities.fillLightMode; | ||
| 111 | + if (fillLightMode != null) { | ||
| 112 | + return fillLightMode; | ||
| 113 | + } | ||
| 114 | + } | ||
| 115 | + } catch (e) { | ||
| 116 | + // ImageCapture is not supported by some browsers: | ||
| 117 | + // https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture#browser_compatibility | ||
| 118 | + } | ||
| 119 | + return []; | ||
| 120 | + } | ||
| 121 | + | ||
| 99 | @override | 122 | @override |
| 100 | - bool get hasTorch { | ||
| 101 | - // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 102 | - // final track = _localStream?.getVideoTracks(); | ||
| 103 | - // if (track != null) { | ||
| 104 | - // final imageCapture = html.ImageCapture(track.first); | ||
| 105 | - // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 106 | - // } | ||
| 107 | - return false; | 123 | + Future<bool> hasTorch() async { |
| 124 | + return (await getSupportedTorchStates()).isNotEmpty; | ||
| 108 | } | 125 | } |
| 109 | 126 | ||
| 110 | @override | 127 | @override |
| 111 | Future<void> toggleTorch({required bool enabled}) async { | 128 | Future<void> toggleTorch({required bool enabled}) async { |
| 129 | + final hasTorch = await this.hasTorch(); | ||
| 112 | if (hasTorch) { | 130 | if (hasTorch) { |
| 113 | final track = localMediaStream?.getVideoTracks(); | 131 | final track = localMediaStream?.getVideoTracks(); |
| 114 | await track?.first.applyConstraints({ | 132 | await track?.first.applyConstraints({ |
| @@ -119,3 +137,36 @@ mixin InternalTorchDetection on InternalStreamCreation { | @@ -119,3 +137,36 @@ mixin InternalTorchDetection on InternalStreamCreation { | ||
| 119 | } | 137 | } |
| 120 | } | 138 | } |
| 121 | } | 139 | } |
| 140 | + | ||
| 141 | +@JS('Promise') | ||
| 142 | +@staticInterop | ||
| 143 | +class Promise<T> {} | ||
| 144 | + | ||
| 145 | +@JS() | ||
| 146 | +@anonymous | ||
| 147 | +class PhotoCapabilities { | ||
| 148 | + /// Returns an array of available fill light options. Options include auto, off, or flash. | ||
| 149 | + external List<String>? get fillLightMode; | ||
| 150 | +} | ||
| 151 | + | ||
| 152 | +@JS('ImageCapture') | ||
| 153 | +@staticInterop | ||
| 154 | +class ImageCapture { | ||
| 155 | + /// MediaStreamTrack | ||
| 156 | + external factory ImageCapture(dynamic track); | ||
| 157 | +} | ||
| 158 | + | ||
| 159 | +extension ImageCaptureExt on ImageCapture { | ||
| 160 | + external Promise<PhotoCapabilities> getPhotoCapabilities(); | ||
| 161 | +} | ||
| 162 | + | ||
| 163 | +@JS('Map') | ||
| 164 | +@staticInterop | ||
| 165 | +class JsMap { | ||
| 166 | + external factory JsMap(); | ||
| 167 | +} | ||
| 168 | + | ||
| 169 | +extension JsMapExt on JsMap { | ||
| 170 | + external void set(dynamic key, dynamic value); | ||
| 171 | + external dynamic get(dynamic key); | ||
| 172 | +} |
| @@ -20,6 +20,11 @@ class Code { | @@ -20,6 +20,11 @@ class Code { | ||
| 20 | external Uint8ClampedList get binaryData; | 20 | external Uint8ClampedList get binaryData; |
| 21 | } | 21 | } |
| 22 | 22 | ||
| 23 | +/// Barcode reader that uses jsQR library. | ||
| 24 | +/// jsQR supports only QR codes format. | ||
| 25 | +/// | ||
| 26 | +/// Include jsQR to your index.html file: | ||
| 27 | +/// <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> | ||
| 23 | class JsQrCodeReader extends WebBarcodeReaderBase | 28 | class JsQrCodeReader extends WebBarcodeReaderBase |
| 24 | with InternalStreamCreation, InternalTorchDetection { | 29 | with InternalStreamCreation, InternalTorchDetection { |
| 25 | JsQrCodeReader({required super.videoContainer}); | 30 | JsQrCodeReader({required super.videoContainer}); |
| @@ -30,9 +35,15 @@ class JsQrCodeReader extends WebBarcodeReaderBase | @@ -30,9 +35,15 @@ class JsQrCodeReader extends WebBarcodeReaderBase | ||
| 30 | @override | 35 | @override |
| 31 | Future<void> start({ | 36 | Future<void> start({ |
| 32 | required CameraFacing cameraFacing, | 37 | required CameraFacing cameraFacing, |
| 38 | + List<BarcodeFormat>? formats, | ||
| 39 | + Duration? detectionTimeout, | ||
| 33 | }) async { | 40 | }) async { |
| 34 | videoContainer.children = [video]; | 41 | videoContainer.children = [video]; |
| 35 | 42 | ||
| 43 | + if (detectionTimeout != null) { | ||
| 44 | + frameInterval = detectionTimeout; | ||
| 45 | + } | ||
| 46 | + | ||
| 36 | final stream = await initMediaStream(cameraFacing); | 47 | final stream = await initMediaStream(cameraFacing); |
| 37 | 48 | ||
| 38 | prepareVideoElement(video); | 49 | prepareVideoElement(video); |
| @@ -7,10 +7,6 @@ import 'package:mobile_scanner/src/enums/camera_facing.dart'; | @@ -7,10 +7,6 @@ import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 7 | import 'package:mobile_scanner/src/objects/barcode.dart'; | 7 | import 'package:mobile_scanner/src/objects/barcode.dart'; |
| 8 | import 'package:mobile_scanner/src/web/base.dart'; | 8 | import 'package:mobile_scanner/src/web/base.dart'; |
| 9 | 9 | ||
| 10 | -@JS('Promise') | ||
| 11 | -@staticInterop | ||
| 12 | -class Promise<T> {} | ||
| 13 | - | ||
| 14 | @JS('ZXing.BrowserMultiFormatReader') | 10 | @JS('ZXing.BrowserMultiFormatReader') |
| 15 | @staticInterop | 11 | @staticInterop |
| 16 | class JsZXingBrowserMultiFormatReader { | 12 | class JsZXingBrowserMultiFormatReader { |
| @@ -47,12 +43,14 @@ extension ResultExt on Result { | @@ -47,12 +43,14 @@ extension ResultExt on Result { | ||
| 47 | /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/core/BarcodeFormat.ts#L28 | 43 | /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/core/BarcodeFormat.ts#L28 |
| 48 | BarcodeFormat get barcodeFormat { | 44 | BarcodeFormat get barcodeFormat { |
| 49 | switch (format) { | 45 | switch (format) { |
| 50 | - case 1: | 46 | + case 0: |
| 51 | return BarcodeFormat.aztec; | 47 | return BarcodeFormat.aztec; |
| 52 | - case 2: | 48 | + case 1: |
| 53 | return BarcodeFormat.codebar; | 49 | return BarcodeFormat.codebar; |
| 54 | - case 3: | 50 | + case 2: |
| 55 | return BarcodeFormat.code39; | 51 | return BarcodeFormat.code39; |
| 52 | + case 3: | ||
| 53 | + return BarcodeFormat.code93; | ||
| 56 | case 4: | 54 | case 4: |
| 57 | return BarcodeFormat.code128; | 55 | return BarcodeFormat.code128; |
| 58 | case 5: | 56 | case 5: |
| @@ -83,6 +81,42 @@ extension ResultExt on Result { | @@ -83,6 +81,42 @@ extension ResultExt on Result { | ||
| 83 | } | 81 | } |
| 84 | } | 82 | } |
| 85 | 83 | ||
| 84 | +extension ZXingBarcodeFormat on BarcodeFormat { | ||
| 85 | + int get zxingBarcodeFormat { | ||
| 86 | + switch (this) { | ||
| 87 | + case BarcodeFormat.aztec: | ||
| 88 | + return 0; | ||
| 89 | + case BarcodeFormat.codebar: | ||
| 90 | + return 1; | ||
| 91 | + case BarcodeFormat.code39: | ||
| 92 | + return 2; | ||
| 93 | + case BarcodeFormat.code93: | ||
| 94 | + return 3; | ||
| 95 | + case BarcodeFormat.code128: | ||
| 96 | + return 4; | ||
| 97 | + case BarcodeFormat.dataMatrix: | ||
| 98 | + return 5; | ||
| 99 | + case BarcodeFormat.ean8: | ||
| 100 | + return 6; | ||
| 101 | + case BarcodeFormat.ean13: | ||
| 102 | + return 7; | ||
| 103 | + case BarcodeFormat.itf: | ||
| 104 | + return 8; | ||
| 105 | + case BarcodeFormat.pdf417: | ||
| 106 | + return 10; | ||
| 107 | + case BarcodeFormat.qrCode: | ||
| 108 | + return 11; | ||
| 109 | + case BarcodeFormat.upcA: | ||
| 110 | + return 14; | ||
| 111 | + case BarcodeFormat.upcE: | ||
| 112 | + return 15; | ||
| 113 | + case BarcodeFormat.unknown: | ||
| 114 | + case BarcodeFormat.all: | ||
| 115 | + return -1; | ||
| 116 | + } | ||
| 117 | + } | ||
| 118 | +} | ||
| 119 | + | ||
| 86 | typedef BarcodeDetectionCallback = void Function( | 120 | typedef BarcodeDetectionCallback = void Function( |
| 87 | Result? result, | 121 | Result? result, |
| 88 | dynamic error, | 122 | dynamic error, |
| @@ -134,13 +168,13 @@ extension JsZXingBrowserMultiFormatReaderExt | @@ -134,13 +168,13 @@ extension JsZXingBrowserMultiFormatReaderExt | ||
| 134 | external MediaStream? stream; | 168 | external MediaStream? stream; |
| 135 | } | 169 | } |
| 136 | 170 | ||
| 171 | +/// Barcode reader that uses zxing-js library. | ||
| 172 | +/// | ||
| 173 | +/// Include zxing-js to your index.html file: | ||
| 174 | +/// <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script> | ||
| 137 | class ZXingBarcodeReader extends WebBarcodeReaderBase | 175 | class ZXingBarcodeReader extends WebBarcodeReaderBase |
| 138 | with InternalStreamCreation, InternalTorchDetection { | 176 | with InternalStreamCreation, InternalTorchDetection { |
| 139 | - late final JsZXingBrowserMultiFormatReader _reader = | ||
| 140 | - JsZXingBrowserMultiFormatReader( | ||
| 141 | - null, | ||
| 142 | - frameInterval.inMilliseconds, | ||
| 143 | - ); | 177 | + JsZXingBrowserMultiFormatReader? _reader; |
| 144 | 178 | ||
| 145 | ZXingBarcodeReader({required super.videoContainer}); | 179 | ZXingBarcodeReader({required super.videoContainer}); |
| 146 | 180 | ||
| @@ -150,7 +184,27 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | @@ -150,7 +184,27 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | ||
| 150 | @override | 184 | @override |
| 151 | Future<void> start({ | 185 | Future<void> start({ |
| 152 | required CameraFacing cameraFacing, | 186 | required CameraFacing cameraFacing, |
| 187 | + List<BarcodeFormat>? formats, | ||
| 188 | + Duration? detectionTimeout, | ||
| 153 | }) async { | 189 | }) async { |
| 190 | + final JsMap? hints; | ||
| 191 | + if (formats != null && !formats.contains(BarcodeFormat.all)) { | ||
| 192 | + hints = JsMap(); | ||
| 193 | + final zxingFormats = | ||
| 194 | + formats.map((e) => e.zxingBarcodeFormat).where((e) => e > 0).toList(); | ||
| 195 | + // set hint DecodeHintType.POSSIBLE_FORMATS | ||
| 196 | + // https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/core/DecodeHintType.ts#L28 | ||
| 197 | + hints.set(2, zxingFormats); | ||
| 198 | + } else { | ||
| 199 | + hints = null; | ||
| 200 | + } | ||
| 201 | + if (detectionTimeout != null) { | ||
| 202 | + frameInterval = detectionTimeout; | ||
| 203 | + } | ||
| 204 | + _reader = JsZXingBrowserMultiFormatReader( | ||
| 205 | + hints, | ||
| 206 | + frameInterval.inMilliseconds, | ||
| 207 | + ); | ||
| 154 | videoContainer.children = [video]; | 208 | videoContainer.children = [video]; |
| 155 | 209 | ||
| 156 | final stream = await initMediaStream(cameraFacing); | 210 | final stream = await initMediaStream(cameraFacing); |
| @@ -163,7 +217,7 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | @@ -163,7 +217,7 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | ||
| 163 | 217 | ||
| 164 | @override | 218 | @override |
| 165 | void prepareVideoElement(VideoElement videoSource) { | 219 | void prepareVideoElement(VideoElement videoSource) { |
| 166 | - _reader.prepareVideoElement(videoSource); | 220 | + _reader?.prepareVideoElement(videoSource); |
| 167 | } | 221 | } |
| 168 | 222 | ||
| 169 | @override | 223 | @override |
| @@ -171,9 +225,9 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | @@ -171,9 +225,9 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | ||
| 171 | MediaStream stream, | 225 | MediaStream stream, |
| 172 | VideoElement videoSource, | 226 | VideoElement videoSource, |
| 173 | ) async { | 227 | ) async { |
| 174 | - _reader.addVideoSource(videoSource, stream); | ||
| 175 | - _reader.videoElement = videoSource; | ||
| 176 | - _reader.stream = stream; | 228 | + _reader?.addVideoSource(videoSource, stream); |
| 229 | + _reader?.videoElement = videoSource; | ||
| 230 | + _reader?.stream = stream; | ||
| 177 | localMediaStream = stream; | 231 | localMediaStream = stream; |
| 178 | await videoSource.play(); | 232 | await videoSource.play(); |
| 179 | } | 233 | } |
| @@ -182,7 +236,7 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | @@ -182,7 +236,7 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | ||
| 182 | Stream<Barcode?> detectBarcodeContinuously() { | 236 | Stream<Barcode?> detectBarcodeContinuously() { |
| 183 | final controller = StreamController<Barcode?>(); | 237 | final controller = StreamController<Barcode?>(); |
| 184 | controller.onListen = () async { | 238 | controller.onListen = () async { |
| 185 | - _reader.decodeContinuously( | 239 | + _reader?.decodeContinuously( |
| 186 | video, | 240 | video, |
| 187 | allowInterop((result, error) { | 241 | allowInterop((result, error) { |
| 188 | if (result != null) { | 242 | if (result != null) { |
| @@ -192,14 +246,14 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | @@ -192,14 +246,14 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase | ||
| 192 | ); | 246 | ); |
| 193 | }; | 247 | }; |
| 194 | controller.onCancel = () { | 248 | controller.onCancel = () { |
| 195 | - _reader.stopContinuousDecode(); | 249 | + _reader?.stopContinuousDecode(); |
| 196 | }; | 250 | }; |
| 197 | return controller.stream; | 251 | return controller.stream; |
| 198 | } | 252 | } |
| 199 | 253 | ||
| 200 | @override | 254 | @override |
| 201 | Future<void> stop() async { | 255 | Future<void> stop() async { |
| 202 | - _reader.reset(); | 256 | + _reader?.reset(); |
| 203 | super.stop(); | 257 | super.stop(); |
| 204 | } | 258 | } |
| 205 | } | 259 | } |
| 1 | import AVFoundation | 1 | import AVFoundation |
| 2 | import FlutterMacOS | 2 | import FlutterMacOS |
| 3 | import Vision | 3 | import Vision |
| 4 | +import UIKit | ||
| 4 | 5 | ||
| 5 | public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate { | 6 | public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate { |
| 6 | 7 | ||
| @@ -20,6 +21,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -20,6 +21,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 20 | 21 | ||
| 21 | // Image to be sent to the texture | 22 | // Image to be sent to the texture |
| 22 | var latestBuffer: CVImageBuffer! | 23 | var latestBuffer: CVImageBuffer! |
| 24 | + | ||
| 25 | + // optional window to limit scan search | ||
| 26 | + var scanWindow: CGRect? | ||
| 23 | 27 | ||
| 24 | 28 | ||
| 25 | // var analyzeMode: Int = 0 | 29 | // var analyzeMode: Int = 0 |
| @@ -57,6 +61,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -57,6 +61,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 57 | // switchAnalyzeMode(call, result) | 61 | // switchAnalyzeMode(call, result) |
| 58 | case "stop": | 62 | case "stop": |
| 59 | stop(result) | 63 | stop(result) |
| 64 | + case "updateScanWindow": | ||
| 65 | + updateScanWindow(call) | ||
| 60 | default: | 66 | default: |
| 61 | result(FlutterMethodNotImplemented) | 67 | result(FlutterMethodNotImplemented) |
| 62 | } | 68 | } |
| @@ -109,10 +115,17 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -109,10 +115,17 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 109 | try imageRequestHandler.perform([VNDetectBarcodesRequest { (request, error) in | 115 | try imageRequestHandler.perform([VNDetectBarcodesRequest { (request, error) in |
| 110 | if error == nil { | 116 | if error == nil { |
| 111 | if let results = request.results as? [VNBarcodeObservation] { | 117 | if let results = request.results as? [VNBarcodeObservation] { |
| 112 | - for barcode in results { | ||
| 113 | - let barcodeType = String(barcode.symbology.rawValue).replacingOccurrences(of: "VNBarcodeSymbology", with: "") | ||
| 114 | - let event: [String: Any?] = ["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcodeType]] | ||
| 115 | - self.sink?(event) | 118 | + for barcode in results { |
| 119 | + if scanWindow != nil { | ||
| 120 | + let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image) | ||
| 121 | + if (!match) { | ||
| 122 | + continue | ||
| 123 | + } | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + let barcodeType = String(barcode.symbology.rawValue).replacingOccurrences(of: "VNBarcodeSymbology", with: "") | ||
| 127 | + let event: [String: Any?] = ["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcodeType]] | ||
| 128 | + self.sink?(event) | ||
| 116 | 129 | ||
| 117 | // if barcodeType == "QR" { | 130 | // if barcodeType == "QR" { |
| 118 | // let image = CIImage(image: source) | 131 | // let image = CIImage(image: source) |
| @@ -158,6 +171,38 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -158,6 +171,38 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 158 | } | 171 | } |
| 159 | } | 172 | } |
| 160 | 173 | ||
| 174 | + func updateScanWindow(_ call: FlutterMethodCall) { | ||
| 175 | + let argReader = MapArgumentReader(call.arguments as? [String: Any]) | ||
| 176 | + let scanWindowData: Array? = argReader.floatArray(key: "rect") | ||
| 177 | + | ||
| 178 | + if (scanWindowData == nil) { | ||
| 179 | + return | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + let minX = scanWindowData![0] | ||
| 183 | + let minY = scanWindowData![1] | ||
| 184 | + | ||
| 185 | + let width = scanWindowData![2] - minX | ||
| 186 | + let height = scanWindowData![3] - minY | ||
| 187 | + | ||
| 188 | + scanWindow = CGRect(x: minX, y: minY, width: width, height: height) | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: Barcode, _ inputImage: UIImage) -> Bool { | ||
| 192 | + let barcodeBoundingBox = barcode.frame | ||
| 193 | + | ||
| 194 | + let imageWidth = inputImage.size.width; | ||
| 195 | + let imageHeight = inputImage.size.height; | ||
| 196 | + | ||
| 197 | + let minX = scanWindow.minX * imageWidth | ||
| 198 | + let minY = scanWindow.minY * imageHeight | ||
| 199 | + let width = scanWindow.width * imageWidth | ||
| 200 | + let height = scanWindow.height * imageHeight | ||
| 201 | + | ||
| 202 | + let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height) | ||
| 203 | + return scaledScanWindow.contains(barcodeBoundingBox) | ||
| 204 | + } | ||
| 205 | + | ||
| 161 | func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 206 | func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 162 | if (device != nil) { | 207 | if (device != nil) { |
| 163 | result(FlutterError(code: "MobileScanner", | 208 | result(FlutterError(code: "MobileScanner", |
| @@ -318,5 +363,9 @@ class MapArgumentReader { | @@ -318,5 +363,9 @@ class MapArgumentReader { | ||
| 318 | func stringArray(key: String) -> [String]? { | 363 | func stringArray(key: String) -> [String]? { |
| 319 | return args?[key] as? [String] | 364 | return args?[key] as? [String] |
| 320 | } | 365 | } |
| 366 | + | ||
| 367 | + func floatArray(key: String) -> [CGFloat]? { | ||
| 368 | + return args?[key] as? [CGFloat] | ||
| 369 | + } | ||
| 321 | 370 | ||
| 322 | } | 371 | } |
-
Please register or login to post a comment