Navaron Bracke
Committed by GitHub

Merge pull request #1169 from navaronbracke/size_fix_android

fix: Fix discrepancy between barcode bounding box and barcode capture size
  1 +## 5.2.2
  2 +
  3 +Improvements:
  4 +* [MacOS] Adds Swift Package Manager support.
  5 +* [MacOS] Adds support for `returnImage`.
  6 +* Added a new `size` property to `Barcode`, that denotes the bounding box of the barcode.
  7 +
  8 +Bugs fixed:
  9 +* Fixed some documentation errors for the `size` and `image` of `BarcodeCapture`.
  10 +* [iOS] Fixed a bug with `returnImage`.
  11 +* [Android/iOS] Adjusted the raw barcode scan value to pass the raw event data, like on MacOS.
  12 +
1 ## 5.2.1 13 ## 5.2.1
2 14
3 * Updates the `package:web` dependency to use a version range. 15 * Updates the `package:web` dependency to use a version range.
@@ -44,7 +56,7 @@ Improvements: @@ -44,7 +56,7 @@ Improvements:
44 This major release contains all the changes from the 5.0.0 beta releases, along with the following changes: 56 This major release contains all the changes from the 5.0.0 beta releases, along with the following changes:
45 57
46 Improvements: 58 Improvements:
47 -- [Android] Remove the Kotlin Standard Library from the dependencies, as it is automatically included in Kotlin 1.4+ 59 +* [Android] Remove the Kotlin Standard Library from the dependencies, as it is automatically included in Kotlin 1.4+
48 60
49 ## 5.0.0-beta.3 61 ## 5.0.0-beta.3
50 **BREAKING CHANGES:** 62 **BREAKING CHANGES:**
@@ -50,9 +50,11 @@ class MobileScannerHandler( @@ -50,9 +50,11 @@ class MobileScannerHandler(
50 barcodeHandler.publishEvent(mapOf( 50 barcodeHandler.publishEvent(mapOf(
51 "name" to "barcode", 51 "name" to "barcode",
52 "data" to barcodes, 52 "data" to barcodes,
53 - "image" to image,  
54 - "width" to width!!.toDouble(),  
55 - "height" to height!!.toDouble() 53 + "image" to mapOf(
  54 + "bytes" to image,
  55 + "width" to width?.toDouble(),
  56 + "height" to height?.toDouble(),
  57 + )
56 )) 58 ))
57 } else { 59 } else {
58 barcodeHandler.publishEvent(mapOf( 60 barcodeHandler.publishEvent(mapOf(
@@ -28,12 +28,22 @@ fun Image.toByteArray(): ByteArray { @@ -28,12 +28,22 @@ fun Image.toByteArray(): ByteArray {
28 28
29 val Barcode.data: Map<String, Any?> 29 val Barcode.data: Map<String, Any?>
30 get() = mapOf( 30 get() = mapOf(
31 - "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,  
32 - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,  
33 - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,  
34 - "driverLicense" to driverLicense?.data, "email" to email?.data,  
35 - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,  
36 - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue 31 + "calendarEvent" to calendarEvent?.data,
  32 + "contactInfo" to contactInfo?.data,
  33 + "corners" to cornerPoints?.map { corner -> corner.data },
  34 + "displayValue" to displayValue,
  35 + "driverLicense" to driverLicense?.data,
  36 + "email" to email?.data,
  37 + "format" to format,
  38 + "geoPoint" to geoPoint?.data,
  39 + "phone" to phone?.data,
  40 + "rawBytes" to rawBytes,
  41 + "rawValue" to rawValue,
  42 + "size" to boundingBox?.size,
  43 + "sms" to sms?.data,
  44 + "type" to valueType,
  45 + "url" to url?.data,
  46 + "wifi" to wifi?.data,
37 ) 47 )
38 48
39 private val Point.data: Map<String, Double> 49 private val Point.data: Map<String, Double>
@@ -92,4 +102,14 @@ private val Barcode.UrlBookmark.data: Map<String, Any?> @@ -92,4 +102,14 @@ private val Barcode.UrlBookmark.data: Map<String, Any?>
92 get() = mapOf("title" to title, "url" to url) 102 get() = mapOf("title" to title, "url" to url)
93 103
94 private val Barcode.WiFi.data: Map<String, Any?> 104 private val Barcode.WiFi.data: Map<String, Any?>
95 - get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)  
  105 + get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
  106 +
  107 +private val Rect.size: Map<String, Any?>
  108 + get() {
  109 + // Rect.isValid can't be accessed for some reason, so just do the check manually.
  110 + if (left <= right && top <= bottom) {
  111 + return mapOf("width" to width().toDouble(), "height" to height().toDouble())
  112 + }
  113 +
  114 + return emptyMap()
  115 + }
1 import UIKit 1 import UIKit
2 import Flutter 2 import Flutter
3 3
4 -@UIApplicationMain 4 +@main
5 @objc class AppDelegate: FlutterAppDelegate { 5 @objc class AppDelegate: FlutterAppDelegate {
6 override func application( 6 override func application(
7 _ application: UIApplication, 7 _ application: UIApplication,
1 import Cocoa 1 import Cocoa
2 import FlutterMacOS 2 import FlutterMacOS
3 3
4 -@NSApplicationMain 4 +@main
5 class AppDelegate: FlutterAppDelegate { 5 class AppDelegate: FlutterAppDelegate {
6 override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 6 override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
7 return true 7 return true
@@ -25,9 +25,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -25,9 +25,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
25 /// Barcode scanner for results 25 /// Barcode scanner for results
26 var scanner = BarcodeScanner.barcodeScanner() 26 var scanner = BarcodeScanner.barcodeScanner()
27 27
28 - /// Return image buffer with the Barcode event  
29 - var returnImage: Bool = false  
30 -  
31 /// Default position of camera 28 /// Default position of camera
32 var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back 29 var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back
33 30
@@ -127,7 +124,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -127,7 +124,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
127 /// Gets called when a new image is added to the buffer 124 /// Gets called when a new image is added to the buffer
128 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 125 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
129 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 126 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
130 - print("Failed to get image buffer from sample buffer.")  
131 return 127 return
132 } 128 }
133 latestBuffer = imageBuffer 129 latestBuffer = imageBuffer
@@ -160,7 +156,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -160,7 +156,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
160 156
161 if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { 157 if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
162 return 158 return
163 - } else if (newScannedBarcodes?.isEmpty == false) { 159 + }
  160 +
  161 + if (newScannedBarcodes?.isEmpty == false) {
164 barcodesString = newScannedBarcodes 162 barcodesString = newScannedBarcodes
165 } 163 }
166 } 164 }
@@ -171,7 +169,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -171,7 +169,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
171 } 169 }
172 170
173 /// Start scanning for barcodes 171 /// Start scanning for barcodes
174 - func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws { 172 + func start(barcodeScannerOptions: BarcodeScannerOptions?, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws {
175 self.detectionSpeed = detectionSpeed 173 self.detectionSpeed = detectionSpeed
176 if (device != nil || captureSession != nil) { 174 if (device != nil || captureSession != nil) {
177 throw MobileScannerError.alreadyStarted 175 throw MobileScannerError.alreadyStarted
@@ -446,17 +444,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -446,17 +444,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
446 444
447 var barcodesString: Array<String?>? 445 var barcodesString: Array<String?>?
448 446
449 - // /// Convert image buffer to jpeg  
450 - // private func ciImageToJpeg(ciImage: CIImage) -> Data {  
451 - //  
452 - // // let ciImage = CIImage(cvPixelBuffer: latestBuffer)  
453 - // let context:CIContext = CIContext.init(options: nil)  
454 - // let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!  
455 - // let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)  
456 - //  
457 - // return uiImage.jpegData(compressionQuality: 0.8)!  
458 - // }  
459 -  
460 /// Rotates images accordingly 447 /// Rotates images accordingly
461 func imageOrientation( 448 func imageOrientation(
462 deviceOrientation: UIDeviceOrientation, 449 deviceOrientation: UIDeviceOrientation,
@@ -11,6 +11,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -11,6 +11,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
11 11
12 /// 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
13 private let barcodeHandler: BarcodeHandler 13 private let barcodeHandler: BarcodeHandler
  14 +
  15 + /// Whether to return the input image with the barcode event.
  16 + /// This is static to avoid accessing `self` in the callback in the constructor.
  17 + private static var returnImage: Bool = false
14 18
15 /// The points for the scan window. 19 /// The points for the scan window.
16 static var scanWindow: [CGFloat]? 20 static var scanWindow: [CGFloat]?
@@ -37,24 +41,48 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -37,24 +41,48 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
37 41
38 init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { 42 init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
39 self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in 43 self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
40 - if barcodes != nil {  
41 - let barcodesMap: [Any?] = barcodes!.compactMap { barcode in  
42 - if (MobileScannerPlugin.scanWindow != nil) {  
43 - if (MobileScannerPlugin.isBarcodeInScanWindow(barcode: barcode, imageSize: image.size)) {  
44 - return barcode.data  
45 - } else {  
46 - return nil  
47 - }  
48 - } else {  
49 - return barcode.data  
50 - } 44 + if error != nil {
  45 + barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
  46 + return
  47 + }
  48 +
  49 + if barcodes == nil {
  50 + return
  51 + }
  52 +
  53 + let barcodesMap: [Any?] = barcodes!.compactMap { barcode in
  54 + if (MobileScannerPlugin.scanWindow == nil) {
  55 + return barcode.data
51 } 56 }
52 - if (!barcodesMap.isEmpty) {  
53 - barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!), "width": image.size.width, "height": image.size.height]) 57 +
  58 + if (MobileScannerPlugin.isBarcodeInScanWindow(barcode: barcode, imageSize: image.size)) {
  59 + return barcode.data
54 } 60 }
55 - } else if (error != nil){  
56 - barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) 61 +
  62 + return nil
57 } 63 }
  64 +
  65 + if (barcodesMap.isEmpty) {
  66 + return
  67 + }
  68 +
  69 + if (!MobileScannerPlugin.returnImage) {
  70 + barcodeHandler.publishEvent([
  71 + "name": "barcode",
  72 + "data": barcodesMap,
  73 + ])
  74 + return
  75 + }
  76 +
  77 + barcodeHandler.publishEvent([
  78 + "name": "barcode",
  79 + "data": barcodesMap,
  80 + "image": [
  81 + "bytes": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!),
  82 + "width": image.size.width,
  83 + "height": image.size.height,
  84 + ],
  85 + ])
58 }, torchModeChangeCallback: { torchState in 86 }, torchModeChangeCallback: { torchState in
59 barcodeHandler.publishEvent(["name": "torchState", "data": torchState]) 87 barcodeHandler.publishEvent(["name": "torchState", "data": torchState])
60 }, zoomScaleChangeCallback: { zoomScale in 88 }, zoomScaleChangeCallback: { zoomScale in
@@ -104,6 +132,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -104,6 +132,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
104 let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0 132 let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0
105 let timeoutMs: Int = (call.arguments as! Dictionary<String, Any?>)["timeout"] as? Int ?? 0 133 let timeoutMs: Int = (call.arguments as! Dictionary<String, Any?>)["timeout"] as? Int ?? 0
106 self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000) 134 self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000)
  135 + MobileScannerPlugin.returnImage = returnImage
107 136
108 let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} 137 let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
109 var barcodeOptions: BarcodeScannerOptions? = nil 138 var barcodeOptions: BarcodeScannerOptions? = nil
@@ -120,7 +149,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -120,7 +149,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
120 let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! 149 let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
121 150
122 do { 151 do {
123 - try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch, detectionSpeed: detectionSpeed) { parameters in 152 + try mobileScanner.start(barcodeScannerOptions: barcodeOptions, cameraPosition: position, torch: torch, detectionSpeed: detectionSpeed) { parameters in
124 DispatchQueue.main.async { 153 DispatchQueue.main.async {
125 result([ 154 result([
126 "textureId": parameters.textureId, 155 "textureId": parameters.textureId,
@@ -257,12 +286,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -257,12 +286,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
257 DispatchQueue.main.async { 286 DispatchQueue.main.async {
258 result(nil) 287 result(nil)
259 } 288 }
260 - } else {  
261 - let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data }  
262 289
263 - DispatchQueue.main.async {  
264 - result(["name": "barcode", "data": barcodesMap])  
265 - } 290 + return
  291 + }
  292 +
  293 + let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data }
  294 +
  295 + DispatchQueue.main.async {
  296 + result(["name": "barcode", "data": barcodesMap])
266 } 297 }
267 }) 298 })
268 } 299 }
@@ -29,8 +29,27 @@ extension UIDeviceOrientation { @@ -29,8 +29,27 @@ extension UIDeviceOrientation {
29 29
30 extension Barcode { 30 extension Barcode {
31 var data: [String: Any?] { 31 var data: [String: Any?] {
32 - let corners = cornerPoints?.map({$0.cgPointValue.data})  
33 - return ["corners": corners, "format": format.rawValue, "rawBytes": rawData, "rawValue": rawValue, "type": valueType.rawValue, "calendarEvent": calendarEvent?.data, "contactInfo": contactInfo?.data, "driverLicense": driverLicense?.data, "email": email?.data, "geoPoint": geoPoint?.data, "phone": phone?.data, "sms": sms?.data, "url": url?.data, "wifi": wifi?.data, "displayValue": displayValue] 32 + return [
  33 + "calendarEvent": calendarEvent?.data,
  34 + "contactInfo": contactInfo?.data,
  35 + "corners": cornerPoints?.map({$0.cgPointValue.data}),
  36 + "displayValue": displayValue,
  37 + "driverLicense": driverLicense?.data,
  38 + "email": email?.data,
  39 + "format": format.rawValue,
  40 + "geoPoint": geoPoint?.data,
  41 + "phone": phone?.data,
  42 + "rawBytes": rawData,
  43 + "rawValue": rawValue,
  44 + "size": frame.isNull ? nil : [
  45 + "width": frame.width,
  46 + "height": frame.height,
  47 + ],
  48 + "sms": sms?.data,
  49 + "type": valueType.rawValue,
  50 + "url": url?.data,
  51 + "wifi": wifi?.data,
  52 + ]
34 } 53 }
35 } 54 }
36 55
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '5.2.1' 7 + s.version = '5.2.2'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
@@ -3,7 +3,6 @@ import 'dart:async'; @@ -3,7 +3,6 @@ import 'dart:async';
3 import 'package:flutter/foundation.dart'; 3 import 'package:flutter/foundation.dart';
4 import 'package:flutter/services.dart'; 4 import 'package:flutter/services.dart';
5 import 'package:flutter/widgets.dart'; 5 import 'package:flutter/widgets.dart';
6 -import 'package:mobile_scanner/src/enums/barcode_format.dart';  
7 import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart'; 6 import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart';
8 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; 7 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
9 import 'package:mobile_scanner/src/enums/torch_state.dart'; 8 import 'package:mobile_scanner/src/enums/torch_state.dart';
@@ -54,31 +53,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -54,31 +53,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
54 final List<Map<Object?, Object?>> barcodes = 53 final List<Map<Object?, Object?>> barcodes =
55 data.cast<Map<Object?, Object?>>(); 54 data.cast<Map<Object?, Object?>>();
56 55
57 - if (defaultTargetPlatform == TargetPlatform.macOS) {  
58 - return BarcodeCapture(  
59 - raw: event,  
60 - barcodes: barcodes  
61 - .map(  
62 - (barcode) => Barcode(  
63 - rawValue: barcode['payload'] as String?,  
64 - format: BarcodeFormat.fromRawValue(  
65 - barcode['symbology'] as int? ?? -1,  
66 - ),  
67 - ),  
68 - )  
69 - .toList(),  
70 - );  
71 - }  
72 -  
73 if (defaultTargetPlatform == TargetPlatform.android || 56 if (defaultTargetPlatform == TargetPlatform.android ||
74 - defaultTargetPlatform == TargetPlatform.iOS) {  
75 - final double? width = event['width'] as double?;  
76 - final double? height = event['height'] as double?; 57 + defaultTargetPlatform == TargetPlatform.iOS ||
  58 + defaultTargetPlatform == TargetPlatform.macOS) {
  59 + final Map<Object?, Object?>? imageData =
  60 + event['image'] as Map<Object?, Object?>?;
  61 + final Uint8List? image = imageData?['bytes'] as Uint8List?;
  62 + final double? width = imageData?['width'] as double?;
  63 + final double? height = imageData?['height'] as double?;
77 64
78 return BarcodeCapture( 65 return BarcodeCapture(
79 - raw: data, 66 + raw: event,
80 barcodes: barcodes.map(Barcode.fromNative).toList(), 67 barcodes: barcodes.map(Barcode.fromNative).toList(),
81 - image: event['image'] as Uint8List?, 68 + image: image,
82 size: width == null || height == null ? Size.zero : Size(width, height), 69 size: width == null || height == null ? Size.zero : Size(width, height),
83 ); 70 );
84 } 71 }
@@ -154,8 +141,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -154,8 +141,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
154 141
155 @override 142 @override
156 Future<BarcodeCapture?> analyzeImage(String path) async { 143 Future<BarcodeCapture?> analyzeImage(String path) async {
157 - final Map<String, Object?>? result =  
158 - await methodChannel.invokeMapMethod<String, Object?>( 144 + final Map<Object?, Object?>? result =
  145 + await methodChannel.invokeMapMethod<Object?, Object?>(
159 'analyzeImage', 146 'analyzeImage',
160 path, 147 path,
161 ); 148 );
@@ -80,7 +80,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -80,7 +80,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
80 /// 80 ///
81 /// If this is false, [BarcodeCapture.image] will always be null. 81 /// If this is false, [BarcodeCapture.image] will always be null.
82 /// 82 ///
83 - /// Defaults to false, and is only supported on iOS and Android. 83 + /// Defaults to false, and is only supported on iOS, MacOS and Android.
84 final bool returnImage; 84 final bool returnImage;
85 85
86 /// Whether the flashlight should be turned on when the camera is started. 86 /// Whether the flashlight should be turned on when the camera is started.
@@ -28,6 +28,7 @@ class Barcode { @@ -28,6 +28,7 @@ class Barcode {
28 this.phone, 28 this.phone,
29 this.rawBytes, 29 this.rawBytes,
30 this.rawValue, 30 this.rawValue,
  31 + this.size = Size.zero,
31 this.sms, 32 this.sms,
32 this.type = BarcodeType.unknown, 33 this.type = BarcodeType.unknown,
33 this.url, 34 this.url,
@@ -38,9 +39,9 @@ class Barcode { @@ -38,9 +39,9 @@ class Barcode {
38 factory Barcode.fromNative(Map<Object?, Object?> data) { 39 factory Barcode.fromNative(Map<Object?, Object?> data) {
39 final Map<Object?, Object?>? calendarEvent = 40 final Map<Object?, Object?>? calendarEvent =
40 data['calendarEvent'] as Map<Object?, Object?>?; 41 data['calendarEvent'] as Map<Object?, Object?>?;
41 - final List<Object?>? corners = data['corners'] as List<Object?>?;  
42 final Map<Object?, Object?>? contactInfo = 42 final Map<Object?, Object?>? contactInfo =
43 data['contactInfo'] as Map<Object?, Object?>?; 43 data['contactInfo'] as Map<Object?, Object?>?;
  44 + final List<Object?>? corners = data['corners'] as List<Object?>?;
44 final Map<Object?, Object?>? driverLicense = 45 final Map<Object?, Object?>? driverLicense =
45 data['driverLicense'] as Map<Object?, Object?>?; 46 data['driverLicense'] as Map<Object?, Object?>?;
46 final Map<Object?, Object?>? email = 47 final Map<Object?, Object?>? email =
@@ -50,9 +51,13 @@ class Barcode { @@ -50,9 +51,13 @@ class Barcode {
50 final Map<Object?, Object?>? phone = 51 final Map<Object?, Object?>? phone =
51 data['phone'] as Map<Object?, Object?>?; 52 data['phone'] as Map<Object?, Object?>?;
52 final Map<Object?, Object?>? sms = data['sms'] as Map<Object?, Object?>?; 53 final Map<Object?, Object?>? sms = data['sms'] as Map<Object?, Object?>?;
  54 + final Map<Object?, Object?>? size = data['size'] as Map<Object?, Object?>?;
53 final Map<Object?, Object?>? url = data['url'] as Map<Object?, Object?>?; 55 final Map<Object?, Object?>? url = data['url'] as Map<Object?, Object?>?;
54 final Map<Object?, Object?>? wifi = data['wifi'] as Map<Object?, Object?>?; 56 final Map<Object?, Object?>? wifi = data['wifi'] as Map<Object?, Object?>?;
55 57
  58 + final double? barcodeWidth = size?['width'] as double?;
  59 + final double? barcodeHeight = size?['height'] as double?;
  60 +
56 return Barcode( 61 return Barcode(
57 calendarEvent: calendarEvent == null 62 calendarEvent: calendarEvent == null
58 ? null 63 ? null
@@ -81,6 +86,9 @@ class Barcode { @@ -81,6 +86,9 @@ class Barcode {
81 phone: phone == null ? null : Phone.fromNative(phone), 86 phone: phone == null ? null : Phone.fromNative(phone),
82 rawBytes: data['rawBytes'] as Uint8List?, 87 rawBytes: data['rawBytes'] as Uint8List?,
83 rawValue: data['rawValue'] as String?, 88 rawValue: data['rawValue'] as String?,
  89 + size: barcodeWidth == null || barcodeHeight == null
  90 + ? Size.zero
  91 + : Size(barcodeWidth, barcodeHeight),
84 sms: sms == null ? null : SMS.fromNative(sms), 92 sms: sms == null ? null : SMS.fromNative(sms),
85 type: BarcodeType.fromRawValue(data['type'] as int? ?? 0), 93 type: BarcodeType.fromRawValue(data['type'] as int? ?? 0),
86 url: url == null ? null : UrlBookmark.fromNative(url), 94 url: url == null ? null : UrlBookmark.fromNative(url),
@@ -144,6 +152,11 @@ class Barcode { @@ -144,6 +152,11 @@ class Barcode {
144 /// This is null if the raw value is not available. 152 /// This is null if the raw value is not available.
145 final String? rawValue; 153 final String? rawValue;
146 154
  155 + /// The size of the barcode bounding box.
  156 + ///
  157 + /// If the bounding box is unavailable, this will be [Size.zero].
  158 + final Size size;
  159 +
147 /// The SMS message that is embedded in the barcode. 160 /// The SMS message that is embedded in the barcode.
148 final SMS? sms; 161 final SMS? sms;
149 162
  1 +/// @docImport 'package:mobile_scanner/src/mobile_scanner_controller.dart';
  2 +library;
  3 +
1 import 'dart:typed_data'; 4 import 'dart:typed_data';
2 import 'dart:ui'; 5 import 'dart:ui';
3 6
@@ -16,15 +19,21 @@ class BarcodeCapture { @@ -16,15 +19,21 @@ class BarcodeCapture {
16 /// The list of scanned barcodes. 19 /// The list of scanned barcodes.
17 final List<Barcode> barcodes; 20 final List<Barcode> barcodes;
18 21
19 - /// The bytes of the image that is embedded in the barcode. 22 + /// The input image of the barcode capture.
  23 + ///
  24 + /// This is the image that was used to detect the available [barcodes],
  25 + /// not the image from a specific barcode.
20 /// 26 ///
21 - /// This null if [MobileScannerController.returnImage] is false,  
22 - /// or if there is no available image. 27 + /// This is always null if [MobileScannerController.returnImage] is false.
23 final Uint8List? image; 28 final Uint8List? image;
24 29
25 - /// The raw data of the scanned barcode. 30 + /// The raw data of the barcode scan.
  31 + ///
  32 + /// This is the data that was used to detect the available [barcodes], the input [image] and the [size].
26 final Object? raw; 33 final Object? raw;
27 34
28 - /// The size of the scanned barcode. 35 + /// The size of the input [image].
  36 + ///
  37 + /// If [image] is null, this will be [Size.zero].
29 final Size size; 38 final Size size;
30 } 39 }
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '5.2.1' 7 + s.version = '5.2.2'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
@@ -25,6 +25,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -25,6 +25,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
25 25
26 // optional window to limit scan search 26 // optional window to limit scan search
27 var scanWindow: CGRect? 27 var scanWindow: CGRect?
  28 +
  29 + /// Whether to return the input image with the barcode event.
  30 + /// This is static to avoid accessing `self` in the `VNDetectBarcodesRequest` callback.
  31 + private static var returnImage: Bool = false
28 32
29 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates 33 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
30 34
@@ -32,8 +36,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -32,8 +36,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
32 36
33 var symbologies:[VNBarcodeSymbology] = [] 37 var symbologies:[VNBarcodeSymbology] = []
34 38
35 - // var analyzeMode: Int = 0  
36 - var analyzing: Bool = false  
37 var position = AVCaptureDevice.Position.back 39 var position = AVCaptureDevice.Position.back
38 40
39 public static func register(with registrar: FlutterPluginRegistrar) { 41 public static func register(with registrar: FlutterPluginRegistrar) {
@@ -65,8 +67,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -65,8 +67,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
65 setScale(call, result) 67 setScale(call, result)
66 case "resetScale": 68 case "resetScale":
67 resetScale(call, result) 69 resetScale(call, result)
68 - // case "analyze":  
69 - // switchAnalyzeMode(call, result)  
70 case "stop": 70 case "stop":
71 stop(result) 71 stop(result)
72 case "updateScanWindow": 72 case "updateScanWindow":
@@ -101,12 +101,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -101,12 +101,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
101 101
102 // Gets called when a new image is added to the buffer 102 // Gets called when a new image is added to the buffer
103 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 103 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
104 - // Ignore invalid textureId 104 + // Ignore invalid texture id.
105 if textureId == nil { 105 if textureId == nil {
106 return 106 return
107 } 107 }
108 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 108 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
109 - print("Failed to get image buffer from sample buffer.")  
110 return 109 return
111 } 110 }
112 latestBuffer = imageBuffer 111 latestBuffer = imageBuffer
@@ -118,7 +117,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -118,7 +117,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
118 nextScanTime = currentTime + timeoutSeconds 117 nextScanTime = currentTime + timeoutSeconds
119 imagesCurrentlyBeingProcessed = true 118 imagesCurrentlyBeingProcessed = true
120 DispatchQueue.global(qos: .userInitiated).async { [weak self] in 119 DispatchQueue.global(qos: .userInitiated).async { [weak self] in
121 - if(self!.latestBuffer == nil){ 120 + if self!.latestBuffer == nil {
122 return 121 return
123 } 122 }
124 var cgImage: CGImage? 123 var cgImage: CGImage?
@@ -127,44 +126,57 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -127,44 +126,57 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
127 do { 126 do {
128 let barcodeRequest:VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in 127 let barcodeRequest:VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in
129 self?.imagesCurrentlyBeingProcessed = false 128 self?.imagesCurrentlyBeingProcessed = false
130 - if error == nil {  
131 - if let results = request.results as? [VNBarcodeObservation] {  
132 - for barcode in results {  
133 - if self?.scanWindow != nil && cgImage != nil {  
134 - let match = self?.isBarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!) ?? false  
135 - if (!match) {  
136 - continue  
137 - }  
138 - }  
139 -  
140 - DispatchQueue.main.async {  
141 - self?.sink?([  
142 - "name": "barcode",  
143 - "data": [  
144 - [  
145 - "payload": barcode.payloadStringValue ?? "",  
146 - "symbology": barcode.symbology.toInt ?? -1,  
147 - ],  
148 - ],  
149 - ])  
150 - }  
151 - // if barcodeType == "QR" {  
152 - // let image = CIImage(image: source)  
153 - // image?.cropping(to: barcode.boundingBox)  
154 - // self.qrCodeDescriptor(qrCode: barcode, qrCodeImage: image!)  
155 - // }  
156 - }  
157 - }  
158 - } else { 129 +
  130 + if error != nil {
159 DispatchQueue.main.async { 131 DispatchQueue.main.async {
160 self?.sink?(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil)) 132 self?.sink?(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil))
161 } 133 }
  134 + return
  135 + }
  136 +
  137 + guard let results: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else {
  138 + return
  139 + }
  140 +
  141 + if results.isEmpty {
  142 + return
  143 + }
  144 +
  145 + let barcodes: [VNBarcodeObservation] = results.compactMap({ barcode in
  146 + // If there is a scan window, check if the barcode is within said scan window.
  147 + if self?.scanWindow != nil && cgImage != nil && !(self?.isBarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!) ?? false) {
  148 + return nil
  149 + }
  150 +
  151 + return barcode
  152 + })
  153 +
  154 + DispatchQueue.main.async {
  155 + if (!MobileScannerPlugin.returnImage) {
  156 + self?.sink?([
  157 + "name": "barcode",
  158 + "data": barcodes.map({ $0.toMap() }),
  159 + ])
  160 + return
  161 + }
  162 +
  163 + self?.sink?([
  164 + "name": "barcode",
  165 + "data": barcodes.map({ $0.toMap() }),
  166 + "image": cgImage == nil ? nil : [
  167 + "bytes": FlutterStandardTypedData(bytes: cgImage!.jpegData(compressionQuality: 0.8)!),
  168 + "width": Double(cgImage!.width),
  169 + "height": Double(cgImage!.height),
  170 + ],
  171 + ])
162 } 172 }
163 }) 173 })
164 - if(self?.symbologies.isEmpty == false){  
165 - // add the symbologies the user wishes to support 174 +
  175 + if self?.symbologies.isEmpty == false {
  176 + // Add the symbologies the user wishes to support.
166 barcodeRequest.symbologies = self!.symbologies 177 barcodeRequest.symbologies = self!.symbologies
167 } 178 }
  179 +
168 try imageRequestHandler.perform([barcodeRequest]) 180 try imageRequestHandler.perform([barcodeRequest])
169 } catch let e { 181 } catch let e {
170 DispatchQueue.main.async { 182 DispatchQueue.main.async {
@@ -265,6 +277,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -265,6 +277,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
265 let speed:Int = argReader.int(key: "speed") ?? 0 277 let speed:Int = argReader.int(key: "speed") ?? 0
266 let timeoutMs:Int = argReader.int(key: "timeout") ?? 0 278 let timeoutMs:Int = argReader.int(key: "timeout") ?? 0
267 symbologies = argReader.toSymbology() 279 symbologies = argReader.toSymbology()
  280 + MobileScannerPlugin.returnImage = argReader.bool(key: "returnImage") ?? false
268 281
269 timeoutSeconds = Double(timeoutMs) / 1000.0 282 timeoutSeconds = Double(timeoutMs) / 1000.0
270 detectionSpeed = DetectionSpeed(rawValue: speed)! 283 detectionSpeed = DetectionSpeed(rawValue: speed)!
@@ -415,11 +428,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -415,11 +428,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
415 result(nil) 428 result(nil)
416 } 429 }
417 430
418 - // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
419 - // analyzeMode = call.arguments as! Int  
420 - // result(nil)  
421 - // }  
422 -  
423 func stop(_ result: FlutterResult) { 431 func stop(_ result: FlutterResult) {
424 if (device == nil || captureSession == nil) { 432 if (device == nil || captureSession == nil) {
425 result(nil) 433 result(nil)
@@ -436,7 +444,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -436,7 +444,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
436 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) 444 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
437 registry.unregisterTexture(textureId) 445 registry.unregisterTexture(textureId)
438 446
439 - // analyzeMode = 0  
440 latestBuffer = nil 447 latestBuffer = nil
441 captureSession = nil 448 captureSession = nil
442 device = nil 449 device = nil
@@ -504,6 +511,45 @@ class MapArgumentReader { @@ -504,6 +511,45 @@ class MapArgumentReader {
504 511
505 } 512 }
506 513
  514 +extension CGImage {
  515 + public func jpegData(compressionQuality: CGFloat) -> Data? {
  516 + let mutableData = CFDataCreateMutable(nil, 0)
  517 +
  518 + let formatHint: CFString
  519 +
  520 + if #available(macOS 11.0, *) {
  521 + formatHint = UTType.jpeg.identifier as CFString
  522 + } else {
  523 + formatHint = kUTTypeJPEG
  524 + }
  525 +
  526 + guard let destination = CGImageDestinationCreateWithData(mutableData!, formatHint, 1, nil) else {
  527 + return nil
  528 + }
  529 +
  530 + let options: NSDictionary = [
  531 + kCGImageDestinationLossyCompressionQuality: compressionQuality,
  532 + ]
  533 +
  534 + CGImageDestinationAddImage(destination, self, options)
  535 +
  536 + if !CGImageDestinationFinalize(destination) {
  537 + return nil
  538 + }
  539 +
  540 + return mutableData as Data?
  541 + }
  542 +}
  543 +
  544 +extension VNBarcodeObservation {
  545 + public func toMap() -> [String: Any?] {
  546 + return [
  547 + "rawValue": self.payloadStringValue ?? "",
  548 + "format": self.symbology.toInt ?? -1,
  549 + ]
  550 + }
  551 +}
  552 +
507 extension VNBarcodeSymbology { 553 extension VNBarcodeSymbology {
508 static func fromInt(_ mapValue:Int) -> VNBarcodeSymbology? { 554 static func fromInt(_ mapValue:Int) -> VNBarcodeSymbology? {
509 if #available(macOS 12.0, *) { 555 if #available(macOS 12.0, *) {
1 name: mobile_scanner 1 name: mobile_scanner
2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. 2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
3 -version: 5.2.1 3 +version: 5.2.2
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 screenshots: 6 screenshots: