Committed by
GitHub
Merge pull request #176 from casvanluijtelaar/master
Add scanWindow to optionally limit scan area
Showing
10 changed files
with
488 additions
and
36 deletions
| @@ -4,9 +4,12 @@ import android.Manifest | @@ -4,9 +4,12 @@ import android.Manifest | ||
| 4 | import android.app.Activity | 4 | import android.app.Activity |
| 5 | import android.content.pm.PackageManager | 5 | import android.content.pm.PackageManager |
| 6 | import android.graphics.Point | 6 | import android.graphics.Point |
| 7 | +import android.graphics.Rect | ||
| 8 | +import android.graphics.RectF | ||
| 7 | import android.net.Uri | 9 | import android.net.Uri |
| 8 | import android.util.Log | 10 | import android.util.Log |
| 9 | import android.util.Size | 11 | import android.util.Size |
| 12 | +import android.media.Image | ||
| 10 | import android.view.Surface | 13 | import android.view.Surface |
| 11 | import androidx.annotation.NonNull | 14 | import androidx.annotation.NonNull |
| 12 | import androidx.camera.core.* | 15 | import androidx.camera.core.* |
| @@ -18,12 +21,14 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions | @@ -18,12 +21,14 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions | ||
| 18 | import com.google.mlkit.vision.barcode.BarcodeScanning | 21 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 19 | import com.google.mlkit.vision.barcode.common.Barcode | 22 | import com.google.mlkit.vision.barcode.common.Barcode |
| 20 | import com.google.mlkit.vision.common.InputImage | 23 | import com.google.mlkit.vision.common.InputImage |
| 24 | +import com.google.mlkit.vision.common.InputImage.IMAGE_FORMAT_NV21 | ||
| 21 | import io.flutter.plugin.common.EventChannel | 25 | import io.flutter.plugin.common.EventChannel |
| 22 | import io.flutter.plugin.common.MethodCall | 26 | import io.flutter.plugin.common.MethodCall |
| 23 | import io.flutter.plugin.common.MethodChannel | 27 | import io.flutter.plugin.common.MethodChannel |
| 24 | import io.flutter.plugin.common.PluginRegistry | 28 | import io.flutter.plugin.common.PluginRegistry |
| 25 | import io.flutter.view.TextureRegistry | 29 | import io.flutter.view.TextureRegistry |
| 26 | import java.io.File | 30 | import java.io.File |
| 31 | +import kotlin.math.roundToInt | ||
| 27 | 32 | ||
| 28 | 33 | ||
| 29 | class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) | 34 | class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) |
| @@ -40,6 +45,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -40,6 +45,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 40 | private var camera: Camera? = null | 45 | private var camera: Camera? = null |
| 41 | private var preview: Preview? = null | 46 | private var preview: Preview? = null |
| 42 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null | 47 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null |
| 48 | + private var scanWindow: List<Float>? = null; | ||
| 43 | 49 | ||
| 44 | // @AnalyzeMode | 50 | // @AnalyzeMode |
| 45 | // private var analyzeMode: Int = AnalyzeMode.NONE | 51 | // private var analyzeMode: Int = AnalyzeMode.NONE |
| @@ -54,6 +60,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -54,6 +60,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 54 | // "analyze" -> switchAnalyzeMode(call, result) | 60 | // "analyze" -> switchAnalyzeMode(call, result) |
| 55 | "stop" -> stop(result) | 61 | "stop" -> stop(result) |
| 56 | "analyzeImage" -> analyzeImage(call, result) | 62 | "analyzeImage" -> analyzeImage(call, result) |
| 63 | + "updateScanWindow" -> updateScanWindow(call) | ||
| 57 | else -> result.notImplemented() | 64 | else -> result.notImplemented() |
| 58 | } | 65 | } |
| 59 | } | 66 | } |
| @@ -99,11 +106,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -99,11 +106,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 99 | // when (analyzeMode) { | 106 | // when (analyzeMode) { |
| 100 | // AnalyzeMode.BARCODE -> { | 107 | // AnalyzeMode.BARCODE -> { |
| 101 | val mediaImage = imageProxy.image ?: return@Analyzer | 108 | val mediaImage = imageProxy.image ?: return@Analyzer |
| 102 | - val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | 109 | + var inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) |
| 103 | 110 | ||
| 104 | scanner.process(inputImage) | 111 | scanner.process(inputImage) |
| 105 | .addOnSuccessListener { barcodes -> | 112 | .addOnSuccessListener { barcodes -> |
| 106 | for (barcode in barcodes) { | 113 | for (barcode in barcodes) { |
| 114 | + | ||
| 115 | + if(scanWindow != null) { | ||
| 116 | + val match = isbarCodeInScanWindow(scanWindow!!, barcode, inputImage) | ||
| 117 | + if(!match) continue | ||
| 118 | + } | ||
| 119 | + | ||
| 107 | val event = mapOf("name" to "barcode", "data" to barcode.data) | 120 | val event = mapOf("name" to "barcode", "data" to barcode.data) |
| 108 | sink?.success(event) | 121 | sink?.success(event) |
| 109 | } | 122 | } |
| @@ -115,9 +128,32 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -115,9 +128,32 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 115 | // } | 128 | // } |
| 116 | } | 129 | } |
| 117 | 130 | ||
| 118 | - | ||
| 119 | private var scanner = BarcodeScanning.getClient() | 131 | private var scanner = BarcodeScanning.getClient() |
| 120 | 132 | ||
| 133 | + private fun updateScanWindow(call: MethodCall) { | ||
| 134 | + scanWindow = call.argument<List<Float>>("rect") | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + // scales the scanWindow to the provided inputImage and checks if that scaled | ||
| 138 | + // scanWindow contains the barcode | ||
| 139 | + private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: InputImage): Boolean { | ||
| 140 | + val barcodeBoundingBox = barcode.getBoundingBox() | ||
| 141 | + if(barcodeBoundingBox == null) return false | ||
| 142 | + | ||
| 143 | + val imageWidth = inputImage.getWidth(); | ||
| 144 | + val imageHeight = inputImage.getHeight(); | ||
| 145 | + | ||
| 146 | + val left = (scanWindow[0] * imageWidth).roundToInt() | ||
| 147 | + val top = (scanWindow[1] * imageHeight).roundToInt() | ||
| 148 | + val right = (scanWindow[2] * imageWidth).roundToInt() | ||
| 149 | + val bottom = (scanWindow[3] * imageHeight).roundToInt() | ||
| 150 | + | ||
| 151 | + val scaledScanWindow = Rect(left, top, right, bottom) | ||
| 152 | + return scaledScanWindow.contains(barcodeBoundingBox) | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + | ||
| 156 | + | ||
| 121 | @ExperimentalGetImage | 157 | @ExperimentalGetImage |
| 122 | private fun start(call: MethodCall, result: MethodChannel.Result) { | 158 | private fun start(call: MethodCall, result: MethodChannel.Result) { |
| 123 | if (camera?.cameraInfo != null && preview != null && textureEntry != null) { | 159 | if (camera?.cameraInfo != null && preview != null && textureEntry != null) { |
| @@ -130,7 +166,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -130,7 +166,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 130 | result.success(answer) | 166 | result.success(answer) |
| 131 | } else { | 167 | } else { |
| 132 | val facing: Int = call.argument<Int>("facing") ?: 0 | 168 | val facing: Int = call.argument<Int>("facing") ?: 0 |
| 133 | - val ratio: Int? = call.argument<Int>("ratio") | 169 | + val ratio: Int = call.argument<Int>("ratio") ?: 1 |
| 134 | val torch: Boolean = call.argument<Boolean>("torch") ?: false | 170 | val torch: Boolean = call.argument<Boolean>("torch") ?: false |
| 135 | val formats: List<Int>? = call.argument<List<Int>>("formats") | 171 | val formats: List<Int>? = call.argument<List<Int>>("formats") |
| 136 | 172 | ||
| @@ -161,6 +197,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -161,6 +197,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 161 | result.error("textureEntry", "textureEntry is null", null) | 197 | result.error("textureEntry", "textureEntry is null", null) |
| 162 | return@addListener | 198 | return@addListener |
| 163 | } | 199 | } |
| 200 | + | ||
| 164 | // Preview | 201 | // Preview |
| 165 | val surfaceProvider = Preview.SurfaceProvider { request -> | 202 | val surfaceProvider = Preview.SurfaceProvider { request -> |
| 166 | val texture = textureEntry!!.surfaceTexture() | 203 | val texture = textureEntry!!.surfaceTexture() |
| @@ -171,17 +208,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -171,17 +208,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 171 | 208 | ||
| 172 | // Build the preview to be shown on the Flutter texture | 209 | // Build the preview to be shown on the Flutter texture |
| 173 | val previewBuilder = Preview.Builder() | 210 | val previewBuilder = Preview.Builder() |
| 174 | - if (ratio != null) { | ||
| 175 | - previewBuilder.setTargetAspectRatio(ratio) | ||
| 176 | - } | 211 | + .setTargetAspectRatio(ratio) |
| 212 | + | ||
| 177 | preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } | 213 | preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } |
| 178 | 214 | ||
| 179 | // Build the analyzer to be passed on to MLKit | 215 | // Build the analyzer to be passed on to MLKit |
| 180 | val analysisBuilder = ImageAnalysis.Builder() | 216 | val analysisBuilder = ImageAnalysis.Builder() |
| 181 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | 217 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) |
| 182 | - if (ratio != null) { | ||
| 183 | - analysisBuilder.setTargetAspectRatio(ratio) | ||
| 184 | - } | 218 | + .setTargetAspectRatio(ratio) |
| 219 | + | ||
| 185 | val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } | 220 | val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } |
| 186 | 221 | ||
| 187 | // Select the correct camera | 222 | // Select the correct camera |
| @@ -191,6 +226,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -191,6 +226,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 191 | 226 | ||
| 192 | val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) | 227 | val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) |
| 193 | val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) | 228 | val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) |
| 229 | + | ||
| 194 | Log.i("LOG", "Analyzer: $analysisSize") | 230 | Log.i("LOG", "Analyzer: $analysisSize") |
| 195 | Log.i("LOG", "Preview: $previewSize") | 231 | Log.i("LOG", "Preview: $previewSize") |
| 196 | 232 | ||
| @@ -241,6 +277,11 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -241,6 +277,11 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 241 | scanner.process(inputImage) | 277 | scanner.process(inputImage) |
| 242 | .addOnSuccessListener { barcodes -> | 278 | .addOnSuccessListener { barcodes -> |
| 243 | for (barcode in barcodes) { | 279 | for (barcode in barcodes) { |
| 280 | + if(scanWindow != null) { | ||
| 281 | + val match = isbarCodeInScanWindow(scanWindow!!, barcode, inputImage) | ||
| 282 | + if(!match) continue | ||
| 283 | + } | ||
| 284 | + | ||
| 244 | barcodeFound = true | 285 | barcodeFound = true |
| 245 | sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) | 286 | sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) |
| 246 | } | 287 | } |
| @@ -354,16 +354,19 @@ | @@ -354,16 +354,19 @@ | ||
| 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 = QAJQ4586J2; |
| 359 | ENABLE_BITCODE = NO; | 361 | ENABLE_BITCODE = NO; |
| 360 | INFOPLIST_FILE = Runner/Info.plist; | 362 | INFOPLIST_FILE = Runner/Info.plist; |
| 361 | LD_RUNPATH_SEARCH_PATHS = ( | 363 | LD_RUNPATH_SEARCH_PATHS = ( |
| 362 | "$(inherited)", | 364 | "$(inherited)", |
| 363 | "@executable_path/Frameworks", | 365 | "@executable_path/Frameworks", |
| 364 | ); | 366 | ); |
| 365 | - PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScannerExample; | 367 | + PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; |
| 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,16 +486,19 @@ | @@ -483,16 +486,19 @@ | ||
| 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 = QAJQ4586J2; |
| 488 | ENABLE_BITCODE = NO; | 493 | ENABLE_BITCODE = NO; |
| 489 | INFOPLIST_FILE = Runner/Info.plist; | 494 | INFOPLIST_FILE = Runner/Info.plist; |
| 490 | LD_RUNPATH_SEARCH_PATHS = ( | 495 | LD_RUNPATH_SEARCH_PATHS = ( |
| 491 | "$(inherited)", | 496 | "$(inherited)", |
| 492 | "@executable_path/Frameworks", | 497 | "@executable_path/Frameworks", |
| 493 | ); | 498 | ); |
| 494 | - PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScannerExample; | 499 | + PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; |
| 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,16 +512,19 @@ | @@ -506,16 +512,19 @@ | ||
| 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 = QAJQ4586J2; |
| 511 | ENABLE_BITCODE = NO; | 519 | ENABLE_BITCODE = NO; |
| 512 | INFOPLIST_FILE = Runner/Info.plist; | 520 | INFOPLIST_FILE = Runner/Info.plist; |
| 513 | LD_RUNPATH_SEARCH_PATHS = ( | 521 | LD_RUNPATH_SEARCH_PATHS = ( |
| 514 | "$(inherited)", | 522 | "$(inherited)", |
| 515 | "@executable_path/Frameworks", | 523 | "@executable_path/Frameworks", |
| 516 | ); | 524 | ); |
| 517 | - PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScannerExample; | 525 | + PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; |
| 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"; |
| @@ -2,10 +2,8 @@ | @@ -2,10 +2,8 @@ | ||
| 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 3 | <plist version="1.0"> | 3 | <plist version="1.0"> |
| 4 | <dict> | 4 | <dict> |
| 5 | - <key>NSPhotoLibraryUsageDescription</key> | ||
| 6 | - <string>We need access in order to open photos of barcodes</string> | ||
| 7 | - <key>NSCameraUsageDescription</key> | ||
| 8 | - <string>We use the camera to scan barcodes</string> | 5 | + <key>CADisableMinimumFrameDurationOnPhone</key> |
| 6 | + <true/> | ||
| 9 | <key>CFBundleDevelopmentRegion</key> | 7 | <key>CFBundleDevelopmentRegion</key> |
| 10 | <string>$(DEVELOPMENT_LANGUAGE)</string> | 8 | <string>$(DEVELOPMENT_LANGUAGE)</string> |
| 11 | <key>CFBundleDisplayName</key> | 9 | <key>CFBundleDisplayName</key> |
| @@ -28,6 +26,10 @@ | @@ -28,6 +26,10 @@ | ||
| 28 | <string>$(FLUTTER_BUILD_NUMBER)</string> | 26 | <string>$(FLUTTER_BUILD_NUMBER)</string> |
| 29 | <key>LSRequiresIPhoneOS</key> | 27 | <key>LSRequiresIPhoneOS</key> |
| 30 | <true/> | 28 | <true/> |
| 29 | + <key>NSCameraUsageDescription</key> | ||
| 30 | + <string>We use the camera to scan barcodes</string> | ||
| 31 | + <key>NSPhotoLibraryUsageDescription</key> | ||
| 32 | + <string>We need access in order to open photos of barcodes</string> | ||
| 31 | <key>UILaunchStoryboardName</key> | 33 | <key>UILaunchStoryboardName</key> |
| 32 | <string>LaunchScreen</string> | 34 | <string>LaunchScreen</string> |
| 33 | <key>UIMainStoryboardFile</key> | 35 | <key>UIMainStoryboardFile</key> |
| @@ -47,7 +49,5 @@ | @@ -47,7 +49,5 @@ | ||
| 47 | </array> | 49 | </array> |
| 48 | <key>UIViewControllerBasedStatusBarAppearance</key> | 50 | <key>UIViewControllerBasedStatusBarAppearance</key> |
| 49 | <false/> | 51 | <false/> |
| 50 | - <key>CADisableMinimumFrameDurationOnPhone</key> | ||
| 51 | - <true/> | ||
| 52 | </dict> | 52 | </dict> |
| 53 | </plist> | 53 | </plist> |
example/lib/barcode_scanner_window.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 3 | + | ||
| 4 | +class BarcodeScannerWithScanWindow extends StatefulWidget { | ||
| 5 | + const BarcodeScannerWithScanWindow({Key? key}) : super(key: key); | ||
| 6 | + | ||
| 7 | + @override | ||
| 8 | + _BarcodeScannerWithScanWindowState createState() => | ||
| 9 | + _BarcodeScannerWithScanWindowState(); | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +class _BarcodeScannerWithScanWindowState | ||
| 13 | + extends State<BarcodeScannerWithScanWindow> { | ||
| 14 | + late MobileScannerController controller; | ||
| 15 | + String? barcode; | ||
| 16 | + | ||
| 17 | + @override | ||
| 18 | + void initState() { | ||
| 19 | + super.initState(); | ||
| 20 | + controller = MobileScannerController(); | ||
| 21 | + restart(); | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + Future<void> restart() async { | ||
| 25 | + // await controller.stop(); | ||
| 26 | + await controller.start(); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + Future<void> onDetect(Barcode barcode, MobileScannerArguments? _) async { | ||
| 30 | + setState(() => this.barcode = barcode.rawValue); | ||
| 31 | + await Future.delayed(const Duration(seconds: 1)); | ||
| 32 | + setState(() => this.barcode = ''); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + @override | ||
| 36 | + Widget build(BuildContext context) { | ||
| 37 | + final scanWindow = Rect.fromCenter( | ||
| 38 | + center: MediaQuery.of(context).size.center(Offset.zero), | ||
| 39 | + width: 200, | ||
| 40 | + height: 200, | ||
| 41 | + ); | ||
| 42 | + | ||
| 43 | + return Scaffold( | ||
| 44 | + backgroundColor: Colors.black, | ||
| 45 | + body: Builder( | ||
| 46 | + builder: (context) { | ||
| 47 | + return Stack( | ||
| 48 | + children: [ | ||
| 49 | + MobileScanner( | ||
| 50 | + fit: BoxFit.cover, | ||
| 51 | + scanWindow: scanWindow, | ||
| 52 | + controller: controller, | ||
| 53 | + onDetect: onDetect, | ||
| 54 | + allowDuplicates: true, | ||
| 55 | + ), | ||
| 56 | + CustomPaint( | ||
| 57 | + painter: ScannerOverlay(scanWindow), | ||
| 58 | + ), | ||
| 59 | + Align( | ||
| 60 | + alignment: Alignment.bottomCenter, | ||
| 61 | + child: Container( | ||
| 62 | + alignment: Alignment.bottomCenter, | ||
| 63 | + height: 100, | ||
| 64 | + color: Colors.black.withOpacity(0.4), | ||
| 65 | + child: Row( | ||
| 66 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 67 | + children: [ | ||
| 68 | + Center( | ||
| 69 | + child: SizedBox( | ||
| 70 | + width: MediaQuery.of(context).size.width - 120, | ||
| 71 | + height: 50, | ||
| 72 | + child: FittedBox( | ||
| 73 | + child: Text( | ||
| 74 | + barcode ?? 'Scan something!', | ||
| 75 | + overflow: TextOverflow.fade, | ||
| 76 | + style: Theme.of(context) | ||
| 77 | + .textTheme | ||
| 78 | + .headline4! | ||
| 79 | + .copyWith(color: Colors.white), | ||
| 80 | + ), | ||
| 81 | + ), | ||
| 82 | + ), | ||
| 83 | + ), | ||
| 84 | + ], | ||
| 85 | + ), | ||
| 86 | + ), | ||
| 87 | + ), | ||
| 88 | + ], | ||
| 89 | + ); | ||
| 90 | + }, | ||
| 91 | + ), | ||
| 92 | + ); | ||
| 93 | + } | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +class ScannerOverlay extends CustomPainter { | ||
| 97 | + ScannerOverlay(this.scanWindow); | ||
| 98 | + | ||
| 99 | + final Rect scanWindow; | ||
| 100 | + | ||
| 101 | + @override | ||
| 102 | + void paint(Canvas canvas, Size size) { | ||
| 103 | + final backgroundPath = Path()..addRect(Rect.largest); | ||
| 104 | + final cutoutPath = Path()..addRect(scanWindow); | ||
| 105 | + | ||
| 106 | + final backgroundPaint = Paint() | ||
| 107 | + ..color = Colors.black.withOpacity(0.5) | ||
| 108 | + ..style = PaintingStyle.fill | ||
| 109 | + ..blendMode = BlendMode.dstOut; | ||
| 110 | + | ||
| 111 | + final backgroundWithCutout = Path.combine( | ||
| 112 | + PathOperation.difference, | ||
| 113 | + backgroundPath, | ||
| 114 | + cutoutPath, | ||
| 115 | + ); | ||
| 116 | + canvas.drawPath(backgroundWithCutout, backgroundPaint); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + @override | ||
| 120 | + bool shouldRepaint(covariant CustomPainter oldDelegate) { | ||
| 121 | + return false; | ||
| 122 | + } | ||
| 123 | +} |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; | 2 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; |
| 3 | +import 'package:mobile_scanner_example/barcode_scanner_window.dart'; | ||
| 3 | import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; | 4 | import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; |
| 4 | 5 | ||
| 5 | void main() => runApp(const MaterialApp(home: MyHome())); | 6 | void main() => runApp(const MaterialApp(home: MyHome())); |
| @@ -31,6 +32,16 @@ class MyHome extends StatelessWidget { | @@ -31,6 +32,16 @@ class MyHome extends StatelessWidget { | ||
| 31 | onPressed: () { | 32 | onPressed: () { |
| 32 | Navigator.of(context).push( | 33 | Navigator.of(context).push( |
| 33 | MaterialPageRoute( | 34 | MaterialPageRoute( |
| 35 | + builder: (context) => const BarcodeScannerWithScanWindow(), | ||
| 36 | + ), | ||
| 37 | + ); | ||
| 38 | + }, | ||
| 39 | + child: const Text('MobileScanner with ScanWindow'), | ||
| 40 | + ), | ||
| 41 | + ElevatedButton( | ||
| 42 | + onPressed: () { | ||
| 43 | + Navigator.of(context).push( | ||
| 44 | + MaterialPageRoute( | ||
| 34 | builder: (context) => | 45 | builder: (context) => |
| 35 | const BarcodeScannerWithoutController(), | 46 | const BarcodeScannerWithoutController(), |
| 36 | ), | 47 | ), |
| @@ -2,6 +2,7 @@ import AVFoundation | @@ -2,6 +2,7 @@ import AVFoundation | ||
| 2 | import Flutter | 2 | import Flutter |
| 3 | import MLKitVision | 3 | import MLKitVision |
| 4 | import MLKitBarcodeScanning | 4 | import MLKitBarcodeScanning |
| 5 | +import UIKit | ||
| 5 | 6 | ||
| 6 | public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate { | 7 | public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate { |
| 7 | 8 | ||
| @@ -25,6 +26,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -25,6 +26,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 25 | // var analyzeMode: Int = 0 | 26 | // var analyzeMode: Int = 0 |
| 26 | var analyzing: Bool = false | 27 | var analyzing: Bool = false |
| 27 | var position = AVCaptureDevice.Position.back | 28 | var position = AVCaptureDevice.Position.back |
| 29 | + | ||
| 30 | + var scanWindow: CGRect? | ||
| 28 | 31 | ||
| 29 | var scanner = BarcodeScanner.barcodeScanner() | 32 | var scanner = BarcodeScanner.barcodeScanner() |
| 30 | 33 | ||
| @@ -61,6 +64,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -61,6 +64,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 61 | stop(result) | 64 | stop(result) |
| 62 | case "analyzeImage": | 65 | case "analyzeImage": |
| 63 | analyzeImage(call, result) | 66 | analyzeImage(call, result) |
| 67 | + case "updateScanWindow": | ||
| 68 | + updateScanWindow(call) | ||
| 64 | default: | 69 | default: |
| 65 | result(FlutterMethodNotImplemented) | 70 | result(FlutterMethodNotImplemented) |
| 66 | } | 71 | } |
| @@ -98,8 +103,9 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -98,8 +103,9 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 98 | return | 103 | return |
| 99 | } | 104 | } |
| 100 | analyzing = true | 105 | analyzing = true |
| 101 | - let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) | ||
| 102 | - let image = VisionImage(image: buffer!.image) | 106 | + let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) |
| 107 | + var image = VisionImage(image: buffer!.image) | ||
| 108 | + | ||
| 103 | image.orientation = imageOrientation( | 109 | image.orientation = imageOrientation( |
| 104 | deviceOrientation: UIDevice.current.orientation, | 110 | deviceOrientation: UIDevice.current.orientation, |
| 105 | defaultOrientation: .portrait | 111 | defaultOrientation: .portrait |
| @@ -108,6 +114,14 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -108,6 +114,14 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 108 | scanner.process(image) { [self] barcodes, error in | 114 | scanner.process(image) { [self] barcodes, error in |
| 109 | if error == nil && barcodes != nil { | 115 | if error == nil && barcodes != nil { |
| 110 | for barcode in barcodes! { | 116 | for barcode in barcodes! { |
| 117 | + | ||
| 118 | + if scanWindow != nil { | ||
| 119 | + let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image) | ||
| 120 | + if (!match) { | ||
| 121 | + continue | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | + | ||
| 111 | let event: [String: Any?] = ["name": "barcode", "data": barcode.data] | 125 | let event: [String: Any?] = ["name": "barcode", "data": barcode.data] |
| 112 | sink?(event) | 126 | sink?(event) |
| 113 | } | 127 | } |
| @@ -155,6 +169,38 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -155,6 +169,38 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 155 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | 169 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) |
| 156 | } | 170 | } |
| 157 | 171 | ||
| 172 | + func updateScanWindow(_ call: FlutterMethodCall) { | ||
| 173 | + let argReader = MapArgumentReader(call.arguments as? [String: Any]) | ||
| 174 | + let scanWindowData: Array? = argReader.floatArray(key: "rect") | ||
| 175 | + | ||
| 176 | + if (scanWindowData == nil) { | ||
| 177 | + return | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + let minX = scanWindowData![0] | ||
| 181 | + let minY = scanWindowData![1] | ||
| 182 | + | ||
| 183 | + let width = scanWindowData![2] - minX | ||
| 184 | + let height = scanWindowData![3] - minY | ||
| 185 | + | ||
| 186 | + scanWindow = CGRect(x: minX, y: minY, width: width, height: height) | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: Barcode, _ inputImage: UIImage) -> Bool { | ||
| 190 | + let barcodeBoundingBox = barcode.frame | ||
| 191 | + | ||
| 192 | + let imageWidth = inputImage.size.width; | ||
| 193 | + let imageHeight = inputImage.size.height; | ||
| 194 | + | ||
| 195 | + let minX = scanWindow.minX * imageWidth | ||
| 196 | + let minY = scanWindow.minY * imageHeight | ||
| 197 | + let width = scanWindow.width * imageWidth | ||
| 198 | + let height = scanWindow.height * imageHeight | ||
| 199 | + | ||
| 200 | + let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height) | ||
| 201 | + return scaledScanWindow.contains(barcodeBoundingBox) | ||
| 202 | + } | ||
| 203 | + | ||
| 158 | func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 204 | func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 159 | if (device != nil) { | 205 | if (device != nil) { |
| 160 | result(FlutterError(code: "MobileScanner", | 206 | result(FlutterError(code: "MobileScanner", |
| @@ -172,7 +218,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -172,7 +218,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 172 | let torch: Bool = argReader.bool(key: "torch") ?? false | 218 | let torch: Bool = argReader.bool(key: "torch") ?? false |
| 173 | let facing: Int = argReader.int(key: "facing") ?? 1 | 219 | let facing: Int = argReader.int(key: "facing") ?? 1 |
| 174 | let formats: Array = argReader.intArray(key: "formats") ?? [] | 220 | let formats: Array = argReader.intArray(key: "formats") ?? [] |
| 175 | - | 221 | + |
| 176 | let formatList: NSMutableArray = [] | 222 | let formatList: NSMutableArray = [] |
| 177 | for index in formats { | 223 | for index in formats { |
| 178 | formatList.add(BarcodeFormat(rawValue: index)) | 224 | formatList.add(BarcodeFormat(rawValue: index)) |
| @@ -229,6 +275,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -229,6 +275,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 229 | videoOutput.alwaysDiscardsLateVideoFrames = true | 275 | videoOutput.alwaysDiscardsLateVideoFrames = true |
| 230 | 276 | ||
| 231 | videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) | 277 | videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) |
| 278 | + | ||
| 232 | captureSession.addOutput(videoOutput) | 279 | captureSession.addOutput(videoOutput) |
| 233 | for connection in videoOutput.connections { | 280 | for connection in videoOutput.connections { |
| 234 | connection.videoOrientation = .portrait | 281 | connection.videoOrientation = .portrait |
| @@ -238,6 +285,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -238,6 +285,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 238 | } | 285 | } |
| 239 | captureSession.commitConfiguration() | 286 | captureSession.commitConfiguration() |
| 240 | captureSession.startRunning() | 287 | captureSession.startRunning() |
| 288 | + | ||
| 241 | let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | 289 | let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) |
| 242 | let width = Double(demensions.height) | 290 | let width = Double(demensions.height) |
| 243 | let height = Double(demensions.width) | 291 | let height = Double(demensions.width) |
| @@ -289,6 +337,14 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -289,6 +337,14 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 289 | scanner.process(image) { [self] barcodes, error in | 337 | scanner.process(image) { [self] barcodes, error in |
| 290 | if error == nil && barcodes != nil { | 338 | if error == nil && barcodes != nil { |
| 291 | for barcode in barcodes! { | 339 | for barcode in barcodes! { |
| 340 | + | ||
| 341 | + if scanWindow != nil { | ||
| 342 | + let match = isbarCodeInScanWindow(scanWindow!, barcode, uiImage!) | ||
| 343 | + if (!match) { | ||
| 344 | + continue | ||
| 345 | + } | ||
| 346 | + } | ||
| 347 | + | ||
| 292 | barcodeFound = true | 348 | barcodeFound = true |
| 293 | let event: [String: Any?] = ["name": "barcode", "data": barcode.data] | 349 | let event: [String: Any?] = ["name": "barcode", "data": barcode.data] |
| 294 | sink?(event) | 350 | sink?(event) |
| @@ -368,8 +424,12 @@ class MapArgumentReader { | @@ -368,8 +424,12 @@ class MapArgumentReader { | ||
| 368 | return args?[key] as? [String] | 424 | return args?[key] as? [String] |
| 369 | } | 425 | } |
| 370 | 426 | ||
| 371 | - func intArray(key: String) -> [Int]? { | ||
| 372 | - return args?[key] as? [Int] | ||
| 373 | - } | 427 | + func intArray(key: String) -> [Int]? { |
| 428 | + return args?[key] as? [Int] | ||
| 429 | + } | ||
| 430 | + | ||
| 431 | + func floatArray(key: String) -> [CGFloat]? { | ||
| 432 | + return args?[key] as? [CGFloat] | ||
| 433 | + } | ||
| 374 | 434 | ||
| 375 | } | 435 | } |
| 1 | import 'package:flutter/foundation.dart'; | 1 | import 'package:flutter/foundation.dart'; |
| 2 | -import 'package:flutter/material.dart'; | 2 | +import 'package:flutter/material.dart' hide applyBoxFit; |
| 3 | import 'package:mobile_scanner/mobile_scanner.dart'; | 3 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 4 | +import 'package:mobile_scanner/src/objects/barcode_utility.dart'; | ||
| 4 | 5 | ||
| 5 | enum Ratio { ratio_4_3, ratio_16_9 } | 6 | enum Ratio { ratio_4_3, ratio_16_9 } |
| 6 | 7 | ||
| @@ -27,6 +28,13 @@ class MobileScanner extends StatefulWidget { | @@ -27,6 +28,13 @@ class MobileScanner extends StatefulWidget { | ||
| 27 | /// Set to false if you don't want duplicate scans. | 28 | /// Set to false if you don't want duplicate scans. |
| 28 | final bool allowDuplicates; | 29 | final bool allowDuplicates; |
| 29 | 30 | ||
| 31 | + /// if set barcodes will only be scanned if they fall within this [Rect] | ||
| 32 | + /// useful for having a cut-out overlay for example. these [Rect] | ||
| 33 | + /// coordinates are relative to the widget size, so by how much your | ||
| 34 | + /// rectangle overlays the actual image can depend on things like the | ||
| 35 | + /// [BoxFit] | ||
| 36 | + final Rect? scanWindow; | ||
| 37 | + | ||
| 30 | /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. | 38 | /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. |
| 31 | const MobileScanner({ | 39 | const MobileScanner({ |
| 32 | Key? key, | 40 | Key? key, |
| @@ -34,6 +42,7 @@ class MobileScanner extends StatefulWidget { | @@ -34,6 +42,7 @@ class MobileScanner extends StatefulWidget { | ||
| 34 | this.controller, | 42 | this.controller, |
| 35 | this.fit = BoxFit.cover, | 43 | this.fit = BoxFit.cover, |
| 36 | this.allowDuplicates = false, | 44 | this.allowDuplicates = false, |
| 45 | + this.scanWindow, | ||
| 37 | }) : super(key: key); | 46 | }) : super(key: key); |
| 38 | 47 | ||
| 39 | @override | 48 | @override |
| @@ -67,6 +76,55 @@ class _MobileScannerState extends State<MobileScanner> | @@ -67,6 +76,55 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 67 | 76 | ||
| 68 | String? lastScanned; | 77 | String? lastScanned; |
| 69 | 78 | ||
| 79 | + /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible, | ||
| 80 | + /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize] | ||
| 81 | + /// | ||
| 82 | + /// since when using a [BoxFit] the content will always be centered on its parent. we can convert the rect | ||
| 83 | + /// to be relative to the texture. | ||
| 84 | + /// | ||
| 85 | + /// since the textures size and the actuall image (on the texture size) might not be the same, we also need to | ||
| 86 | + /// calculate the scanWindow in terms of percentages of the texture, not pixels. | ||
| 87 | + Rect calculateScanWindowRelativeToTextureInPercentage( | ||
| 88 | + BoxFit fit, | ||
| 89 | + Rect scanWindow, | ||
| 90 | + Size textureSize, | ||
| 91 | + Size widgetSize, | ||
| 92 | + ) { | ||
| 93 | + /// map the texture size to get its new size after fitted to screen | ||
| 94 | + final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize); | ||
| 95 | + | ||
| 96 | + /// create a new rectangle that represents the texture on the screen | ||
| 97 | + final minX = widgetSize.width / 2 - fittedTextureSize.width / 2; | ||
| 98 | + final minY = widgetSize.height / 2 - fittedTextureSize.height / 2; | ||
| 99 | + final textureWindow = Offset(minX, minY) & fittedTextureSize; | ||
| 100 | + | ||
| 101 | + /// create a new scan window and with only the area of the rect intersecting the texture window | ||
| 102 | + final scanWindowInTexture = scanWindow.intersect(textureWindow); | ||
| 103 | + | ||
| 104 | + /// update the scanWindow left and top to be relative to the texture not the widget | ||
| 105 | + final newLeft = scanWindowInTexture.left - textureWindow.left; | ||
| 106 | + final newTop = scanWindowInTexture.top - textureWindow.top; | ||
| 107 | + final newWidth = scanWindowInTexture.width; | ||
| 108 | + final newHeight = scanWindowInTexture.height; | ||
| 109 | + | ||
| 110 | + /// new scanWindow that is adapted to the boxfit and relative to the texture | ||
| 111 | + final windowInTexture = Rect.fromLTWH(newLeft, newTop, newWidth, newHeight); | ||
| 112 | + | ||
| 113 | + /// get the scanWindow as a percentage of the texture | ||
| 114 | + final percentageLeft = windowInTexture.left / fittedTextureSize.width; | ||
| 115 | + final percentageTop = windowInTexture.top / fittedTextureSize.height; | ||
| 116 | + final percentageRight = windowInTexture.right / fittedTextureSize.width; | ||
| 117 | + final percentagebottom = windowInTexture.bottom / fittedTextureSize.height; | ||
| 118 | + | ||
| 119 | + /// this rectangle can be send to native code and used to cut out a rectangle of the scan image | ||
| 120 | + return Rect.fromLTRB( | ||
| 121 | + percentageLeft, | ||
| 122 | + percentageTop, | ||
| 123 | + percentageRight, | ||
| 124 | + percentagebottom, | ||
| 125 | + ); | ||
| 126 | + } | ||
| 127 | + | ||
| 70 | @override | 128 | @override |
| 71 | Widget build(BuildContext context) { | 129 | Widget build(BuildContext context) { |
| 72 | return LayoutBuilder( | 130 | return LayoutBuilder( |
| @@ -78,12 +136,21 @@ class _MobileScannerState extends State<MobileScanner> | @@ -78,12 +136,21 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 78 | if (value == null) { | 136 | if (value == null) { |
| 79 | return Container(color: Colors.black); | 137 | return Container(color: Colors.black); |
| 80 | } else { | 138 | } else { |
| 139 | + if (widget.scanWindow != null) { | ||
| 140 | + final window = calculateScanWindowRelativeToTextureInPercentage( | ||
| 141 | + widget.fit, | ||
| 142 | + widget.scanWindow!, | ||
| 143 | + value.size, | ||
| 144 | + Size(constraints.maxWidth, constraints.maxHeight), | ||
| 145 | + ); | ||
| 146 | + controller.updateScanWindow(window); | ||
| 147 | + } | ||
| 148 | + | ||
| 81 | controller.barcodes.listen((barcode) { | 149 | controller.barcodes.listen((barcode) { |
| 82 | if (!widget.allowDuplicates) { | 150 | if (!widget.allowDuplicates) { |
| 83 | - if (lastScanned != barcode.rawValue) { | ||
| 84 | - lastScanned = barcode.rawValue; | ||
| 85 | - widget.onDetect(barcode, value! as MobileScannerArguments); | ||
| 86 | - } | 151 | + if (lastScanned == barcode.rawValue) return; |
| 152 | + lastScanned = barcode.rawValue; | ||
| 153 | + widget.onDetect(barcode, value! as MobileScannerArguments); | ||
| 87 | } else { | 154 | } else { |
| 88 | widget.onDetect(barcode, value! as MobileScannerArguments); | 155 | widget.onDetect(barcode, value! as MobileScannerArguments); |
| 89 | } | 156 | } |
| @@ -156,6 +156,14 @@ class MobileScannerController { | @@ -156,6 +156,14 @@ class MobileScannerController { | ||
| 156 | // Set the starting arguments for the camera | 156 | // Set the starting arguments for the camera |
| 157 | final Map arguments = {}; | 157 | final Map arguments = {}; |
| 158 | arguments['facing'] = facing.index; | 158 | arguments['facing'] = facing.index; |
| 159 | + /* if (scanWindow != null) { | ||
| 160 | + arguments['scanWindow'] = [ | ||
| 161 | + scanWindow!.left, | ||
| 162 | + scanWindow!.top, | ||
| 163 | + scanWindow!.right, | ||
| 164 | + scanWindow!.bottom, | ||
| 165 | + ]; | ||
| 166 | + } */ | ||
| 159 | if (ratio != null) arguments['ratio'] = ratio; | 167 | if (ratio != null) arguments['ratio'] = ratio; |
| 160 | if (torchEnabled != null) arguments['torch'] = torchEnabled; | 168 | if (torchEnabled != null) arguments['torch'] = torchEnabled; |
| 161 | 169 | ||
| @@ -283,4 +291,10 @@ class MobileScannerController { | @@ -283,4 +291,10 @@ class MobileScannerController { | ||
| 283 | 'MobileScannerController methods should not be used after calling dispose.'; | 291 | 'MobileScannerController methods should not be used after calling dispose.'; |
| 284 | assert(hashCode == _controllerHashcode, message); | 292 | assert(hashCode == _controllerHashcode, message); |
| 285 | } | 293 | } |
| 294 | + | ||
| 295 | + /// updates the native scanwindow | ||
| 296 | + Future<void> updateScanWindow(Rect window) async { | ||
| 297 | + final data = [window.left, window.top, window.right, window.bottom]; | ||
| 298 | + await methodChannel.invokeMethod('updateScanWindow', {'rect': data}); | ||
| 299 | + } | ||
| 286 | } | 300 | } |
| 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 | +} |
| 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", |
| @@ -321,5 +366,9 @@ class MapArgumentReader { | @@ -321,5 +366,9 @@ class MapArgumentReader { | ||
| 321 | func stringArray(key: String) -> [String]? { | 366 | func stringArray(key: String) -> [String]? { |
| 322 | return args?[key] as? [String] | 367 | return args?[key] as? [String] |
| 323 | } | 368 | } |
| 369 | + | ||
| 370 | + func floatArray(key: String) -> [CGFloat]? { | ||
| 371 | + return args?[key] as? [CGFloat] | ||
| 372 | + } | ||
| 324 | 373 | ||
| 325 | } | 374 | } |
-
Please register or login to post a comment