Handle padded camera rows in QR scanner (#6012)

Camera HALs may expose a Y plane whose row stride is wider than the visible image. Pass that stride to ZXing so padded rows remain aligned, and cover the vendor-layout case with a valid VLESS QR regression test.
This commit is contained in:
eliotcougar authored and GitHub committed 2026-08-04 20:30:25 +08:00
1 parent 559a00e66e
commit 320fb6a10b
2 files changed
+78 -7

No files matched your search

@@ -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,
)
}
@@ -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)
}
}