commit afc74f27519cdd3677dead1b5214cba341f4b4ca
parent 5c34c05d7efc86e0ee6a96e158bd874d69556a04
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Sun, 11 May 2025 17:21:48 +0200
[pos] moving to the zxing from mlkit
Diffstat:
2 files changed, 64 insertions(+), 23 deletions(-)
diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle
@@ -78,8 +78,8 @@ dependencies {
implementation "androidx.camera:camera-lifecycle:1.4.2"
implementation "androidx.camera:camera-view:1.4.2"
- // ML Kit – on-device barcode/QR detector
- implementation "com.google.mlkit:barcode-scanning:17.3.0"
+ // ZXING core – on-device barcode/QR detector
+ implementation "com.google.zxing:core:3.5.2"
// Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt
@@ -21,6 +21,7 @@ import android.app.TimePickerDialog
import android.app.DatePickerDialog
import android.content.Intent
import android.content.pm.PackageManager
+import android.media.Image
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@@ -57,12 +58,10 @@ import net.taler.merchantpos.R
import net.taler.merchantpos.config.ConfigFragmentDirections.Companion.actionSettingsToOrder
import net.taler.merchantpos.databinding.FragmentMerchantConfigBinding
import androidx.core.view.isVisible
-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 com.google.zxing.*
import net.taler.merchantpos.MainActivity
import android.text.format.DateFormat
+import com.google.zxing.common.HybridBinarizer
import java.util.Calendar
import java.util.Locale
@@ -77,18 +76,17 @@ class ConfigFragment : Fragment() {
private lateinit var ui: FragmentMerchantConfigBinding
- private val scanner by lazy {
- BarcodeScanning.getClient(
- BarcodeScannerOptions.Builder()
- .setBarcodeFormats(Barcode.FORMAT_QR_CODE)
- .build()
- )
- }
-
private val cameraExecutor by lazy {
ContextCompat.getMainExecutor(requireContext())
}
+ private val qrReader = MultiFormatReader().apply {
+ setHints(mapOf(
+ DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE),
+ DecodeHintType.CHARACTER_SET to "UTF-8"
+ ))
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -359,16 +357,40 @@ class ConfigFragment : Fragment() {
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build().also { useCase ->
useCase.setAnalyzer(cameraExecutor) { proxy ->
- val media = proxy.image ?: run { proxy.close(); return@setAnalyzer }
- val image = InputImage.fromMediaImage(
- media,
- proxy.imageInfo.rotationDegrees
- )
- scanner.process(image)
- .addOnSuccessListener { codes ->
- codes.firstOrNull()?.rawValue?.let { onQrDecoded(it) }
+ val mediaImage = proxy.image
+ if (mediaImage != null) {
+ // 1) Convert YUV_420_888 to a ZXing-friendly NV21 byte array
+ val nv21 = yuv420888ToNv21(mediaImage)
+ val width = mediaImage.width
+ val height = mediaImage.height
+
+ // 2) Build ZXing’s LuminanceSource
+ val source = PlanarYUVLuminanceSource(
+ nv21, width, height,
+ 0, 0, width, height,
+ false
+ )
+
+ // 3) (Optional) rotate the source if your sensor’s orientation needs it
+ val rotated = when (proxy.imageInfo.rotationDegrees) {
+ 90 -> source.rotateCounterClockwise()
+ 270 -> source.rotateCounterClockwise()
+ else -> source
+ }
+
+ // 4) Try to decode
+ val bitmap = BinaryBitmap(HybridBinarizer(rotated))
+ try {
+ val result = qrReader.decodeWithState(bitmap)
+ onQrDecoded(result.text)
+ } catch (e: NotFoundException) {
+ // no QR code in this frame
+ } finally {
+ proxy.close()
}
- .addOnCompleteListener { proxy.close() }
+ } else {
+ proxy.close()
+ }
}
}
@@ -382,6 +404,25 @@ class ConfigFragment : Fragment() {
}, cameraExecutor)
}
+ // 3) convert YUV_420_888 to NV21
+ private fun yuv420888ToNv21(image: Image): ByteArray {
+ val yPlane = image.planes[0].buffer
+ val uPlane = image.planes[1].buffer
+ val vPlane = image.planes[2].buffer
+
+ val ySize = yPlane.remaining()
+ val uSize = uPlane.remaining()
+ val vSize = vPlane.remaining()
+ val nv21 = ByteArray(ySize + uSize + vSize)
+
+ // U and V are swapped
+ yPlane.get(nv21, 0, ySize)
+ vPlane.get(nv21, ySize, vSize)
+ uPlane.get(nv21, ySize + vSize, uSize)
+
+ return nv21
+ }
+
private fun onQrDecoded(raw: String) {
if (!raw.startsWith("taler-pos://")) return // guard