diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt index 8eb61d78..5ecf4e11 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt @@ -69,6 +69,7 @@ import com.v2ray.ang.ui.base.HelperBaseComponentActivity import com.v2ray.ang.ui.compose.AppTopBar import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.QRCodeDecoder +import java.nio.ByteBuffer import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean import android.util.Size as TargetSize @@ -410,15 +411,14 @@ private fun processImageProxy( return } try { - val buffer = imageProxy.planes[0].buffer - val bytes = ByteArray(buffer.remaining()) - buffer.get(bytes) val width = imageProxy.width val height = imageProxy.height - val source = PlanarYUVLuminanceSource( - bytes, width, height, - 0, 0, width, height, - false + val yPlane = imageProxy.planes[0] + val source = createYPlaneLuminanceSource( + buffer = yPlane.buffer, + width = width, + height = height, + rowStride = yPlane.rowStride, ) val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) val hints = mapOf( @@ -437,3 +437,26 @@ private fun processImageProxy( imageProxy.close() } } + +internal fun createYPlaneLuminanceSource( + buffer: ByteBuffer, + width: Int, + height: Int, + rowStride: Int, +): PlanarYUVLuminanceSource { + val bufferCopy = buffer.duplicate() + val bytes = ByteArray(bufferCopy.remaining()) + bufferCopy.get(bytes) + // Camera HALs may pad each Y row. ZXing advances rows by dataWidth, so the + // reported row stride must be used instead of the visible image width. + return PlanarYUVLuminanceSource( + bytes, + rowStride, + height, + 0, + 0, + width, + height, + false, + ) +} diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/ui/ScannerActivityTest.kt b/V2rayNG/app/src/test/java/com/v2ray/ang/ui/ScannerActivityTest.kt new file mode 100644 index 00000000..0b346472 --- /dev/null +++ b/V2rayNG/app/src/test/java/com/v2ray/ang/ui/ScannerActivityTest.kt @@ -0,0 +1,48 @@ +package com.v2ray.ang.ui + +import com.google.zxing.BarcodeFormat +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.MultiFormatReader +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.qrcode.QRCodeWriter +import org.junit.Assert.assertEquals +import org.junit.Test +import java.nio.ByteBuffer + +class ScannerActivityTest { + + @Test + fun paddedCameraRowsDecodeQrCode() { + val payload = "vless://00000000-0000-4000-8000-000000006007@example.com:443" + + "?encryption=none&security=tls&type=tcp#issue-6007-gallery" + val width = 320 + val height = 320 + val rowStride = width + 32 + val qrCode = QRCodeWriter().encode(payload, BarcodeFormat.QR_CODE, width, height) + val paddedYPlane = ByteArray(rowStride * height) + + for (y in 0 until height) { + for (x in 0 until width) { + paddedYPlane[y * rowStride + x] = if (qrCode[x, y]) 0 else 0xFF.toByte() + } + for (x in width until rowStride) { + paddedYPlane[y * rowStride + x] = (x + y).toByte() + } + } + + val source = createYPlaneLuminanceSource( + buffer = ByteBuffer.wrap(paddedYPlane), + width = width, + height = height, + rowStride = rowStride, + ) + val hints = mapOf( + DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE), + DecodeHintType.TRY_HARDER to true, + ) + val result = MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)), hints) + + assertEquals(payload, result.text) + } +}