Skip to content

Commit 40f1c0d

Browse files
committed
Merge branch 'develop' of github.com:ohnobug/mobile_scanner into develop
2 parents 44d10ee + be40fec commit 40f1c0d

File tree

10 files changed

+271
-96
lines changed

10 files changed

+271
-96
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ Known issues:
5454
* [Apple] The scan window does not work correctly.
5555
* [Apple] The camera flash briefly shows when the camera is started.
5656

57+
## 6.0.4
58+
Bugs fixed:
59+
* [Android] Fixed UI jank when `returnImage` is true.
60+
5761
## 6.0.3
5862
New features:
5963
* Adds pause function to pause the camera but keep textures in place.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ See the example app for detailed implementation information.
3838
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
3939
| returnImage | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
4040
| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
41+
| autoZoom | :heavy_check_mark: | :x: | :x: | :x: |
4142

4243
## Platform Support
4344

android/build.gradle

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ buildscript {
99
}
1010

1111
dependencies {
12-
classpath 'com.android.tools.build:gradle:8.3.2'
12+
classpath 'com.android.tools.build:gradle:8.8.0'
1313
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1414
}
1515
}
@@ -77,9 +77,10 @@ dependencies {
7777
// See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7
7878
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
7979

80-
implementation 'androidx.camera:camera-lifecycle:1.3.4'
81-
implementation 'androidx.camera:camera-camera2:1.3.4'
80+
implementation 'androidx.camera:camera-camera2:1.4.1'
81+
implementation 'androidx.camera:camera-lifecycle:1.4.1'
82+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
8283

8384
testImplementation 'org.jetbrains.kotlin:kotlin-test'
84-
testImplementation 'org.mockito:mockito-core:5.15.2'
85+
testImplementation 'org.mockito:mockito-core:5.15.2'
8586
}

android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt

Lines changed: 102 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner
33
import android.app.Activity
44
import android.content.Context
55
import android.graphics.Bitmap
6+
import android.graphics.Canvas
7+
import android.graphics.ColorMatrix
8+
import android.graphics.ColorMatrixColorFilter
69
import android.graphics.Matrix
10+
import android.graphics.Paint
711
import android.graphics.Rect
812
import android.hardware.display.DisplayManager
913
import android.net.Uri
@@ -34,6 +38,10 @@ import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
3438
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
3539
import dev.steenbakker.mobile_scanner.utils.YuvToRgbConverter
3640
import io.flutter.view.TextureRegistry
41+
import kotlinx.coroutines.CoroutineScope
42+
import kotlinx.coroutines.Dispatchers
43+
import kotlinx.coroutines.coroutineScope
44+
import kotlinx.coroutines.launch
3745
import java.io.ByteArrayOutputStream
3846
import java.io.IOException
3947
import kotlin.math.roundToInt
@@ -59,6 +67,7 @@ class MobileScanner(
5967

6068
/// Configurable variables
6169
var scanWindow: List<Float>? = null
70+
private var invertImage: Boolean = false
6271
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
6372
private var detectionTimeout: Long = 250
6473
private var returnImage = false
@@ -79,7 +88,12 @@ class MobileScanner(
7988
@ExperimentalGetImage
8089
val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
8190
val mediaImage = imageProxy.image ?: return@Analyzer
82-
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
91+
92+
val inputImage = if (invertImage) {
93+
invertInputImage(imageProxy)
94+
} else {
95+
InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
96+
}
8397

8498
if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
8599
imageProxy.close()
@@ -97,6 +111,7 @@ class MobileScanner(
97111

98112
if (newScannedBarcodes == lastScanned) {
99113
// New scanned is duplicate, returning
114+
imageProxy.close()
100115
return@addOnSuccessListener
101116
}
102117
if (newScannedBarcodes.isNotEmpty()) {
@@ -118,45 +133,52 @@ class MobileScanner(
118133
}
119134

120135
if (barcodeMap.isEmpty()) {
136+
imageProxy.close()
121137
return@addOnSuccessListener
122138
}
123139

124140
val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0
125141

126142
if (!returnImage) {
143+
imageProxy.close()
127144
mobileScannerCallback(
128145
barcodeMap,
129146
null,
130-
if (portrait) mediaImage.width else mediaImage.height,
131-
if (portrait) mediaImage.height else mediaImage.width)
147+
if (portrait) inputImage.width else inputImage.height,
148+
if (portrait) inputImage.height else inputImage.width)
132149
return@addOnSuccessListener
133150
}
134151

135-
val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888)
136-
val imageFormat = YuvToRgbConverter(activity.applicationContext)
152+
CoroutineScope(Dispatchers.IO).launch {
153+
val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888)
154+
val imageFormat = YuvToRgbConverter(activity.applicationContext)
137155

138-
imageFormat.yuvToRgb(mediaImage, bitmap)
156+
imageFormat.yuvToRgb(mediaImage, bitmap)
139157

140-
val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f)
158+
val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f)
141159

142-
val stream = ByteArrayOutputStream()
143-
bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream)
144-
val byteArray = stream.toByteArray()
145-
val bmWidth = bmResult.width
146-
val bmHeight = bmResult.height
147-
bmResult.recycle()
160+
val stream = ByteArrayOutputStream()
161+
bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream)
162+
val byteArray = stream.toByteArray()
163+
val bmWidth = bmResult.width
164+
val bmHeight = bmResult.height
165+
166+
bmResult.recycle()
167+
imageProxy.close()
168+
169+
mobileScannerCallback(
170+
barcodeMap,
171+
byteArray,
172+
bmWidth,
173+
bmHeight
174+
)
175+
}
148176

149-
mobileScannerCallback(
150-
barcodeMap,
151-
byteArray,
152-
bmWidth,
153-
bmHeight
154-
)
155177
}.addOnFailureListener { e ->
156178
mobileScannerErrorCallback(
157179
e.localizedMessage ?: e.toString()
158180
)
159-
}.addOnCompleteListener { imageProxy.close() }
181+
}
160182
}
161183

162184
if (detectionSpeed == DetectionSpeed.NORMAL) {
@@ -219,11 +241,13 @@ class MobileScanner(
219241
mobileScannerStartedCallback: MobileScannerStartedCallback,
220242
mobileScannerErrorCallback: (exception: Exception) -> Unit,
221243
detectionTimeout: Long,
222-
cameraResolutionWanted: Size?
244+
cameraResolutionWanted: Size?,
245+
invertImage: Boolean,
223246
) {
224247
this.detectionSpeed = detectionSpeed
225248
this.detectionTimeout = detectionTimeout
226249
this.returnImage = returnImage
250+
this.invertImage = invertImage
227251

228252
if (camera?.cameraInfo != null && preview != null && textureEntry != null && !isPaused) {
229253

@@ -416,14 +440,14 @@ class MobileScanner(
416440
isPaused = true
417441
}
418442

419-
private fun resumeCamera() {
420-
// Resume camera by rebinding use cases
421-
cameraProvider?.let { provider ->
422-
val owner = activity as LifecycleOwner
423-
cameraSelector?.let { provider.bindToLifecycle(owner, it, preview) }
424-
}
425-
isPaused = false
426-
}
443+
// private fun resumeCamera() {
444+
// // Resume camera by rebinding use cases
445+
// cameraProvider?.let { provider ->
446+
// val owner = activity as LifecycleOwner
447+
// cameraSelector?.let { provider.bindToLifecycle(owner, it, preview) }
448+
// }
449+
// isPaused = false
450+
// }
427451

428452
private fun releaseCamera() {
429453
if (displayListener != null) {
@@ -472,6 +496,50 @@ class MobileScanner(
472496
}
473497
}
474498

499+
/**
500+
* Inverts the image colours respecting the alpha channel
501+
*/
502+
@ExperimentalGetImage
503+
fun invertInputImage(imageProxy: ImageProxy): InputImage {
504+
val image = imageProxy.image ?: throw IllegalArgumentException("Image is null")
505+
506+
// Convert YUV_420_888 image to RGB Bitmap
507+
val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
508+
try {
509+
val imageFormat = YuvToRgbConverter(activity.applicationContext)
510+
imageFormat.yuvToRgb(image, bitmap)
511+
512+
// Create an inverted bitmap
513+
val invertedBitmap = invertBitmapColors(bitmap)
514+
imageFormat.release()
515+
516+
return InputImage.fromBitmap(invertedBitmap, imageProxy.imageInfo.rotationDegrees)
517+
} finally {
518+
// Release resources
519+
bitmap.recycle() // Free up bitmap memory
520+
imageProxy.close() // Close ImageProxy
521+
}
522+
}
523+
524+
// Efficiently invert bitmap colors using ColorMatrix
525+
private fun invertBitmapColors(bitmap: Bitmap): Bitmap {
526+
val colorMatrix = ColorMatrix().apply {
527+
set(floatArrayOf(
528+
-1f, 0f, 0f, 0f, 255f, // Red
529+
0f, -1f, 0f, 0f, 255f, // Green
530+
0f, 0f, -1f, 0f, 255f, // Blue
531+
0f, 0f, 0f, 1f, 0f // Alpha
532+
))
533+
}
534+
val paint = Paint().apply { colorFilter = ColorMatrixColorFilter(colorMatrix) }
535+
536+
val invertedBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config)
537+
val canvas = Canvas(invertedBitmap)
538+
canvas.drawBitmap(bitmap, 0f, 0f, paint)
539+
540+
return invertedBitmap
541+
}
542+
475543
/**
476544
* Analyze a single image.
477545
*/
@@ -517,6 +585,11 @@ class MobileScanner(
517585
camera?.cameraControl?.setLinearZoom(scale.toFloat())
518586
}
519587

588+
fun setZoomRatio(zoomRatio: Double) {
589+
if (camera == null) throw ZoomWhenStopped()
590+
camera?.cameraControl?.setZoomRatio(zoomRatio.toFloat())
591+
}
592+
520593
/**
521594
* Reset the zoom rate of the camera.
522595
*/

0 commit comments

Comments
 (0)