Julian Steenbakker
Committed by GitHub

Merge branch 'master' into pavel/ios-torch

@@ -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.
@@ -22,6 +23,8 @@ Features: @@ -22,6 +23,8 @@ Features:
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.
24 * [iOS] Support `torchEnabled` parameter from MobileScannerController() on iOS 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`
25 28
26 Fixes: 29 Fixes:
27 * 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.
@@ -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";
@@ -77,33 +77,41 @@ class _BarcodeScannerWithControllerState @@ -77,33 +77,41 @@ class _BarcodeScannerWithControllerState
77 child: Row( 77 child: Row(
78 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 78 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
79 children: [ 79 children: [
80 - IconButton(  
81 - color: Colors.white,  
82 - icon: ValueListenableBuilder(  
83 - valueListenable: controller.torchState,  
84 - builder: (context, state, child) {  
85 - if (state == null) {  
86 - return const Icon(  
87 - Icons.flash_off,  
88 - color: Colors.grey,  
89 - );  
90 - }  
91 - switch (state as TorchState) {  
92 - case TorchState.off:  
93 - return const Icon(  
94 - Icons.flash_off,  
95 - color: Colors.grey,  
96 - );  
97 - case TorchState.on:  
98 - return const Icon(  
99 - Icons.flash_on,  
100 - color: Colors.yellow,  
101 - );  
102 - }  
103 - },  
104 - ),  
105 - iconSize: 32.0,  
106 - onPressed: () => controller.toggleTorch(), 80 + ValueListenableBuilder(
  81 + valueListenable: controller.hasTorchState,
  82 + builder: (context, state, child) {
  83 + if (state != true) {
  84 + return const SizedBox.shrink();
  85 + }
  86 + return IconButton(
  87 + color: Colors.white,
  88 + icon: ValueListenableBuilder(
  89 + valueListenable: controller.torchState,
  90 + builder: (context, state, child) {
  91 + if (state == null) {
  92 + return const Icon(
  93 + Icons.flash_off,
  94 + color: Colors.grey,
  95 + );
  96 + }
  97 + switch (state as TorchState) {
  98 + case TorchState.off:
  99 + return const Icon(
  100 + Icons.flash_off,
  101 + color: Colors.grey,
  102 + );
  103 + case TorchState.on:
  104 + return const Icon(
  105 + Icons.flash_on,
  106 + color: Colors.yellow,
  107 + );
  108 + }
  109 + },
  110 + ),
  111 + iconSize: 32.0,
  112 + onPressed: () => controller.toggleTorch(),
  113 + );
  114 + },
107 ), 115 ),
108 IconButton( 116 IconButton(
109 color: Colors.white, 117 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 +}
@@ -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 );
@@ -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,15 +11,46 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -10,15 +11,46 @@ 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])
@@ -51,6 +83,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -51,6 +83,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
51 toggleTorch(call, result) 83 toggleTorch(call, result)
52 case "analyzeImage": 84 case "analyzeImage":
53 analyzeImage(call, result) 85 analyzeImage(call, result)
  86 + case "updateScanWindow":
  87 + updateScanWindow(call, result)
54 default: 88 default:
55 result(FlutterMethodNotImplemented) 89 result(FlutterMethodNotImplemented)
56 } 90 }
@@ -66,7 +100,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -66,7 +100,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
66 100
67 let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} 101 let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
68 var barcodeOptions: BarcodeScannerOptions? = nil 102 var barcodeOptions: BarcodeScannerOptions? = nil
69 - 103 +
70 if (formatList.count != 0) { 104 if (formatList.count != 0) {
71 var barcodeFormats: BarcodeFormat = [] 105 var barcodeFormats: BarcodeFormat = []
72 for index in formats { 106 for index in formats {
@@ -125,6 +159,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -125,6 +159,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
125 result(nil) 159 result(nil)
126 } 160 }
127 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 +
128 /// Analyzes a single image 184 /// Analyzes a single image
129 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 185 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
130 let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") 186 let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
@@ -35,6 +35,17 @@ class MobileScannerWebPlugin { @@ -35,6 +35,17 @@ class MobileScannerWebPlugin {
35 35
36 static final html.DivElement vidDiv = html.DivElement(); 36 static final html.DivElement vidDiv = html.DivElement();
37 37
  38 + /// Represents barcode reader library.
  39 + /// Change this property if you want to use a custom implementation.
  40 + ///
  41 + /// Example of using the jsQR library:
  42 + /// void main() {
  43 + /// if (kIsWeb) {
  44 + /// MobileScannerWebPlugin.barCodeReader =
  45 + /// JsQrCodeReader(videoContainer: MobileScannerWebPlugin.vidDiv);
  46 + /// }
  47 + /// runApp(const MaterialApp(home: MyHome()));
  48 + /// }
38 static WebBarcodeReaderBase barCodeReader = 49 static WebBarcodeReaderBase barCodeReader =
39 ZXingBarcodeReader(videoContainer: vidDiv); 50 ZXingBarcodeReader(videoContainer: vidDiv);
40 StreamSubscription? _barCodeStreamSubscription; 51 StreamSubscription? _barCodeStreamSubscription;
@@ -82,11 +93,12 @@ class MobileScannerWebPlugin { @@ -82,11 +93,12 @@ class MobileScannerWebPlugin {
82 93
83 // Check if stream is running 94 // Check if stream is running
84 if (barCodeReader.isStarted) { 95 if (barCodeReader.isStarted) {
  96 + final hasTorch = await barCodeReader.hasTorch();
85 return { 97 return {
86 'ViewID': viewID, 98 'ViewID': viewID,
87 'videoWidth': barCodeReader.videoWidth, 99 'videoWidth': barCodeReader.videoWidth,
88 'videoHeight': barCodeReader.videoHeight, 100 'videoHeight': barCodeReader.videoHeight,
89 - 'torchable': barCodeReader.hasTorch, 101 + 'torchable': hasTorch,
90 }; 102 };
91 } 103 }
92 try { 104 try {
@@ -106,12 +118,17 @@ class MobileScannerWebPlugin { @@ -106,12 +118,17 @@ class MobileScannerWebPlugin {
106 }); 118 });
107 } 119 }
108 }); 120 });
  121 + final hasTorch = await barCodeReader.hasTorch();
  122 +
  123 + if (hasTorch && arguments.containsKey('torch')) {
  124 + barCodeReader.toggleTorch(enabled: arguments['torch'] as bool);
  125 + }
109 126
110 return { 127 return {
111 'ViewID': viewID, 128 'ViewID': viewID,
112 'videoWidth': barCodeReader.videoWidth, 129 'videoWidth': barCodeReader.videoWidth,
113 'videoHeight': barCodeReader.videoHeight, 130 'videoHeight': barCodeReader.videoHeight,
114 - 'torchable': barCodeReader.hasTorch, 131 + 'torchable': hasTorch,
115 }; 132 };
116 } catch (e) { 133 } catch (e) {
117 throw PlatformException(code: 'MobileScannerWeb', message: '$e'); 134 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 +}
@@ -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 );
@@ -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,6 +126,15 @@ class MobileScannerController { @@ -124,6 +126,15 @@ 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) { 139 if (Platform.isAndroid) {
129 arguments['formats'] = formats!.map((e) => e.index).toList(); 140 arguments['formats'] = formats!.map((e) => e.index).toList();
@@ -210,8 +221,9 @@ class MobileScannerController { @@ -210,8 +221,9 @@ class MobileScannerController {
210 ); 221 );
211 } 222 }
212 223
213 - _hasTorch = startResult['torchable'] as bool? ?? false;  
214 - if (_hasTorch! && torchEnabled) { 224 + final hasTorch = startResult['torchable'] as bool? ?? false;
  225 + hasTorchState.value = hasTorch;
  226 + if (hasTorch && torchEnabled) {
215 torchState.value = TorchState.on; 227 torchState.value = TorchState.on;
216 } 228 }
217 229
@@ -223,7 +235,7 @@ class MobileScannerController { @@ -223,7 +235,7 @@ class MobileScannerController {
223 startResult['videoHeight'] as double? ?? 0, 235 startResult['videoHeight'] as double? ?? 0,
224 ) 236 )
225 : toSize(startResult['size'] as Map? ?? {}), 237 : toSize(startResult['size'] as Map? ?? {}),
226 - hasTorch: _hasTorch!, 238 + hasTorch: hasTorch,
227 textureId: kIsWeb ? null : startResult['textureId'] as int?, 239 textureId: kIsWeb ? null : startResult['textureId'] as int?,
228 webId: kIsWeb ? startResult['ViewID'] as String? : null, 240 webId: kIsWeb ? startResult['ViewID'] as String? : null,
229 ); 241 );
@@ -244,7 +256,7 @@ class MobileScannerController { @@ -244,7 +256,7 @@ class MobileScannerController {
244 /// 256 ///
245 /// Throws if the controller was not initialized. 257 /// Throws if the controller was not initialized.
246 Future<void> toggleTorch() async { 258 Future<void> toggleTorch() async {
247 - final hasTorch = _hasTorch; 259 + final hasTorch = hasTorchState.value;
248 260
249 if (hasTorch == null) { 261 if (hasTorch == null) {
250 throw const MobileScannerException( 262 throw const MobileScannerException(
@@ -314,6 +326,8 @@ class MobileScannerController { @@ -314,6 +326,8 @@ class MobileScannerController {
314 BarcodeCapture( 326 BarcodeCapture(
315 barcodes: parsed, 327 barcodes: parsed,
316 image: event['image'] as Uint8List?, 328 image: event['image'] as Uint8List?,
  329 + width: event['width'] as double?,
  330 + height: event['height'] as double?,
317 ), 331 ),
318 ); 332 );
319 break; 333 break;
@@ -350,4 +364,10 @@ class MobileScannerController { @@ -350,4 +364,10 @@ class MobileScannerController {
350 throw UnimplementedError(name as String?); 364 throw UnimplementedError(name as String?);
351 } 365 }
352 } 366 }
  367 +
  368 + /// updates the native scanwindow
  369 + Future<void> updateScanWindow(Rect window) async {
  370 + final data = [window.left, window.top, window.right, window.bottom];
  371 + await _methodChannel.invokeMethod('updateScanWindow', {'rect': data});
  372 + }
353 } 373 }
@@ -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';
@@ -8,7 +10,7 @@ import 'package:mobile_scanner/src/web/media.dart'; @@ -8,7 +10,7 @@ import 'package:mobile_scanner/src/web/media.dart';
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; 12 final Duration frameInterval;
11 - final DivElement videoContainer; 13 + final html.DivElement videoContainer;
12 14
13 const WebBarcodeReaderBase({ 15 const WebBarcodeReaderBase({
14 required this.videoContainer, 16 required this.videoContainer,
@@ -35,24 +37,24 @@ abstract class WebBarcodeReaderBase { @@ -35,24 +37,24 @@ abstract class WebBarcodeReaderBase {
35 Future<void> toggleTorch({required bool enabled}); 37 Future<void> toggleTorch({required bool enabled});
36 38
37 /// Determine whether device has flash 39 /// Determine whether device has flash
38 - bool get hasTorch; 40 + Future<bool> hasTorch();
39 } 41 }
40 42
41 mixin InternalStreamCreation on WebBarcodeReaderBase { 43 mixin InternalStreamCreation on WebBarcodeReaderBase {
42 /// The video stream. 44 /// The video stream.
43 /// Will be initialized later to see which camera needs to be used. 45 /// Will be initialized later to see which camera needs to be used.
44 - MediaStream? localMediaStream;  
45 - final VideoElement video = VideoElement(); 46 + html.MediaStream? localMediaStream;
  47 + final html.VideoElement video = html.VideoElement();
46 48
47 @override 49 @override
48 int get videoWidth => video.videoWidth; 50 int get videoWidth => video.videoWidth;
49 @override 51 @override
50 int get videoHeight => video.videoHeight; 52 int get videoHeight => video.videoHeight;
51 53
52 - Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async { 54 + Future<html.MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
53 // Check if browser supports multiple camera's and set if supported 55 // Check if browser supports multiple camera's and set if supported
54 final Map? capabilities = 56 final Map? capabilities =
55 - window.navigator.mediaDevices?.getSupportedConstraints(); 57 + html.window.navigator.mediaDevices?.getSupportedConstraints();
56 final Map<String, dynamic> constraints; 58 final Map<String, dynamic> constraints;
57 if (capabilities != null && capabilities['facingMode'] as bool) { 59 if (capabilities != null && capabilities['facingMode'] as bool) {
58 constraints = { 60 constraints = {
@@ -65,15 +67,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { @@ -65,15 +67,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase {
65 constraints = {'video': true}; 67 constraints = {'video': true};
66 } 68 }
67 final stream = 69 final stream =
68 - await window.navigator.mediaDevices?.getUserMedia(constraints); 70 + await html.window.navigator.mediaDevices?.getUserMedia(constraints);
69 return stream; 71 return stream;
70 } 72 }
71 73
72 - void prepareVideoElement(VideoElement videoSource); 74 + void prepareVideoElement(html.VideoElement videoSource);
73 75
74 Future<void> attachStreamToVideo( 76 Future<void> attachStreamToVideo(
75 - MediaStream stream,  
76 - VideoElement videoSource, 77 + html.MediaStream stream,
  78 + html.VideoElement videoSource,
77 ); 79 );
78 80
79 @override 81 @override
@@ -96,19 +98,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { @@ -96,19 +98,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase {
96 98
97 /// Mixin for libraries that don't have built-in torch support 99 /// Mixin for libraries that don't have built-in torch support
98 mixin InternalTorchDetection on InternalStreamCreation { 100 mixin InternalTorchDetection on InternalStreamCreation {
  101 + Future<List<String>> getSupportedTorchStates() async {
  102 + try {
  103 + final track = localMediaStream?.getVideoTracks();
  104 + if (track != null) {
  105 + final imageCapture = ImageCapture(track.first);
  106 + final photoCapabilities = await promiseToFuture<PhotoCapabilities>(
  107 + imageCapture.getPhotoCapabilities(),
  108 + );
  109 + final fillLightMode = photoCapabilities.fillLightMode;
  110 + if (fillLightMode != null) {
  111 + return fillLightMode;
  112 + }
  113 + }
  114 + } catch (e) {
  115 + // ImageCapture is not supported by some browsers:
  116 + // https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture#browser_compatibility
  117 + }
  118 + return [];
  119 + }
  120 +
99 @override 121 @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; 122 + Future<bool> hasTorch() async {
  123 + return (await getSupportedTorchStates()).isNotEmpty;
108 } 124 }
109 125
110 @override 126 @override
111 Future<void> toggleTorch({required bool enabled}) async { 127 Future<void> toggleTorch({required bool enabled}) async {
  128 + final hasTorch = await this.hasTorch();
112 if (hasTorch) { 129 if (hasTorch) {
113 final track = localMediaStream?.getVideoTracks(); 130 final track = localMediaStream?.getVideoTracks();
114 await track?.first.applyConstraints({ 131 await track?.first.applyConstraints({
@@ -119,3 +136,25 @@ mixin InternalTorchDetection on InternalStreamCreation { @@ -119,3 +136,25 @@ mixin InternalTorchDetection on InternalStreamCreation {
119 } 136 }
120 } 137 }
121 } 138 }
  139 +
  140 +@JS('Promise')
  141 +@staticInterop
  142 +class Promise<T> {}
  143 +
  144 +@JS()
  145 +@anonymous
  146 +class PhotoCapabilities {
  147 + /// Returns an array of available fill light options. Options include auto, off, or flash.
  148 + external List<String>? get fillLightMode;
  149 +}
  150 +
  151 +@JS('ImageCapture')
  152 +@staticInterop
  153 +class ImageCapture {
  154 + /// MediaStreamTrack
  155 + external factory ImageCapture(dynamic track);
  156 +}
  157 +
  158 +extension ImageCaptureExt on ImageCapture {
  159 + external Promise<PhotoCapabilities> getPhotoCapabilities();
  160 +}
@@ -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});
@@ -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 {
@@ -134,6 +130,10 @@ extension JsZXingBrowserMultiFormatReaderExt @@ -134,6 +130,10 @@ extension JsZXingBrowserMultiFormatReaderExt
134 external MediaStream? stream; 130 external MediaStream? stream;
135 } 131 }
136 132
  133 +/// Barcode reader that uses zxing-js library.
  134 +///
  135 +/// Include zxing-js to your index.html file:
  136 +/// <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>
137 class ZXingBarcodeReader extends WebBarcodeReaderBase 137 class ZXingBarcodeReader extends WebBarcodeReaderBase
138 with InternalStreamCreation, InternalTorchDetection { 138 with InternalStreamCreation, InternalTorchDetection {
139 late final JsZXingBrowserMultiFormatReader _reader = 139 late final 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 }