Julian Steenbakker
Committed by GitHub

Merge branch 'master' into pavel/web-format

@@ -12,6 +12,7 @@ Breaking changes: @@ -12,6 +12,7 @@ Breaking changes:
12 * The `autoResume` attribute has been removed from the `MobileScanner` widget. 12 * The `autoResume` attribute has been removed from the `MobileScanner` widget.
13 The controller already automatically resumes, so it had no effect. 13 The controller already automatically resumes, so it had no effect.
14 * Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef. 14 * Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef.
  15 +* [Web] Replaced `jsqr` library with `zxing-js` for full barcode support.
15 16
16 Improvements: 17 Improvements:
17 * Toggling the device torch now does nothing if the device has no torch, rather than throwing an error. 18 * Toggling the device torch now does nothing if the device has no torch, rather than throwing an error.
@@ -21,6 +22,9 @@ Features: @@ -21,6 +22,9 @@ Features:
21 * Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder. 22 * Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder.
22 * Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically. 23 * Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically.
23 * Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch. 24 * Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch.
  25 +* [iOS] Support `torchEnabled` parameter from MobileScannerController() on iOS
  26 +* [Web] Added ability to use custom barcode scanning js libraries
  27 + by extending `WebBarcodeReaderBase` class and changing `barCodeReader` property in `MobileScannerWebPlugin`
24 28
25 Fixes: 29 Fixes:
26 * Fixes the missing gradle setup for the Android project, which prevented gradle sync from working. 30 * Fixes the missing gradle setup for the Android project, which prevented gradle sync from working.
@@ -31,6 +35,7 @@ Fixes: @@ -31,6 +35,7 @@ Fixes:
31 * Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`. 35 * Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`.
32 Now it only depends on its layout constraints. 36 Now it only depends on its layout constraints.
33 * Fixed a potential crash when the scanner is restarted due to the app being resumed. 37 * Fixed a potential crash when the scanner is restarted due to the app being resumed.
  38 +* [iOS] Fix crash when changing torch state
34 39
35 ## 3.0.0-beta.2 40 ## 3.0.0-beta.2
36 Breaking changes: 41 Breaking changes:
@@ -3,9 +3,11 @@ package dev.steenbakker.mobile_scanner @@ -3,9 +3,11 @@ package dev.steenbakker.mobile_scanner
3 import android.Manifest 3 import android.Manifest
4 import android.app.Activity 4 import android.app.Activity
5 import android.content.pm.PackageManager 5 import android.content.pm.PackageManager
  6 +import android.graphics.Rect
6 import android.net.Uri 7 import android.net.Uri
7 import android.os.Handler 8 import android.os.Handler
8 import android.os.Looper 9 import android.os.Looper
  10 +import android.util.Log
9 import android.view.Surface 11 import android.view.Surface
10 import androidx.camera.core.* 12 import androidx.camera.core.*
11 import androidx.camera.lifecycle.ProcessCameraProvider 13 import androidx.camera.lifecycle.ProcessCameraProvider
@@ -14,18 +16,23 @@ import androidx.core.content.ContextCompat @@ -14,18 +16,23 @@ import androidx.core.content.ContextCompat
14 import androidx.lifecycle.LifecycleOwner 16 import androidx.lifecycle.LifecycleOwner
15 import com.google.mlkit.vision.barcode.BarcodeScannerOptions 17 import com.google.mlkit.vision.barcode.BarcodeScannerOptions
16 import com.google.mlkit.vision.barcode.BarcodeScanning 18 import com.google.mlkit.vision.barcode.BarcodeScanning
  19 +import com.google.mlkit.vision.barcode.common.Barcode
17 import com.google.mlkit.vision.common.InputImage 20 import com.google.mlkit.vision.common.InputImage
18 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed 21 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
19 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters 22 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
20 import io.flutter.plugin.common.MethodChannel 23 import io.flutter.plugin.common.MethodChannel
21 import io.flutter.plugin.common.PluginRegistry 24 import io.flutter.plugin.common.PluginRegistry
22 import io.flutter.view.TextureRegistry 25 import io.flutter.view.TextureRegistry
23 -typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit 26 +import kotlin.math.roundToInt
  27 +
  28 +
  29 +typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit
24 typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit 30 typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
25 typealias MobileScannerErrorCallback = (error: String) -> Unit 31 typealias MobileScannerErrorCallback = (error: String) -> Unit
26 typealias TorchStateCallback = (state: Int) -> Unit 32 typealias TorchStateCallback = (state: Int) -> Unit
27 typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit 33 typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
28 34
  35 +
29 class NoCamera : Exception() 36 class NoCamera : Exception()
30 class AlreadyStarted : Exception() 37 class AlreadyStarted : Exception()
31 class AlreadyStopped : Exception() 38 class AlreadyStopped : Exception()
@@ -53,6 +60,7 @@ class MobileScanner( @@ -53,6 +60,7 @@ class MobileScanner(
53 private var pendingPermissionResult: MethodChannel.Result? = null 60 private var pendingPermissionResult: MethodChannel.Result? = null
54 private var preview: Preview? = null 61 private var preview: Preview? = null
55 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null 62 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
  63 + var scanWindow: List<Float>? = null
56 64
57 private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES 65 private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
58 private var detectionTimeout: Long = 250 66 private var detectionTimeout: Long = 250
@@ -138,12 +146,27 @@ class MobileScanner( @@ -138,12 +146,27 @@ class MobileScanner(
138 lastScanned = newScannedBarcodes 146 lastScanned = newScannedBarcodes
139 } 147 }
140 148
141 - val barcodeMap = barcodes.map { barcode -> barcode.data } 149 + val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf()
  150 +
  151 + for ( barcode in barcodes) {
  152 + if(scanWindow != null) {
  153 + val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy)
  154 + if(!match) {
  155 + continue
  156 + } else {
  157 + barcodeMap.add(barcode.data)
  158 + }
  159 + } else {
  160 + barcodeMap.add(barcode.data)
  161 + }
  162 + }
142 163
143 if (barcodeMap.isNotEmpty()) { 164 if (barcodeMap.isNotEmpty()) {
144 mobileScannerCallback( 165 mobileScannerCallback(
145 barcodeMap, 166 barcodeMap,
146 - if (returnImage) mediaImage.toByteArray() else null 167 + if (returnImage) mediaImage.toByteArray() else null,
  168 + if (returnImage) mediaImage.width else null,
  169 + if (returnImage) mediaImage.height else null
147 ) 170 )
148 } 171 }
149 } 172 }
@@ -162,6 +185,23 @@ class MobileScanner( @@ -162,6 +185,23 @@ class MobileScanner(
162 } 185 }
163 } 186 }
164 187
  188 + // scales the scanWindow to the provided inputImage and checks if that scaled
  189 + // scanWindow contains the barcode
  190 + private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: ImageProxy): Boolean {
  191 + val barcodeBoundingBox = barcode.boundingBox ?: return false
  192 +
  193 + val imageWidth = inputImage.height
  194 + val imageHeight = inputImage.width
  195 +
  196 + val left = (scanWindow[0] * imageWidth).roundToInt()
  197 + val top = (scanWindow[1] * imageHeight).roundToInt()
  198 + val right = (scanWindow[2] * imageWidth).roundToInt()
  199 + val bottom = (scanWindow[3] * imageHeight).roundToInt()
  200 +
  201 + val scaledScanWindow = Rect(left, top, right, bottom)
  202 + return scaledScanWindow.contains(barcodeBoundingBox)
  203 + }
  204 +
165 /** 205 /**
166 * Start barcode scanning by initializing the camera and barcode scanner. 206 * Start barcode scanning by initializing the camera and barcode scanner.
167 */ 207 */
@@ -244,7 +284,7 @@ class MobileScanner( @@ -244,7 +284,7 @@ class MobileScanner(
244 // Enable torch if provided 284 // Enable torch if provided
245 camera!!.cameraControl.enableTorch(torch) 285 camera!!.cameraControl.enableTorch(torch)
246 286
247 - val resolution = preview!!.resolutionInfo!!.resolution 287 + val resolution = analysis.resolutionInfo!!.resolution
248 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 288 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
249 val width = resolution.width.toDouble() 289 val width = resolution.width.toDouble()
250 val height = resolution.height.toDouble() 290 val height = resolution.height.toDouble()
@@ -25,12 +25,14 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -25,12 +25,14 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
25 25
26 private var analyzerResult: MethodChannel.Result? = null 26 private var analyzerResult: MethodChannel.Result? = null
27 27
28 - private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? -> 28 + private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? ->
29 if (image != null) { 29 if (image != null) {
30 barcodeHandler.publishEvent(mapOf( 30 barcodeHandler.publishEvent(mapOf(
31 "name" to "barcode", 31 "name" to "barcode",
32 "data" to barcodes, 32 "data" to barcodes,
33 - "image" to image 33 + "image" to image,
  34 + "width" to width!!.toDouble(),
  35 + "height" to height!!.toDouble()
34 )) 36 ))
35 } else { 37 } else {
36 barcodeHandler.publishEvent(mapOf( 38 barcodeHandler.publishEvent(mapOf(
@@ -77,6 +79,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -77,6 +79,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
77 "torch" -> toggleTorch(call, result) 79 "torch" -> toggleTorch(call, result)
78 "stop" -> stop(result) 80 "stop" -> stop(result)
79 "analyzeImage" -> analyzeImage(call, result) 81 "analyzeImage" -> analyzeImage(call, result)
  82 + "updateScanWindow" -> updateScanWindow(call)
80 else -> result.notImplemented() 83 else -> result.notImplemented()
81 } 84 }
82 } 85 }
@@ -215,4 +218,8 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -215,4 +218,8 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
215 result.error("MobileScanner", "Called toggleTorch() while stopped!", null) 218 result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
216 } 219 }
217 } 220 }
  221 +
  222 + private fun updateScanWindow(call: MethodCall) {
  223 + handler!!.scanWindow = call.argument<List<Float>>("rect")
  224 + }
218 } 225 }
@@ -354,6 +354,8 @@ @@ -354,6 +354,8 @@
354 buildSettings = { 354 buildSettings = {
355 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
356 CLANG_ENABLE_MODULES = YES; 356 CLANG_ENABLE_MODULES = YES;
  357 + CODE_SIGN_IDENTITY = "Apple Development";
  358 + CODE_SIGN_STYLE = Automatic;
357 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 359 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
358 DEVELOPMENT_TEAM = 75Y2P2WSQQ; 360 DEVELOPMENT_TEAM = 75Y2P2WSQQ;
359 ENABLE_BITCODE = NO; 361 ENABLE_BITCODE = NO;
@@ -364,6 +366,7 @@ @@ -364,6 +366,7 @@
364 ); 366 );
365 PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; 367 PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
366 PRODUCT_NAME = "$(TARGET_NAME)"; 368 PRODUCT_NAME = "$(TARGET_NAME)";
  369 + PROVISIONING_PROFILE_SPECIFIER = "";
367 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 370 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
368 SWIFT_VERSION = 5.0; 371 SWIFT_VERSION = 5.0;
369 VERSIONING_SYSTEM = "apple-generic"; 372 VERSIONING_SYSTEM = "apple-generic";
@@ -483,6 +486,8 @@ @@ -483,6 +486,8 @@
483 buildSettings = { 486 buildSettings = {
484 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 487 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
485 CLANG_ENABLE_MODULES = YES; 488 CLANG_ENABLE_MODULES = YES;
  489 + CODE_SIGN_IDENTITY = "Apple Development";
  490 + CODE_SIGN_STYLE = Automatic;
486 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 491 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
487 DEVELOPMENT_TEAM = 75Y2P2WSQQ; 492 DEVELOPMENT_TEAM = 75Y2P2WSQQ;
488 ENABLE_BITCODE = NO; 493 ENABLE_BITCODE = NO;
@@ -493,6 +498,7 @@ @@ -493,6 +498,7 @@
493 ); 498 );
494 PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; 499 PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
495 PRODUCT_NAME = "$(TARGET_NAME)"; 500 PRODUCT_NAME = "$(TARGET_NAME)";
  501 + PROVISIONING_PROFILE_SPECIFIER = "";
496 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 502 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
497 SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 503 SWIFT_OPTIMIZATION_LEVEL = "-Onone";
498 SWIFT_VERSION = 5.0; 504 SWIFT_VERSION = 5.0;
@@ -506,6 +512,8 @@ @@ -506,6 +512,8 @@
506 buildSettings = { 512 buildSettings = {
507 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 513 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
508 CLANG_ENABLE_MODULES = YES; 514 CLANG_ENABLE_MODULES = YES;
  515 + CODE_SIGN_IDENTITY = "Apple Development";
  516 + CODE_SIGN_STYLE = Automatic;
509 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 517 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
510 DEVELOPMENT_TEAM = 75Y2P2WSQQ; 518 DEVELOPMENT_TEAM = 75Y2P2WSQQ;
511 ENABLE_BITCODE = NO; 519 ENABLE_BITCODE = NO;
@@ -516,6 +524,7 @@ @@ -516,6 +524,7 @@
516 ); 524 );
517 PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; 525 PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
518 PRODUCT_NAME = "$(TARGET_NAME)"; 526 PRODUCT_NAME = "$(TARGET_NAME)";
  527 + PROVISIONING_PROFILE_SPECIFIER = "";
519 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 528 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
520 SWIFT_VERSION = 5.0; 529 SWIFT_VERSION = 5.0;
521 VERSIONING_SYSTEM = "apple-generic"; 530 VERSIONING_SYSTEM = "apple-generic";
  1 +import 'dart:io';
  2 +
  3 +import 'package:flutter/material.dart';
  4 +import 'package:mobile_scanner/mobile_scanner.dart';
  5 +
  6 +class BarcodeScannerWithScanWindow extends StatefulWidget {
  7 + const BarcodeScannerWithScanWindow({Key? key}) : super(key: key);
  8 +
  9 + @override
  10 + _BarcodeScannerWithScanWindowState createState() =>
  11 + _BarcodeScannerWithScanWindowState();
  12 +}
  13 +
  14 +class _BarcodeScannerWithScanWindowState
  15 + extends State<BarcodeScannerWithScanWindow> {
  16 + late MobileScannerController controller = MobileScannerController();
  17 + Barcode? barcode;
  18 + BarcodeCapture? capture;
  19 +
  20 + Future<void> onDetect(BarcodeCapture barcode) async {
  21 + capture = barcode;
  22 + setState(() => this.barcode = barcode.barcodes.first);
  23 + }
  24 +
  25 + MobileScannerArguments? arguments;
  26 +
  27 + @override
  28 + Widget build(BuildContext context) {
  29 + final scanWindow = Rect.fromCenter(
  30 + center: MediaQuery.of(context).size.center(Offset.zero),
  31 + width: 200,
  32 + height: 200,
  33 + );
  34 + return Scaffold(
  35 + backgroundColor: Colors.black,
  36 + body: Builder(
  37 + builder: (context) {
  38 + return Stack(
  39 + fit: StackFit.expand,
  40 + children: [
  41 + MobileScanner(
  42 + fit: BoxFit.contain,
  43 + scanWindow: scanWindow,
  44 + controller: controller,
  45 + onScannerStarted: (arguments) {
  46 + setState(() {
  47 + this.arguments = arguments;
  48 + });
  49 + },
  50 + onDetect: onDetect,
  51 + ),
  52 + if (barcode != null &&
  53 + barcode?.corners != null &&
  54 + arguments != null)
  55 + CustomPaint(
  56 + painter: BarcodeOverlay(
  57 + barcode!,
  58 + arguments!,
  59 + BoxFit.contain,
  60 + MediaQuery.of(context).devicePixelRatio,
  61 + capture!,
  62 + ),
  63 + ),
  64 + CustomPaint(
  65 + painter: ScannerOverlay(scanWindow),
  66 + ),
  67 + Align(
  68 + alignment: Alignment.bottomCenter,
  69 + child: Container(
  70 + alignment: Alignment.bottomCenter,
  71 + height: 100,
  72 + color: Colors.black.withOpacity(0.4),
  73 + child: Row(
  74 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  75 + children: [
  76 + Center(
  77 + child: SizedBox(
  78 + width: MediaQuery.of(context).size.width - 120,
  79 + height: 50,
  80 + child: FittedBox(
  81 + child: Text(
  82 + barcode?.displayValue ?? 'Scan something!',
  83 + overflow: TextOverflow.fade,
  84 + style: Theme.of(context)
  85 + .textTheme
  86 + .headline4!
  87 + .copyWith(color: Colors.white),
  88 + ),
  89 + ),
  90 + ),
  91 + ),
  92 + ],
  93 + ),
  94 + ),
  95 + ),
  96 + ],
  97 + );
  98 + },
  99 + ),
  100 + );
  101 + }
  102 +}
  103 +
  104 +class ScannerOverlay extends CustomPainter {
  105 + ScannerOverlay(this.scanWindow);
  106 +
  107 + final Rect scanWindow;
  108 +
  109 + @override
  110 + void paint(Canvas canvas, Size size) {
  111 + final backgroundPath = Path()..addRect(Rect.largest);
  112 + final cutoutPath = Path()..addRect(scanWindow);
  113 +
  114 + final backgroundPaint = Paint()
  115 + ..color = Colors.black.withOpacity(0.5)
  116 + ..style = PaintingStyle.fill
  117 + ..blendMode = BlendMode.dstOut;
  118 +
  119 + final backgroundWithCutout = Path.combine(
  120 + PathOperation.difference,
  121 + backgroundPath,
  122 + cutoutPath,
  123 + );
  124 + canvas.drawPath(backgroundWithCutout, backgroundPaint);
  125 + }
  126 +
  127 + @override
  128 + bool shouldRepaint(covariant CustomPainter oldDelegate) {
  129 + return false;
  130 + }
  131 +}
  132 +
  133 +class BarcodeOverlay extends CustomPainter {
  134 + BarcodeOverlay(
  135 + this.barcode,
  136 + this.arguments,
  137 + this.boxFit,
  138 + this.devicePixelRatio,
  139 + this.capture,
  140 + );
  141 +
  142 + final BarcodeCapture capture;
  143 + final Barcode barcode;
  144 + final MobileScannerArguments arguments;
  145 + final BoxFit boxFit;
  146 + final double devicePixelRatio;
  147 +
  148 + @override
  149 + void paint(Canvas canvas, Size size) {
  150 + if (barcode.corners == null) return;
  151 + final adjustedSize = applyBoxFit(boxFit, arguments.size, size);
  152 +
  153 + double verticalPadding = size.height - adjustedSize.destination.height;
  154 + double horizontalPadding = size.width - adjustedSize.destination.width;
  155 + if (verticalPadding > 0) {
  156 + verticalPadding = verticalPadding / 2;
  157 + } else {
  158 + verticalPadding = 0;
  159 + }
  160 +
  161 + if (horizontalPadding > 0) {
  162 + horizontalPadding = horizontalPadding / 2;
  163 + } else {
  164 + horizontalPadding = 0;
  165 + }
  166 +
  167 + final ratioWidth =
  168 + (Platform.isIOS ? capture.width! : arguments.size.width) /
  169 + adjustedSize.destination.width;
  170 + final ratioHeight =
  171 + (Platform.isIOS ? capture.height! : arguments.size.height) /
  172 + adjustedSize.destination.height;
  173 +
  174 + final List<Offset> adjustedOffset = [];
  175 + for (final offset in barcode.corners!) {
  176 + adjustedOffset.add(
  177 + Offset(
  178 + offset.dx / ratioWidth + horizontalPadding,
  179 + offset.dy / ratioHeight + verticalPadding,
  180 + ),
  181 + );
  182 + }
  183 + final cutoutPath = Path()..addPolygon(adjustedOffset, true);
  184 +
  185 + final backgroundPaint = Paint()
  186 + ..color = Colors.red.withOpacity(0.3)
  187 + ..style = PaintingStyle.fill
  188 + ..blendMode = BlendMode.dstOut;
  189 +
  190 + canvas.drawPath(cutoutPath, backgroundPaint);
  191 + }
  192 +
  193 + @override
  194 + bool shouldRepaint(covariant CustomPainter oldDelegate) {
  195 + return false;
  196 + }
  197 +}
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
2 import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; 2 import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';
3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; 3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
4 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; 4 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
  5 +import 'package:mobile_scanner_example/barcode_scanner_window.dart';
5 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; 6 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
6 7
7 void main() => runApp(const MaterialApp(home: MyHome())); 8 void main() => runApp(const MaterialApp(home: MyHome()));
@@ -44,6 +45,16 @@ class MyHome extends StatelessWidget { @@ -44,6 +45,16 @@ class MyHome extends StatelessWidget {
44 onPressed: () { 45 onPressed: () {
45 Navigator.of(context).push( 46 Navigator.of(context).push(
46 MaterialPageRoute( 47 MaterialPageRoute(
  48 + builder: (context) => const BarcodeScannerWithScanWindow(),
  49 + ),
  50 + );
  51 + },
  52 + child: const Text('MobileScanner with ScanWindow'),
  53 + ),
  54 + ElevatedButton(
  55 + onPressed: () {
  56 + Navigator.of(context).push(
  57 + MaterialPageRoute(
47 builder: (context) => const BarcodeScannerReturningImage(), 58 builder: (context) => const BarcodeScannerReturningImage(),
48 ), 59 ),
49 ); 60 );
@@ -12,6 +12,7 @@ import MLKitVision @@ -12,6 +12,7 @@ import MLKitVision
12 import MLKitBarcodeScanning 12 import MLKitBarcodeScanning
13 13
14 typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ()) 14 typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ())
  15 +typealias TorchModeChangeCallback = ((Int?) -> ())
15 16
16 public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture { 17 public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture {
17 /// Capture session of the camera 18 /// Capture session of the camera
@@ -32,6 +33,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -32,6 +33,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
32 /// When results are found, this callback will be called 33 /// When results are found, this callback will be called
33 let mobileScannerCallback: MobileScannerCallback 34 let mobileScannerCallback: MobileScannerCallback
34 35
  36 + /// When torch mode is changes, this callback will be called
  37 + let torchModeChangeCallback: TorchModeChangeCallback
  38 +
35 /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture. 39 /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
36 private let registry: FlutterTextureRegistry? 40 private let registry: FlutterTextureRegistry?
37 41
@@ -43,9 +47,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -43,9 +47,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
43 47
44 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates 48 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
45 49
46 - init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) { 50 + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback) {
47 self.registry = registry 51 self.registry = registry
48 self.mobileScannerCallback = mobileScannerCallback 52 self.mobileScannerCallback = mobileScannerCallback
  53 + self.torchModeChangeCallback = torchModeChangeCallback
49 super.init() 54 super.init()
50 } 55 }
51 56
@@ -127,17 +132,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -127,17 +132,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
127 throw MobileScannerError.noCamera 132 throw MobileScannerError.noCamera
128 } 133 }
129 134
130 - // Enable the torch if parameter is set and torch is available  
131 - if (device.hasTorch && device.isTorchAvailable) {  
132 - do {  
133 - try device.lockForConfiguration()  
134 - device.torchMode = torch  
135 - device.unlockForConfiguration()  
136 - } catch {  
137 - throw MobileScannerError.torchError(error)  
138 - }  
139 - }  
140 -  
141 device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) 135 device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
142 captureSession.beginConfiguration() 136 captureSession.beginConfiguration()
143 137
@@ -169,6 +163,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -169,6 +163,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
169 } 163 }
170 captureSession.commitConfiguration() 164 captureSession.commitConfiguration()
171 captureSession.startRunning() 165 captureSession.startRunning()
  166 + // Enable the torch if parameter is set and torch is available
  167 + // torch should be set after 'startRunning' is called
  168 + do {
  169 + try toggleTorch(torch)
  170 + } catch {
  171 + print("Failed to set initial torch state.")
  172 + }
172 let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) 173 let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
173 174
174 return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) 175 return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
@@ -198,12 +199,26 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -198,12 +199,26 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
198 if (device == nil) { 199 if (device == nil) {
199 throw MobileScannerError.torchWhenStopped 200 throw MobileScannerError.torchWhenStopped
200 } 201 }
201 - do {  
202 - try device.lockForConfiguration()  
203 - device.torchMode = torch  
204 - device.unlockForConfiguration()  
205 - } catch {  
206 - throw MobileScannerError.torchError(error) 202 + if (device.hasTorch && device.isTorchAvailable) {
  203 + do {
  204 + try device.lockForConfiguration()
  205 + device.torchMode = torch
  206 + device.unlockForConfiguration()
  207 + } catch {
  208 + throw MobileScannerError.torchError(error)
  209 + }
  210 + }
  211 + }
  212 +
  213 + // Observer for torch state
  214 + public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  215 + switch keyPath {
  216 + case "torchMode":
  217 + // off = 0; on = 1; auto = 2;
  218 + let state = change?[.newKey] as? Int
  219 + torchModeChangeCallback(state)
  220 + default:
  221 + break
207 } 222 }
208 } 223 }
209 224
@@ -2,6 +2,7 @@ import Flutter @@ -2,6 +2,7 @@ import Flutter
2 import MLKitVision 2 import MLKitVision
3 import MLKitBarcodeScanning 3 import MLKitBarcodeScanning
4 import AVFoundation 4 import AVFoundation
  5 +import UIKit
5 6
6 public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { 7 public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
7 8
@@ -10,19 +11,52 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -10,19 +11,52 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
10 11
11 /// The handler sends all information via an event channel back to Flutter 12 /// The handler sends all information via an event channel back to Flutter
12 private let barcodeHandler: BarcodeHandler 13 private let barcodeHandler: BarcodeHandler
  14 +
  15 + static var scanWindow: [CGFloat]?
  16 +
  17 + private static func isBarcodeInScanWindow(barcode: Barcode, imageSize: CGSize) -> Bool {
  18 + let scanwindow = SwiftMobileScannerPlugin.scanWindow!
  19 + let barcodeminX = barcode.cornerPoints![0].cgPointValue.x
  20 + let barcodeminY = barcode.cornerPoints![1].cgPointValue.y
  21 +
  22 + let barcodewidth = barcode.cornerPoints![2].cgPointValue.x - barcodeminX
  23 + let barcodeheight = barcode.cornerPoints![3].cgPointValue.y - barcodeminY
  24 + let barcodeBox = CGRect(x: barcodeminX, y: barcodeminY, width: barcodewidth, height: barcodeheight)
  25 +
  26 +
  27 + let minX = scanwindow[0] * imageSize.width
  28 + let minY = scanwindow[1] * imageSize.height
  29 +
  30 + let width = (scanwindow[2] * imageSize.width) - minX
  31 + let height = (scanwindow[3] * imageSize.height) - minY
  32 +
  33 + let scaledWindow = CGRect(x: minX, y: minY, width: width, height: height)
  34 +
  35 + return scaledWindow.contains(barcodeBox)
  36 + }
13 37
14 init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { 38 init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
15 self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in 39 self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
16 if barcodes != nil { 40 if barcodes != nil {
17 - let barcodesMap = barcodes!.map { barcode in  
18 - return barcode.data 41 + let barcodesMap = barcodes!.compactMap { barcode in
  42 + if (SwiftMobileScannerPlugin.scanWindow != nil) {
  43 + if (SwiftMobileScannerPlugin.isBarcodeInScanWindow(barcode: barcode, imageSize: image.size)) {
  44 + return barcode.data
  45 + } else {
  46 + return nil
  47 + }
  48 + } else {
  49 + return barcode.data
  50 + }
19 } 51 }
20 if (!barcodesMap.isEmpty) { 52 if (!barcodesMap.isEmpty) {
21 - barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)]) 53 + barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!), "width": image.size.width, "height": image.size.height])
22 } 54 }
23 } else if (error != nil){ 55 } else if (error != nil){
24 barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) 56 barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
25 } 57 }
  58 + }, torchModeChangeCallback: { torchState in
  59 + barcodeHandler.publishEvent(["name": "torchState", "data": torchState])
26 }) 60 })
27 self.barcodeHandler = barcodeHandler 61 self.barcodeHandler = barcodeHandler
28 super.init() 62 super.init()
@@ -49,6 +83,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -49,6 +83,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
49 toggleTorch(call, result) 83 toggleTorch(call, result)
50 case "analyzeImage": 84 case "analyzeImage":
51 analyzeImage(call, result) 85 analyzeImage(call, result)
  86 + case "updateScanWindow":
  87 + updateScanWindow(call, result)
52 default: 88 default:
53 result(FlutterMethodNotImplemented) 89 result(FlutterMethodNotImplemented)
54 } 90 }
@@ -64,7 +100,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -64,7 +100,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
64 100
65 let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} 101 let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
66 var barcodeOptions: BarcodeScannerOptions? = nil 102 var barcodeOptions: BarcodeScannerOptions? = nil
67 - 103 +
68 if (formatList.count != 0) { 104 if (formatList.count != 0) {
69 var barcodeFormats: BarcodeFormat = [] 105 var barcodeFormats: BarcodeFormat = []
70 for index in formats { 106 for index in formats {
@@ -123,6 +159,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -123,6 +159,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
123 result(nil) 159 result(nil)
124 } 160 }
125 161
  162 + /// Toggles the torch
  163 + func updateScanWindow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  164 + let scanWindowData: Array? = (call.arguments as? [String: Any])?["rect"] as? [CGFloat]
  165 + SwiftMobileScannerPlugin.scanWindow = scanWindowData
  166 +
  167 + result(nil)
  168 + }
  169 +
  170 + static func arrayToRect(scanWindowData: [CGFloat]?) -> CGRect? {
  171 + if (scanWindowData == nil) {
  172 + return nil
  173 + }
  174 +
  175 + let minX = scanWindowData![0]
  176 + let minY = scanWindowData![1]
  177 +
  178 + let width = scanWindowData![2] - minX
  179 + let height = scanWindowData![3] - minY
  180 +
  181 + return CGRect(x: minX, y: minY, width: width, height: height)
  182 + }
  183 +
126 /// Analyzes a single image 184 /// Analyzes a single image
127 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 185 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
128 let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") 186 let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
@@ -145,16 +203,4 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -145,16 +203,4 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
145 }) 203 })
146 result(nil) 204 result(nil)
147 } 205 }
148 -  
149 - /// Observer for torch state  
150 - public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {  
151 - switch keyPath {  
152 - case "torchMode":  
153 - // off = 0; on = 1; auto = 2;  
154 - let state = change?[.newKey] as? Int  
155 - barcodeHandler.publishEvent(["name": "torchState", "data": state])  
156 - default:  
157 - break  
158 - }  
159 - }  
160 } 206 }
@@ -37,6 +37,17 @@ class MobileScannerWebPlugin { @@ -37,6 +37,17 @@ class MobileScannerWebPlugin {
37 37
38 static final html.DivElement vidDiv = html.DivElement(); 38 static final html.DivElement vidDiv = html.DivElement();
39 39
  40 + /// Represents barcode reader library.
  41 + /// Change this property if you want to use a custom implementation.
  42 + ///
  43 + /// Example of using the jsQR library:
  44 + /// void main() {
  45 + /// if (kIsWeb) {
  46 + /// MobileScannerWebPlugin.barCodeReader =
  47 + /// JsQrCodeReader(videoContainer: MobileScannerWebPlugin.vidDiv);
  48 + /// }
  49 + /// runApp(const MaterialApp(home: MyHome()));
  50 + /// }
40 static WebBarcodeReaderBase barCodeReader = 51 static WebBarcodeReaderBase barCodeReader =
41 ZXingBarcodeReader(videoContainer: vidDiv); 52 ZXingBarcodeReader(videoContainer: vidDiv);
42 StreamSubscription? _barCodeStreamSubscription; 53 StreamSubscription? _barCodeStreamSubscription;
  1 +import 'dart:math' as math;
  2 +
1 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
2 import 'package:mobile_scanner/mobile_scanner.dart'; 4 import 'package:mobile_scanner/mobile_scanner.dart';
3 5
@@ -147,3 +149,79 @@ WiFi? toWiFi(Map? data) { @@ -147,3 +149,79 @@ WiFi? toWiFi(Map? data) {
147 return null; 149 return null;
148 } 150 }
149 } 151 }
  152 +
  153 +Size applyBoxFit(BoxFit fit, Size input, Size output) {
  154 + if (input.height <= 0.0 ||
  155 + input.width <= 0.0 ||
  156 + output.height <= 0.0 ||
  157 + output.width <= 0.0) {
  158 + return Size.zero;
  159 + }
  160 +
  161 + Size destination;
  162 +
  163 + final inputAspectRatio = input.width / input.height;
  164 + final outputAspectRatio = output.width / output.height;
  165 +
  166 + switch (fit) {
  167 + case BoxFit.fill:
  168 + destination = output;
  169 + break;
  170 + case BoxFit.contain:
  171 + if (outputAspectRatio > inputAspectRatio) {
  172 + destination = Size(
  173 + input.width * output.height / input.height,
  174 + output.height,
  175 + );
  176 + } else {
  177 + destination = Size(
  178 + output.width,
  179 + input.height * output.width / input.width,
  180 + );
  181 + }
  182 + break;
  183 +
  184 + case BoxFit.cover:
  185 + if (outputAspectRatio > inputAspectRatio) {
  186 + destination = Size(
  187 + output.width,
  188 + input.height * (output.width / input.width),
  189 + );
  190 + } else {
  191 + destination = Size(
  192 + input.width * (output.height / input.height),
  193 + output.height,
  194 + );
  195 + }
  196 + break;
  197 + case BoxFit.fitWidth:
  198 + destination = Size(
  199 + output.width,
  200 + input.height * (output.width / input.width),
  201 + );
  202 + break;
  203 + case BoxFit.fitHeight:
  204 + destination = Size(
  205 + input.width * (output.height / input.height),
  206 + output.height,
  207 + );
  208 + break;
  209 + case BoxFit.none:
  210 + destination = Size(
  211 + math.min(input.width, output.width),
  212 + math.min(input.height, output.height),
  213 + );
  214 + break;
  215 + case BoxFit.scaleDown:
  216 + destination = input;
  217 + if (destination.height > output.height) {
  218 + destination = Size(output.height * inputAspectRatio, output.height);
  219 + }
  220 + if (destination.width > output.width) {
  221 + destination = Size(output.width, output.width / inputAspectRatio);
  222 + }
  223 + break;
  224 + }
  225 +
  226 + return destination;
  227 +}
@@ -34,6 +34,13 @@ class MobileScanner extends StatefulWidget { @@ -34,6 +34,13 @@ class MobileScanner extends StatefulWidget {
34 /// If this is null, a black [ColoredBox] is used as placeholder. 34 /// If this is null, a black [ColoredBox] is used as placeholder.
35 final Widget Function(BuildContext, Widget?)? placeholderBuilder; 35 final Widget Function(BuildContext, Widget?)? placeholderBuilder;
36 36
  37 + /// if set barcodes will only be scanned if they fall within this [Rect]
  38 + /// useful for having a cut-out overlay for example. these [Rect]
  39 + /// coordinates are relative to the widget size, so by how much your
  40 + /// rectangle overlays the actual image can depend on things like the
  41 + /// [BoxFit]
  42 + final Rect? scanWindow;
  43 +
37 /// Create a new [MobileScanner] using the provided [controller] 44 /// Create a new [MobileScanner] using the provided [controller]
38 /// and [onBarcodeDetected] callback. 45 /// and [onBarcodeDetected] callback.
39 const MobileScanner({ 46 const MobileScanner({
@@ -43,6 +50,7 @@ class MobileScanner extends StatefulWidget { @@ -43,6 +50,7 @@ class MobileScanner extends StatefulWidget {
43 @Deprecated('Use onScannerStarted() instead.') this.onStart, 50 @Deprecated('Use onScannerStarted() instead.') this.onStart,
44 this.onScannerStarted, 51 this.onScannerStarted,
45 this.placeholderBuilder, 52 this.placeholderBuilder,
  53 + this.scanWindow,
46 super.key, 54 super.key,
47 }); 55 });
48 56
@@ -117,34 +125,102 @@ class _MobileScannerState extends State<MobileScanner> @@ -117,34 +125,102 @@ class _MobileScannerState extends State<MobileScanner>
117 } 125 }
118 } 126 }
119 127
  128 + /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible,
  129 + /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize]
  130 + ///
  131 + /// since when using a [BoxFit] the content will always be centered on its parent. we can convert the rect
  132 + /// to be relative to the texture.
  133 + ///
  134 + /// since the textures size and the actuall image (on the texture size) might not be the same, we also need to
  135 + /// calculate the scanWindow in terms of percentages of the texture, not pixels.
  136 + Rect calculateScanWindowRelativeToTextureInPercentage(
  137 + BoxFit fit,
  138 + Rect scanWindow,
  139 + Size textureSize,
  140 + Size widgetSize,
  141 + ) {
  142 + /// map the texture size to get its new size after fitted to screen
  143 + final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize);
  144 +
  145 + /// create a new rectangle that represents the texture on the screen
  146 + final minX = widgetSize.width / 2 - fittedTextureSize.destination.width / 2;
  147 + final minY =
  148 + widgetSize.height / 2 - fittedTextureSize.destination.height / 2;
  149 + final textureWindow = Offset(minX, minY) & fittedTextureSize.destination;
  150 +
  151 + /// create a new scan window and with only the area of the rect intersecting the texture window
  152 + final scanWindowInTexture = scanWindow.intersect(textureWindow);
  153 +
  154 + /// update the scanWindow left and top to be relative to the texture not the widget
  155 + final newLeft = scanWindowInTexture.left - textureWindow.left;
  156 + final newTop = scanWindowInTexture.top - textureWindow.top;
  157 + final newWidth = scanWindowInTexture.width;
  158 + final newHeight = scanWindowInTexture.height;
  159 +
  160 + /// new scanWindow that is adapted to the boxfit and relative to the texture
  161 + final windowInTexture = Rect.fromLTWH(newLeft, newTop, newWidth, newHeight);
  162 +
  163 + /// get the scanWindow as a percentage of the texture
  164 + final percentageLeft =
  165 + windowInTexture.left / fittedTextureSize.destination.width;
  166 + final percentageTop =
  167 + windowInTexture.top / fittedTextureSize.destination.height;
  168 + final percentageRight =
  169 + windowInTexture.right / fittedTextureSize.destination.width;
  170 + final percentagebottom =
  171 + windowInTexture.bottom / fittedTextureSize.destination.height;
  172 +
  173 + /// this rectangle can be send to native code and used to cut out a rectangle of the scan image
  174 + return Rect.fromLTRB(
  175 + percentageLeft,
  176 + percentageTop,
  177 + percentageRight,
  178 + percentagebottom,
  179 + );
  180 + }
  181 +
120 @override 182 @override
121 Widget build(BuildContext context) { 183 Widget build(BuildContext context) {
122 - return ValueListenableBuilder<MobileScannerArguments?>(  
123 - valueListenable: _controller.startArguments,  
124 - builder: (context, value, child) {  
125 - if (value == null) {  
126 - return widget.placeholderBuilder?.call(context, child) ??  
127 - const ColoredBox(color: Colors.black);  
128 - }  
129 -  
130 - return ClipRect(  
131 - child: LayoutBuilder(  
132 - builder: (_, constraints) {  
133 - return SizedBox.fromSize(  
134 - size: constraints.biggest,  
135 - child: FittedBox(  
136 - fit: widget.fit,  
137 - child: SizedBox(  
138 - width: value.size.width,  
139 - height: value.size.height,  
140 - child: kIsWeb  
141 - ? HtmlElementView(viewType: value.webId!)  
142 - : Texture(textureId: value.textureId!),  
143 - ),  
144 - ), 184 + return LayoutBuilder(
  185 + builder: (context, constraints) {
  186 + return ValueListenableBuilder<MobileScannerArguments?>(
  187 + valueListenable: _controller.startArguments,
  188 + builder: (context, value, child) {
  189 + if (value == null) {
  190 + return widget.placeholderBuilder?.call(context, child) ??
  191 + const ColoredBox(color: Colors.black);
  192 + }
  193 +
  194 + if (widget.scanWindow != null) {
  195 + final window = calculateScanWindowRelativeToTextureInPercentage(
  196 + widget.fit,
  197 + widget.scanWindow!,
  198 + value.size,
  199 + Size(constraints.maxWidth, constraints.maxHeight),
145 ); 200 );
146 - },  
147 - ), 201 + _controller.updateScanWindow(window);
  202 + }
  203 +
  204 + return ClipRect(
  205 + child: LayoutBuilder(
  206 + builder: (_, constraints) {
  207 + return SizedBox.fromSize(
  208 + size: constraints.biggest,
  209 + child: FittedBox(
  210 + fit: widget.fit,
  211 + child: SizedBox(
  212 + width: value.size.width,
  213 + height: value.size.height,
  214 + child: kIsWeb
  215 + ? HtmlElementView(viewType: value.webId!)
  216 + : Texture(textureId: value.textureId!),
  217 + ),
  218 + ),
  219 + );
  220 + },
  221 + ),
  222 + );
  223 + },
148 ); 224 );
149 }, 225 },
150 ); 226 );
@@ -126,6 +126,15 @@ class MobileScannerController { @@ -126,6 +126,15 @@ class MobileScannerController {
126 arguments['speed'] = detectionSpeed.index; 126 arguments['speed'] = detectionSpeed.index;
127 arguments['timeout'] = detectionTimeoutMs; 127 arguments['timeout'] = detectionTimeoutMs;
128 128
  129 + /* if (scanWindow != null) {
  130 + arguments['scanWindow'] = [
  131 + scanWindow!.left,
  132 + scanWindow!.top,
  133 + scanWindow!.right,
  134 + scanWindow!.bottom,
  135 + ];
  136 + } */
  137 +
129 if (formats != null) { 138 if (formats != null) {
130 if (kIsWeb || Platform.isIOS || Platform.isMacOS) { 139 if (kIsWeb || Platform.isIOS || Platform.isMacOS) {
131 arguments['formats'] = formats!.map((e) => e.rawValue).toList(); 140 arguments['formats'] = formats!.map((e) => e.rawValue).toList();
@@ -317,6 +326,8 @@ class MobileScannerController { @@ -317,6 +326,8 @@ class MobileScannerController {
317 BarcodeCapture( 326 BarcodeCapture(
318 barcodes: parsed, 327 barcodes: parsed,
319 image: event['image'] as Uint8List?, 328 image: event['image'] as Uint8List?,
  329 + width: event['width'] as double?,
  330 + height: event['height'] as double?,
320 ), 331 ),
321 ); 332 );
322 break; 333 break;
@@ -355,4 +366,10 @@ class MobileScannerController { @@ -355,4 +366,10 @@ class MobileScannerController {
355 throw UnimplementedError(name as String?); 366 throw UnimplementedError(name as String?);
356 } 367 }
357 } 368 }
  369 +
  370 + /// updates the native scanwindow
  371 + Future<void> updateScanWindow(Rect window) async {
  372 + final data = [window.left, window.top, window.right, window.bottom];
  373 + await _methodChannel.invokeMethod('updateScanWindow', {'rect': data});
  374 + }
358 } 375 }
@@ -12,8 +12,14 @@ class BarcodeCapture { @@ -12,8 +12,14 @@ class BarcodeCapture {
12 12
13 final Uint8List? image; 13 final Uint8List? image;
14 14
  15 + final double? width;
  16 +
  17 + final double? height;
  18 +
15 BarcodeCapture({ 19 BarcodeCapture({
16 required this.barcodes, 20 required this.barcodes,
17 this.image, 21 this.image,
  22 + this.width,
  23 + this.height,
18 }); 24 });
19 } 25 }
@@ -20,6 +20,11 @@ class Code { @@ -20,6 +20,11 @@ class Code {
20 external Uint8ClampedList get binaryData; 20 external Uint8ClampedList get binaryData;
21 } 21 }
22 22
  23 +/// Barcode reader that uses jsQR library.
  24 +/// jsQR supports only QR codes format.
  25 +///
  26 +/// Include jsQR to your index.html file:
  27 +/// <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
23 class JsQrCodeReader extends WebBarcodeReaderBase 28 class JsQrCodeReader extends WebBarcodeReaderBase
24 with InternalStreamCreation, InternalTorchDetection { 29 with InternalStreamCreation, InternalTorchDetection {
25 JsQrCodeReader({required super.videoContainer}); 30 JsQrCodeReader({required super.videoContainer});
@@ -168,6 +168,10 @@ extension JsZXingBrowserMultiFormatReaderExt @@ -168,6 +168,10 @@ extension JsZXingBrowserMultiFormatReaderExt
168 external MediaStream? stream; 168 external MediaStream? stream;
169 } 169 }
170 170
  171 +/// Barcode reader that uses zxing-js library.
  172 +///
  173 +/// Include zxing-js to your index.html file:
  174 +/// <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>
171 class ZXingBarcodeReader extends WebBarcodeReaderBase 175 class ZXingBarcodeReader extends WebBarcodeReaderBase
172 with InternalStreamCreation, InternalTorchDetection { 176 with InternalStreamCreation, InternalTorchDetection {
173 JsZXingBrowserMultiFormatReader? _reader; 177 JsZXingBrowserMultiFormatReader? _reader;
1 import AVFoundation 1 import AVFoundation
2 import FlutterMacOS 2 import FlutterMacOS
3 import Vision 3 import Vision
  4 +import UIKit
4 5
5 public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate { 6 public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {
6 7
@@ -20,6 +21,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -20,6 +21,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
20 21
21 // Image to be sent to the texture 22 // Image to be sent to the texture
22 var latestBuffer: CVImageBuffer! 23 var latestBuffer: CVImageBuffer!
  24 +
  25 + // optional window to limit scan search
  26 + var scanWindow: CGRect?
23 27
24 28
25 // var analyzeMode: Int = 0 29 // var analyzeMode: Int = 0
@@ -57,6 +61,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -57,6 +61,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
57 // switchAnalyzeMode(call, result) 61 // switchAnalyzeMode(call, result)
58 case "stop": 62 case "stop":
59 stop(result) 63 stop(result)
  64 + case "updateScanWindow":
  65 + updateScanWindow(call)
60 default: 66 default:
61 result(FlutterMethodNotImplemented) 67 result(FlutterMethodNotImplemented)
62 } 68 }
@@ -109,10 +115,17 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -109,10 +115,17 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
109 try imageRequestHandler.perform([VNDetectBarcodesRequest { (request, error) in 115 try imageRequestHandler.perform([VNDetectBarcodesRequest { (request, error) in
110 if error == nil { 116 if error == nil {
111 if let results = request.results as? [VNBarcodeObservation] { 117 if let results = request.results as? [VNBarcodeObservation] {
112 - for barcode in results {  
113 - let barcodeType = String(barcode.symbology.rawValue).replacingOccurrences(of: "VNBarcodeSymbology", with: "")  
114 - let event: [String: Any?] = ["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcodeType]]  
115 - self.sink?(event) 118 + for barcode in results {
  119 + if scanWindow != nil {
  120 + let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image)
  121 + if (!match) {
  122 + continue
  123 + }
  124 + }
  125 +
  126 + let barcodeType = String(barcode.symbology.rawValue).replacingOccurrences(of: "VNBarcodeSymbology", with: "")
  127 + let event: [String: Any?] = ["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcodeType]]
  128 + self.sink?(event)
116 129
117 // if barcodeType == "QR" { 130 // if barcodeType == "QR" {
118 // let image = CIImage(image: source) 131 // let image = CIImage(image: source)
@@ -158,6 +171,38 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -158,6 +171,38 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
158 } 171 }
159 } 172 }
160 173
  174 + func updateScanWindow(_ call: FlutterMethodCall) {
  175 + let argReader = MapArgumentReader(call.arguments as? [String: Any])
  176 + let scanWindowData: Array? = argReader.floatArray(key: "rect")
  177 +
  178 + if (scanWindowData == nil) {
  179 + return
  180 + }
  181 +
  182 + let minX = scanWindowData![0]
  183 + let minY = scanWindowData![1]
  184 +
  185 + let width = scanWindowData![2] - minX
  186 + let height = scanWindowData![3] - minY
  187 +
  188 + scanWindow = CGRect(x: minX, y: minY, width: width, height: height)
  189 + }
  190 +
  191 + func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: Barcode, _ inputImage: UIImage) -> Bool {
  192 + let barcodeBoundingBox = barcode.frame
  193 +
  194 + let imageWidth = inputImage.size.width;
  195 + let imageHeight = inputImage.size.height;
  196 +
  197 + let minX = scanWindow.minX * imageWidth
  198 + let minY = scanWindow.minY * imageHeight
  199 + let width = scanWindow.width * imageWidth
  200 + let height = scanWindow.height * imageHeight
  201 +
  202 + let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height)
  203 + return scaledScanWindow.contains(barcodeBoundingBox)
  204 + }
  205 +
161 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 206 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
162 if (device != nil) { 207 if (device != nil) {
163 result(FlutterError(code: "MobileScanner", 208 result(FlutterError(code: "MobileScanner",
@@ -318,5 +363,9 @@ class MapArgumentReader { @@ -318,5 +363,9 @@ class MapArgumentReader {
318 func stringArray(key: String) -> [String]? { 363 func stringArray(key: String) -> [String]? {
319 return args?[key] as? [String] 364 return args?[key] as? [String]
320 } 365 }
  366 +
  367 + func floatArray(key: String) -> [CGFloat]? {
  368 + return args?[key] as? [CGFloat]
  369 + }
321 370
322 } 371 }