Julian Steenbakker
Committed by GitHub

Merge branch 'master' into overlay

@@ -11,7 +11,7 @@ jobs: @@ -11,7 +11,7 @@ jobs:
11 analysis: 11 analysis:
12 runs-on: ubuntu-latest 12 runs-on: ubuntu-latest
13 steps: 13 steps:
14 - - uses: actions/checkout@v3.5.2 14 + - uses: actions/checkout@v3.5.3
15 - uses: actions/setup-java@v3.11.0 15 - uses: actions/setup-java@v3.11.0
16 with: 16 with:
17 java-version: 11 17 java-version: 11
@@ -28,7 +28,7 @@ jobs: @@ -28,7 +28,7 @@ jobs:
28 formatting: 28 formatting:
29 runs-on: ubuntu-latest 29 runs-on: ubuntu-latest
30 steps: 30 steps:
31 - - uses: actions/checkout@v3.5.2 31 + - uses: actions/checkout@v3.5.3
32 - uses: actions/setup-java@v3.11.0 32 - uses: actions/setup-java@v3.11.0
33 with: 33 with:
34 java-version: 11 34 java-version: 11
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 release-please: 7 release-please:
8 runs-on: ubuntu-latest 8 runs-on: ubuntu-latest
9 steps: 9 steps:
10 - - uses: GoogleCloudPlatform/release-please-action@v3.7.9 10 + - uses: GoogleCloudPlatform/release-please-action@v3.7.10
11 with: 11 with:
12 token: ${{ secrets.GITHUB_TOKEN }} 12 token: ${{ secrets.GITHUB_TOKEN }}
13 release-type: simple 13 release-type: simple
1 # This file tracks properties of this Flutter project. 1 # This file tracks properties of this Flutter project.
2 # Used by Flutter tool to assess capabilities and perform upgrades etc. 2 # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 # 3 #
4 -# This file should be version controlled and should not be manually edited. 4 +# This file should be version controlled.
5 5
6 version: 6 version:
7 - revision: 5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c 7 + revision: 796c8ef79279f9c774545b3771238c3098dbefab
8 channel: stable 8 channel: stable
9 9
10 project_type: plugin 10 project_type: plugin
  11 +
  12 +# Tracks metadata for the flutter migrate command
  13 +migration:
  14 + platforms:
  15 + - platform: root
  16 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  17 + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  18 + - platform: android
  19 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  20 + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  21 + - platform: ios
  22 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  23 + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  24 + - platform: macos
  25 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  26 + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  27 + - platform: web
  28 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  29 + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
  30 +
  31 + # User provided section
  32 +
  33 + # List of Local paths (relative to this file) that should be
  34 + # ignored by the migrate tool.
  35 + #
  36 + # Files that are not part of the templates will be ignored by default.
  37 + unmanaged_files:
  38 + - 'lib/main.dart'
  39 + - 'ios/Runner.xcodeproj/project.pbxproj'
1 -## 3.2.1 1 +## 3.3.0
2 Bugs fixed: 2 Bugs fixed:
  3 +* Fixed bug where onDetect method was being called multiple times
3 * [Android] Fix Gradle 8 compatibility by adding the `namespace` attribute to the build.gradle. 4 * [Android] Fix Gradle 8 compatibility by adding the `namespace` attribute to the build.gradle.
4 5
  6 +Improvements:
  7 +* [Android] Upgraded camera2 dependency
  8 +* Added zoomScale value notifier in MobileScannerController for the application to know the zoom scale value set actually.
  9 + The value is notified from the native SDK(CameraX/AVFoundation).
  10 +* Added resetZoomScale() in MobileScannerController to reset zoom ratio with 1x.
  11 + Both Android and iOS, if the device have ultra-wide camera, calling setZoomScale with small value causes to use ultra-wide camera and may be diffcult to detect barcodes.
  12 + resetZoomScale() is useful to use standard camera with zoom 1x.
  13 + setZoomScale() with the specific value can realize same effect, but added resetZoomScale for avoiding floating point errors.
  14 + The application can know what zoom scale value is selected actually by subscribing zoomScale above after calling resetZoomScale.
  15 +* [iOS] Call resetZoomScale while starting scan.
  16 + Android camera is initialized with a zoom of 1x, whereas iOS is initialized with the minimum zoom value, which causes to select the ultra-wide camera unintentionally ([iOS] Impossible to focus and scan the QR code due to picking the wide back camera #554).
  17 + Fixed this issue by calling resetZoomScale
  18 +* [iOS] Remove zoom animation with ramp function to match Android behavior.
  19 +
5 ## 3.2.0 20 ## 3.2.0
6 Improvements: 21 Improvements:
7 * [iOS] Updated GoogleMLKit/BarcodeScanning to 4.0.0 22 * [iOS] Updated GoogleMLKit/BarcodeScanning to 4.0.0
@@ -53,6 +53,12 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: @@ -53,6 +53,12 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities:
53 53
54 <img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png"> 54 <img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png">
55 55
  56 +## Web
  57 +This package uses ZXing on web to read barcodes so it needs to be included in `index.html` as script.
  58 +```html
  59 +<script src="https://unpkg.com/@zxing/library@0.19.1" type="application/javascript"></script>
  60 +```
  61 +
56 ## Usage 62 ## Usage
57 63
58 Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller. 64 Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller.
@@ -2,14 +2,14 @@ group 'dev.steenbakker.mobile_scanner' @@ -2,14 +2,14 @@ group 'dev.steenbakker.mobile_scanner'
2 version '1.0-SNAPSHOT' 2 version '1.0-SNAPSHOT'
3 3
4 buildscript { 4 buildscript {
5 - ext.kotlin_version = '1.7.22' 5 + ext.kotlin_version = '1.8.22'
6 repositories { 6 repositories {
7 google() 7 google()
8 mavenCentral() 8 mavenCentral()
9 } 9 }
10 10
11 dependencies { 11 dependencies {
12 - classpath 'com.android.tools.build:gradle:8.0.0' 12 + classpath 'com.android.tools.build:gradle:8.0.2'
13 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 } 14 }
15 } 15 }
@@ -56,5 +56,6 @@ dependencies { @@ -56,5 +56,6 @@ dependencies {
56 // implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0' 56 // implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0'
57 57
58 implementation 'androidx.camera:camera-camera2:1.2.2' 58 implementation 'androidx.camera:camera-camera2:1.2.2'
59 - implementation 'androidx.camera:camera-lifecycle:1.2.2' 59 + implementation 'androidx.camera:camera-lifecycle:1.2.3'
  60 + implementation 'androidx.camera:camera-camera2:1.2.3'
60 } 61 }
1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 import android.app.Activity 3 import android.app.Activity
  4 +import android.graphics.Bitmap
  5 +import android.graphics.Matrix
4 import android.graphics.Rect 6 import android.graphics.Rect
5 import android.net.Uri 7 import android.net.Uri
6 import android.os.Handler 8 import android.os.Handler
@@ -16,9 +18,12 @@ import com.google.mlkit.vision.barcode.common.Barcode @@ -16,9 +18,12 @@ import com.google.mlkit.vision.barcode.common.Barcode
16 import com.google.mlkit.vision.common.InputImage 18 import com.google.mlkit.vision.common.InputImage
17 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed 19 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
18 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters 20 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
  21 +import dev.steenbakker.mobile_scanner.utils.YuvToRgbConverter
19 import io.flutter.view.TextureRegistry 22 import io.flutter.view.TextureRegistry
  23 +import java.io.ByteArrayOutputStream
20 import kotlin.math.roundToInt 24 import kotlin.math.roundToInt
21 25
  26 +
22 class MobileScanner( 27 class MobileScanner(
23 private val activity: Activity, 28 private val activity: Activity,
24 private val textureRegistry: TextureRegistry, 29 private val textureRegistry: TextureRegistry,
@@ -82,15 +87,42 @@ class MobileScanner( @@ -82,15 +87,42 @@ class MobileScanner(
82 } 87 }
83 } 88 }
84 89
  90 +
85 if (barcodeMap.isNotEmpty()) { 91 if (barcodeMap.isNotEmpty()) {
  92 + if (returnImage) {
  93 +
  94 + val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888)
  95 +
  96 + val imageFormat = YuvToRgbConverter(activity.applicationContext)
  97 +
  98 + imageFormat.yuvToRgb(mediaImage, bitmap)
  99 +
  100 + val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f)
  101 +
  102 + val stream = ByteArrayOutputStream()
  103 + bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream)
  104 + val byteArray = stream.toByteArray()
  105 + bmResult.recycle()
  106 +
  107 +
  108 + mobileScannerCallback(
  109 + barcodeMap,
  110 + byteArray,
  111 + bmResult.width,
  112 + bmResult.height
  113 + )
  114 +
  115 + } else {
  116 +
86 mobileScannerCallback( 117 mobileScannerCallback(
87 barcodeMap, 118 barcodeMap,
88 - if (returnImage) mediaImage.toByteArray() else null,  
89 - if (returnImage) mediaImage.width else null,  
90 - if (returnImage) mediaImage.height else null 119 + null,
  120 + null,
  121 + null
91 ) 122 )
92 } 123 }
93 } 124 }
  125 + }
94 .addOnFailureListener { e -> 126 .addOnFailureListener { e ->
95 mobileScannerErrorCallback( 127 mobileScannerErrorCallback(
96 e.localizedMessage ?: e.toString() 128 e.localizedMessage ?: e.toString()
@@ -106,6 +138,13 @@ class MobileScanner( @@ -106,6 +138,13 @@ class MobileScanner(
106 } 138 }
107 } 139 }
108 140
  141 + fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
  142 + val matrix = Matrix()
  143 + matrix.postRotate(degrees)
  144 + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
  145 + }
  146 +
  147 +
109 // scales the scanWindow to the provided inputImage and checks if that scaled 148 // scales the scanWindow to the provided inputImage and checks if that scaled
110 // scanWindow contains the barcode 149 // scanWindow contains the barcode
111 private fun isBarcodeInScanWindow( 150 private fun isBarcodeInScanWindow(
@@ -227,7 +266,6 @@ class MobileScanner( @@ -227,7 +266,6 @@ class MobileScanner(
227 }, executor) 266 }, executor)
228 267
229 } 268 }
230 -  
231 /** 269 /**
232 * Stop barcode scanning. 270 * Stop barcode scanning.
233 */ 271 */
  1 +package dev.steenbakker.mobile_scanner.utils
  2 +
  3 +import android.graphics.ImageFormat
  4 +import android.media.Image
  5 +import androidx.annotation.IntDef
  6 +import java.nio.ByteBuffer
  7 +
  8 +/*
  9 +This file is converted from part of https://github.com/gordinmitya/yuv2buf.
  10 +Follow the link to find demo app, performance benchmarks and unit tests.
  11 +
  12 +Intro to YUV image formats:
  13 +YUV_420_888 - is a generic format that can be represented as I420, YV12, NV21, and NV12.
  14 +420 means that for each 4 luminosity pixels we have 2 chroma pixels: U and V.
  15 +
  16 +* I420 format represents an image as Y plane followed by U then followed by V plane
  17 + without chroma channels interleaving.
  18 + For example:
  19 + Y Y Y Y
  20 + Y Y Y Y
  21 + U U V V
  22 +
  23 +* NV21 format represents an image as Y plane followed by V and U interleaved. First V then U.
  24 + For example:
  25 + Y Y Y Y
  26 + Y Y Y Y
  27 + V U V U
  28 +
  29 +* YV12 and NV12 are the same as previous formats but with swapped order of V and U. (U then V)
  30 +
  31 +Visualization of these 4 formats:
  32 +https://user-images.githubusercontent.com/9286092/89119601-4f6f8100-d4b8-11ea-9a51-2765f7e513c2.jpg
  33 +
  34 +It's guaranteed that image.getPlanes() always returns planes in order Y U V for YUV_420_888.
  35 +https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
  36 +
  37 +Because I420 and NV21 are more widely supported (RenderScript, OpenCV, MNN)
  38 +the conversion is done into these formats.
  39 +
  40 +More about each format: https://www.fourcc.org/yuv.php
  41 +*/
  42 +
  43 +@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
  44 +@IntDef(ImageFormat.NV21, ImageFormat.YUV_420_888)
  45 +annotation class YuvType
  46 +
  47 +class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) {
  48 + @YuvType
  49 + val type: Int
  50 + val buffer: ByteBuffer
  51 +
  52 + init {
  53 + val wrappedImage = ImageWrapper(image)
  54 +
  55 + type = if (wrappedImage.u.pixelStride == 1) {
  56 + ImageFormat.YUV_420_888
  57 + } else {
  58 + ImageFormat.NV21
  59 + }
  60 + val size = image.width * image.height * 3 / 2
  61 + buffer = if (
  62 + dstBuffer == null || dstBuffer.capacity() < size ||
  63 + dstBuffer.isReadOnly || !dstBuffer.isDirect
  64 + ) {
  65 + ByteBuffer.allocateDirect(size) }
  66 + else {
  67 + dstBuffer
  68 + }
  69 + buffer.rewind()
  70 +
  71 + removePadding(wrappedImage)
  72 + }
  73 +
  74 + // Input buffers are always direct as described in
  75 + // https://developer.android.com/reference/android/media/Image.Plane#getBuffer()
  76 + private fun removePadding(image: ImageWrapper) {
  77 + val sizeLuma = image.y.width * image.y.height
  78 + val sizeChroma = image.u.width * image.u.height
  79 + if (image.y.rowStride > image.y.width) {
  80 + removePaddingCompact(image.y, buffer, 0)
  81 + } else {
  82 + buffer.position(0)
  83 + buffer.put(image.y.buffer)
  84 + }
  85 + if (type == ImageFormat.YUV_420_888) {
  86 + if (image.u.rowStride > image.u.width) {
  87 + removePaddingCompact(image.u, buffer, sizeLuma)
  88 + removePaddingCompact(image.v, buffer, sizeLuma + sizeChroma)
  89 + } else {
  90 + buffer.position(sizeLuma)
  91 + buffer.put(image.u.buffer)
  92 + buffer.position(sizeLuma + sizeChroma)
  93 + buffer.put(image.v.buffer)
  94 + }
  95 + } else {
  96 + if (image.u.rowStride > image.u.width * 2) {
  97 + removePaddingNotCompact(image, buffer, sizeLuma)
  98 + } else {
  99 + buffer.position(sizeLuma)
  100 + var uv = image.v.buffer
  101 + val properUVSize = image.v.height * image.v.rowStride - 1
  102 + if (uv.capacity() > properUVSize) {
  103 + uv = clipBuffer(image.v.buffer, 0, properUVSize)
  104 + }
  105 + buffer.put(uv)
  106 + val lastOne = image.u.buffer[image.u.buffer.capacity() - 1]
  107 + buffer.put(buffer.capacity() - 1, lastOne)
  108 + }
  109 + }
  110 + buffer.rewind()
  111 + }
  112 +
  113 + private fun removePaddingCompact(
  114 + plane: PlaneWrapper,
  115 + dst: ByteBuffer,
  116 + offset: Int
  117 + ) {
  118 + require(plane.pixelStride == 1) {
  119 + "use removePaddingCompact with pixelStride == 1"
  120 + }
  121 +
  122 + val src = plane.buffer
  123 + val rowStride = plane.rowStride
  124 + var row: ByteBuffer
  125 + dst.position(offset)
  126 + for (i in 0 until plane.height) {
  127 + row = clipBuffer(src, i * rowStride, plane.width)
  128 + dst.put(row)
  129 + }
  130 + }
  131 +
  132 + private fun removePaddingNotCompact(
  133 + image: ImageWrapper,
  134 + dst: ByteBuffer,
  135 + offset: Int
  136 + ) {
  137 + require(image.u.pixelStride == 2) {
  138 + "use removePaddingNotCompact pixelStride == 2"
  139 + }
  140 + val width = image.u.width
  141 + val height = image.u.height
  142 + val rowStride = image.u.rowStride
  143 + var row: ByteBuffer
  144 + dst.position(offset)
  145 + for (i in 0 until height - 1) {
  146 + row = clipBuffer(image.v.buffer, i * rowStride, width * 2)
  147 + dst.put(row)
  148 + }
  149 + row = clipBuffer(image.u.buffer, (height - 1) * rowStride - 1, width * 2)
  150 + dst.put(row)
  151 + }
  152 +
  153 + private fun clipBuffer(buffer: ByteBuffer, start: Int, size: Int): ByteBuffer {
  154 + val duplicate = buffer.duplicate()
  155 + duplicate.position(start)
  156 + duplicate.limit(start + size)
  157 + return duplicate.slice()
  158 + }
  159 +
  160 + private class ImageWrapper(image:Image) {
  161 + val width= image.width
  162 + val height = image.height
  163 + val y = PlaneWrapper(width, height, image.planes[0])
  164 + val u = PlaneWrapper(width / 2, height / 2, image.planes[1])
  165 + val v = PlaneWrapper(width / 2, height / 2, image.planes[2])
  166 +
  167 + // Check this is a supported image format
  168 + // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
  169 + init {
  170 + require(y.pixelStride == 1) {
  171 + "Pixel stride for Y plane must be 1 but got ${y.pixelStride} instead."
  172 + }
  173 + require(u.pixelStride == v.pixelStride && u.rowStride == v.rowStride) {
  174 + "U and V planes must have the same pixel and row strides " +
  175 + "but got pixel=${u.pixelStride} row=${u.rowStride} for U " +
  176 + "and pixel=${v.pixelStride} and row=${v.rowStride} for V"
  177 + }
  178 + require(u.pixelStride == 1 || u.pixelStride == 2) {
  179 + "Supported" + " pixel strides for U and V planes are 1 and 2"
  180 + }
  181 + }
  182 + }
  183 +
  184 + private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) {
  185 + val width = width
  186 + val height = height
  187 + val buffer: ByteBuffer = plane.buffer
  188 + val rowStride = plane.rowStride
  189 + val pixelStride = plane.pixelStride
  190 + }
  191 +}
  1 +package dev.steenbakker.mobile_scanner.utils
  2 +
  3 +import android.content.Context
  4 +import android.graphics.Bitmap
  5 +import android.graphics.ImageFormat
  6 +import android.media.Image
  7 +import android.os.Build
  8 +import android.renderscript.Allocation
  9 +import android.renderscript.Element
  10 +import android.renderscript.RenderScript
  11 +import android.renderscript.ScriptIntrinsicYuvToRGB
  12 +import android.renderscript.Type
  13 +import androidx.annotation.RequiresApi
  14 +import java.nio.ByteBuffer
  15 +
  16 +
  17 +/**
  18 + * Helper class used to efficiently convert a [Media.Image] object from
  19 + * YUV_420_888 format to an RGB [Bitmap] object.
  20 + *
  21 + * Copied from https://github.com/owahltinez/camerax-tflite/blob/master/app/src/main/java/com/android/example/camerax/tflite/YuvToRgbConverter.kt
  22 + *
  23 + * The [yuvToRgb] method is able to achieve the same FPS as the CameraX image
  24 + * analysis use case at the default analyzer resolution, which is 30 FPS with
  25 + * 640x480 on a Pixel 3 XL device.
  26 + */class YuvToRgbConverter(context: Context) {
  27 + private val rs = RenderScript.create(context)
  28 + private val scriptYuvToRgb =
  29 + ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
  30 +
  31 + // Do not add getters/setters functions to these private variables
  32 + // because yuvToRgb() assume they won't be modified elsewhere
  33 + private var yuvBits: ByteBuffer? = null
  34 + private var bytes: ByteArray = ByteArray(0)
  35 + private var inputAllocation: Allocation? = null
  36 + private var outputAllocation: Allocation? = null
  37 +
  38 + @Synchronized
  39 + fun yuvToRgb(image: Image, output: Bitmap) {
  40 + val yuvBuffer = YuvByteBuffer(image, yuvBits)
  41 + yuvBits = yuvBuffer.buffer
  42 +
  43 + if (needCreateAllocations(image, yuvBuffer)) {
  44 + val yuvType = Type.Builder(rs, Element.U8(rs))
  45 + .setX(image.width)
  46 + .setY(image.height)
  47 + .setYuvFormat(yuvBuffer.type)
  48 + inputAllocation = Allocation.createTyped(
  49 + rs,
  50 + yuvType.create(),
  51 + Allocation.USAGE_SCRIPT
  52 + )
  53 + bytes = ByteArray(yuvBuffer.buffer.capacity())
  54 + val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs))
  55 + .setX(image.width)
  56 + .setY(image.height)
  57 + outputAllocation = Allocation.createTyped(
  58 + rs,
  59 + rgbaType.create(),
  60 + Allocation.USAGE_SCRIPT
  61 + )
  62 + }
  63 +
  64 + yuvBuffer.buffer.get(bytes)
  65 + inputAllocation!!.copyFrom(bytes)
  66 +
  67 + // Convert NV21 or YUV_420_888 format to RGB
  68 + inputAllocation!!.copyFrom(bytes)
  69 + scriptYuvToRgb.setInput(inputAllocation)
  70 + scriptYuvToRgb.forEach(outputAllocation)
  71 + outputAllocation!!.copyTo(output)
  72 + }
  73 +
  74 + private fun needCreateAllocations(image: Image, yuvBuffer: YuvByteBuffer): Boolean {
  75 + return (inputAllocation == null || // the very 1st call
  76 + inputAllocation!!.type.x != image.width || // image size changed
  77 + inputAllocation!!.type.y != image.height ||
  78 + inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed
  79 + bytes.size == yuvBuffer.buffer.capacity())
  80 + }
  81 +}
1 buildscript { 1 buildscript {
2 - ext.kotlin_version = '1.7.22' 2 + ext.kotlin_version = '1.8.22'
3 repositories { 3 repositories {
4 google() 4 google()
5 mavenCentral() 5 mavenCentral()
6 } 6 }
7 7
8 dependencies { 8 dependencies {
9 - classpath 'com.android.tools.build:gradle:8.0.0' 9 + classpath 'com.android.tools.build:gradle:8.0.2'
10 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 } 11 }
12 } 12 }
@@ -26,6 +26,6 @@ subprojects { @@ -26,6 +26,6 @@ subprojects {
26 project.evaluationDependsOn(':app') 26 project.evaluationDependsOn(':app')
27 } 27 }
28 28
29 -task clean(type: Delete) { 29 +tasks.register("clean", Delete) {
30 delete rootProject.buildDir 30 delete rootProject.buildDir
31 } 31 }
1 -#Tue Aug 23 15:51:00 CEST 2022 1 +#Tue Jun 27 18:47:05 CEST 2023
2 distributionBase=GRADLE_USER_HOME 2 distributionBase=GRADLE_USER_HOME
3 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip  
4 distributionPath=wrapper/dists 3 distributionPath=wrapper/dists
5 -zipStorePath=wrapper/dists 4 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
6 zipStoreBase=GRADLE_USER_HOME 5 zipStoreBase=GRADLE_USER_HOME
  6 +zipStorePath=wrapper/dists
@@ -204,6 +204,7 @@ @@ -204,6 +204,7 @@
204 files = ( 204 files = (
205 ); 205 );
206 inputPaths = ( 206 inputPaths = (
  207 + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
207 ); 208 );
208 name = "Thin Binary"; 209 name = "Thin Binary";
209 outputPaths = ( 210 outputPaths = (
@@ -50,6 +50,7 @@ class _BarcodeListScannerWithControllerState @@ -50,6 +50,7 @@ class _BarcodeListScannerWithControllerState
50 @override 50 @override
51 Widget build(BuildContext context) { 51 Widget build(BuildContext context) {
52 return Scaffold( 52 return Scaffold(
  53 + appBar: AppBar(title: const Text('With ValueListenableBuilder')),
53 backgroundColor: Colors.black, 54 backgroundColor: Colors.black,
54 body: Builder( 55 body: Builder(
55 builder: (context) { 56 builder: (context) {
@@ -50,6 +50,7 @@ class _BarcodeScannerWithControllerState @@ -50,6 +50,7 @@ class _BarcodeScannerWithControllerState
50 @override 50 @override
51 Widget build(BuildContext context) { 51 Widget build(BuildContext context) {
52 return Scaffold( 52 return Scaffold(
  53 + appBar: AppBar(title: const Text('With controller')),
53 backgroundColor: Colors.black, 54 backgroundColor: Colors.black,
54 body: Builder( 55 body: Builder(
55 builder: (context) { 56 builder: (context) {
@@ -70,6 +70,7 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> @@ -70,6 +70,7 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView>
70 @override 70 @override
71 Widget build(BuildContext context) { 71 Widget build(BuildContext context) {
72 return Scaffold( 72 return Scaffold(
  73 + appBar: AppBar(title: const Text('With PageView')),
73 backgroundColor: Colors.black, 74 backgroundColor: Colors.black,
74 body: PageView( 75 body: PageView(
75 children: [ 76 children: [
@@ -52,6 +52,7 @@ class _BarcodeScannerReturningImageState @@ -52,6 +52,7 @@ class _BarcodeScannerReturningImageState
52 @override 52 @override
53 Widget build(BuildContext context) { 53 Widget build(BuildContext context) {
54 return Scaffold( 54 return Scaffold(
  55 + appBar: AppBar(title: const Text('Returning image')),
55 body: SafeArea( 56 body: SafeArea(
56 child: Column( 57 child: Column(
57 children: [ 58 children: [
@@ -3,6 +3,8 @@ import 'dart:io'; @@ -3,6 +3,8 @@ import 'dart:io';
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
4 import 'package:mobile_scanner/mobile_scanner.dart'; 4 import 'package:mobile_scanner/mobile_scanner.dart';
5 5
  6 +import 'package:mobile_scanner_example/scanner_error_widget.dart';
  7 +
6 class BarcodeScannerWithScanWindow extends StatefulWidget { 8 class BarcodeScannerWithScanWindow extends StatefulWidget {
7 const BarcodeScannerWithScanWindow({Key? key}) : super(key: key); 9 const BarcodeScannerWithScanWindow({Key? key}) : super(key: key);
8 10
@@ -32,6 +34,7 @@ class _BarcodeScannerWithScanWindowState @@ -32,6 +34,7 @@ class _BarcodeScannerWithScanWindowState
32 height: 200, 34 height: 200,
33 ); 35 );
34 return Scaffold( 36 return Scaffold(
  37 + appBar: AppBar(title: const Text('With Scan window')),
35 backgroundColor: Colors.black, 38 backgroundColor: Colors.black,
36 body: Builder( 39 body: Builder(
37 builder: (context) { 40 builder: (context) {
@@ -47,6 +50,9 @@ class _BarcodeScannerWithScanWindowState @@ -47,6 +50,9 @@ class _BarcodeScannerWithScanWindowState
47 this.arguments = arguments; 50 this.arguments = arguments;
48 }); 51 });
49 }, 52 },
  53 + errorBuilder: (context, error, child) {
  54 + return ScannerErrorWidget(error: error);
  55 + },
50 onDetect: onDetect, 56 onDetect: onDetect,
51 ), 57 ),
52 if (barcode != null && 58 if (barcode != null &&
@@ -18,6 +18,7 @@ class _BarcodeScannerWithoutControllerState @@ -18,6 +18,7 @@ class _BarcodeScannerWithoutControllerState
18 @override 18 @override
19 Widget build(BuildContext context) { 19 Widget build(BuildContext context) {
20 return Scaffold( 20 return Scaffold(
  21 + appBar: AppBar(title: const Text('Without controller')),
21 backgroundColor: Colors.black, 22 backgroundColor: Colors.black,
22 body: Builder( 23 body: Builder(
23 builder: (context) { 24 builder: (context) {
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; @@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
2 import 'package:image_picker/image_picker.dart'; 2 import 'package:image_picker/image_picker.dart';
3 import 'package:mobile_scanner/mobile_scanner.dart'; 3 import 'package:mobile_scanner/mobile_scanner.dart';
4 4
  5 +import 'package:mobile_scanner_example/scanner_error_widget.dart';
  6 +
5 class BarcodeScannerWithZoom extends StatefulWidget { 7 class BarcodeScannerWithZoom extends StatefulWidget {
6 const BarcodeScannerWithZoom({Key? key}) : super(key: key); 8 const BarcodeScannerWithZoom({Key? key}) : super(key: key);
7 9
@@ -23,6 +25,7 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> @@ -23,6 +25,7 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>
23 @override 25 @override
24 Widget build(BuildContext context) { 26 Widget build(BuildContext context) {
25 return Scaffold( 27 return Scaffold(
  28 + appBar: AppBar(title: const Text('With zoom slider')),
26 backgroundColor: Colors.black, 29 backgroundColor: Colors.black,
27 body: Builder( 30 body: Builder(
28 builder: (context) { 31 builder: (context) {
@@ -31,6 +34,9 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> @@ -31,6 +34,9 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>
31 MobileScanner( 34 MobileScanner(
32 controller: controller, 35 controller: controller,
33 fit: BoxFit.contain, 36 fit: BoxFit.contain,
  37 + errorBuilder: (context, error, child) {
  38 + return ScannerErrorWidget(error: error);
  39 + },
34 onDetect: (barcode) { 40 onDetect: (barcode) {
35 setState(() { 41 setState(() {
36 this.barcode = barcode; 42 this.barcode = barcode;
@@ -17,6 +17,9 @@ class ScannerErrorWidget extends StatelessWidget { @@ -17,6 +17,9 @@ class ScannerErrorWidget extends StatelessWidget {
17 case MobileScannerErrorCode.permissionDenied: 17 case MobileScannerErrorCode.permissionDenied:
18 errorMessage = 'Permission denied'; 18 errorMessage = 'Permission denied';
19 break; 19 break;
  20 + case MobileScannerErrorCode.unsupported:
  21 + errorMessage = 'Scanning is unsupported on this device';
  22 + break;
20 default: 23 default:
21 errorMessage = 'Generic Error'; 24 errorMessage = 'Generic Error';
22 break; 25 break;
@@ -3,12 +3,12 @@ description: Demonstrates how to use the mobile_scanner plugin. @@ -3,12 +3,12 @@ description: Demonstrates how to use the mobile_scanner plugin.
3 publish_to: 'none' # Remove this line if you wish to publish to pub.dev 3 publish_to: 'none' # Remove this line if you wish to publish to pub.dev
4 4
5 environment: 5 environment:
6 - sdk: ">=2.12.0 <3.0.0" 6 + sdk: ">=2.12.0 <4.0.0"
7 7
8 dependencies: 8 dependencies:
9 flutter: 9 flutter:
10 sdk: flutter 10 sdk: flutter
11 - image_picker: ^0.8.7 11 + image_picker: ^1.0.0
12 12
13 mobile_scanner: 13 mobile_scanner:
14 path: ../ 14 path: ../
@@ -8,38 +8,53 @@ @@ -8,38 +8,53 @@
8 The path provided below has to start and end with a slash "/" in order for 8 The path provided below has to start and end with a slash "/" in order for
9 it to work correctly. 9 it to work correctly.
10 10
11 - Fore more details: 11 + For more details:
12 * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base 12 * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
  13 +
  14 + This is a placeholder for base href that will be replaced by the value of
  15 + the `--base-href` argument provided to `flutter build`.
13 --> 16 -->
14 - <base href="/"> 17 + <base href="$FLUTTER_BASE_HREF">
15 18
16 <meta charset="UTF-8"> 19 <meta charset="UTF-8">
17 <meta content="IE=Edge" http-equiv="X-UA-Compatible"> 20 <meta content="IE=Edge" http-equiv="X-UA-Compatible">
18 - <meta name="description" content="A new Flutter project."> 21 + <meta name="description" content="Demonstrates how to use the mobile_scanner plugin.">
19 22
20 <!-- iOS meta tags & icons --> 23 <!-- iOS meta tags & icons -->
21 <meta name="apple-mobile-web-app-capable" content="yes"> 24 <meta name="apple-mobile-web-app-capable" content="yes">
22 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 25 <meta name="apple-mobile-web-app-status-bar-style" content="black">
23 - <meta name="apple-mobile-web-app-title" content="example"> 26 + <meta name="apple-mobile-web-app-title" content="mobile_scanner_example">
24 <link rel="apple-touch-icon" href="icons/Icon-192.png"> 27 <link rel="apple-touch-icon" href="icons/Icon-192.png">
25 28
26 <!-- Favicon --> 29 <!-- Favicon -->
27 <link rel="icon" type="image/png" href="favicon.png"/> 30 <link rel="icon" type="image/png" href="favicon.png"/>
28 31
29 - <title>example</title> 32 + <title>mobile_scanner_example</title>
30 <link rel="manifest" href="manifest.json"> 33 <link rel="manifest" href="manifest.json">
  34 +
  35 + <script>
  36 + // The value below is injected by flutter build, do not touch.
  37 + var serviceWorkerVersion = null;
  38 + </script>
  39 + <!-- This script adds the flutter initialization JS code -->
  40 + <script src="flutter.js" defer></script>
31 </head> 41 </head>
32 <body> 42 <body>
33 - <!-- This script installs service_worker.js to provide PWA functionality to  
34 - application. For more information, see:  
35 - https://developers.google.com/web/fundamentals/primers/service-workers --> 43 + <script src="https://unpkg.com/@zxing/library@0.19.1" type="application/javascript"></script>
36 <script> 44 <script>
37 - if ('serviceWorker' in navigator) {  
38 - window.addEventListener('flutter-first-frame', function () {  
39 - navigator.serviceWorker.register('flutter_service_worker.js'); 45 + window.addEventListener('load', function(ev) {
  46 + // Download main.dart.js
  47 + _flutter.loader.loadEntrypoint({
  48 + serviceWorker: {
  49 + serviceWorkerVersion: serviceWorkerVersion,
  50 + },
  51 + onEntrypointLoaded: function(engineInitializer) {
  52 + engineInitializer.initializeEngine().then(function(appRunner) {
  53 + appRunner.runApp();
40 }); 54 });
41 } 55 }
  56 + });
  57 + });
42 </script> 58 </script>
43 - <script src="main.dart.js" type="application/javascript"></script>  
44 </body> 59 </body>
45 </html> 60 </html>
1 { 1 {
2 - "name": "Mobile Scanner Example", 2 + "name": "mobile_scanner_example",
3 "short_name": "mobile_scanner_example", 3 "short_name": "mobile_scanner_example",
4 "start_url": ".", 4 "start_url": ".",
5 "display": "standalone", 5 "display": "standalone",
6 "background_color": "#0175C2", 6 "background_color": "#0175C2",
7 "theme_color": "#0175C2", 7 "theme_color": "#0175C2",
8 - "description": "A barcode and qr code scanner example.", 8 + "description": "Demonstrates how to use the mobile_scanner plugin.",
9 "orientation": "portrait-primary", 9 "orientation": "portrait-primary",
10 "prefer_related_applications": false, 10 "prefer_related_applications": false,
11 "icons": [ 11 "icons": [
@@ -18,6 +18,18 @@ @@ -18,6 +18,18 @@
18 "src": "icons/Icon-512.png", 18 "src": "icons/Icon-512.png",
19 "sizes": "512x512", 19 "sizes": "512x512",
20 "type": "image/png" 20 "type": "image/png"
  21 + },
  22 + {
  23 + "src": "icons/Icon-maskable-192.png",
  24 + "sizes": "192x192",
  25 + "type": "image/png",
  26 + "purpose": "maskable"
  27 + },
  28 + {
  29 + "src": "icons/Icon-maskable-512.png",
  30 + "sizes": "512x512",
  31 + "type": "image/png",
  32 + "purpose": "maskable"
21 } 33 }
22 ] 34 ]
23 } 35 }
@@ -51,6 +51,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -51,6 +51,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
51 51
52 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates 52 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
53 53
  54 + private let backgroundQueue = DispatchQueue(label: "camera-handling")
  55 +
54 var standardZoomFactor: CGFloat = 1 56 var standardZoomFactor: CGFloat = 1
55 57
56 init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) { 58 init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) {
@@ -118,7 +120,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -118,7 +120,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
118 } 120 }
119 121
120 /// Start scanning for barcodes 122 /// Start scanning for barcodes
121 - func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters { 123 + func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws {
122 self.detectionSpeed = detectionSpeed 124 self.detectionSpeed = detectionSpeed
123 if (device != nil) { 125 if (device != nil) {
124 throw MobileScannerError.alreadyStarted 126 throw MobileScannerError.alreadyStarted
@@ -195,24 +197,36 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -195,24 +197,36 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
195 } 197 }
196 } 198 }
197 captureSession.commitConfiguration() 199 captureSession.commitConfiguration()
198 - captureSession.startRunning() 200 +
  201 + backgroundQueue.async {
  202 + self.captureSession.startRunning()
199 // Enable the torch if parameter is set and torch is available 203 // Enable the torch if parameter is set and torch is available
200 // torch should be set after 'startRunning' is called 204 // torch should be set after 'startRunning' is called
201 do { 205 do {
202 - try toggleTorch(torch) 206 + try self.toggleTorch(torch)
203 } catch { 207 } catch {
204 print("Failed to set initial torch state.") 208 print("Failed to set initial torch state.")
205 } 209 }
206 210
207 do { 211 do {
208 - try resetScale() 212 + try self.resetScale()
209 } catch { 213 } catch {
210 print("Failed to reset zoom scale") 214 print("Failed to reset zoom scale")
211 } 215 }
212 216
213 - let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) 217 + let dimensions = CMVideoFormatDescriptionGetDimensions(self.device.activeFormat.formatDescription)
214 218
215 - return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) 219 + DispatchQueue.main.async {
  220 + completion(
  221 + MobileScannerStartParameters(
  222 + width: Double(dimensions.height),
  223 + height: Double(dimensions.width),
  224 + hasTorch: self.device.hasTorch,
  225 + textureId: self.textureId
  226 + )
  227 + )
  228 + }
  229 + }
216 } 230 }
217 231
218 /// Stop scanning for barcodes 232 /// Stop scanning for barcodes
@@ -227,6 +241,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -227,6 +241,8 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
227 for output in captureSession.outputs { 241 for output in captureSession.outputs {
228 captureSession.removeOutput(output) 242 captureSession.removeOutput(output)
229 } 243 }
  244 +
  245 + latestBuffer = nil
230 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) 246 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
231 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor)) 247 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor))
232 registry?.unregisterTexture(textureId) 248 registry?.unregisterTexture(textureId)
@@ -120,8 +120,9 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -120,8 +120,9 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
120 let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! 120 let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
121 121
122 do { 122 do {
123 - let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed) 123 + try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed) { parameters in
124 result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch]) 124 result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
  125 + }
125 } catch MobileScannerError.alreadyStarted { 126 } catch MobileScannerError.alreadyStarted {
126 result(FlutterError(code: "MobileScanner", 127 result(FlutterError(code: "MobileScanner",
127 message: "Called start() while already started!", 128 message: "Called start() while already started!",
@@ -8,7 +8,6 @@ import 'package:mobile_scanner/mobile_scanner_web.dart'; @@ -8,7 +8,6 @@ import 'package:mobile_scanner/mobile_scanner_web.dart';
8 import 'package:mobile_scanner/src/barcode_utility.dart'; 8 import 'package:mobile_scanner/src/barcode_utility.dart';
9 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 9 import 'package:mobile_scanner/src/enums/camera_facing.dart';
10 import 'package:mobile_scanner/src/objects/barcode.dart'; 10 import 'package:mobile_scanner/src/objects/barcode.dart';
11 -import 'package:mobile_scanner/src/web/utils.dart';  
12 11
13 /// This plugin is the web implementation of mobile_scanner. 12 /// This plugin is the web implementation of mobile_scanner.
14 /// It only supports QR codes. 13 /// It only supports QR codes.
@@ -26,8 +25,6 @@ class MobileScannerWebPlugin { @@ -26,8 +25,6 @@ class MobileScannerWebPlugin {
26 ); 25 );
27 final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); 26 final MobileScannerWebPlugin instance = MobileScannerWebPlugin();
28 27
29 - _jsLibrariesLoadingFuture = injectJSLibraries(barCodeReader.jsLibraries);  
30 -  
31 channel.setMethodCallHandler(instance.handleMethodCall); 28 channel.setMethodCallHandler(instance.handleMethodCall);
32 event.setController(instance.controller); 29 event.setController(instance.controller);
33 } 30 }
@@ -55,11 +52,8 @@ class MobileScannerWebPlugin { @@ -55,11 +52,8 @@ class MobileScannerWebPlugin {
55 ZXingBarcodeReader(videoContainer: vidDiv); 52 ZXingBarcodeReader(videoContainer: vidDiv);
56 StreamSubscription? _barCodeStreamSubscription; 53 StreamSubscription? _barCodeStreamSubscription;
57 54
58 - static late Future _jsLibrariesLoadingFuture;  
59 -  
60 /// Handle incomming messages 55 /// Handle incomming messages
61 Future<dynamic> handleMethodCall(MethodCall call) async { 56 Future<dynamic> handleMethodCall(MethodCall call) async {
62 - await _jsLibrariesLoadingFuture;  
63 switch (call.method) { 57 switch (call.method) {
64 case 'start': 58 case 'start':
65 return _start(call.arguments as Map); 59 return _start(call.arguments as Map);
@@ -67,6 +61,8 @@ class MobileScannerWebPlugin { @@ -67,6 +61,8 @@ class MobileScannerWebPlugin {
67 return _torch(call.arguments); 61 return _torch(call.arguments);
68 case 'stop': 62 case 'stop':
69 return cancel(); 63 return cancel();
  64 + case 'updateScanWindow':
  65 + return Future<void>.value();
70 default: 66 default:
71 throw PlatformException( 67 throw PlatformException(
72 code: 'Unimplemented', 68 code: 'Unimplemented',
@@ -117,12 +113,14 @@ class MobileScannerWebPlugin { @@ -117,12 +113,14 @@ class MobileScannerWebPlugin {
117 .map((e) => toFormat(e)) 113 .map((e) => toFormat(e))
118 .toList(); 114 .toList();
119 } 115 }
  116 +
120 final Duration? detectionTimeout; 117 final Duration? detectionTimeout;
121 if (arguments.containsKey('timeout')) { 118 if (arguments.containsKey('timeout')) {
122 detectionTimeout = Duration(milliseconds: arguments['timeout'] as int); 119 detectionTimeout = Duration(milliseconds: arguments['timeout'] as int);
123 } else { 120 } else {
124 detectionTimeout = null; 121 detectionTimeout = null;
125 } 122 }
  123 +
126 await barCodeReader.start( 124 await barCodeReader.start(
127 cameraFacing: cameraFacing, 125 cameraFacing: cameraFacing,
128 formats: formats, 126 formats: formats,
@@ -132,20 +130,31 @@ class MobileScannerWebPlugin { @@ -132,20 +130,31 @@ class MobileScannerWebPlugin {
132 _barCodeStreamSubscription = 130 _barCodeStreamSubscription =
133 barCodeReader.detectBarcodeContinuously().listen((code) { 131 barCodeReader.detectBarcodeContinuously().listen((code) {
134 if (code != null) { 132 if (code != null) {
  133 + final List<Offset>? corners = code.corners;
  134 +
135 controller.add({ 135 controller.add({
136 'name': 'barcodeWeb', 136 'name': 'barcodeWeb',
137 'data': { 137 'data': {
138 'rawValue': code.rawValue, 138 'rawValue': code.rawValue,
139 'rawBytes': code.rawBytes, 139 'rawBytes': code.rawBytes,
140 'format': code.format.rawValue, 140 'format': code.format.rawValue,
  141 + 'displayValue': code.displayValue,
  142 + 'type': code.type.index,
  143 + if (corners != null && corners.isNotEmpty)
  144 + 'corners': corners
  145 + .map(
  146 + (Offset c) => <Object?, Object?>{'x': c.dx, 'y': c.dy},
  147 + )
  148 + .toList(),
141 }, 149 },
142 }); 150 });
143 } 151 }
144 }); 152 });
  153 +
145 final hasTorch = await barCodeReader.hasTorch(); 154 final hasTorch = await barCodeReader.hasTorch();
146 155
147 if (hasTorch && arguments.containsKey('torch')) { 156 if (hasTorch && arguments.containsKey('torch')) {
148 - barCodeReader.toggleTorch(enabled: arguments['torch'] as bool); 157 + await barCodeReader.toggleTorch(enabled: arguments['torch'] as bool);
149 } 158 }
150 159
151 return { 160 return {
@@ -154,8 +163,12 @@ class MobileScannerWebPlugin { @@ -154,8 +163,12 @@ class MobileScannerWebPlugin {
154 'videoHeight': barCodeReader.videoHeight, 163 'videoHeight': barCodeReader.videoHeight,
155 'torchable': hasTorch, 164 'torchable': hasTorch,
156 }; 165 };
157 - } catch (e) {  
158 - throw PlatformException(code: 'MobileScannerWeb', message: '$e'); 166 + } catch (e, stackTrace) {
  167 + throw PlatformException(
  168 + code: 'MobileScannerWeb',
  169 + message: '$e',
  170 + details: stackTrace.toString(),
  171 + );
159 } 172 }
160 } 173 }
161 174
@@ -9,14 +9,16 @@ Size toSize(Map data) { @@ -9,14 +9,16 @@ Size toSize(Map data) {
9 return Size(width, height); 9 return Size(width, height);
10 } 10 }
11 11
12 -List<Offset>? toCorners(List? data) {  
13 - if (data != null) {  
14 - return List.unmodifiable(  
15 - data.map((e) => Offset((e as Map)['x'] as double, e['y'] as double)),  
16 - );  
17 - } else { 12 +List<Offset>? toCorners(List<Map<Object?, Object?>>? data) {
  13 + if (data == null) {
18 return null; 14 return null;
19 } 15 }
  16 +
  17 + return List.unmodifiable(
  18 + data.map((Map<Object?, Object?> e) {
  19 + return Offset(e['x']! as double, e['y']! as double);
  20 + }),
  21 + );
20 } 22 }
21 23
22 BarcodeFormat toFormat(int value) { 24 BarcodeFormat toFormat(int value) {
@@ -11,4 +11,7 @@ enum MobileScannerErrorCode { @@ -11,4 +11,7 @@ enum MobileScannerErrorCode {
11 11
12 /// The permission to use the camera was denied. 12 /// The permission to use the camera was denied.
13 permissionDenied, 13 permissionDenied,
  14 +
  15 + /// Scanning is unsupported on the current device.
  16 + unsupported,
14 } 17 }
@@ -137,7 +137,6 @@ class _MobileScannerState extends State<MobileScanner> @@ -137,7 +137,6 @@ class _MobileScannerState extends State<MobileScanner>
137 widget.onStart?.call(arguments); 137 widget.onStart?.call(arguments);
138 widget.onScannerStarted?.call(arguments); 138 widget.onScannerStarted?.call(arguments);
139 }).catchError((error) { 139 }).catchError((error) {
140 - debugPrint('mobile_scanner: $error');  
141 if (mounted) { 140 if (mounted) {
142 setState(() { 141 setState(() {
143 _startException = error as MobileScannerException; 142 _startException = error as MobileScannerException;
@@ -237,8 +236,8 @@ class _MobileScannerState extends State<MobileScanner> @@ -237,8 +236,8 @@ class _MobileScannerState extends State<MobileScanner>
237 236
238 @override 237 @override
239 Widget build(BuildContext context) { 238 Widget build(BuildContext context) {
240 - return LayoutBuilder(  
241 - builder: (context, constraints) { 239 + final Size size = MediaQuery.sizeOf(context);
  240 +
242 return ValueListenableBuilder<MobileScannerArguments?>( 241 return ValueListenableBuilder<MobileScannerArguments?>(
243 valueListenable: _controller.startArguments, 242 valueListenable: _controller.startArguments,
244 builder: (context, value, child) { 243 builder: (context, value, child) {
@@ -251,7 +250,7 @@ class _MobileScannerState extends State<MobileScanner> @@ -251,7 +250,7 @@ class _MobileScannerState extends State<MobileScanner>
251 widget.fit, 250 widget.fit,
252 widget.scanWindow!, 251 widget.scanWindow!,
253 value.size, 252 value.size,
254 - Size(constraints.maxWidth, constraints.maxHeight), 253 + size,
255 ); 254 );
256 255
257 _controller.updateScanWindow(scanWindow); 256 _controller.updateScanWindow(scanWindow);
@@ -285,8 +284,6 @@ class _MobileScannerState extends State<MobileScanner> @@ -285,8 +284,6 @@ class _MobileScannerState extends State<MobileScanner>
285 ); 284 );
286 }, 285 },
287 ); 286 );
288 - },  
289 - );  
290 } 287 }
291 288
292 @override 289 @override
@@ -207,9 +207,21 @@ class MobileScannerController { @@ -207,9 +207,21 @@ class MobileScannerController {
207 } on PlatformException catch (error) { 207 } on PlatformException catch (error) {
208 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; 208 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
209 209
210 - if (error.code == "MobileScannerWeb") { 210 + final String? errorMessage = error.message;
  211 +
  212 + if (kIsWeb) {
  213 + if (errorMessage == null) {
  214 + errorCode = MobileScannerErrorCode.genericError;
  215 + } else if (errorMessage.contains('NotFoundError') ||
  216 + errorMessage.contains('NotSupportedError')) {
  217 + errorCode = MobileScannerErrorCode.unsupported;
  218 + } else if (errorMessage.contains('NotAllowedError')) {
211 errorCode = MobileScannerErrorCode.permissionDenied; 219 errorCode = MobileScannerErrorCode.permissionDenied;
  220 + } else {
  221 + errorCode = MobileScannerErrorCode.genericError;
212 } 222 }
  223 + }
  224 +
213 isStarting = false; 225 isStarting = false;
214 226
215 throw MobileScannerException( 227 throw MobileScannerException(
@@ -388,6 +400,10 @@ class MobileScannerController { @@ -388,6 +400,10 @@ class MobileScannerController {
388 rawValue: barcode['rawValue'] as String?, 400 rawValue: barcode['rawValue'] as String?,
389 rawBytes: barcode['rawBytes'] as Uint8List?, 401 rawBytes: barcode['rawBytes'] as Uint8List?,
390 format: toFormat(barcode['format'] as int), 402 format: toFormat(barcode['format'] as int),
  403 + corners: toCorners(
  404 + (barcode['corners'] as List<Object?>? ?? [])
  405 + .cast<Map<Object?, Object?>>(),
  406 + ),
391 ), 407 ),
392 ], 408 ],
393 ), 409 ),
@@ -92,7 +92,9 @@ class Barcode { @@ -92,7 +92,9 @@ class Barcode {
92 92
93 /// Create a [Barcode] from native data. 93 /// Create a [Barcode] from native data.
94 Barcode.fromNative(Map data) 94 Barcode.fromNative(Map data)
95 - : corners = toCorners(data['corners'] as List?), 95 + : corners = toCorners(
  96 + (data['corners'] as List?)?.cast<Map<Object?, Object?>>(),
  97 + ),
96 format = toFormat(data['format'] as int), 98 format = toFormat(data['format'] as int),
97 rawBytes = data['rawBytes'] as Uint8List?, 99 rawBytes = data['rawBytes'] as Uint8List?,
98 rawValue = data['rawValue'] as String?, 100 rawValue = data['rawValue'] as String?,
@@ -201,18 +203,20 @@ class ContactInfo { @@ -201,18 +203,20 @@ class ContactInfo {
201 /// Create a [ContactInfo] from native data. 203 /// Create a [ContactInfo] from native data.
202 ContactInfo.fromNative(Map data) 204 ContactInfo.fromNative(Map data)
203 : addresses = List.unmodifiable( 205 : addresses = List.unmodifiable(
204 - (data['addresses'] as List).map((e) => Address.fromNative(e as Map)), 206 + (data['addresses'] as List? ?? [])
  207 + .cast<Map>()
  208 + .map(Address.fromNative),
205 ), 209 ),
206 emails = List.unmodifiable( 210 emails = List.unmodifiable(
207 - (data['emails'] as List).map((e) => Email.fromNative(e as Map)), 211 + (data['emails'] as List? ?? []).cast<Map>().map(Email.fromNative),
208 ), 212 ),
209 name = toName(data['name'] as Map?), 213 name = toName(data['name'] as Map?),
210 organization = data['organization'] as String?, 214 organization = data['organization'] as String?,
211 phones = List.unmodifiable( 215 phones = List.unmodifiable(
212 - (data['phones'] as List).map((e) => Phone.fromNative(e as Map)), 216 + (data['phones'] as List? ?? []).cast<Map>().map(Phone.fromNative),
213 ), 217 ),
214 title = data['title'] as String?, 218 title = data['title'] as String?,
215 - urls = List.unmodifiable(data['urls'] as List); 219 + urls = List.unmodifiable((data['urls'] as List? ?? []).cast<String>());
216 } 220 }
217 221
218 /// An address. 222 /// An address.
@@ -227,7 +231,9 @@ class Address { @@ -227,7 +231,9 @@ class Address {
227 231
228 /// Create a [Address] from native data. 232 /// Create a [Address] from native data.
229 Address.fromNative(Map data) 233 Address.fromNative(Map data)
230 - : addressLines = List.unmodifiable(data['addressLines'] as List), 234 + : addressLines = List.unmodifiable(
  235 + (data['addressLines'] as List? ?? []).cast<String>(),
  236 + ),
231 type = AddressType.values[data['type'] as int]; 237 type = AddressType.values[data['type'] as int];
232 } 238 }
233 239
@@ -39,9 +39,6 @@ abstract class WebBarcodeReaderBase { @@ -39,9 +39,6 @@ abstract class WebBarcodeReaderBase {
39 int get videoWidth; 39 int get videoWidth;
40 int get videoHeight; 40 int get videoHeight;
41 41
42 - /// JS libraries to be injected into html page.  
43 - List<JsLibrary> get jsLibraries;  
44 -  
45 /// Starts streaming video 42 /// Starts streaming video
46 Future<void> start({ 43 Future<void> start({
47 required CameraFacing cameraFacing, 44 required CameraFacing cameraFacing,
@@ -128,10 +125,8 @@ mixin InternalTorchDetection on InternalStreamCreation { @@ -128,10 +125,8 @@ mixin InternalTorchDetection on InternalStreamCreation {
128 final photoCapabilities = await promiseToFuture<PhotoCapabilities>( 125 final photoCapabilities = await promiseToFuture<PhotoCapabilities>(
129 imageCapture.getPhotoCapabilities(), 126 imageCapture.getPhotoCapabilities(),
130 ); 127 );
131 - final fillLightMode = photoCapabilities.fillLightMode;  
132 - if (fillLightMode != null) {  
133 - return fillLightMode;  
134 - } 128 +
  129 + return photoCapabilities.fillLightMode;
135 } 130 }
136 } catch (e) { 131 } catch (e) {
137 // ImageCapture is not supported by some browsers: 132 // ImageCapture is not supported by some browsers:
@@ -165,9 +160,16 @@ class Promise<T> {} @@ -165,9 +160,16 @@ class Promise<T> {}
165 160
166 @JS() 161 @JS()
167 @anonymous 162 @anonymous
168 -class PhotoCapabilities { 163 +@staticInterop
  164 +class PhotoCapabilities {}
  165 +
  166 +extension PhotoCapabilitiesExtension on PhotoCapabilities {
  167 + @JS('fillLightMode')
  168 + external List<dynamic>? get _fillLightMode;
  169 +
169 /// Returns an array of available fill light options. Options include auto, off, or flash. 170 /// Returns an array of available fill light options. Options include auto, off, or flash.
170 - external List<String>? get fillLightMode; 171 + List<String> get fillLightMode =>
  172 + _fillLightMode?.cast<String>() ?? <String>[];
171 } 173 }
172 174
173 @JS('ImageCapture') 175 @JS('ImageCapture')
@@ -20,12 +20,6 @@ class Code { @@ -20,12 +20,6 @@ class Code {
20 external Uint8ClampedList get binaryData; 20 external Uint8ClampedList get binaryData;
21 } 21 }
22 22
23 -const jsqrLibrary = JsLibrary(  
24 - contextName: 'jsQR',  
25 - url: 'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js',  
26 - usesRequireJs: true,  
27 -);  
28 -  
29 /// Barcode reader that uses jsQR library. 23 /// Barcode reader that uses jsQR library.
30 /// jsQR supports only QR codes format. 24 /// jsQR supports only QR codes format.
31 class JsQrCodeReader extends WebBarcodeReaderBase 25 class JsQrCodeReader extends WebBarcodeReaderBase
@@ -36,9 +30,6 @@ class JsQrCodeReader extends WebBarcodeReaderBase @@ -36,9 +30,6 @@ class JsQrCodeReader extends WebBarcodeReaderBase
36 bool get isStarted => localMediaStream != null; 30 bool get isStarted => localMediaStream != null;
37 31
38 @override 32 @override
39 - List<JsLibrary> get jsLibraries => [jsqrLibrary];  
40 -  
41 - @override  
42 Future<void> start({ 33 Future<void> start({
43 required CameraFacing cameraFacing, 34 required CameraFacing cameraFacing,
44 List<BarcodeFormat>? formats, 35 List<BarcodeFormat>? formats,
1 -import 'dart:async';  
2 -import 'dart:html' as html;  
3 -import 'dart:js' show JsObject, context;  
4 -  
5 -import 'package:mobile_scanner/src/web/base.dart';  
6 -  
7 -Future<void> loadScript(JsLibrary library) async {  
8 - dynamic amd;  
9 - dynamic define;  
10 - // ignore: avoid_dynamic_calls  
11 - if (library.usesRequireJs && context['define']?['amd'] != null) {  
12 - // In dev, requireJs is loaded in. Disable it here.  
13 - // see https://github.com/dart-lang/sdk/issues/33979  
14 - define = JsObject.fromBrowserObject(context['define'] as Object);  
15 - // ignore: avoid_dynamic_calls  
16 - amd = define['amd'];  
17 - // ignore: avoid_dynamic_calls  
18 - define['amd'] = false;  
19 - }  
20 - try {  
21 - await loadScriptUsingScriptTag(library.url);  
22 - } finally {  
23 - if (amd != null) {  
24 - // Re-enable requireJs  
25 - // ignore: avoid_dynamic_calls  
26 - define['amd'] = amd;  
27 - }  
28 - }  
29 -}  
30 -  
31 -Future<void> loadScriptUsingScriptTag(String url) {  
32 - final script = html.ScriptElement()  
33 - ..async = true  
34 - ..defer = false  
35 - ..crossOrigin = 'anonymous'  
36 - ..type = 'text/javascript'  
37 - // ignore: unsafe_html  
38 - ..src = url;  
39 -  
40 - html.document.head!.append(script);  
41 -  
42 - return script.onLoad.first;  
43 -}  
44 -  
45 -/// Injects JS [libraries]  
46 -///  
47 -/// Returns a [Future] that resolves when all of the `script` tags `onLoad` events trigger.  
48 -Future<void> injectJSLibraries(List<JsLibrary> libraries) {  
49 - final List<Future<void>> loading = [];  
50 -  
51 - for (final library in libraries) {  
52 - final future = loadScript(library);  
53 - loading.add(future);  
54 - }  
55 -  
56 - return Future.wait(loading);  
57 -}  
1 import 'dart:async'; 1 import 'dart:async';
2 import 'dart:html'; 2 import 'dart:html';
3 import 'dart:typed_data'; 3 import 'dart:typed_data';
  4 +import 'dart:ui';
4 5
5 import 'package:js/js.dart'; 6 import 'package:js/js.dart';
6 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 7 import 'package:mobile_scanner/src/enums/camera_facing.dart';
@@ -19,6 +20,16 @@ class JsZXingBrowserMultiFormatReader { @@ -19,6 +20,16 @@ class JsZXingBrowserMultiFormatReader {
19 20
20 @JS() 21 @JS()
21 @anonymous 22 @anonymous
  23 +abstract class ResultPoint {
  24 + /// The x coordinate of the point.
  25 + external double get x;
  26 +
  27 + /// The y coordinate of the point.
  28 + external double get y;
  29 +}
  30 +
  31 +@JS()
  32 +@anonymous
22 abstract class Result { 33 abstract class Result {
23 /// raw text encoded by the barcode 34 /// raw text encoded by the barcode
24 external String get text; 35 external String get text;
@@ -28,15 +39,24 @@ abstract class Result { @@ -28,15 +39,24 @@ abstract class Result {
28 39
29 /// Representing the format of the barcode that was decoded 40 /// Representing the format of the barcode that was decoded
30 external int? format; 41 external int? format;
  42 +
  43 + /// Returns the result points of the barcode. These points represent the corners of the barcode.
  44 + external List<Object?> get resultPoints;
31 } 45 }
32 46
33 extension ResultExt on Result { 47 extension ResultExt on Result {
34 Barcode toBarcode() { 48 Barcode toBarcode() {
  49 + final corners = resultPoints
  50 + .cast<ResultPoint>()
  51 + .map((ResultPoint rp) => Offset(rp.x, rp.y))
  52 + .toList();
  53 +
35 final rawBytes = this.rawBytes; 54 final rawBytes = this.rawBytes;
36 return Barcode( 55 return Barcode(
37 rawValue: text, 56 rawValue: text,
38 rawBytes: rawBytes != null ? Uint8List.fromList(rawBytes) : null, 57 rawBytes: rawBytes != null ? Uint8List.fromList(rawBytes) : null,
39 format: barcodeFormat, 58 format: barcodeFormat,
  59 + corners: corners,
40 ); 60 );
41 } 61 }
42 62
@@ -168,12 +188,6 @@ extension JsZXingBrowserMultiFormatReaderExt @@ -168,12 +188,6 @@ extension JsZXingBrowserMultiFormatReaderExt
168 external MediaStream? stream; 188 external MediaStream? stream;
169 } 189 }
170 190
171 -const zxingJsLibrary = JsLibrary(  
172 - contextName: 'ZXing',  
173 - url: 'https://unpkg.com/@zxing/library@0.19.1',  
174 - usesRequireJs: true,  
175 -);  
176 -  
177 /// Barcode reader that uses zxing-js library. 191 /// Barcode reader that uses zxing-js library.
178 class ZXingBarcodeReader extends WebBarcodeReaderBase 192 class ZXingBarcodeReader extends WebBarcodeReaderBase
179 with InternalStreamCreation, InternalTorchDetection { 193 with InternalStreamCreation, InternalTorchDetection {
@@ -185,9 +199,6 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase @@ -185,9 +199,6 @@ class ZXingBarcodeReader extends WebBarcodeReaderBase
185 bool get isStarted => localMediaStream != null; 199 bool get isStarted => localMediaStream != null;
186 200
187 @override 201 @override
188 - List<JsLibrary> get jsLibraries => [zxingJsLibrary];  
189 -  
190 - @override  
191 Future<void> start({ 202 Future<void> start({
192 required CameraFacing cameraFacing, 203 required CameraFacing cameraFacing,
193 List<BarcodeFormat>? formats, 204 List<BarcodeFormat>? formats,
1 name: mobile_scanner 1 name: mobile_scanner
2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. 2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
3 -version: 3.2.1 3 +version: 3.3.0
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 environment: 6 environment:
7 - sdk: ">=2.17.0 <3.0.0" 7 + sdk: ">=2.17.0 <4.0.0"
8 flutter: ">=3.0.0" 8 flutter: ">=3.0.0"
9 9
10 dependencies: 10 dependencies:
@@ -14,7 +14,6 @@ dependencies: @@ -14,7 +14,6 @@ dependencies:
14 sdk: flutter 14 sdk: flutter
15 js: ^0.6.3 15 js: ^0.6.3
16 16
17 -  
18 dev_dependencies: 17 dev_dependencies:
19 flutter_test: 18 flutter_test:
20 sdk: flutter 19 sdk: flutter