Committed by
GitHub
Merge branch 'master' into pavel/web-format
Showing
16 changed files
with
645 additions
and
69 deletions
| @@ -12,6 +12,7 @@ Breaking changes: | @@ -12,6 +12,7 @@ Breaking changes: | ||
| 12 | * The `autoResume` attribute has been removed from the `MobileScanner` widget. | 12 | * The `autoResume` attribute has been removed from the `MobileScanner` widget. |
| 13 | The controller already automatically resumes, so it had no effect. | 13 | The controller already automatically resumes, so it had no effect. |
| 14 | * Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef. | 14 | * Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef. |
| 15 | +* [Web] Replaced `jsqr` library with `zxing-js` for full barcode support. | ||
| 15 | 16 | ||
| 16 | Improvements: | 17 | Improvements: |
| 17 | * Toggling the device torch now does nothing if the device has no torch, rather than throwing an error. | 18 | * Toggling the device torch now does nothing if the device has no torch, rather than throwing an error. |
| @@ -21,6 +22,9 @@ Features: | @@ -21,6 +22,9 @@ Features: | ||
| 21 | * Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder. | 22 | * Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder. |
| 22 | * Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically. | 23 | * Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically. |
| 23 | * Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch. | 24 | * Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch. |
| 25 | +* [iOS] Support `torchEnabled` parameter from MobileScannerController() on iOS | ||
| 26 | +* [Web] Added ability to use custom barcode scanning js libraries | ||
| 27 | + by extending `WebBarcodeReaderBase` class and changing `barCodeReader` property in `MobileScannerWebPlugin` | ||
| 24 | 28 | ||
| 25 | Fixes: | 29 | Fixes: |
| 26 | * Fixes the missing gradle setup for the Android project, which prevented gradle sync from working. | 30 | * Fixes the missing gradle setup for the Android project, which prevented gradle sync from working. |
| @@ -31,6 +35,7 @@ Fixes: | @@ -31,6 +35,7 @@ Fixes: | ||
| 31 | * Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`. | 35 | * Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`. |
| 32 | Now it only depends on its layout constraints. | 36 | Now it only depends on its layout constraints. |
| 33 | * Fixed a potential crash when the scanner is restarted due to the app being resumed. | 37 | * Fixed a potential crash when the scanner is restarted due to the app being resumed. |
| 38 | +* [iOS] Fix crash when changing torch state | ||
| 34 | 39 | ||
| 35 | ## 3.0.0-beta.2 | 40 | ## 3.0.0-beta.2 |
| 36 | Breaking changes: | 41 | Breaking changes: |
| @@ -3,9 +3,11 @@ package dev.steenbakker.mobile_scanner | @@ -3,9 +3,11 @@ package dev.steenbakker.mobile_scanner | ||
| 3 | import android.Manifest | 3 | import android.Manifest |
| 4 | import android.app.Activity | 4 | import android.app.Activity |
| 5 | import android.content.pm.PackageManager | 5 | import android.content.pm.PackageManager |
| 6 | +import android.graphics.Rect | ||
| 6 | import android.net.Uri | 7 | import android.net.Uri |
| 7 | import android.os.Handler | 8 | import android.os.Handler |
| 8 | import android.os.Looper | 9 | import android.os.Looper |
| 10 | +import android.util.Log | ||
| 9 | import android.view.Surface | 11 | import android.view.Surface |
| 10 | import androidx.camera.core.* | 12 | import androidx.camera.core.* |
| 11 | import androidx.camera.lifecycle.ProcessCameraProvider | 13 | import androidx.camera.lifecycle.ProcessCameraProvider |
| @@ -14,18 +16,23 @@ import androidx.core.content.ContextCompat | @@ -14,18 +16,23 @@ import androidx.core.content.ContextCompat | ||
| 14 | import androidx.lifecycle.LifecycleOwner | 16 | import androidx.lifecycle.LifecycleOwner |
| 15 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 17 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 16 | import com.google.mlkit.vision.barcode.BarcodeScanning | 18 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 19 | +import com.google.mlkit.vision.barcode.common.Barcode | ||
| 17 | import com.google.mlkit.vision.common.InputImage | 20 | import com.google.mlkit.vision.common.InputImage |
| 18 | import dev.steenbakker.mobile_scanner.objects.DetectionSpeed | 21 | import dev.steenbakker.mobile_scanner.objects.DetectionSpeed |
| 19 | import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters | 22 | import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters |
| 20 | import io.flutter.plugin.common.MethodChannel | 23 | import io.flutter.plugin.common.MethodChannel |
| 21 | import io.flutter.plugin.common.PluginRegistry | 24 | import io.flutter.plugin.common.PluginRegistry |
| 22 | import io.flutter.view.TextureRegistry | 25 | import io.flutter.view.TextureRegistry |
| 23 | -typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit | 26 | +import kotlin.math.roundToInt |
| 27 | + | ||
| 28 | + | ||
| 29 | +typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit | ||
| 24 | typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit | 30 | typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit |
| 25 | typealias MobileScannerErrorCallback = (error: String) -> Unit | 31 | typealias MobileScannerErrorCallback = (error: String) -> Unit |
| 26 | typealias TorchStateCallback = (state: Int) -> Unit | 32 | typealias TorchStateCallback = (state: Int) -> Unit |
| 27 | typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit | 33 | typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit |
| 28 | 34 | ||
| 35 | + | ||
| 29 | class NoCamera : Exception() | 36 | class NoCamera : Exception() |
| 30 | class AlreadyStarted : Exception() | 37 | class AlreadyStarted : Exception() |
| 31 | class AlreadyStopped : Exception() | 38 | class AlreadyStopped : Exception() |
| @@ -53,6 +60,7 @@ class MobileScanner( | @@ -53,6 +60,7 @@ class MobileScanner( | ||
| 53 | private var pendingPermissionResult: MethodChannel.Result? = null | 60 | private var pendingPermissionResult: MethodChannel.Result? = null |
| 54 | private var preview: Preview? = null | 61 | private var preview: Preview? = null |
| 55 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null | 62 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null |
| 63 | + var scanWindow: List<Float>? = null | ||
| 56 | 64 | ||
| 57 | private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES | 65 | private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES |
| 58 | private var detectionTimeout: Long = 250 | 66 | private var detectionTimeout: Long = 250 |
| @@ -138,12 +146,27 @@ class MobileScanner( | @@ -138,12 +146,27 @@ class MobileScanner( | ||
| 138 | lastScanned = newScannedBarcodes | 146 | lastScanned = newScannedBarcodes |
| 139 | } | 147 | } |
| 140 | 148 | ||
| 141 | - val barcodeMap = barcodes.map { barcode -> barcode.data } | 149 | + val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf() |
| 150 | + | ||
| 151 | + for ( barcode in barcodes) { | ||
| 152 | + if(scanWindow != null) { | ||
| 153 | + val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy) | ||
| 154 | + if(!match) { | ||
| 155 | + continue | ||
| 156 | + } else { | ||
| 157 | + barcodeMap.add(barcode.data) | ||
| 158 | + } | ||
| 159 | + } else { | ||
| 160 | + barcodeMap.add(barcode.data) | ||
| 161 | + } | ||
| 162 | + } | ||
| 142 | 163 | ||
| 143 | if (barcodeMap.isNotEmpty()) { | 164 | if (barcodeMap.isNotEmpty()) { |
| 144 | mobileScannerCallback( | 165 | mobileScannerCallback( |
| 145 | barcodeMap, | 166 | barcodeMap, |
| 146 | - if (returnImage) mediaImage.toByteArray() else null | 167 | + if (returnImage) mediaImage.toByteArray() else null, |
| 168 | + if (returnImage) mediaImage.width else null, | ||
| 169 | + if (returnImage) mediaImage.height else null | ||
| 147 | ) | 170 | ) |
| 148 | } | 171 | } |
| 149 | } | 172 | } |
| @@ -162,6 +185,23 @@ class MobileScanner( | @@ -162,6 +185,23 @@ class MobileScanner( | ||
| 162 | } | 185 | } |
| 163 | } | 186 | } |
| 164 | 187 | ||
| 188 | + // scales the scanWindow to the provided inputImage and checks if that scaled | ||
| 189 | + // scanWindow contains the barcode | ||
| 190 | + private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: ImageProxy): Boolean { | ||
| 191 | + val barcodeBoundingBox = barcode.boundingBox ?: return false | ||
| 192 | + | ||
| 193 | + val imageWidth = inputImage.height | ||
| 194 | + val imageHeight = inputImage.width | ||
| 195 | + | ||
| 196 | + val left = (scanWindow[0] * imageWidth).roundToInt() | ||
| 197 | + val top = (scanWindow[1] * imageHeight).roundToInt() | ||
| 198 | + val right = (scanWindow[2] * imageWidth).roundToInt() | ||
| 199 | + val bottom = (scanWindow[3] * imageHeight).roundToInt() | ||
| 200 | + | ||
| 201 | + val scaledScanWindow = Rect(left, top, right, bottom) | ||
| 202 | + return scaledScanWindow.contains(barcodeBoundingBox) | ||
| 203 | + } | ||
| 204 | + | ||
| 165 | /** | 205 | /** |
| 166 | * Start barcode scanning by initializing the camera and barcode scanner. | 206 | * Start barcode scanning by initializing the camera and barcode scanner. |
| 167 | */ | 207 | */ |
| @@ -244,7 +284,7 @@ class MobileScanner( | @@ -244,7 +284,7 @@ class MobileScanner( | ||
| 244 | // Enable torch if provided | 284 | // Enable torch if provided |
| 245 | camera!!.cameraControl.enableTorch(torch) | 285 | camera!!.cameraControl.enableTorch(torch) |
| 246 | 286 | ||
| 247 | - val resolution = preview!!.resolutionInfo!!.resolution | 287 | + val resolution = analysis.resolutionInfo!!.resolution |
| 248 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | 288 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 |
| 249 | val width = resolution.width.toDouble() | 289 | val width = resolution.width.toDouble() |
| 250 | val height = resolution.height.toDouble() | 290 | val height = resolution.height.toDouble() |
| @@ -25,12 +25,14 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa | @@ -25,12 +25,14 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa | ||
| 25 | 25 | ||
| 26 | private var analyzerResult: MethodChannel.Result? = null | 26 | private var analyzerResult: MethodChannel.Result? = null |
| 27 | 27 | ||
| 28 | - private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? -> | 28 | + private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? -> |
| 29 | if (image != null) { | 29 | if (image != null) { |
| 30 | barcodeHandler.publishEvent(mapOf( | 30 | barcodeHandler.publishEvent(mapOf( |
| 31 | "name" to "barcode", | 31 | "name" to "barcode", |
| 32 | "data" to barcodes, | 32 | "data" to barcodes, |
| 33 | - "image" to image | 33 | + "image" to image, |
| 34 | + "width" to width!!.toDouble(), | ||
| 35 | + "height" to height!!.toDouble() | ||
| 34 | )) | 36 | )) |
| 35 | } else { | 37 | } else { |
| 36 | barcodeHandler.publishEvent(mapOf( | 38 | barcodeHandler.publishEvent(mapOf( |
| @@ -77,6 +79,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa | @@ -77,6 +79,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa | ||
| 77 | "torch" -> toggleTorch(call, result) | 79 | "torch" -> toggleTorch(call, result) |
| 78 | "stop" -> stop(result) | 80 | "stop" -> stop(result) |
| 79 | "analyzeImage" -> analyzeImage(call, result) | 81 | "analyzeImage" -> analyzeImage(call, result) |
| 82 | + "updateScanWindow" -> updateScanWindow(call) | ||
| 80 | else -> result.notImplemented() | 83 | else -> result.notImplemented() |
| 81 | } | 84 | } |
| 82 | } | 85 | } |
| @@ -215,4 +218,8 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa | @@ -215,4 +218,8 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa | ||
| 215 | result.error("MobileScanner", "Called toggleTorch() while stopped!", null) | 218 | result.error("MobileScanner", "Called toggleTorch() while stopped!", null) |
| 216 | } | 219 | } |
| 217 | } | 220 | } |
| 221 | + | ||
| 222 | + private fun updateScanWindow(call: MethodCall) { | ||
| 223 | + handler!!.scanWindow = call.argument<List<Float>>("rect") | ||
| 224 | + } | ||
| 218 | } | 225 | } |
| @@ -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"; |
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 | +} |
| @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; | @@ -2,6 +2,7 @@ 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'; |
| 6 | 7 | ||
| 7 | void main() => runApp(const MaterialApp(home: MyHome())); | 8 | void main() => runApp(const MaterialApp(home: MyHome())); |
| @@ -44,6 +45,16 @@ class MyHome extends StatelessWidget { | @@ -44,6 +45,16 @@ class MyHome extends StatelessWidget { | ||
| 44 | onPressed: () { | 45 | onPressed: () { |
| 45 | Navigator.of(context).push( | 46 | Navigator.of(context).push( |
| 46 | MaterialPageRoute( | 47 | MaterialPageRoute( |
| 48 | + builder: (context) => const BarcodeScannerWithScanWindow(), | ||
| 49 | + ), | ||
| 50 | + ); | ||
| 51 | + }, | ||
| 52 | + child: const Text('MobileScanner with ScanWindow'), | ||
| 53 | + ), | ||
| 54 | + ElevatedButton( | ||
| 55 | + onPressed: () { | ||
| 56 | + Navigator.of(context).push( | ||
| 57 | + MaterialPageRoute( | ||
| 47 | builder: (context) => const BarcodeScannerReturningImage(), | 58 | builder: (context) => const BarcodeScannerReturningImage(), |
| 48 | ), | 59 | ), |
| 49 | ); | 60 | ); |
| @@ -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,12 +199,26 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -198,12 +199,26 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 198 | if (device == nil) { | 199 | if (device == nil) { |
| 199 | throw MobileScannerError.torchWhenStopped | 200 | throw MobileScannerError.torchWhenStopped |
| 200 | } | 201 | } |
| 201 | - do { | ||
| 202 | - try device.lockForConfiguration() | ||
| 203 | - device.torchMode = torch | ||
| 204 | - device.unlockForConfiguration() | ||
| 205 | - } catch { | ||
| 206 | - throw MobileScannerError.torchError(error) | 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 | ||
| 207 | } | 222 | } |
| 208 | } | 223 | } |
| 209 | 224 |
| @@ -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,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -49,6 +83,8 @@ 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 "updateScanWindow": | ||
| 87 | + updateScanWindow(call, result) | ||
| 52 | default: | 88 | default: |
| 53 | result(FlutterMethodNotImplemented) | 89 | result(FlutterMethodNotImplemented) |
| 54 | } | 90 | } |
| @@ -64,7 +100,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -64,7 +100,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 64 | 100 | ||
| 65 | let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} | 101 | let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} |
| 66 | var barcodeOptions: BarcodeScannerOptions? = nil | 102 | var barcodeOptions: BarcodeScannerOptions? = nil |
| 67 | - | 103 | + |
| 68 | if (formatList.count != 0) { | 104 | if (formatList.count != 0) { |
| 69 | var barcodeFormats: BarcodeFormat = [] | 105 | var barcodeFormats: BarcodeFormat = [] |
| 70 | for index in formats { | 106 | for index in formats { |
| @@ -123,6 +159,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -123,6 +159,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 123 | result(nil) | 159 | result(nil) |
| 124 | } | 160 | } |
| 125 | 161 | ||
| 162 | + /// Toggles the torch | ||
| 163 | + func updateScanWindow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 164 | + let scanWindowData: Array? = (call.arguments as? [String: Any])?["rect"] as? [CGFloat] | ||
| 165 | + SwiftMobileScannerPlugin.scanWindow = scanWindowData | ||
| 166 | + | ||
| 167 | + result(nil) | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + static func arrayToRect(scanWindowData: [CGFloat]?) -> CGRect? { | ||
| 171 | + if (scanWindowData == nil) { | ||
| 172 | + return nil | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + let minX = scanWindowData![0] | ||
| 176 | + let minY = scanWindowData![1] | ||
| 177 | + | ||
| 178 | + let width = scanWindowData![2] - minX | ||
| 179 | + let height = scanWindowData![3] - minY | ||
| 180 | + | ||
| 181 | + return CGRect(x: minX, y: minY, width: width, height: height) | ||
| 182 | + } | ||
| 183 | + | ||
| 126 | /// Analyzes a single image | 184 | /// Analyzes a single image |
| 127 | private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 185 | private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 128 | let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") | 186 | let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") |
| @@ -145,16 +203,4 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -145,16 +203,4 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 145 | }) | 203 | }) |
| 146 | result(nil) | 204 | result(nil) |
| 147 | } | 205 | } |
| 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 | } | 206 | } |
| @@ -37,6 +37,17 @@ class MobileScannerWebPlugin { | @@ -37,6 +37,17 @@ class MobileScannerWebPlugin { | ||
| 37 | 37 | ||
| 38 | static final html.DivElement vidDiv = html.DivElement(); | 38 | static final html.DivElement vidDiv = html.DivElement(); |
| 39 | 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 | + /// } | ||
| 40 | static WebBarcodeReaderBase barCodeReader = | 51 | static WebBarcodeReaderBase barCodeReader = |
| 41 | ZXingBarcodeReader(videoContainer: vidDiv); | 52 | ZXingBarcodeReader(videoContainer: vidDiv); |
| 42 | StreamSubscription? _barCodeStreamSubscription; | 53 | StreamSubscription? _barCodeStreamSubscription; |
| 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 | +} |
| @@ -34,6 +34,13 @@ class MobileScanner extends StatefulWidget { | @@ -34,6 +34,13 @@ class MobileScanner extends StatefulWidget { | ||
| 34 | /// If this is null, a black [ColoredBox] is used as placeholder. | 34 | /// If this is null, a black [ColoredBox] is used as placeholder. |
| 35 | final Widget Function(BuildContext, Widget?)? placeholderBuilder; | 35 | final Widget Function(BuildContext, Widget?)? placeholderBuilder; |
| 36 | 36 | ||
| 37 | + /// if set barcodes will only be scanned if they fall within this [Rect] | ||
| 38 | + /// useful for having a cut-out overlay for example. these [Rect] | ||
| 39 | + /// coordinates are relative to the widget size, so by how much your | ||
| 40 | + /// rectangle overlays the actual image can depend on things like the | ||
| 41 | + /// [BoxFit] | ||
| 42 | + final Rect? scanWindow; | ||
| 43 | + | ||
| 37 | /// Create a new [MobileScanner] using the provided [controller] | 44 | /// Create a new [MobileScanner] using the provided [controller] |
| 38 | /// and [onBarcodeDetected] callback. | 45 | /// and [onBarcodeDetected] callback. |
| 39 | const MobileScanner({ | 46 | const MobileScanner({ |
| @@ -43,6 +50,7 @@ class MobileScanner extends StatefulWidget { | @@ -43,6 +50,7 @@ class MobileScanner extends StatefulWidget { | ||
| 43 | @Deprecated('Use onScannerStarted() instead.') this.onStart, | 50 | @Deprecated('Use onScannerStarted() instead.') this.onStart, |
| 44 | this.onScannerStarted, | 51 | this.onScannerStarted, |
| 45 | this.placeholderBuilder, | 52 | this.placeholderBuilder, |
| 53 | + this.scanWindow, | ||
| 46 | super.key, | 54 | super.key, |
| 47 | }); | 55 | }); |
| 48 | 56 | ||
| @@ -117,34 +125,102 @@ class _MobileScannerState extends State<MobileScanner> | @@ -117,34 +125,102 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 117 | } | 125 | } |
| 118 | } | 126 | } |
| 119 | 127 | ||
| 128 | + /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible, | ||
| 129 | + /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize] | ||
| 130 | + /// | ||
| 131 | + /// since when using a [BoxFit] the content will always be centered on its parent. we can convert the rect | ||
| 132 | + /// to be relative to the texture. | ||
| 133 | + /// | ||
| 134 | + /// since the textures size and the actuall image (on the texture size) might not be the same, we also need to | ||
| 135 | + /// calculate the scanWindow in terms of percentages of the texture, not pixels. | ||
| 136 | + Rect calculateScanWindowRelativeToTextureInPercentage( | ||
| 137 | + BoxFit fit, | ||
| 138 | + Rect scanWindow, | ||
| 139 | + Size textureSize, | ||
| 140 | + Size widgetSize, | ||
| 141 | + ) { | ||
| 142 | + /// map the texture size to get its new size after fitted to screen | ||
| 143 | + final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize); | ||
| 144 | + | ||
| 145 | + /// create a new rectangle that represents the texture on the screen | ||
| 146 | + final minX = widgetSize.width / 2 - fittedTextureSize.destination.width / 2; | ||
| 147 | + final minY = | ||
| 148 | + widgetSize.height / 2 - fittedTextureSize.destination.height / 2; | ||
| 149 | + final textureWindow = Offset(minX, minY) & fittedTextureSize.destination; | ||
| 150 | + | ||
| 151 | + /// create a new scan window and with only the area of the rect intersecting the texture window | ||
| 152 | + final scanWindowInTexture = scanWindow.intersect(textureWindow); | ||
| 153 | + | ||
| 154 | + /// update the scanWindow left and top to be relative to the texture not the widget | ||
| 155 | + final newLeft = scanWindowInTexture.left - textureWindow.left; | ||
| 156 | + final newTop = scanWindowInTexture.top - textureWindow.top; | ||
| 157 | + final newWidth = scanWindowInTexture.width; | ||
| 158 | + final newHeight = scanWindowInTexture.height; | ||
| 159 | + | ||
| 160 | + /// new scanWindow that is adapted to the boxfit and relative to the texture | ||
| 161 | + final windowInTexture = Rect.fromLTWH(newLeft, newTop, newWidth, newHeight); | ||
| 162 | + | ||
| 163 | + /// get the scanWindow as a percentage of the texture | ||
| 164 | + final percentageLeft = | ||
| 165 | + windowInTexture.left / fittedTextureSize.destination.width; | ||
| 166 | + final percentageTop = | ||
| 167 | + windowInTexture.top / fittedTextureSize.destination.height; | ||
| 168 | + final percentageRight = | ||
| 169 | + windowInTexture.right / fittedTextureSize.destination.width; | ||
| 170 | + final percentagebottom = | ||
| 171 | + windowInTexture.bottom / fittedTextureSize.destination.height; | ||
| 172 | + | ||
| 173 | + /// this rectangle can be send to native code and used to cut out a rectangle of the scan image | ||
| 174 | + return Rect.fromLTRB( | ||
| 175 | + percentageLeft, | ||
| 176 | + percentageTop, | ||
| 177 | + percentageRight, | ||
| 178 | + percentagebottom, | ||
| 179 | + ); | ||
| 180 | + } | ||
| 181 | + | ||
| 120 | @override | 182 | @override |
| 121 | Widget build(BuildContext context) { | 183 | Widget build(BuildContext context) { |
| 122 | - return ValueListenableBuilder<MobileScannerArguments?>( | ||
| 123 | - valueListenable: _controller.startArguments, | ||
| 124 | - builder: (context, value, child) { | ||
| 125 | - if (value == null) { | ||
| 126 | - return widget.placeholderBuilder?.call(context, child) ?? | ||
| 127 | - const ColoredBox(color: Colors.black); | ||
| 128 | - } | ||
| 129 | - | ||
| 130 | - return ClipRect( | ||
| 131 | - child: LayoutBuilder( | ||
| 132 | - builder: (_, constraints) { | ||
| 133 | - return SizedBox.fromSize( | ||
| 134 | - size: constraints.biggest, | ||
| 135 | - child: FittedBox( | ||
| 136 | - fit: widget.fit, | ||
| 137 | - child: SizedBox( | ||
| 138 | - width: value.size.width, | ||
| 139 | - height: value.size.height, | ||
| 140 | - child: kIsWeb | ||
| 141 | - ? HtmlElementView(viewType: value.webId!) | ||
| 142 | - : Texture(textureId: value.textureId!), | ||
| 143 | - ), | ||
| 144 | - ), | 184 | + return LayoutBuilder( |
| 185 | + builder: (context, constraints) { | ||
| 186 | + return ValueListenableBuilder<MobileScannerArguments?>( | ||
| 187 | + valueListenable: _controller.startArguments, | ||
| 188 | + builder: (context, value, child) { | ||
| 189 | + if (value == null) { | ||
| 190 | + return widget.placeholderBuilder?.call(context, child) ?? | ||
| 191 | + const ColoredBox(color: Colors.black); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + if (widget.scanWindow != null) { | ||
| 195 | + final window = calculateScanWindowRelativeToTextureInPercentage( | ||
| 196 | + widget.fit, | ||
| 197 | + widget.scanWindow!, | ||
| 198 | + value.size, | ||
| 199 | + Size(constraints.maxWidth, constraints.maxHeight), | ||
| 145 | ); | 200 | ); |
| 146 | - }, | ||
| 147 | - ), | 201 | + _controller.updateScanWindow(window); |
| 202 | + } | ||
| 203 | + | ||
| 204 | + return ClipRect( | ||
| 205 | + child: LayoutBuilder( | ||
| 206 | + builder: (_, constraints) { | ||
| 207 | + return SizedBox.fromSize( | ||
| 208 | + size: constraints.biggest, | ||
| 209 | + child: FittedBox( | ||
| 210 | + fit: widget.fit, | ||
| 211 | + child: SizedBox( | ||
| 212 | + width: value.size.width, | ||
| 213 | + height: value.size.height, | ||
| 214 | + child: kIsWeb | ||
| 215 | + ? HtmlElementView(viewType: value.webId!) | ||
| 216 | + : Texture(textureId: value.textureId!), | ||
| 217 | + ), | ||
| 218 | + ), | ||
| 219 | + ); | ||
| 220 | + }, | ||
| 221 | + ), | ||
| 222 | + ); | ||
| 223 | + }, | ||
| 148 | ); | 224 | ); |
| 149 | }, | 225 | }, |
| 150 | ); | 226 | ); |
| @@ -126,6 +126,15 @@ class MobileScannerController { | @@ -126,6 +126,15 @@ class MobileScannerController { | ||
| 126 | arguments['speed'] = detectionSpeed.index; | 126 | arguments['speed'] = detectionSpeed.index; |
| 127 | arguments['timeout'] = detectionTimeoutMs; | 127 | arguments['timeout'] = detectionTimeoutMs; |
| 128 | 128 | ||
| 129 | + /* if (scanWindow != null) { | ||
| 130 | + arguments['scanWindow'] = [ | ||
| 131 | + scanWindow!.left, | ||
| 132 | + scanWindow!.top, | ||
| 133 | + scanWindow!.right, | ||
| 134 | + scanWindow!.bottom, | ||
| 135 | + ]; | ||
| 136 | + } */ | ||
| 137 | + | ||
| 129 | if (formats != null) { | 138 | if (formats != null) { |
| 130 | if (kIsWeb || 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(); |
| @@ -317,6 +326,8 @@ class MobileScannerController { | @@ -317,6 +326,8 @@ class MobileScannerController { | ||
| 317 | BarcodeCapture( | 326 | BarcodeCapture( |
| 318 | barcodes: parsed, | 327 | barcodes: parsed, |
| 319 | image: event['image'] as Uint8List?, | 328 | image: event['image'] as Uint8List?, |
| 329 | + width: event['width'] as double?, | ||
| 330 | + height: event['height'] as double?, | ||
| 320 | ), | 331 | ), |
| 321 | ); | 332 | ); |
| 322 | break; | 333 | break; |
| @@ -355,4 +366,10 @@ class MobileScannerController { | @@ -355,4 +366,10 @@ class MobileScannerController { | ||
| 355 | throw UnimplementedError(name as String?); | 366 | throw UnimplementedError(name as String?); |
| 356 | } | 367 | } |
| 357 | } | 368 | } |
| 369 | + | ||
| 370 | + /// updates the native scanwindow | ||
| 371 | + Future<void> updateScanWindow(Rect window) async { | ||
| 372 | + final data = [window.left, window.top, window.right, window.bottom]; | ||
| 373 | + await _methodChannel.invokeMethod('updateScanWindow', {'rect': data}); | ||
| 374 | + } | ||
| 358 | } | 375 | } |
| @@ -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 | } |
| @@ -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}); |
| @@ -168,6 +168,10 @@ extension JsZXingBrowserMultiFormatReaderExt | @@ -168,6 +168,10 @@ extension JsZXingBrowserMultiFormatReaderExt | ||
| 168 | external MediaStream? stream; | 168 | external MediaStream? stream; |
| 169 | } | 169 | } |
| 170 | 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> | ||
| 171 | class ZXingBarcodeReader extends WebBarcodeReaderBase | 175 | class ZXingBarcodeReader extends WebBarcodeReaderBase |
| 172 | with InternalStreamCreation, InternalTorchDetection { | 176 | with InternalStreamCreation, InternalTorchDetection { |
| 173 | JsZXingBrowserMultiFormatReader? _reader; | 177 | JsZXingBrowserMultiFormatReader? _reader; |
| 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