Toggle navigation
Toggle navigation
This project
Loading...
Sign in
flutter_package
/
mobile_scanner
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
Julian Steenbakker
2022-12-12 15:57:36 +0100
Browse Files
Options
Browse Files
Download
Plain Diff
Committed by
GitHub
2022-12-12 15:57:36 +0100
Commit
882ff648b2913bb8083109cf4c58a9de35e0f922
882ff648
2 parents
3c7aa46b
6dc5b094
Merge branch 'master' into pavel/web-format
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
645 additions
and
69 deletions
CHANGELOG.md
android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt
android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerPlugin.kt
example/ios/Runner.xcodeproj/project.pbxproj
example/lib/barcode_scanner_window.dart
example/lib/main.dart
ios/Classes/MobileScanner.swift
ios/Classes/SwiftMobileScannerPlugin.swift
lib/mobile_scanner_web_plugin.dart
lib/src/barcode_utility.dart
lib/src/mobile_scanner.dart
lib/src/mobile_scanner_controller.dart
lib/src/objects/barcode_capture.dart
lib/src/web/jsqr.dart
lib/src/web/zxing.dart
macos/Classes/MobileScannerPlugin.swift
CHANGELOG.md
View file @
882ff64
...
...
@@ -12,6 +12,7 @@ Breaking changes:
*
The
`autoResume` attribute has been removed from the `MobileScanner`
widget.
The controller already automatically resumes, so it had no effect.
*
Removed
`MobileScannerCallback` and `MobileScannerArgumentsCallback`
typedef.
*
[
Web
]
Replaced
`jsqr` library with `zxing-js`
for full barcode support.
Improvements:
*
Toggling the device torch now does nothing if the device has no torch, rather than throwing an error.
...
...
@@ -21,6 +22,9 @@ Features:
*
Added a new
`placeholderBuilder` function to the `MobileScanner`
widget to customize the preview placeholder.
*
Added
`autoStart`
parameter to MobileScannerController(). If set to false, controller won't start automatically.
*
Added
`hasTorch`
function on MobileScannerController(). After starting the controller, you can check if the device has a torch.
*
[
iOS
]
Support
`torchEnabled`
parameter from MobileScannerController() on iOS
*
[
Web
]
Added ability to use custom barcode scanning js libraries
by extending
`WebBarcodeReaderBase` class and changing `barCodeReader` property in `MobileScannerWebPlugin`
Fixes:
*
Fixes the missing gradle setup for the Android project, which prevented gradle sync from working.
...
...
@@ -31,6 +35,7 @@ Fixes:
*
Fixes the
`MobileScanner` preview depending on all attributes of `MediaQueryData`
.
Now it only depends on its layout constraints.
*
Fixed a potential crash when the scanner is restarted due to the app being resumed.
*
[
iOS
]
Fix crash when changing torch state
## 3.0.0-beta.2
Breaking changes:
...
...
android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt
View file @
882ff64
...
...
@@ -3,9 +3,11 @@ package dev.steenbakker.mobile_scanner
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.graphics.Rect
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Surface
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
...
...
@@ -14,18 +16,23 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
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 dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit
import kotlin.math.roundToInt
typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit
typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
typealias MobileScannerErrorCallback = (error: String) -> Unit
typealias TorchStateCallback = (state: Int) -> Unit
typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
class NoCamera : Exception()
class AlreadyStarted : Exception()
class AlreadyStopped : Exception()
...
...
@@ -53,6 +60,7 @@ class MobileScanner(
private var pendingPermissionResult: MethodChannel.Result? = null
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
var scanWindow: List<Float>? = null
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
private var detectionTimeout: Long = 250
...
...
@@ -138,12 +146,27 @@ class MobileScanner(
lastScanned = newScannedBarcodes
}
val barcodeMap = barcodes.map { barcode -> barcode.data }
val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf()
for ( barcode in barcodes) {
if(scanWindow != null) {
val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy)
if(!match) {
continue
} else {
barcodeMap.add(barcode.data)
}
} else {
barcodeMap.add(barcode.data)
}
}
if (barcodeMap.isNotEmpty()) {
mobileScannerCallback(
barcodeMap,
if (returnImage) mediaImage.toByteArray() else null
if (returnImage) mediaImage.toByteArray() else null,
if (returnImage) mediaImage.width else null,
if (returnImage) mediaImage.height else null
)
}
}
...
...
@@ -162,6 +185,23 @@ class MobileScanner(
}
}
// 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: ImageProxy): Boolean {
val barcodeBoundingBox = barcode.boundingBox ?: return false
val imageWidth = inputImage.height
val imageHeight = inputImage.width
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)
}
/**
* Start barcode scanning by initializing the camera and barcode scanner.
*/
...
...
@@ -244,7 +284,7 @@ class MobileScanner(
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
val resolution =
preview!!
.resolutionInfo!!.resolution
val resolution =
analysis
.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
...
...
android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerPlugin.kt
View file @
882ff64
...
...
@@ -25,12 +25,14 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
private var analyzerResult: MethodChannel.Result? = null
private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?
, width: Int?, height: Int?
->
if (image != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes,
"image" to image
"image" to image,
"width" to width!!.toDouble(),
"height" to height!!.toDouble()
))
} else {
barcodeHandler.publishEvent(mapOf(
...
...
@@ -77,6 +79,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
"updateScanWindow" -> updateScanWindow(call)
else -> result.notImplemented()
}
}
...
...
@@ -215,4 +218,8 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
}
}
private fun updateScanWindow(call: MethodCall) {
handler!!.scanWindow = call.argument<List<Float>>("rect")
}
}
...
...
example/ios/Runner.xcodeproj/project.pbxproj
View file @
882ff64
...
...
@@ -354,6 +354,8 @@
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;
ENABLE_BITCODE = NO;
...
...
@@ -364,6 +366,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
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,6 +486,8 @@
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;
ENABLE_BITCODE = NO;
...
...
@@ -493,6 +498,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
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,6 +512,8 @@
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;
ENABLE_BITCODE = NO;
...
...
@@ -516,6 +524,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
...
...
example/lib/barcode_scanner_window.dart
0 → 100644
View file @
882ff64
import
'dart:io'
;
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
=
MobileScannerController
();
Barcode
?
barcode
;
BarcodeCapture
?
capture
;
Future
<
void
>
onDetect
(
BarcodeCapture
barcode
)
async
{
capture
=
barcode
;
setState
(()
=>
this
.
barcode
=
barcode
.
barcodes
.
first
);
}
MobileScannerArguments
?
arguments
;
@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
(
fit:
StackFit
.
expand
,
children:
[
MobileScanner
(
fit:
BoxFit
.
contain
,
scanWindow:
scanWindow
,
controller:
controller
,
onScannerStarted:
(
arguments
)
{
setState
(()
{
this
.
arguments
=
arguments
;
});
},
onDetect:
onDetect
,
),
if
(
barcode
!=
null
&&
barcode
?.
corners
!=
null
&&
arguments
!=
null
)
CustomPaint
(
painter:
BarcodeOverlay
(
barcode
!,
arguments
!,
BoxFit
.
contain
,
MediaQuery
.
of
(
context
).
devicePixelRatio
,
capture
!,
),
),
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
?.
displayValue
??
'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
;
}
}
class
BarcodeOverlay
extends
CustomPainter
{
BarcodeOverlay
(
this
.
barcode
,
this
.
arguments
,
this
.
boxFit
,
this
.
devicePixelRatio
,
this
.
capture
,
);
final
BarcodeCapture
capture
;
final
Barcode
barcode
;
final
MobileScannerArguments
arguments
;
final
BoxFit
boxFit
;
final
double
devicePixelRatio
;
@override
void
paint
(
Canvas
canvas
,
Size
size
)
{
if
(
barcode
.
corners
==
null
)
return
;
final
adjustedSize
=
applyBoxFit
(
boxFit
,
arguments
.
size
,
size
);
double
verticalPadding
=
size
.
height
-
adjustedSize
.
destination
.
height
;
double
horizontalPadding
=
size
.
width
-
adjustedSize
.
destination
.
width
;
if
(
verticalPadding
>
0
)
{
verticalPadding
=
verticalPadding
/
2
;
}
else
{
verticalPadding
=
0
;
}
if
(
horizontalPadding
>
0
)
{
horizontalPadding
=
horizontalPadding
/
2
;
}
else
{
horizontalPadding
=
0
;
}
final
ratioWidth
=
(
Platform
.
isIOS
?
capture
.
width
!
:
arguments
.
size
.
width
)
/
adjustedSize
.
destination
.
width
;
final
ratioHeight
=
(
Platform
.
isIOS
?
capture
.
height
!
:
arguments
.
size
.
height
)
/
adjustedSize
.
destination
.
height
;
final
List
<
Offset
>
adjustedOffset
=
[];
for
(
final
offset
in
barcode
.
corners
!)
{
adjustedOffset
.
add
(
Offset
(
offset
.
dx
/
ratioWidth
+
horizontalPadding
,
offset
.
dy
/
ratioHeight
+
verticalPadding
,
),
);
}
final
cutoutPath
=
Path
()..
addPolygon
(
adjustedOffset
,
true
);
final
backgroundPaint
=
Paint
()
..
color
=
Colors
.
red
.
withOpacity
(
0.3
)
..
style
=
PaintingStyle
.
fill
..
blendMode
=
BlendMode
.
dstOut
;
canvas
.
drawPath
(
cutoutPath
,
backgroundPaint
);
}
@override
bool
shouldRepaint
(
covariant
CustomPainter
oldDelegate
)
{
return
false
;
}
}
...
...
example/lib/main.dart
View file @
882ff64
...
...
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import
'package:mobile_scanner_example/barcode_list_scanner_controller.dart'
;
import
'package:mobile_scanner_example/barcode_scanner_controller.dart'
;
import
'package:mobile_scanner_example/barcode_scanner_returning_image.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
()));
...
...
@@ -44,6 +45,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
BarcodeScannerReturningImage
(),
),
);
...
...
ios/Classes/MobileScanner.swift
View file @
882ff64
...
...
@@ -12,6 +12,7 @@ import MLKitVision
import
MLKitBarcodeScanning
typealias
MobileScannerCallback
=
((
Array
<
Barcode
>
?,
Error
?,
UIImage
)
->
())
typealias
TorchModeChangeCallback
=
((
Int
?)
->
())
public
class
MobileScanner
:
NSObject
,
AVCaptureVideoDataOutputSampleBufferDelegate
,
FlutterTexture
{
/// Capture session of the camera
...
...
@@ -32,6 +33,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
/// When results are found, this callback will be called
let
mobileScannerCallback
:
MobileScannerCallback
/// When torch mode is changes, this callback will be called
let
torchModeChangeCallback
:
TorchModeChangeCallback
/// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
private
let
registry
:
FlutterTextureRegistry
?
...
...
@@ -43,9 +47,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
var
detectionSpeed
:
DetectionSpeed
=
DetectionSpeed
.
noDuplicates
init
(
registry
:
FlutterTextureRegistry
?,
mobileScannerCallback
:
@escaping
MobileScannerCallback
)
{
init
(
registry
:
FlutterTextureRegistry
?,
mobileScannerCallback
:
@escaping
MobileScannerCallback
,
torchModeChangeCallback
:
@escaping
TorchModeChangeCallback
)
{
self
.
registry
=
registry
self
.
mobileScannerCallback
=
mobileScannerCallback
self
.
torchModeChangeCallback
=
torchModeChangeCallback
super
.
init
()
}
...
...
@@ -127,17 +132,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
throw
MobileScannerError
.
noCamera
}
// Enable the torch if parameter is set and torch is available
if
(
device
.
hasTorch
&&
device
.
isTorchAvailable
)
{
do
{
try
device
.
lockForConfiguration
()
device
.
torchMode
=
torch
device
.
unlockForConfiguration
()
}
catch
{
throw
MobileScannerError
.
torchError
(
error
)
}
}
device
.
addObserver
(
self
,
forKeyPath
:
#
keyPath
(
AVCaptureDevice
.
torchMode
),
options
:
.
new
,
context
:
nil
)
captureSession
.
beginConfiguration
()
...
...
@@ -169,6 +163,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
captureSession
.
commitConfiguration
()
captureSession
.
startRunning
()
// Enable the torch if parameter is set and torch is available
// torch should be set after 'startRunning' is called
do
{
try
toggleTorch
(
torch
)
}
catch
{
print
(
"Failed to set initial torch state."
)
}
let
dimensions
=
CMVideoFormatDescriptionGetDimensions
(
device
.
activeFormat
.
formatDescription
)
return
MobileScannerStartParameters
(
width
:
Double
(
dimensions
.
height
),
height
:
Double
(
dimensions
.
width
),
hasTorch
:
device
.
hasTorch
,
textureId
:
textureId
)
...
...
@@ -198,12 +199,26 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
if
(
device
==
nil
)
{
throw
MobileScannerError
.
torchWhenStopped
}
do
{
try
device
.
lockForConfiguration
()
device
.
torchMode
=
torch
device
.
unlockForConfiguration
()
}
catch
{
throw
MobileScannerError
.
torchError
(
error
)
if
(
device
.
hasTorch
&&
device
.
isTorchAvailable
)
{
do
{
try
device
.
lockForConfiguration
()
device
.
torchMode
=
torch
device
.
unlockForConfiguration
()
}
catch
{
throw
MobileScannerError
.
torchError
(
error
)
}
}
}
// Observer for torch state
public
override
func
observeValue
(
forKeyPath
keyPath
:
String
?,
of
object
:
Any
?,
change
:
[
NSKeyValueChangeKey
:
Any
]?,
context
:
UnsafeMutableRawPointer
?)
{
switch
keyPath
{
case
"torchMode"
:
// off = 0; on = 1; auto = 2;
let
state
=
change
?[
.
newKey
]
as?
Int
torchModeChangeCallback
(
state
)
default
:
break
}
}
...
...
ios/Classes/SwiftMobileScannerPlugin.swift
View file @
882ff64
...
...
@@ -2,6 +2,7 @@ import Flutter
import
MLKitVision
import
MLKitBarcodeScanning
import
AVFoundation
import
UIKit
public
class
SwiftMobileScannerPlugin
:
NSObject
,
FlutterPlugin
{
...
...
@@ -10,19 +11,52 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
/// The handler sends all information via an event channel back to Flutter
private
let
barcodeHandler
:
BarcodeHandler
static
var
scanWindow
:
[
CGFloat
]?
private
static
func
isBarcodeInScanWindow
(
barcode
:
Barcode
,
imageSize
:
CGSize
)
->
Bool
{
let
scanwindow
=
SwiftMobileScannerPlugin
.
scanWindow
!
let
barcodeminX
=
barcode
.
cornerPoints
!
[
0
]
.
cgPointValue
.
x
let
barcodeminY
=
barcode
.
cornerPoints
!
[
1
]
.
cgPointValue
.
y
let
barcodewidth
=
barcode
.
cornerPoints
!
[
2
]
.
cgPointValue
.
x
-
barcodeminX
let
barcodeheight
=
barcode
.
cornerPoints
!
[
3
]
.
cgPointValue
.
y
-
barcodeminY
let
barcodeBox
=
CGRect
(
x
:
barcodeminX
,
y
:
barcodeminY
,
width
:
barcodewidth
,
height
:
barcodeheight
)
let
minX
=
scanwindow
[
0
]
*
imageSize
.
width
let
minY
=
scanwindow
[
1
]
*
imageSize
.
height
let
width
=
(
scanwindow
[
2
]
*
imageSize
.
width
)
-
minX
let
height
=
(
scanwindow
[
3
]
*
imageSize
.
height
)
-
minY
let
scaledWindow
=
CGRect
(
x
:
minX
,
y
:
minY
,
width
:
width
,
height
:
height
)
return
scaledWindow
.
contains
(
barcodeBox
)
}
init
(
barcodeHandler
:
BarcodeHandler
,
registry
:
FlutterTextureRegistry
)
{
self
.
mobileScanner
=
MobileScanner
(
registry
:
registry
,
mobileScannerCallback
:
{
barcodes
,
error
,
image
in
if
barcodes
!=
nil
{
let
barcodesMap
=
barcodes
!.
map
{
barcode
in
return
barcode
.
data
let
barcodesMap
=
barcodes
!.
compactMap
{
barcode
in
if
(
SwiftMobileScannerPlugin
.
scanWindow
!=
nil
)
{
if
(
SwiftMobileScannerPlugin
.
isBarcodeInScanWindow
(
barcode
:
barcode
,
imageSize
:
image
.
size
))
{
return
barcode
.
data
}
else
{
return
nil
}
}
else
{
return
barcode
.
data
}
}
if
(
!
barcodesMap
.
isEmpty
)
{
barcodeHandler
.
publishEvent
([
"name"
:
"barcode"
,
"data"
:
barcodesMap
,
"image"
:
FlutterStandardTypedData
(
bytes
:
image
.
jpegData
(
compressionQuality
:
0.8
)
!
)])
barcodeHandler
.
publishEvent
([
"name"
:
"barcode"
,
"data"
:
barcodesMap
,
"image"
:
FlutterStandardTypedData
(
bytes
:
image
.
jpegData
(
compressionQuality
:
0.8
)
!
)
,
"width"
:
image
.
size
.
width
,
"height"
:
image
.
size
.
height
])
}
}
else
if
(
error
!=
nil
){
barcodeHandler
.
publishEvent
([
"name"
:
"error"
,
"data"
:
error
!.
localizedDescription
])
}
},
torchModeChangeCallback
:
{
torchState
in
barcodeHandler
.
publishEvent
([
"name"
:
"torchState"
,
"data"
:
torchState
])
})
self
.
barcodeHandler
=
barcodeHandler
super
.
init
()
...
...
@@ -49,6 +83,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
toggleTorch
(
call
,
result
)
case
"analyzeImage"
:
analyzeImage
(
call
,
result
)
case
"updateScanWindow"
:
updateScanWindow
(
call
,
result
)
default
:
result
(
FlutterMethodNotImplemented
)
}
...
...
@@ -64,7 +100,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
let
formatList
=
formats
.
map
{
format
in
return
BarcodeFormat
(
rawValue
:
format
)}
var
barcodeOptions
:
BarcodeScannerOptions
?
=
nil
if
(
formatList
.
count
!=
0
)
{
var
barcodeFormats
:
BarcodeFormat
=
[]
for
index
in
formats
{
...
...
@@ -123,6 +159,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
result
(
nil
)
}
/// Toggles the torch
func
updateScanWindow
(
_
call
:
FlutterMethodCall
,
_
result
:
@escaping
FlutterResult
)
{
let
scanWindowData
:
Array
?
=
(
call
.
arguments
as?
[
String
:
Any
])?[
"rect"
]
as?
[
CGFloat
]
SwiftMobileScannerPlugin
.
scanWindow
=
scanWindowData
result
(
nil
)
}
static
func
arrayToRect
(
scanWindowData
:
[
CGFloat
]?)
->
CGRect
?
{
if
(
scanWindowData
==
nil
)
{
return
nil
}
let
minX
=
scanWindowData
!
[
0
]
let
minY
=
scanWindowData
!
[
1
]
let
width
=
scanWindowData
!
[
2
]
-
minX
let
height
=
scanWindowData
!
[
3
]
-
minY
return
CGRect
(
x
:
minX
,
y
:
minY
,
width
:
width
,
height
:
height
)
}
/// Analyzes a single image
private
func
analyzeImage
(
_
call
:
FlutterMethodCall
,
_
result
:
@escaping
FlutterResult
)
{
let
uiImage
=
UIImage
(
contentsOfFile
:
call
.
arguments
as?
String
??
""
)
...
...
@@ -145,16 +203,4 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
})
result
(
nil
)
}
/// Observer for torch state
public
override
func
observeValue
(
forKeyPath
keyPath
:
String
?,
of
object
:
Any
?,
change
:
[
NSKeyValueChangeKey
:
Any
]?,
context
:
UnsafeMutableRawPointer
?)
{
switch
keyPath
{
case
"torchMode"
:
// off = 0; on = 1; auto = 2;
let
state
=
change
?[
.
newKey
]
as?
Int
barcodeHandler
.
publishEvent
([
"name"
:
"torchState"
,
"data"
:
state
])
default
:
break
}
}
}
...
...
lib/mobile_scanner_web_plugin.dart
View file @
882ff64
...
...
@@ -37,6 +37,17 @@ class MobileScannerWebPlugin {
static
final
html
.
DivElement
vidDiv
=
html
.
DivElement
();
/// Represents barcode reader library.
/// Change this property if you want to use a custom implementation.
///
/// Example of using the jsQR library:
/// void main() {
/// if (kIsWeb) {
/// MobileScannerWebPlugin.barCodeReader =
/// JsQrCodeReader(videoContainer: MobileScannerWebPlugin.vidDiv);
/// }
/// runApp(const MaterialApp(home: MyHome()));
/// }
static
WebBarcodeReaderBase
barCodeReader
=
ZXingBarcodeReader
(
videoContainer:
vidDiv
);
StreamSubscription
?
_barCodeStreamSubscription
;
...
...
lib/src/barcode_utility.dart
View file @
882ff64
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
;
}
...
...
lib/src/mobile_scanner.dart
View file @
882ff64
...
...
@@ -34,6 +34,13 @@ class MobileScanner extends StatefulWidget {
/// If this is null, a black [ColoredBox] is used as placeholder.
final
Widget
Function
(
BuildContext
,
Widget
?)?
placeholderBuilder
;
/// 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 new [MobileScanner] using the provided [controller]
/// and [onBarcodeDetected] callback.
const
MobileScanner
({
...
...
@@ -43,6 +50,7 @@ class MobileScanner extends StatefulWidget {
@Deprecated
(
'Use onScannerStarted() instead.'
)
this
.
onStart
,
this
.
onScannerStarted
,
this
.
placeholderBuilder
,
this
.
scanWindow
,
super
.
key
,
});
...
...
@@ -117,34 +125,102 @@ class _MobileScannerState extends State<MobileScanner>
}
}
/// 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
.
destination
.
width
/
2
;
final
minY
=
widgetSize
.
height
/
2
-
fittedTextureSize
.
destination
.
height
/
2
;
final
textureWindow
=
Offset
(
minX
,
minY
)
&
fittedTextureSize
.
destination
;
/// 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
.
destination
.
width
;
final
percentageTop
=
windowInTexture
.
top
/
fittedTextureSize
.
destination
.
height
;
final
percentageRight
=
windowInTexture
.
right
/
fittedTextureSize
.
destination
.
width
;
final
percentagebottom
=
windowInTexture
.
bottom
/
fittedTextureSize
.
destination
.
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
ValueListenableBuilder
<
MobileScannerArguments
?>(
valueListenable:
_controller
.
startArguments
,
builder:
(
context
,
value
,
child
)
{
if
(
value
==
null
)
{
return
widget
.
placeholderBuilder
?.
call
(
context
,
child
)
??
const
ColoredBox
(
color:
Colors
.
black
);
}
return
ClipRect
(
child:
LayoutBuilder
(
builder:
(
_
,
constraints
)
{
return
SizedBox
.
fromSize
(
size:
constraints
.
biggest
,
child:
FittedBox
(
fit:
widget
.
fit
,
child:
SizedBox
(
width:
value
.
size
.
width
,
height:
value
.
size
.
height
,
child:
kIsWeb
?
HtmlElementView
(
viewType:
value
.
webId
!)
:
Texture
(
textureId:
value
.
textureId
!),
),
),
return
LayoutBuilder
(
builder:
(
context
,
constraints
)
{
return
ValueListenableBuilder
<
MobileScannerArguments
?>(
valueListenable:
_controller
.
startArguments
,
builder:
(
context
,
value
,
child
)
{
if
(
value
==
null
)
{
return
widget
.
placeholderBuilder
?.
call
(
context
,
child
)
??
const
ColoredBox
(
color:
Colors
.
black
);
}
if
(
widget
.
scanWindow
!=
null
)
{
final
window
=
calculateScanWindowRelativeToTextureInPercentage
(
widget
.
fit
,
widget
.
scanWindow
!,
value
.
size
,
Size
(
constraints
.
maxWidth
,
constraints
.
maxHeight
),
);
},
),
_controller
.
updateScanWindow
(
window
);
}
return
ClipRect
(
child:
LayoutBuilder
(
builder:
(
_
,
constraints
)
{
return
SizedBox
.
fromSize
(
size:
constraints
.
biggest
,
child:
FittedBox
(
fit:
widget
.
fit
,
child:
SizedBox
(
width:
value
.
size
.
width
,
height:
value
.
size
.
height
,
child:
kIsWeb
?
HtmlElementView
(
viewType:
value
.
webId
!)
:
Texture
(
textureId:
value
.
textureId
!),
),
),
);
},
),
);
},
);
},
);
...
...
lib/src/mobile_scanner_controller.dart
View file @
882ff64
...
...
@@ -126,6 +126,15 @@ class MobileScannerController {
arguments
[
'speed'
]
=
detectionSpeed
.
index
;
arguments
[
'timeout'
]
=
detectionTimeoutMs
;
/* if (scanWindow != null) {
arguments['scanWindow'] = [
scanWindow!.left,
scanWindow!.top,
scanWindow!.right,
scanWindow!.bottom,
];
} */
if
(
formats
!=
null
)
{
if
(
kIsWeb
||
Platform
.
isIOS
||
Platform
.
isMacOS
)
{
arguments
[
'formats'
]
=
formats
!.
map
((
e
)
=>
e
.
rawValue
).
toList
();
...
...
@@ -317,6 +326,8 @@ class MobileScannerController {
BarcodeCapture
(
barcodes:
parsed
,
image:
event
[
'image'
]
as
Uint8List
?,
width:
event
[
'width'
]
as
double
?,
height:
event
[
'height'
]
as
double
?,
),
);
break
;
...
...
@@ -355,4 +366,10 @@ class MobileScannerController {
throw
UnimplementedError
(
name
as
String
?);
}
}
/// 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
});
}
}
...
...
lib/src/objects/barcode_capture.dart
View file @
882ff64
...
...
@@ -12,8 +12,14 @@ class BarcodeCapture {
final
Uint8List
?
image
;
final
double
?
width
;
final
double
?
height
;
BarcodeCapture
({
required
this
.
barcodes
,
this
.
image
,
this
.
width
,
this
.
height
,
});
}
...
...
lib/src/web/jsqr.dart
View file @
882ff64
...
...
@@ -20,6 +20,11 @@ class Code {
external
Uint8ClampedList
get
binaryData
;
}
/// Barcode reader that uses jsQR library.
/// jsQR supports only QR codes format.
///
/// Include jsQR to your index.html file:
/// <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
class
JsQrCodeReader
extends
WebBarcodeReaderBase
with
InternalStreamCreation
,
InternalTorchDetection
{
JsQrCodeReader
({
required
super
.
videoContainer
});
...
...
lib/src/web/zxing.dart
View file @
882ff64
...
...
@@ -168,6 +168,10 @@ extension JsZXingBrowserMultiFormatReaderExt
external
MediaStream
?
stream
;
}
/// Barcode reader that uses zxing-js library.
///
/// Include zxing-js to your index.html file:
/// <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>
class
ZXingBarcodeReader
extends
WebBarcodeReaderBase
with
InternalStreamCreation
,
InternalTorchDetection
{
JsZXingBrowserMultiFormatReader
?
_reader
;
...
...
macos/Classes/MobileScannerPlugin.swift
View file @
882ff64
import
AVFoundation
import
FlutterMacOS
import
Vision
import
UIKit
public
class
MobileScannerPlugin
:
NSObject
,
FlutterPlugin
,
FlutterStreamHandler
,
FlutterTexture
,
AVCaptureVideoDataOutputSampleBufferDelegate
{
...
...
@@ -20,6 +21,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
...
...
@@ -57,6 +61,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
// switchAnalyzeMode(call, result)
case
"stop"
:
stop
(
result
)
case
"updateScanWindow"
:
updateScanWindow
(
call
)
default
:
result
(
FlutterMethodNotImplemented
)
}
...
...
@@ -109,10 +115,17 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
try
imageRequestHandler
.
perform
([
VNDetectBarcodesRequest
{
(
request
,
error
)
in
if
error
==
nil
{
if
let
results
=
request
.
results
as?
[
VNBarcodeObservation
]
{
for
barcode
in
results
{
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
)
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
)
// if barcodeType == "QR" {
// let image = CIImage(image: source)
...
...
@@ -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"
,
...
...
@@ -318,5 +363,9 @@ class MapArgumentReader {
func
stringArray
(
key
:
String
)
->
[
String
]?
{
return
args
?[
key
]
as?
[
String
]
}
func
floatArray
(
key
:
String
)
->
[
CGFloat
]?
{
return
args
?[
key
]
as?
[
CGFloat
]
}
}
...
...
Please
register
or
login
to post a comment