Замена mlkit на свободную библиотеку

This commit is contained in:
2026-05-21 23:21:21 +03:00
parent 763334c488
commit 233a716e47
4 changed files with 117 additions and 30 deletions

View File

@@ -26,7 +26,7 @@ okhttp = "5.3.2"
workRuntime = "2.11.2" workRuntime = "2.11.2"
hiltWork = "1.3.0" hiltWork = "1.3.0"
cameraX = "1.6.1" cameraX = "1.6.1"
mlkitBarcode = "17.3.0" zxing = "3.5.3"
javaOtp = "0.4.0" javaOtp = "0.4.0"
appcompat = "1.7.1" appcompat = "1.7.1"
datastore = "1.2.1" datastore = "1.2.1"
@@ -90,7 +90,7 @@ androidx-camera-core = { group = "androidx.camera", name = "camera-core", versio
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" } androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" } androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraX" } androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraX" }
mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkitBarcode" } zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
java-otp = { group = "com.eatthepath", name = "java-otp", version.ref = "javaOtp" } java-otp = { group = "com.eatthepath", name = "java-otp", version.ref = "javaOtp" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui = { group = "androidx.compose.ui", name = "ui" }

View File

@@ -80,7 +80,7 @@ dependencies {
implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.view)
implementation(libs.mlkit.barcode.scanning) implementation(libs.zxing.core)
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.coroutines.test)

View File

@@ -0,0 +1,106 @@
package com.github.nullptroma.wallenc.ui.elements
import androidx.camera.core.ImageProxy
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.ChecksumException
import com.google.zxing.DecodeHintType
import com.google.zxing.FormatException
import com.google.zxing.NotFoundException
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeReader
internal object QrImageDecoder {
private val hints = mapOf(
DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE),
DecodeHintType.CHARACTER_SET to "UTF-8",
)
private val reader = QRCodeReader()
fun decode(imageProxy: ImageProxy): String? {
val yPlane = imageProxy.planes[0]
val yBuffer = yPlane.buffer
yBuffer.rewind()
val yRowStride = yPlane.rowStride
val yPixelStride = yPlane.pixelStride
val width = imageProxy.width
val height = imageProxy.height
val yBytes = ByteArray(width * height)
var outputOffset = 0
if (yPixelStride == 1 && yRowStride == width) {
yBuffer.get(yBytes, 0, yBytes.size.coerceAtMost(yBuffer.remaining()))
} else {
for (row in 0 until height) {
var inputOffset = row * yRowStride
for (col in 0 until width) {
yBytes[outputOffset++] = yBuffer.get(inputOffset)
inputOffset += yPixelStride
}
}
}
val (luminance, decodeWidth, decodeHeight) = orientLuminance(
yBytes,
width,
height,
imageProxy.imageInfo.rotationDegrees,
)
val source = PlanarYUVLuminanceSource(
luminance,
decodeWidth,
decodeHeight,
0,
0,
decodeWidth,
decodeHeight,
false,
)
return try {
reader.decode(BinaryBitmap(HybridBinarizer(source)), hints).text
} catch (_: NotFoundException) {
null
} catch (_: ChecksumException) {
null
} catch (_: FormatException) {
null
}
}
private fun orientLuminance(
yBytes: ByteArray,
width: Int,
height: Int,
rotationDegrees: Int,
): Triple<ByteArray, Int, Int> = when (rotationDegrees) {
90 -> Triple(rotateY90(yBytes, width, height, clockwise = true), height, width)
180 -> Triple(rotateY180(yBytes, width, height), width, height)
270 -> Triple(rotateY90(yBytes, width, height, clockwise = false), height, width)
else -> Triple(yBytes, width, height)
}
private fun rotateY90(data: ByteArray, width: Int, height: Int, clockwise: Boolean): ByteArray {
val output = ByteArray(data.size)
for (y in 0 until height) {
for (x in 0 until width) {
val srcIndex = y * width + x
val dstIndex = if (clockwise) {
x * height + (height - 1 - y)
} else {
(width - 1 - x) * height + y
}
output[dstIndex] = data[srcIndex]
}
}
return output
}
private fun rotateY180(data: ByteArray, width: Int, height: Int): ByteArray {
val output = ByteArray(data.size)
for (y in 0 until height) {
for (x in 0 until width) {
output[(height - 1 - y) * width + (width - 1 - x)] = data[y * width + x]
}
}
return output
}
}

View File

@@ -31,10 +31,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import com.github.nullptroma.wallenc.ui.R import com.github.nullptroma.wallenc.ui.R
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 java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -48,20 +44,12 @@ fun QrScannerDialog(
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val cameraExecutor = remember { Executors.newSingleThreadExecutor() } val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
val scanner = remember {
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build(),
)
}
val consumed = remember { AtomicBoolean(false) } val consumed = remember { AtomicBoolean(false) }
var previewViewRef by remember { mutableStateOf<PreviewView?>(null) } var previewViewRef by remember { mutableStateOf<PreviewView?>(null) }
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
runCatching { cameraProviderFuture.get().unbindAll() } runCatching { cameraProviderFuture.get().unbindAll() }
runCatching { scanner.close() }
cameraExecutor.shutdown() cameraExecutor.shutdown()
} }
} }
@@ -76,24 +64,17 @@ fun QrScannerDialog(
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build() .build()
.also { imageAnalysis -> .also { imageAnalysis ->
imageAnalysis.targetRotation = previewView.display.rotation
imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
val mediaImage = imageProxy.image try {
if (mediaImage == null) { if (consumed.get()) return@setAnalyzer
val raw = QrImageDecoder.decode(imageProxy)?.trim().orEmpty()
if (raw.isNotBlank() && consumed.compareAndSet(false, true)) {
onScanned(raw)
}
} finally {
imageProxy.close() imageProxy.close()
return@setAnalyzer
} }
val input = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(input)
.addOnSuccessListener { barcodes ->
if (consumed.get()) return@addOnSuccessListener
val raw = barcodes.firstOrNull()?.rawValue?.trim().orEmpty()
if (raw.isNotBlank() && consumed.compareAndSet(false, true)) {
onScanned(raw)
}
}
.addOnCompleteListener {
imageProxy.close()
}
} }
} }
cameraProvider.unbindAll() cameraProvider.unbindAll()