Julian Steenbakker
Committed by GitHub

Merge pull request #176 from casvanluijtelaar/master

Add scanWindow to optionally limit scan area
... ... @@ -4,9 +4,12 @@ import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import android.util.Log
import android.util.Size
import android.media.Image
import android.view.Surface
import androidx.annotation.NonNull
import androidx.camera.core.*
... ... @@ -18,12 +21,14 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.common.InputImage.IMAGE_FORMAT_NV21
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
import java.io.File
import kotlin.math.roundToInt
class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)
... ... @@ -40,6 +45,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
private var camera: Camera? = null
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
private var scanWindow: List<Float>? = null;
// @AnalyzeMode
// private var analyzeMode: Int = AnalyzeMode.NONE
... ... @@ -54,6 +60,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
// "analyze" -> switchAnalyzeMode(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
"updateScanWindow" -> updateScanWindow(call)
else -> result.notImplemented()
}
}
... ... @@ -99,11 +106,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
// when (analyzeMode) {
// AnalyzeMode.BARCODE -> {
val mediaImage = imageProxy.image ?: return@Analyzer
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
var inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
if(scanWindow != null) {
val match = isbarCodeInScanWindow(scanWindow!!, barcode, inputImage)
if(!match) continue
}
val event = mapOf("name" to "barcode", "data" to barcode.data)
sink?.success(event)
}
... ... @@ -115,9 +128,32 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
// }
}
private var scanner = BarcodeScanning.getClient()
private fun updateScanWindow(call: MethodCall) {
scanWindow = call.argument<List<Float>>("rect")
}
// scales the scanWindow to the provided inputImage and checks if that scaled
// scanWindow contains the barcode
private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: InputImage): Boolean {
val barcodeBoundingBox = barcode.getBoundingBox()
if(barcodeBoundingBox == null) return false
val imageWidth = inputImage.getWidth();
val imageHeight = inputImage.getHeight();
val left = (scanWindow[0] * imageWidth).roundToInt()
val top = (scanWindow[1] * imageHeight).roundToInt()
val right = (scanWindow[2] * imageWidth).roundToInt()
val bottom = (scanWindow[3] * imageHeight).roundToInt()
val scaledScanWindow = Rect(left, top, right, bottom)
return scaledScanWindow.contains(barcodeBoundingBox)
}
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
... ... @@ -130,7 +166,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
result.success(answer)
} else {
val facing: Int = call.argument<Int>("facing") ?: 0
val ratio: Int? = call.argument<Int>("ratio")
val ratio: Int = call.argument<Int>("ratio") ?: 1
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val formats: List<Int>? = call.argument<List<Int>>("formats")
... ... @@ -161,6 +197,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
result.error("textureEntry", "textureEntry is null", null)
return@addListener
}
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val texture = textureEntry!!.surfaceTexture()
... ... @@ -171,17 +208,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
// Build the preview to be shown on the Flutter texture
val previewBuilder = Preview.Builder()
if (ratio != null) {
previewBuilder.setTargetAspectRatio(ratio)
}
.setTargetAspectRatio(ratio)
preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) }
// Build the analyzer to be passed on to MLKit
val analysisBuilder = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
if (ratio != null) {
analysisBuilder.setTargetAspectRatio(ratio)
}
.setTargetAspectRatio(ratio)
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) }
// Select the correct camera
... ... @@ -191,6 +226,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
Log.i("LOG", "Analyzer: $analysisSize")
Log.i("LOG", "Preview: $previewSize")
... ... @@ -241,6 +277,11 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
if(scanWindow != null) {
val match = isbarCodeInScanWindow(scanWindow!!, barcode, inputImage)
if(!match) continue
}
barcodeFound = true
sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
}
... ...
... ... @@ -354,16 +354,19 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
DEVELOPMENT_TEAM = QAJQ4586J2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScannerExample;
PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
... ... @@ -483,16 +486,19 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
DEVELOPMENT_TEAM = QAJQ4586J2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScannerExample;
PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
... ... @@ -506,16 +512,19 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
DEVELOPMENT_TEAM = QAJQ4586J2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScannerExample;
PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
... ...
... ... @@ -2,10 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access in order to open photos of barcodes</string>
<key>NSCameraUsageDescription</key>
<string>We use the camera to scan barcodes</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
... ... @@ -28,6 +26,10 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>We use the camera to scan barcodes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access in order to open photos of barcodes</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
... ... @@ -47,7 +49,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeScannerWithScanWindow extends StatefulWidget {
const BarcodeScannerWithScanWindow({Key? key}) : super(key: key);
@override
_BarcodeScannerWithScanWindowState createState() =>
_BarcodeScannerWithScanWindowState();
}
class _BarcodeScannerWithScanWindowState
extends State<BarcodeScannerWithScanWindow> {
late MobileScannerController controller;
String? barcode;
@override
void initState() {
super.initState();
controller = MobileScannerController();
restart();
}
Future<void> restart() async {
// await controller.stop();
await controller.start();
}
Future<void> onDetect(Barcode barcode, MobileScannerArguments? _) async {
setState(() => this.barcode = barcode.rawValue);
await Future.delayed(const Duration(seconds: 1));
setState(() => this.barcode = '');
}
@override
Widget build(BuildContext context) {
final scanWindow = Rect.fromCenter(
center: MediaQuery.of(context).size.center(Offset.zero),
width: 200,
height: 200,
);
return Scaffold(
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
fit: BoxFit.cover,
scanWindow: scanWindow,
controller: controller,
onDetect: onDetect,
allowDuplicates: true,
),
CustomPaint(
painter: ScannerOverlay(scanWindow),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 120,
height: 50,
child: FittedBox(
child: Text(
barcode ?? 'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headline4!
.copyWith(color: Colors.white),
),
),
),
),
],
),
),
),
],
);
},
),
);
}
}
class ScannerOverlay extends CustomPainter {
ScannerOverlay(this.scanWindow);
final Rect scanWindow;
@override
void paint(Canvas canvas, Size size) {
final backgroundPath = Path()..addRect(Rect.largest);
final cutoutPath = Path()..addRect(scanWindow);
final backgroundPaint = Paint()
..color = Colors.black.withOpacity(0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
final backgroundWithCutout = Path.combine(
PathOperation.difference,
backgroundPath,
cutoutPath,
);
canvas.drawPath(backgroundWithCutout, backgroundPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_window.dart';
import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
void main() => runApp(const MaterialApp(home: MyHome()));
... ... @@ -31,6 +32,16 @@ class MyHome extends StatelessWidget {
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BarcodeScannerWithScanWindow(),
),
);
},
child: const Text('MobileScanner with ScanWindow'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const BarcodeScannerWithoutController(),
),
... ...
... ... @@ -2,6 +2,7 @@ import AVFoundation
import Flutter
import MLKitVision
import MLKitBarcodeScanning
import UIKit
public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {
... ... @@ -26,6 +27,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
var analyzing: Bool = false
var position = AVCaptureDevice.Position.back
var scanWindow: CGRect?
var scanner = BarcodeScanner.barcodeScanner()
public static func register(with registrar: FlutterPluginRegistrar) {
... ... @@ -61,6 +64,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
stop(result)
case "analyzeImage":
analyzeImage(call, result)
case "updateScanWindow":
updateScanWindow(call)
default:
result(FlutterMethodNotImplemented)
}
... ... @@ -99,7 +104,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
}
analyzing = true
let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)
let image = VisionImage(image: buffer!.image)
var image = VisionImage(image: buffer!.image)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait
... ... @@ -108,6 +114,14 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
scanner.process(image) { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
if scanWindow != nil {
let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image)
if (!match) {
continue
}
}
let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
sink?(event)
}
... ... @@ -155,6 +169,38 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
func updateScanWindow(_ call: FlutterMethodCall) {
let argReader = MapArgumentReader(call.arguments as? [String: Any])
let scanWindowData: Array? = argReader.floatArray(key: "rect")
if (scanWindowData == nil) {
return
}
let minX = scanWindowData![0]
let minY = scanWindowData![1]
let width = scanWindowData![2] - minX
let height = scanWindowData![3] - minY
scanWindow = CGRect(x: minX, y: minY, width: width, height: height)
}
func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: Barcode, _ inputImage: UIImage) -> Bool {
let barcodeBoundingBox = barcode.frame
let imageWidth = inputImage.size.width;
let imageHeight = inputImage.size.height;
let minX = scanWindow.minX * imageWidth
let minY = scanWindow.minY * imageHeight
let width = scanWindow.width * imageWidth
let height = scanWindow.height * imageHeight
let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height)
return scaledScanWindow.contains(barcodeBoundingBox)
}
func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
if (device != nil) {
result(FlutterError(code: "MobileScanner",
... ... @@ -229,6 +275,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
videoOutput.alwaysDiscardsLateVideoFrames = true
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
captureSession.addOutput(videoOutput)
for connection in videoOutput.connections {
connection.videoOrientation = .portrait
... ... @@ -238,6 +285,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
}
captureSession.commitConfiguration()
captureSession.startRunning()
let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
let width = Double(demensions.height)
let height = Double(demensions.width)
... ... @@ -289,6 +337,14 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
scanner.process(image) { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
if scanWindow != nil {
let match = isbarCodeInScanWindow(scanWindow!, barcode, uiImage!)
if (!match) {
continue
}
}
barcodeFound = true
let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
sink?(event)
... ... @@ -372,4 +428,8 @@ class MapArgumentReader {
return args?[key] as? [Int]
}
func floatArray(key: String) -> [CGFloat]? {
return args?[key] as? [CGFloat]
}
}
... ...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide applyBoxFit;
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/objects/barcode_utility.dart';
enum Ratio { ratio_4_3, ratio_16_9 }
... ... @@ -27,6 +28,13 @@ class MobileScanner extends StatefulWidget {
/// Set to false if you don't want duplicate scans.
final bool allowDuplicates;
/// if set barcodes will only be scanned if they fall within this [Rect]
/// useful for having a cut-out overlay for example. these [Rect]
/// coordinates are relative to the widget size, so by how much your
/// rectangle overlays the actual image can depend on things like the
/// [BoxFit]
final Rect? scanWindow;
/// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
const MobileScanner({
Key? key,
... ... @@ -34,6 +42,7 @@ class MobileScanner extends StatefulWidget {
this.controller,
this.fit = BoxFit.cover,
this.allowDuplicates = false,
this.scanWindow,
}) : super(key: key);
@override
... ... @@ -67,6 +76,55 @@ class _MobileScannerState extends State<MobileScanner>
String? lastScanned;
/// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible,
/// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize]
///
/// since when using a [BoxFit] the content will always be centered on its parent. we can convert the rect
/// to be relative to the texture.
///
/// since the textures size and the actuall image (on the texture size) might not be the same, we also need to
/// calculate the scanWindow in terms of percentages of the texture, not pixels.
Rect calculateScanWindowRelativeToTextureInPercentage(
BoxFit fit,
Rect scanWindow,
Size textureSize,
Size widgetSize,
) {
/// map the texture size to get its new size after fitted to screen
final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize);
/// create a new rectangle that represents the texture on the screen
final minX = widgetSize.width / 2 - fittedTextureSize.width / 2;
final minY = widgetSize.height / 2 - fittedTextureSize.height / 2;
final textureWindow = Offset(minX, minY) & fittedTextureSize;
/// create a new scan window and with only the area of the rect intersecting the texture window
final scanWindowInTexture = scanWindow.intersect(textureWindow);
/// update the scanWindow left and top to be relative to the texture not the widget
final newLeft = scanWindowInTexture.left - textureWindow.left;
final newTop = scanWindowInTexture.top - textureWindow.top;
final newWidth = scanWindowInTexture.width;
final newHeight = scanWindowInTexture.height;
/// new scanWindow that is adapted to the boxfit and relative to the texture
final windowInTexture = Rect.fromLTWH(newLeft, newTop, newWidth, newHeight);
/// get the scanWindow as a percentage of the texture
final percentageLeft = windowInTexture.left / fittedTextureSize.width;
final percentageTop = windowInTexture.top / fittedTextureSize.height;
final percentageRight = windowInTexture.right / fittedTextureSize.width;
final percentagebottom = windowInTexture.bottom / fittedTextureSize.height;
/// this rectangle can be send to native code and used to cut out a rectangle of the scan image
return Rect.fromLTRB(
percentageLeft,
percentageTop,
percentageRight,
percentagebottom,
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
... ... @@ -78,12 +136,21 @@ class _MobileScannerState extends State<MobileScanner>
if (value == null) {
return Container(color: Colors.black);
} else {
if (widget.scanWindow != null) {
final window = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
value.size,
Size(constraints.maxWidth, constraints.maxHeight),
);
controller.updateScanWindow(window);
}
controller.barcodes.listen((barcode) {
if (!widget.allowDuplicates) {
if (lastScanned != barcode.rawValue) {
if (lastScanned == barcode.rawValue) return;
lastScanned = barcode.rawValue;
widget.onDetect(barcode, value! as MobileScannerArguments);
}
} else {
widget.onDetect(barcode, value! as MobileScannerArguments);
}
... ...
... ... @@ -156,6 +156,14 @@ class MobileScannerController {
// Set the starting arguments for the camera
final Map arguments = {};
arguments['facing'] = facing.index;
/* if (scanWindow != null) {
arguments['scanWindow'] = [
scanWindow!.left,
scanWindow!.top,
scanWindow!.right,
scanWindow!.bottom,
];
} */
if (ratio != null) arguments['ratio'] = ratio;
if (torchEnabled != null) arguments['torch'] = torchEnabled;
... ... @@ -283,4 +291,10 @@ class MobileScannerController {
'MobileScannerController methods should not be used after calling dispose.';
assert(hashCode == _controllerHashcode, message);
}
/// updates the native scanwindow
Future<void> updateScanWindow(Rect window) async {
final data = [window.left, window.top, window.right, window.bottom];
await methodChannel.invokeMethod('updateScanWindow', {'rect': data});
}
}
... ...
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
... ... @@ -147,3 +149,79 @@ WiFi? toWiFi(Map? data) {
return null;
}
}
Size applyBoxFit(BoxFit fit, Size input, Size output) {
if (input.height <= 0.0 ||
input.width <= 0.0 ||
output.height <= 0.0 ||
output.width <= 0.0) {
return Size.zero;
}
Size destination;
final inputAspectRatio = input.width / input.height;
final outputAspectRatio = output.width / output.height;
switch (fit) {
case BoxFit.fill:
destination = output;
break;
case BoxFit.contain:
if (outputAspectRatio > inputAspectRatio) {
destination = Size(
input.width * output.height / input.height,
output.height,
);
} else {
destination = Size(
output.width,
input.height * output.width / input.width,
);
}
break;
case BoxFit.cover:
if (outputAspectRatio > inputAspectRatio) {
destination = Size(
output.width,
input.height * (output.width / input.width),
);
} else {
destination = Size(
input.width * (output.height / input.height),
output.height,
);
}
break;
case BoxFit.fitWidth:
destination = Size(
output.width,
input.height * (output.width / input.width),
);
break;
case BoxFit.fitHeight:
destination = Size(
input.width * (output.height / input.height),
output.height,
);
break;
case BoxFit.none:
destination = Size(
math.min(input.width, output.width),
math.min(input.height, output.height),
);
break;
case BoxFit.scaleDown:
destination = input;
if (destination.height > output.height) {
destination = Size(output.height * inputAspectRatio, output.height);
}
if (destination.width > output.width) {
destination = Size(output.width, output.width / inputAspectRatio);
}
break;
}
return destination;
}
... ...
import AVFoundation
import FlutterMacOS
import Vision
import UIKit
public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {
... ... @@ -21,6 +22,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
// Image to be sent to the texture
var latestBuffer: CVImageBuffer!
// optional window to limit scan search
var scanWindow: CGRect?
// var analyzeMode: Int = 0
var analyzing: Bool = false
... ... @@ -57,6 +61,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
// switchAnalyzeMode(call, result)
case "stop":
stop(result)
case "updateScanWindow":
updateScanWindow(call)
default:
result(FlutterMethodNotImplemented)
}
... ... @@ -110,6 +116,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
if error == nil {
if let results = request.results as? [VNBarcodeObservation] {
for barcode in results {
if scanWindow != nil {
let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image)
if (!match) {
continue
}
}
let barcodeType = String(barcode.symbology.rawValue).replacingOccurrences(of: "VNBarcodeSymbology", with: "")
let event: [String: Any?] = ["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcodeType]]
self.sink?(event)
... ... @@ -158,6 +171,38 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
}
}
func updateScanWindow(_ call: FlutterMethodCall) {
let argReader = MapArgumentReader(call.arguments as? [String: Any])
let scanWindowData: Array? = argReader.floatArray(key: "rect")
if (scanWindowData == nil) {
return
}
let minX = scanWindowData![0]
let minY = scanWindowData![1]
let width = scanWindowData![2] - minX
let height = scanWindowData![3] - minY
scanWindow = CGRect(x: minX, y: minY, width: width, height: height)
}
func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: Barcode, _ inputImage: UIImage) -> Bool {
let barcodeBoundingBox = barcode.frame
let imageWidth = inputImage.size.width;
let imageHeight = inputImage.size.height;
let minX = scanWindow.minX * imageWidth
let minY = scanWindow.minY * imageHeight
let width = scanWindow.width * imageWidth
let height = scanWindow.height * imageHeight
let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height)
return scaledScanWindow.contains(barcodeBoundingBox)
}
func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
if (device != nil) {
result(FlutterError(code: "MobileScanner",
... ... @@ -322,4 +367,8 @@ class MapArgumentReader {
return args?[key] as? [String]
}
func floatArray(key: String) -> [CGFloat]? {
return args?[key] as? [CGFloat]
}
}
... ...