package com.peachpasswords.ui.pairing import androidx.camera.core.ImageProxy import com.google.zxing.BinaryBitmap import com.google.zxing.DecodeHintType import com.google.zxing.LuminanceSource import com.google.zxing.PlanarYUVLuminanceSource import com.google.zxing.common.GlobalHistogramBinarizer import com.google.zxing.common.HybridBinarizer import com.google.zxing.qrcode.QRCodeReader /** QR-only CameraX decoder for reachability invitations. */ internal object PairingQrFrame { fun decode(image: ImageProxy): String? { val plane = image.planes.getOrNull(0) ?: return null val buffer = plane.buffer val data = ByteArray(buffer.remaining()) buffer.get(data) val width = image.width val height = image.height val luma = if (plane.rowStride == width) { data } else { ByteArray(width * height).also { packed -> for (row in 0 until height) { System.arraycopy( data, row * plane.rowStride, packed, row * width, width, ) } } } val source = planar(luma, width, height) val hints = mapOf( DecodeHintType.TRY_HARDER to true, DecodeHintType.POSSIBLE_FORMATS to listOf(com.google.zxing.BarcodeFormat.QR_CODE), ) // Lazily, one strategy at a time. Building these eagerly meant a strategy // that threw during CONSTRUCTION escaped the per-attempt catch below and // killed the analyzer thread — which is how the rotated attempt used to // take the whole app down on the first camera frame. val attempts = sequenceOf<() -> BinaryBitmap>( { BinaryBitmap(HybridBinarizer(source)) }, { BinaryBitmap(GlobalHistogramBinarizer(source)) }, { BinaryBitmap(HybridBinarizer(rotatedCounterClockwise(luma, width, height))) }, ) for (attempt in attempts) { try { return QRCodeReader().decode(attempt(), hints).text } catch (_: Exception) { // Try the next lighting/orientation strategy. } } return null } private fun planar(luma: ByteArray, width: Int, height: Int): LuminanceSource = PlanarYUVLuminanceSource(luma, width, height, 0, 0, width, height, false) /** * `PlanarYUVLuminanceSource` does not implement rotation — the base * `LuminanceSource.rotateCounterClockwise()` throws * `UnsupportedOperationException` — so rotate the luma plane directly. * Counter-clockwise sends source (x, y) to (y, width - 1 - x) in an image * that is height wide and width tall. */ private fun rotatedCounterClockwise( luma: ByteArray, width: Int, height: Int, ): LuminanceSource { val rotated = ByteArray(width * height) for (y in 0 until height) { val row = y * width for (x in 0 until width) { rotated[(width - 1 - x) * height + y] = luma[row + x] } } return planar(rotated, height, width) } }