Julian Steenbakker
Committed by GitHub

Merge pull request #176 from casvanluijtelaar/master

Add scanWindow to optionally limit scan area
@@ -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>
  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
@@ -26,6 +27,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -26,6 +27,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
26 var analyzing: Bool = false 27 var analyzing: Bool = false
27 var position = AVCaptureDevice.Position.back 28 var position = AVCaptureDevice.Position.back
28 29
  30 + var scanWindow: CGRect?
  31 +
29 var scanner = BarcodeScanner.barcodeScanner() 32 var scanner = BarcodeScanner.barcodeScanner()
30 33
31 public static func register(with registrar: FlutterPluginRegistrar) { 34 public static func register(with registrar: FlutterPluginRegistrar) {
@@ -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 }
@@ -99,7 +104,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -99,7 +104,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
99 } 104 }
100 analyzing = true 105 analyzing = true
101 let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) 106 let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)
102 - let image = VisionImage(image: buffer!.image) 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",
@@ -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)
@@ -372,4 +428,8 @@ class MapArgumentReader { @@ -372,4 +428,8 @@ class MapArgumentReader {
372 return args?[key] as? [Int] 428 return args?[key] as? [Int]
373 } 429 }
374 430
  431 + func floatArray(key: String) -> [CGFloat]? {
  432 + return args?[key] as? [CGFloat]
  433 + }
  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) { 151 + if (lastScanned == barcode.rawValue) return;
84 lastScanned = barcode.rawValue; 152 lastScanned = barcode.rawValue;
85 widget.onDetect(barcode, value! as MobileScannerArguments); 153 widget.onDetect(barcode, value! as MobileScannerArguments);
86 - }  
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
@@ -21,6 +22,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -21,6 +22,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
21 // Image to be sent to the texture 22 // Image to be sent to the texture
22 var latestBuffer: CVImageBuffer! 23 var latestBuffer: CVImageBuffer!
23 24
  25 + // optional window to limit scan search
  26 + var scanWindow: CGRect?
  27 +
24 28
25 // var analyzeMode: Int = 0 29 // var analyzeMode: Int = 0
26 var analyzing: Bool = false 30 var analyzing: Bool = false
@@ -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 }
@@ -110,6 +116,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -110,6 +116,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
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 { 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 +
113 let barcodeType = String(barcode.symbology.rawValue).replacingOccurrences(of: "VNBarcodeSymbology", with: "") 126 let barcodeType = String(barcode.symbology.rawValue).replacingOccurrences(of: "VNBarcodeSymbology", with: "")
114 let event: [String: Any?] = ["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcodeType]] 127 let event: [String: Any?] = ["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcodeType]]
115 self.sink?(event) 128 self.sink?(event)
@@ -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",
@@ -322,4 +367,8 @@ class MapArgumentReader { @@ -322,4 +367,8 @@ class MapArgumentReader {
322 return args?[key] as? [String] 367 return args?[key] as? [String]
323 } 368 }
324 369
  370 + func floatArray(key: String) -> [CGFloat]? {
  371 + return args?[key] as? [CGFloat]
  372 + }
  373 +
325 } 374 }