Julian Steenbakker
Committed by GitHub

Merge branch 'master' into dependabot/github_actions/GoogleCloudPlatform/release-please-action-3.6.0

Showing 32 changed files with 923 additions and 678 deletions
## NEXT
## 3.0.0-beta.2
Breaking changes:
* The arguments parameter of onDetect is removed. The data is now returned by the onStart callback
in the MobileScanner widget.
* onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image.
* allowDuplicates is removed and replaced by MobileScannerSpeed enum.
* onPermissionSet in MobileScanner widget is deprecated and will be removed. Use the onPermissionSet
onPermissionSet callback in MobileScannerController instead.
* [iOS] The minimum deployment target is now 11.0 or higher.
Features:
* The returnImage is working for both iOS and Android. You can enable it in the MobileScannerController.
The image will be returned in the BarcodeCapture object provided by onDetect.
* You can now control the DetectionSpeed, as well as the timeout of the DetectionSpeed. For more
info see the DetectionSpeed documentation. This replaces the allowDuplicates function.
Other improvements:
* Both the [iOS] and [Android] codebases have been refactored completely.
* [iOS] Updated POD dependencies
## 3.0.0-beta.1
... ...
... ... @@ -55,7 +55,7 @@ Do you have experience with Flutter Web development? [Help me with migrating fro
| Features | Android | iOS | macOS | Web |
|------------------------|--------------------|--------------------|-------|-----|
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
## Usage
... ... @@ -75,15 +75,15 @@ import 'package:mobile_scanner/mobile_scanner.dart';
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
allowDuplicates: false,
onDetect: (barcode, args) {
if (barcode.rawValue == null) {
debugPrint('Failed to scan Barcode');
} else {
final String code = barcode.rawValue!;
debugPrint('Barcode found! $code');
}
}),
// fit: BoxFit.contain,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
}
```
... ... @@ -98,17 +98,18 @@ import 'package:mobile_scanner/mobile_scanner.dart';
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
allowDuplicates: false,
controller: MobileScannerController(
facing: CameraFacing.front, torchEnabled: true),
onDetect: (barcode, args) {
if (barcode.rawValue == null) {
debugPrint('Failed to scan Barcode');
} else {
final String code = barcode.rawValue!;
debugPrint('Barcode found! $code');
}
}),
// fit: BoxFit.contain,
controller: MobileScannerController(
facing: CameraFacing.front, torchEnabled: true,
),
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
}
```
... ... @@ -161,16 +162,17 @@ import 'package:mobile_scanner/mobile_scanner.dart';
],
),
body: MobileScanner(
allowDuplicates: false,
controller: cameraController,
onDetect: (barcode, args) {
if (barcode.rawValue == null) {
debugPrint('Failed to scan Barcode');
} else {
final String code = barcode.rawValue!;
debugPrint('Barcode found! $code');
}
}));
// fit: BoxFit.contain,
controller: cameraController,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
}
```
... ... @@ -184,25 +186,25 @@ import 'package:mobile_scanner/mobile_scanner.dart';
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
fit: BoxFit.contain,
controller: MobileScannerController(
facing: CameraFacing.front,
torchEnabled: true,
// facing: CameraFacing.back,
// torchEnabled: false,
returnImage: true,
),
onDetect: (barcode, args) {
if (barcode.rawValue == null) {
debugPrint('Failed to scan Barcode');
} else {
final String code = barcode.rawValue!;
debugPrint('Barcode found! $code');
debugPrint(
'Image returned! length: ${barcode.image!.lengthInBytes}b');
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
if (image != null) {
showDialog(
context: context,
builder: (context) => Image(image: MemoryImage(barcode.image!)),
builder: (context) =>
Image(image: MemoryImage(image)),
);
Future.delayed(const Duration(seconds: 2), () {
Future.delayed(const Duration(seconds: 5), () {
Navigator.pop(context);
});
}
... ... @@ -212,25 +214,30 @@ import 'package:mobile_scanner/mobile_scanner.dart';
}
```
### Scan result
You can use the following properties of the Barcode, which gets
passed to the `onDetect` function.
| Property name | Type | Description
|---------------|----------------|--------------------
| image | Uint8List? | only if returnImage was set to true
| format | BarcodeFormat |
| rawBytes | Uint8List? | binary scan result
| rawValue | String? | Value if barcode is in UTF-8 format
| displayValue | String? |
| type | BarcodeType |
| calendarEvent | CalendarEvent? |
| contactInfo | ContactInfo? |
| driverLicense | DriverLicense? |
| email | Email? |
| geoPoint | GeoPoint? |
| phone | Phone? |
| sms | SMS? |
| url | UrlBookmark? |
| wifi | WiFi? | WiFi Access-Point details
### BarcodeCapture
The onDetect function returns a BarcodeCapture objects which contains the following items.
| Property name | Type | Description |
|---------------|---------------|-----------------------------------|
| barcodes | List<Barcode> | A list with scanned barcodes. |
| image | Uint8List? | If enabled, an image of the scan. |
You can use the following properties of the Barcode object.
| Property name | Type | Description |
|---------------|----------------|-------------------------------------|
| format | BarcodeFormat | |
| rawBytes | Uint8List? | binary scan result |
| rawValue | String? | Value if barcode is in UTF-8 format |
| displayValue | String? | |
| type | BarcodeType | |
| calendarEvent | CalendarEvent? | |
| contactInfo | ContactInfo? | |
| driverLicense | DriverLicense? | |
| email | Email? | |
| geoPoint | GeoPoint? | |
| phone | Phone? | |
| sms | SMS? | |
| url | UrlBookmark? | |
| wifi | WiFi? | WiFi Access-Point details |
... ...
... ... @@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.7.20'
ext.kotlin_version = '1.7.21'
repositories {
google()
mavenCentral()
... ...
package dev.steenbakker.mobile_scanner
import androidx.annotation.IntDef
@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE)
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class AnalyzeMode {
companion object {
const val NONE = 0
const val BARCODE = 1
}
}
\ No newline at end of file
package dev.steenbakker.mobile_scanner
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler {
private var eventSink: EventChannel.EventSink? = null
private val eventChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
"dev.steenbakker.mobile_scanner/scanner/event"
)
init {
eventChannel.setStreamHandler(this)
}
fun publishEvent(event: Map<String, Any>) {
Handler(Looper.getMainLooper()).post {
eventSink?.success(event)
}
}
override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) {
this.eventSink = eventSink
}
override fun onCancel(event: Any?) {
this.eventSink = null
}
}
\ No newline at end of file
... ...
... ... @@ -3,16 +3,10 @@ package dev.steenbakker.mobile_scanner
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.graphics.ImageFormat
import android.graphics.Point
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import android.net.Uri
import android.util.Log
import android.util.Size
import android.os.Handler
import android.os.Looper
import android.view.Surface
import androidx.annotation.NonNull
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
... ... @@ -20,19 +14,32 @@ 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 io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
import java.io.ByteArrayOutputStream
import java.io.File
class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) :
MethodChannel.MethodCallHandler, EventChannel.StreamHandler,
typealias PermissionCallback = (permissionGranted: Boolean) -> Unit
typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> 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()
class TorchError : Exception()
class CameraError : Exception()
class TorchWhenStopped : Exception()
class MobileScanner(
private val activity: Activity,
private val textureRegistry: TextureRegistry,
private val mobileScannerCallback: MobileScannerCallback,
private val mobileScannerErrorCallback: MobileScannerErrorCallback
) :
PluginRegistry.RequestPermissionsResultListener {
companion object {
/**
... ... @@ -40,10 +47,8 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
* @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
*/
private const val REQUEST_CODE = 0x0786
private val TAG = MobileScanner::class.java.simpleName
}
private var sink: EventChannel.EventSink? = null
private var listener: PluginRegistry.RequestPermissionsResultListener? = null
private var cameraProvider: ProcessCameraProvider? = null
... ... @@ -51,31 +56,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
// @AnalyzeMode
// private var analyzeMode: Int = AnalyzeMode.NONE
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
private var detectionTimeout: Long = 250
private var lastScanned: List<String?>? = null
@ExperimentalGetImage
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
when (call.method) {
"state" -> checkPermission(result)
"request" -> requestPermission(result)
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
// "analyze" -> switchAnalyzeMode(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
else -> result.notImplemented()
}
}
private var scannerTimeout = false
private var returnImage = false
private var scanner = BarcodeScanning.getClient()
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
this.sink = events
/**
* Check if we already have camera permission.
*/
fun hasCameraPermission(): Int {
// Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
val hasPermission = ContextCompat.checkSelfPermission(
activity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
return if (hasPermission) {
1
} else {
0
}
}
override fun onCancel(arguments: Any?) {
sink = null
/**
* Request camera permissions.
*/
fun requestPermission(permissionCallback: PermissionCallback) {
listener
?: PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->
if (requestCode != REQUEST_CODE) {
false
} else {
val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED
permissionCallback(authorized)
true
}
}
val permissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
/**
* Calls the callback after permissions are requested.
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
... ... @@ -84,282 +112,161 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
}
private fun checkPermission(result: MethodChannel.Result) {
// Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
val state =
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) 1
else 0
result.success(state)
}
private fun requestPermission(result: MethodChannel.Result) {
listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->
if (requestCode != REQUEST_CODE) {
false
} else {
val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED
result.success(authorized)
listener = null
true
}
}
val permissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
// var lastScanned: List<Barcode>? = null
// var isAnalyzing: Boolean = false
/**
* callback for the camera. Every frame is passed through this function.
*/
@ExperimentalGetImage
val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
val mediaImage = imageProxy.image ?: return@Analyzer
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
imageProxy.close()
return@Analyzer
} else if (detectionSpeed == DetectionSpeed.NORMAL) {
scannerTimeout = true
}
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
// if (isAnalyzing) {
// Log.d("scanner", "SKIPPING" )
// return@addOnSuccessListener
// }
// isAnalyzing = true
if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) {
val newScannedBarcodes = barcodes.map { barcode -> barcode.rawValue }
if (newScannedBarcodes == lastScanned) {
// New scanned is duplicate, returning
return@addOnSuccessListener
}
lastScanned = newScannedBarcodes
}
val barcodeMap = barcodes.map { barcode -> barcode.data }
if (barcodeMap.isNotEmpty()) {
sink?.success(mapOf(
"name" to "barcode",
"data" to barcodeMap,
"image" to mediaImage.toByteArray()
))
mobileScannerCallback(
barcodeMap,
if (returnImage) mediaImage.toByteArray() else null
)
}
// for (barcode in barcodes) {
//// if (lastScanned?.contains(barcodes.first) == true) continue;
// if (lastScanned == null) {
// lastScanned = barcodes
// } else if (lastScanned!!.contains(barcode)) {
// // Duplicate, don't send image
// sink?.success(mapOf(
// "name" to "barcode",
// "data" to barcode.data,
// ))
// } else {
// if (byteArray.isEmpty()) {
// Log.d("scanner", "EMPTY" )
// return@addOnSuccessListener
// }
//
// Log.d("scanner", "SCANNED IMAGE: $byteArray")
// lastScanned = barcodes;
//
//
// }
//
// }
// isAnalyzing = false
}
.addOnFailureListener { e -> sink?.success(mapOf(
"name" to "error",
"data" to e.localizedMessage
)) }
.addOnFailureListener { e ->
mobileScannerErrorCallback(
e.localizedMessage ?: e.toString()
)
}
.addOnCompleteListener { imageProxy.close() }
}
private fun Image.toByteArray(): ByteArray {
val yBuffer = planes[0].buffer // Y
val vuBuffer = planes[2].buffer // VU
val ySize = yBuffer.remaining()
val vuSize = vuBuffer.remaining()
val nv21 = ByteArray(ySize + vuSize)
yBuffer.get(nv21, 0, ySize)
vuBuffer.get(nv21, ySize, vuSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
return out.toByteArray()
if (detectionSpeed == DetectionSpeed.NORMAL) {
// Set timer and continue
Handler(Looper.getMainLooper()).postDelayed({
scannerTimeout = false
}, detectionTimeout)
}
}
private var scanner = BarcodeScanning.getClient()
/**
* Start barcode scanning by initializing the camera and barcode scanner.
*/
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
fun start(
barcodeScannerOptions: BarcodeScannerOptions?,
returnImage: Boolean,
cameraPosition: CameraSelector,
torch: Boolean,
detectionSpeed: DetectionSpeed,
torchStateCallback: TorchStateCallback,
mobileScannerStartedCallback: MobileScannerStartedCallback,
detectionTimeout: Long
) {
this.detectionSpeed = detectionSpeed
this.detectionTimeout = detectionTimeout
this.returnImage = returnImage
if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val size = if (portrait) mapOf(
"width" to width,
"height" to height
) else mapOf("width" to height, "height" to width)
val answer = mapOf(
"textureId" to textureEntry!!.id(),
"size" to size,
"torchable" to camera!!.cameraInfo.hasFlashUnit()
)
result.success(answer)
throw AlreadyStarted()
}
scanner = if (barcodeScannerOptions != null) {
BarcodeScanning.getClient(barcodeScannerOptions)
} else {
val facing: Int = call.argument<Int>("facing") ?: 0
val ratio: Int? = call.argument<Int>("ratio")
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val formats: List<Int>? = call.argument<List<Int>>("formats")
// val analyzerWidth = call.argument<Int>("ratio")
// val analyzeRHEIG = call.argument<Int>("ratio")
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
for (index in formats) {
formatsList.add(BarcodeFormats.values()[index].intValue)
}
scanner = if (formatsList.size == 1) {
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
)
} else {
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
)
}
}
BarcodeScanning.getClient()
}
val future = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
future.addListener({
cameraProvider = future.get()
if (cameraProvider == null) {
result.error("cameraProvider", "cameraProvider is null", null)
return@addListener
}
cameraProvider!!.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
if (textureEntry == null) {
result.error("textureEntry", "textureEntry is null", null)
return@addListener
}
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(
request.resolution.width,
request.resolution.height
)
val surface = Surface(texture)
request.provideSurface(surface, executor) { }
}
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
if (cameraProvider == null) {
throw CameraError()
}
cameraProvider!!.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(
request.resolution.width,
request.resolution.height
)
// Build the preview to be shown on the Flutter texture
val previewBuilder = Preview.Builder()
if (ratio != null) {
previewBuilder.setTargetAspectRatio(ratio)
}
preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) }
val surface = Surface(texture)
request.provideSurface(surface, executor) { }
}
// 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)
}
// Build the preview to be shown on the Flutter texture
val previewBuilder = Preview.Builder()
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)
// analysisBuilder.setTargetResolution(Size(1440, 1920))
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) }
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) }
// Select the correct camera
val selector =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
camera = cameraProvider!!.bindToLifecycle(
activity as LifecycleOwner,
cameraPosition,
preview,
analysis
)
camera = cameraProvider!!.bindToLifecycle(
activity as LifecycleOwner,
selector,
preview,
analysis
)
// Register the torch listener
camera!!.cameraInfo.torchState.observe(activity) { state ->
// TorchState.OFF = 0; TorchState.ON = 1
torchStateCallback(state)
}
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")
// 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")
if (camera == null) {
result.error("camera", "camera is null", null)
return@addListener
}
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
// Register the torch listener
camera!!.cameraInfo.torchState.observe(activity) { state ->
// TorchState.OFF = 0; TorchState.ON = 1
sink?.success(mapOf("name" to "torchState", "data" to state))
}
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val size = if (portrait) mapOf(
"width" to width,
"height" to height
) else mapOf("width" to height, "height" to width)
val answer = mapOf(
"textureId" to textureEntry!!.id(),
"size" to size,
"torchable" to camera!!.cameraInfo.hasFlashUnit()
mobileScannerStartedCallback(
MobileScannerStartParameters(
if (portrait) width else height,
if (portrait) height else width,
camera!!.cameraInfo.hasFlashUnit(),
textureEntry!!.id()
)
result.success(answer)
}, executor)
}
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
if (camera == null) {
result.error(TAG, "Called toggleTorch() while stopped!", null)
return
}
camera!!.cameraControl.enableTorch(call.arguments == 1)
result.success(null)
}
// private fun switchAnalyzeMode(call: MethodCall, result: MethodChannel.Result) {
// analyzeMode = call.arguments as Int
// result.success(null)
// }
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
val uri = Uri.fromFile(File(call.arguments.toString()))
val inputImage = InputImage.fromFilePath(activity, uri)
var barcodeFound = false
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
barcodeFound = true
sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
}
}
.addOnFailureListener { e ->
Log.e(TAG, e.message, e)
result.error(TAG, e.message, e)
}
.addOnCompleteListener { result.success(barcodeFound) }
)
}, executor)
}
private fun stop(result: MethodChannel.Result) {
/**
* Stop barcode scanning.
*/
fun stop() {
if (camera == null && preview == null) {
result.error(TAG, "Called stop() while already stopped!", null)
return
throw AlreadyStopped()
}
val owner = activity as LifecycleOwner
... ... @@ -367,81 +274,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
cameraProvider?.unbindAll()
textureEntry?.release()
// analyzeMode = AnalyzeMode.NONE
camera = null
preview = null
textureEntry = null
cameraProvider = null
}
result.success(null)
/**
* Toggles the flash light on or off.
*/
fun toggleTorch(enableTorch: Boolean) {
if (camera == null) {
throw TorchWhenStopped()
}
camera!!.cameraControl.enableTorch(enableTorch)
}
/**
* Analyze a single image.
*/
fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) {
val inputImage = InputImage.fromFilePath(activity, image)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
val barcodeMap = barcodes.map { barcode -> barcode.data }
if (barcodeMap.isNotEmpty()) {
analyzerCallback(barcodeMap)
} else {
analyzerCallback(null)
}
}
.addOnFailureListener { e ->
mobileScannerErrorCallback(
e.localizedMessage ?: e.toString()
)
}
}
private val Barcode.data: Map<String, Any?>
get() = mapOf(
"corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
"rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
"calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
"driverLicense" to driverLicense?.data, "email" to email?.data,
"geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
"url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
)
private val Point.data: Map<String, Double>
get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
private val Barcode.CalendarEvent.data: Map<String, Any?>
get() = mapOf(
"description" to description, "end" to end?.rawValue, "location" to location,
"organizer" to organizer, "start" to start?.rawValue, "status" to status,
"summary" to summary
)
private val Barcode.ContactInfo.data: Map<String, Any?>
get() = mapOf(
"addresses" to addresses.map { address -> address.data },
"emails" to emails.map { email -> email.data }, "name" to name?.data,
"organization" to organization, "phones" to phones.map { phone -> phone.data },
"title" to title, "urls" to urls
)
private val Barcode.Address.data: Map<String, Any?>
get() = mapOf(
"addressLines" to addressLines.map { addressLine -> addressLine.toString() },
"type" to type
)
private val Barcode.PersonName.data: Map<String, Any?>
get() = mapOf(
"first" to first, "formattedName" to formattedName, "last" to last,
"middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
"suffix" to suffix
)
private val Barcode.DriverLicense.data: Map<String, Any?>
get() = mapOf(
"addressCity" to addressCity, "addressState" to addressState,
"addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
"documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
"gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
"lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
)
private val Barcode.Email.data: Map<String, Any?>
get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
private val Barcode.GeoPoint.data: Map<String, Any?>
get() = mapOf("latitude" to lat, "longitude" to lng)
private val Barcode.Phone.data: Map<String, Any?>
get() = mapOf("number" to number, "type" to type)
private val Barcode.Sms.data: Map<String, Any?>
get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
private val Barcode.UrlBookmark.data: Map<String, Any?>
get() = mapOf("title" to title, "url" to url)
private val Barcode.WiFi.data: Map<String, Any?>
get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
}
\ No newline at end of file
}
... ...
package dev.steenbakker.mobile_scanner
import androidx.annotation.NonNull
import android.net.Uri
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
/** MobileScannerPlugin */
class MobileScannerPlugin : FlutterPlugin, ActivityAware {
private var flutter: FlutterPlugin.FlutterPluginBinding? = null
private var activity: ActivityPluginBinding? = null
class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler {
private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null
private var activityPluginBinding: ActivityPluginBinding? = null
private var handler: MobileScanner? = null
private var method: MethodChannel? = null
private var event: EventChannel? = null
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
this.flutter = binding
private lateinit var barcodeHandler: BarcodeHandler
private var permissionResult: MethodChannel.Result? = null
private var analyzerResult: MethodChannel.Result? = null
private val permissionCallback: PermissionCallback = {hasPermission: Boolean ->
permissionResult?.success(hasPermission)
permissionResult = null
}
private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
if (image != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes,
"image" to image
))
} else {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
}
}
private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->
if (barcodes != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
analyzerResult?.success(true)
} else {
analyzerResult?.success(false)
}
analyzerResult = null
}
private val errorCallback: MobileScannerErrorCallback = {error: String ->
barcodeHandler.publishEvent(mapOf(
"name" to "error",
"data" to error,
))
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
this.flutter = null
private val torchStateCallback: TorchStateCallback = {state: Int ->
barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding
handler = MobileScanner(activity!!.activity, flutter!!.textureRegistry)
method = MethodChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
event = EventChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event")
method!!.setMethodCallHandler(handler)
event!!.setStreamHandler(handler)
activity!!.addRequestPermissionsResultListener(handler!!)
@ExperimentalGetImage
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (handler == null) {
result.error("MobileScanner", "Called ${call.method} before initializing.", null)
return
}
when (call.method) {
"state" -> result.success(handler!!.hasCameraPermission())
"request" -> requestPermission(result)
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
else -> result.notImplemented()
}
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
method!!.setMethodCallHandler(this)
barcodeHandler = BarcodeHandler(binding)
this.flutterPluginBinding = binding
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
this.flutterPluginBinding = null
}
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback
)
activityPluginBinding.addRequestPermissionsResultListener(handler!!)
this.activityPluginBinding = activityPluginBinding
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
... ... @@ -38,16 +113,117 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware {
}
override fun onDetachedFromActivity() {
activity!!.removeRequestPermissionsResultListener(handler!!)
event!!.setStreamHandler(null)
activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!)
method!!.setMethodCallHandler(null)
event = null
method = null
handler = null
activity = null
activityPluginBinding = null
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
private fun requestPermission(result: MethodChannel.Result) {
permissionResult = result
handler!!.requestPermission(permissionCallback)
}
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val facing: Int = call.argument<Int>("facing") ?: 0
val formats: List<Int>? = call.argument<List<Int>>("formats")
val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
val speed: Int = call.argument<Int>("speed") ?: 1
val timeout: Int = call.argument<Int>("timeout") ?: 250
var barcodeScannerOptions: BarcodeScannerOptions? = null
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
for (index in formats) {
formatsList.add(BarcodeFormats.values()[index].intValue)
}
barcodeScannerOptions = if (formatsList.size == 1) {
BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
} else {
BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
}
}
val position =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
try {
handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"torchable" to it.hasFlashUnit
))
},
timeout.toLong())
} catch (e: AlreadyStarted) {
result.error(
"MobileScanner",
"Called start() while already started",
null
)
} catch (e: NoCamera) {
result.error(
"MobileScanner",
"No camera found or failed to open camera!",
null
)
} catch (e: TorchError) {
result.error(
"MobileScanner",
"Error occurred when setting torch!",
null
)
} catch (e: CameraError) {
result.error(
"MobileScanner",
"Error occurred when setting up camera!",
null
)
} catch (e: Exception) {
result.error(
"MobileScanner",
"Unknown error occurred..",
null
)
}
}
private fun stop(result: MethodChannel.Result) {
try {
handler!!.stop()
result.success(null)
} catch (e: AlreadyStopped) {
result.error("MobileScanner", "Called stop() while already stopped!", null)
}
}
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
analyzerResult = result
val uri = Uri.fromFile(File(call.arguments.toString()))
handler!!.analyzeImage(uri, analyzerCallback)
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
try {
handler!!.toggleTorch(call.arguments == 1)
result.success(null)
} catch (e: AlreadyStopped) {
result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
}
}
}
... ...
package dev.steenbakker.mobile_scanner
import android.graphics.ImageFormat
import android.graphics.Point
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import com.google.mlkit.vision.barcode.common.Barcode
import java.io.ByteArrayOutputStream
fun Image.toByteArray(): ByteArray {
val yBuffer = planes[0].buffer // Y
val vuBuffer = planes[2].buffer // VU
val ySize = yBuffer.remaining()
val vuSize = vuBuffer.remaining()
val nv21 = ByteArray(ySize + vuSize)
yBuffer.get(nv21, 0, ySize)
vuBuffer.get(nv21, ySize, vuSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
return out.toByteArray()
}
val Barcode.data: Map<String, Any?>
get() = mapOf(
"corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
"rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
"calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
"driverLicense" to driverLicense?.data, "email" to email?.data,
"geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
"url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
)
private val Point.data: Map<String, Double>
get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
private val Barcode.CalendarEvent.data: Map<String, Any?>
get() = mapOf(
"description" to description, "end" to end?.rawValue, "location" to location,
"organizer" to organizer, "start" to start?.rawValue, "status" to status,
"summary" to summary
)
private val Barcode.ContactInfo.data: Map<String, Any?>
get() = mapOf(
"addresses" to addresses.map { address -> address.data },
"emails" to emails.map { email -> email.data }, "name" to name?.data,
"organization" to organization, "phones" to phones.map { phone -> phone.data },
"title" to title, "urls" to urls
)
private val Barcode.Address.data: Map<String, Any?>
get() = mapOf(
"addressLines" to addressLines.map { addressLine -> addressLine.toString() },
"type" to type
)
private val Barcode.PersonName.data: Map<String, Any?>
get() = mapOf(
"first" to first, "formattedName" to formattedName, "last" to last,
"middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
"suffix" to suffix
)
private val Barcode.DriverLicense.data: Map<String, Any?>
get() = mapOf(
"addressCity" to addressCity, "addressState" to addressState,
"addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
"documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
"gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
"lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
)
private val Barcode.Email.data: Map<String, Any?>
get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
private val Barcode.GeoPoint.data: Map<String, Any?>
get() = mapOf("latitude" to lat, "longitude" to lng)
private val Barcode.Phone.data: Map<String, Any?>
get() = mapOf("number" to number, "type" to type)
private val Barcode.Sms.data: Map<String, Any?>
get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
private val Barcode.UrlBookmark.data: Map<String, Any?>
get() = mapOf("title" to title, "url" to url)
private val Barcode.WiFi.data: Map<String, Any?>
get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner.exceptions
internal class NoPermissionException : RuntimeException()
//internal class Exception(val reason: Reason) :
// java.lang.Exception("Mobile Scanner failed because $reason") {
//
// internal enum class Reason {
// noHardware, noPermissions, noBackCamera
// }
//}
\ No newline at end of file
package dev.steenbakker.mobile_scanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
package dev.steenbakker.mobile_scanner.objects
enum class BarcodeFormats(val intValue: Int) {
UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN),
... ...
package dev.steenbakker.mobile_scanner.objects
enum class DetectionSpeed(val intValue: Int) {
NO_DUPLICATES(0),
NORMAL(1),
UNRESTRICTED(2)
}
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner.objects
class MobileScannerStartParameters(
val width: Double = 0.0,
val height: Double,
val hasFlashUnit: Boolean,
val id: Long
)
\ No newline at end of file
... ...
buildscript {
ext.kotlin_version = '1.7.20'
ext.kotlin_version = '1.7.21'
repositories {
google()
mavenCentral()
... ...
... ... @@ -19,6 +19,12 @@ class _BarcodeListScannerWithControllerState
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
bool isStarted = true;
... ... @@ -34,16 +40,18 @@ class _BarcodeListScannerWithControllerState
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// allowDuplicates: true,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcodeCapture, arguments) {
onDetect: (barcodeCapture) {
setState(() {
this.barcodeCapture = barcodeCapture;
});
},
onStart: (arguments) {
// Do something with start arguments
},
),
Align(
alignment: Alignment.bottomCenter,
... ... @@ -99,7 +107,7 @@ class _BarcodeListScannerWithControllerState
height: 50,
child: FittedBox(
child: Text(
'${barcodeCapture?.barcodes.map((e) => e.rawValue)}',
'${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
... ...
... ... @@ -16,9 +16,15 @@ class _BarcodeScannerWithControllerState
BarcodeCapture? barcode;
MobileScannerController controller = MobileScannerController(
torchEnabled: true, detectionSpeed: DetectionSpeed.unrestricted,
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
bool isStarted = true;
... ... @@ -34,12 +40,11 @@ class _BarcodeScannerWithControllerState
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// allowDuplicates: true,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode, args) {
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
});
... ...
... ... @@ -18,10 +18,15 @@ class _BarcodeScannerReturningImageState
MobileScannerArguments? arguments;
MobileScannerController controller = MobileScannerController(
// torchEnabled: true,
returnImage: true,
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
returnImage: true,
);
bool isStarted = true;
... ... @@ -29,49 +34,40 @@ class _BarcodeScannerReturningImageState
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Column(
children: [
Container(
color: Colors.blueGrey,
width: double.infinity,
height: 0.33 * MediaQuery.of(context).size.height,
child: barcode?.image != null
? Transform.rotate(
angle: 90 * pi / 180,
child: Image(
gaplessPlayback: true,
image: MemoryImage(barcode!.image!),
fit: BoxFit.contain,
),
)
: const ColoredBox(
color: Colors.white,
child: Center(
child: Text(
'Your scanned barcode will appear here!',
),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: barcode?.image != null
? Transform.rotate(
angle: 90 * pi / 180,
child: Image(
gaplessPlayback: true,
image: MemoryImage(barcode!.image!),
fit: BoxFit.contain,
),
),
Container(
height: 0.66 * MediaQuery.of(context).size.height,
)
: const Center(
child: Text(
'Your scanned barcode will appear here!',
),
),
),
Expanded(
flex: 2,
child: ColoredBox(
color: Colors.grey,
child: Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// allowDuplicates: true,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode, arguments) {
onDetect: (barcode) {
setState(() {
this.arguments = arguments;
this.barcode = barcode;
});
},
... ... @@ -85,38 +81,33 @@ class _BarcodeScannerReturningImageState
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ColoredBox(
color: arguments != null && !arguments!.hasTorch
? Colors.red
: Colors.white,
child: IconButton(
// color: ,
icon: ValueListenableBuilder(
valueListenable: controller.torchState,
builder: (context, state, child) {
if (state == null) {
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.torchState,
builder: (context, state, child) {
if (state == null) {
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
}
switch (state as TorchState) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
}
switch (state as TorchState) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
IconButton(
color: Colors.white,
... ... @@ -174,9 +165,9 @@ class _BarcodeScannerReturningImageState
],
),
),
],
);
},
),
],
),
),
);
}
... ...
... ... @@ -24,8 +24,7 @@ class _BarcodeScannerWithoutControllerState
children: [
MobileScanner(
fit: BoxFit.contain,
// allowDuplicates: false,
onDetect: (capture, arguments) {
onDetect: (capture) {
setState(() {
this.capture = capture;
});
... ...
... ... @@ -8,7 +8,7 @@ environment:
dependencies:
flutter:
sdk: flutter
image_picker: ^0.8.5+3
image_picker: ^0.8.6
mobile_scanner:
path: ../
... ...
//
// DetectionSpeed.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 11/11/2022.
//
enum DetectionSpeed: Int {
case noDuplicates = 0
case normal = 1
case unrestricted = 2
}
... ...
... ... @@ -49,7 +49,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
super.init()
}
/// Check permissions for video
/// Check if we already have camera permission.
func checkPermission() -> Int {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
... ... @@ -66,6 +66,44 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
func requestPermission(_ result: @escaping FlutterResult) {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
/// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
print("Failed to get image buffer from sample buffer.")
return
}
latestBuffer = imageBuffer
registry?.textureFrameAvailable(textureId)
if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
i = 0
let ciImage = latestBuffer.image
let image = VisionImage(image: ciImage)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: videoPosition
)
scanner.process(image) { [self] barcodes, error in
if (detectionSpeed == DetectionSpeed.noDuplicates) {
let newScannedBarcodes = barcodes?.map { barcode in
return barcode.rawValue
}
if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
return
} else {
barcodesString = newScannedBarcodes
}
}
mobileScannerCallback(barcodes, error, ciImage)
}
} else {
i+=1
}
}
/// Start scanning for barcodes
func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters {
... ... @@ -136,13 +174,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
}
struct MobileScannerStartParameters {
var width: Double = 0.0
var height: Double = 0.0
var hasTorch = false
var textureId: Int64 = 0
}
/// Stop scanning for barcodes
func stop() throws {
if (device == nil) {
... ... @@ -192,54 +223,18 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
var barcodesString: Array<String?>?
/// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
print("Failed to get image buffer from sample buffer.")
return
}
latestBuffer = imageBuffer
registry?.textureFrameAvailable(textureId)
if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
i = 0
let ciImage = latestBuffer.image
let image = VisionImage(image: ciImage)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: videoPosition
)
scanner.process(image) { [self] barcodes, error in
if (detectionSpeed == DetectionSpeed.noDuplicates) {
let newScannedBarcodes = barcodes?.map { barcode in
return barcode.rawValue
}
if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
return
} else {
barcodesString = newScannedBarcodes
}
}
mobileScannerCallback(barcodes, error, ciImage)
}
} else {
i+=1
}
}
/// Convert image buffer to jpeg
private func ciImageToJpeg(ciImage: CIImage) -> Data {
// let ciImage = CIImage(cvPixelBuffer: latestBuffer)
let context:CIContext = CIContext.init(options: nil)
let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
return uiImage.jpegData(compressionQuality: 0.8)!;
}
// /// Convert image buffer to jpeg
// private func ciImageToJpeg(ciImage: CIImage) -> Data {
//
// // let ciImage = CIImage(cvPixelBuffer: latestBuffer)
// let context:CIContext = CIContext.init(options: nil)
// let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
// let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
//
// return uiImage.jpegData(compressionQuality: 0.8)!;
// }
/// Rotates images accordingly
func imageOrientation(
... ... @@ -270,6 +265,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
}
struct MobileScannerStartParameters {
var width: Double = 0.0
var height: Double = 0.0
var hasTorch = false
var textureId: Int64 = 0
}
}
... ...
... ... @@ -21,7 +21,7 @@ extension CVBuffer {
var image: UIImage {
let ciImage = CIImage(cvPixelBuffer: self)
let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
return UIImage(cgImage: cgImage!)
return UIImage(cgImage: cgImage!, scale: 1.0, orientation: UIImage.Orientation.left)
}
var image1: UIImage {
... ...
... ... @@ -56,11 +56,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
/// Parses all parameters and starts the mobileScanner
private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
// let ratio: Int = (call.arguments as! Dictionary<String, Any?>)["ratio"] as! Int
let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0
let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
var barcodeOptions: BarcodeScannerOptions? = nil
... ... @@ -75,10 +75,10 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
let position = facing == 0 ? AVCaptureDevice.Position.front : .back
let speed: DetectionSpeed = DetectionSpeed(rawValue: (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0)!
let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
do {
let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed)
let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed)
result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
} catch MobileScannerError.alreadyStarted {
result(FlutterError(code: "MobileScanner",
... ... @@ -90,7 +90,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
details: nil))
} catch MobileScannerError.torchError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error occured when setting toch!",
message: "Error occured when setting torch!",
details: error))
} catch MobileScannerError.cameraError(let error) {
result(FlutterError(code: "MobileScanner",
... ... @@ -162,9 +162,3 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
}
}
}
enum DetectionSpeed: Int {
case noDuplicates = 0
case normal = 1
case unrestricted = 2
}
... ...
library mobile_scanner;
export 'src/barcode.dart';
export 'src/barcode_capture.dart';
export 'src/enums/camera_facing.dart';
export 'src/enums/detection_speed.dart';
export 'src/enums/mobile_scanner_state.dart';
export 'src/enums/ratio.dart';
export 'src/enums/torch_state.dart';
export 'src/mobile_scanner.dart';
export 'src/mobile_scanner_arguments.dart';
export 'src/mobile_scanner_controller.dart';
export 'src/objects/barcode.dart';
export 'src/objects/barcode_capture.dart';
export 'src/objects/mobile_scanner_arguments.dart';
... ...
... ... @@ -2,11 +2,19 @@
enum DetectionSpeed {
/// The scanner will only scan a barcode once, and never again until another
/// barcode has been scanned.
///
/// NOTE: This mode does analyze every frame in order to check if the value
/// has changed.
noDuplicates,
/// Front facing camera.
/// The barcode scanner will scan one barcode, and wait 250 Miliseconds before
/// scanning again. This will prevent memory issues on older devices.
///
/// You can change the timeout duration with [detectionTimeout] parameter.
normal,
/// Back facing camera.
/// Let the scanner detect barcodes without restriction.
///
/// NOTE: This can cause memory issues with older devices.
unrestricted,
}
... ...
enum MobileScannerState { undetermined, authorized, denied }
/// The authorization state of the scanner.
enum MobileScannerState {
/// The scanner has yet to request weather it is [authorized] or [denied]
undetermined,
/// The scanner has the required permissions.
authorized,
/// The user denied the required permissions.
denied
}
... ...
enum Ratio { ratio_4_3, ratio_16_9 }
// This enum is not used yet
// enum Ratio { ratio_4_3, ratio_16_9 }
... ...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/barcode_capture.dart';
import 'package:mobile_scanner/src/mobile_scanner_arguments.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
typedef MobileScannerCallback = void Function(BarcodeCapture barcodes);
typedef MobileScannerArgumentsCallback = void Function(
MobileScannerArguments? arguments,
);
/// A widget showing a live camera preview.
class MobileScanner extends StatefulWidget {
... ... @@ -10,14 +15,21 @@ class MobileScanner extends StatefulWidget {
final MobileScannerController? controller;
/// Calls the provided [onPermissionSet] callback when the permission is set.
// @Deprecated('Use the [onPermissionSet] paremeter in the [MobileScannerController] instead.')
// ignore: deprecated_consistency
final Function(bool permissionGranted)? onPermissionSet;
/// Function that gets called when a Barcode is detected.
///
/// [barcode] The barcode object with all information about the scanned code.
/// [startArguments] Information about the state of the MobileScanner widget
final Function(BarcodeCapture capture, MobileScannerArguments? arguments)
onDetect;
/// [startInternalArguments] Information about the state of the MobileScanner widget
final MobileScannerCallback onDetect;
/// Function that gets called when the scanner is started.
///
/// [arguments] The start arguments of the scanner. This contains the size of
/// the scanner which can be used to draw a box over the scanner.
final MobileScannerArgumentsCallback? onStart;
/// Handles how the widget should fit the screen.
final BoxFit fit;
... ... @@ -29,10 +41,12 @@ class MobileScanner extends StatefulWidget {
const MobileScanner({
super.key,
required this.onDetect,
this.onStart,
this.controller,
this.autoResume = true,
this.fit = BoxFit.cover,
this.onPermissionSet,
@Deprecated('Use the [onPermissionSet] paremeter in the [MobileScannerController] instead.')
this.onPermissionSet,
});
@override
... ... @@ -49,27 +63,39 @@ class _MobileScannerState extends State<MobileScanner>
WidgetsBinding.instance.addObserver(this);
controller = widget.controller ??
MobileScannerController(onPermissionSet: widget.onPermissionSet);
if (!controller.isStarting) controller.start();
if (!controller.isStarting) {
_startScanner();
}
}
AppLifecycleState? _lastState;
Future<void> _startScanner() async {
final arguments = await controller.start();
widget.onStart?.call(arguments);
}
bool resumeFromBackground = false;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// App state changed before it is initialized.
if (controller.isStarting) {
return;
}
switch (state) {
case AppLifecycleState.resumed:
if (!controller.isStarting &&
widget.autoResume &&
_lastState != AppLifecycleState.inactive) controller.start();
resumeFromBackground = false;
_startScanner();
break;
case AppLifecycleState.paused:
case AppLifecycleState.detached:
controller.stop();
resumeFromBackground = true;
break;
case AppLifecycleState.inactive:
if (!resumeFromBackground) controller.stop();
break;
default:
break;
}
_lastState = state;
}
@override
... ... @@ -82,7 +108,7 @@ class _MobileScannerState extends State<MobileScanner>
return const ColoredBox(color: Colors.black);
} else {
controller.barcodes.listen((barcode) {
widget.onDetect(barcode, value! as MobileScannerArguments);
widget.onDetect(barcode);
});
return ClipRect(
child: SizedBox(
... ...
... ... @@ -13,11 +13,10 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
class MobileScannerController {
MobileScannerController({
this.facing = CameraFacing.back,
this.detectionSpeed = DetectionSpeed.noDuplicates,
// this.ratio,
this.detectionSpeed = DetectionSpeed.normal,
this.detectionTimeoutMs = 250,
this.torchEnabled = false,
this.formats,
// this.autoResume = true,
this.returnImage = false,
this.onPermissionSet,
}) {
... ... @@ -31,7 +30,8 @@ class MobileScannerController {
.listen((data) => _handleEvent(data as Map));
}
//Must be static to keep the same value on new instances
/// The hashcode of the controller to check if the correct object is mounted.
/// Must be static to keep the same value on new instances
static int? controllerHashcode;
/// Select which camera should be used.
... ... @@ -39,11 +39,6 @@ class MobileScannerController {
/// Default: CameraFacing.back
final CameraFacing facing;
// /// Analyze the image in 4:3 or 16:9
// ///
// /// Only on Android
// final Ratio? ratio;
/// Enable or disable the torch (Flash) on start
///
/// Default: disabled
... ... @@ -62,6 +57,13 @@ class MobileScannerController {
/// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
final DetectionSpeed detectionSpeed;
/// Sets the timeout of scanner.
/// The timeout is set in miliseconds.
///
/// NOTE: The timeout only works if the [detectionSpeed] is set to
/// [DetectionSpeed.normal] (which is the default value).
final int detectionTimeoutMs;
/// Sets the barcode stream
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
... ... @@ -89,6 +91,7 @@ class MobileScannerController {
ValueNotifier(facing);
bool isStarting = false;
bool? _hasTorch;
/// Set the starting arguments for the camera
... ... @@ -97,10 +100,9 @@ class MobileScannerController {
cameraFacingState.value = cameraFacingOverride ?? facing;
arguments['facing'] = cameraFacingState.value.index;
// if (ratio != null) arguments['ratio'] = ratio;
arguments['torch'] = torchEnabled;
arguments['speed'] = detectionSpeed.index;
arguments['timeout'] = detectionTimeoutMs;
if (formats != null) {
if (Platform.isAndroid) {
... ... @@ -118,9 +120,9 @@ class MobileScannerController {
Future<MobileScannerArguments?> start({
CameraFacing? cameraFacingOverride,
}) async {
debugPrint('Hashcode controller: $hashCode');
if (isStarting) {
debugPrint("Called start() while starting.");
return null;
}
isStarting = true;
... ... @@ -157,10 +159,10 @@ class MobileScannerController {
);
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
isStarting = false;
if (error.code == "MobileScannerWeb") {
onPermissionSet?.call(false);
}
isStarting = false;
return null;
}
... ... @@ -177,32 +179,33 @@ class MobileScannerController {
}
if (kIsWeb) {
// If we reach this line, it means camera permission has been granted
onPermissionSet?.call(
true,
); // If we reach this line, it means camera permission has been granted
startArguments.value = MobileScannerArguments(
webId: startResult['ViewID'] as String?,
size: Size(
startResult['videoWidth'] as double? ?? 0,
startResult['videoHeight'] as double? ?? 0,
),
hasTorch: _hasTorch!,
);
} else {
startArguments.value = MobileScannerArguments(
textureId: startResult['textureId'] as int?,
size: toSize(startResult['size'] as Map? ?? {}),
hasTorch: _hasTorch!,
);
}
isStarting = false;
return startArguments.value!;
return startArguments.value = MobileScannerArguments(
size: kIsWeb
? Size(
startResult['videoWidth'] as double? ?? 0,
startResult['videoHeight'] as double? ?? 0,
)
: toSize(startResult['size'] as Map? ?? {}),
hasTorch: _hasTorch!,
textureId: kIsWeb ? null : startResult['textureId'] as int?,
webId: kIsWeb ? startResult['ViewID'] as String? : null,
);
}
/// Stops the camera, but does not dispose this controller.
Future<void> stop() async {
await _methodChannel.invokeMethod('stop');
try {
await _methodChannel.invokeMethod('stop');
} catch (e) {
debugPrint('$e');
}
}
/// Switches the torch on or off.
... ... @@ -277,7 +280,7 @@ class MobileScannerController {
_barcodesController.add(
BarcodeCapture(
barcodes: parsed,
image: event['image'] as Uint8List,
image: event['image'] as Uint8List?,
),
);
break;
... ...
... ... @@ -12,11 +12,6 @@ class Barcode {
/// Returns null if the corner points can not be determined.
final List<Offset>? corners;
/// Returns raw bytes of the image buffer
///
/// Returns null if the image was not returned
final Uint8List? image;
/// Returns barcode format
final BarcodeFormat format;
... ... @@ -79,7 +74,6 @@ class Barcode {
Barcode({
this.corners,
this.image,
this.format = BarcodeFormat.ean13,
this.rawBytes,
this.type = BarcodeType.text,
... ... @@ -97,7 +91,7 @@ class Barcode {
});
/// Create a [Barcode] from native data.
Barcode.fromNative(Map data, {this.image})
Barcode.fromNative(Map data)
: corners = toCorners(data['corners'] as List?),
format = toFormat(data['format'] as int),
rawBytes = data['rawBytes'] as Uint8List?,
... ...
import 'dart:typed_data';
import 'package:mobile_scanner/src/barcode.dart';
import 'package:mobile_scanner/src/objects/barcode.dart';
/// The return object after a frame is scanned.
///
/// [barcodes] A list with barcodes. A scanned frame can contain multiple
/// barcodes.
/// [image] If enabled, an image of the scanned frame.
class BarcodeCapture {
List<Barcode> barcodes;
Uint8List? image;
final List<Barcode> barcodes;
final Uint8List? image;
BarcodeCapture({
required this.barcodes,
... ...
import 'package:flutter/material.dart';
/// Camera args for [CameraView].
/// The start arguments of the scanner.
class MobileScannerArguments {
/// The texture id.
final int? textureId;
/// Size of the texture.
/// The output size of the camera.
/// This value can be used to draw a box in the image.
final Size size;
/// A bool which is true if the device has a torch.
final bool hasTorch;
/// The texture id of the capture used internally.
final int? textureId;
/// The texture id of the capture used internally if device is web.
final String? webId;
/// Create a [MobileScannerArguments].
MobileScannerArguments({
this.textureId,
required this.size,
required this.hasTorch,
this.textureId,
this.webId,
});
}
... ...
name: mobile_scanner
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.
version: 3.0.0-beta.1
version: 3.0.0-beta.2
repository: https://github.com/juliansteenbakker/mobile_scanner
environment:
... ...