Julian Steenbakker
Committed by GitHub

Merge branch 'master' into fix_android_permission_bug

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