PairingQrFrame.kt

3.2 KB · raw

1package com.peachpasswords.ui.pairing23import androidx.camera.core.ImageProxy4import com.google.zxing.BinaryBitmap5import com.google.zxing.DecodeHintType6import com.google.zxing.LuminanceSource7import com.google.zxing.PlanarYUVLuminanceSource8import com.google.zxing.common.GlobalHistogramBinarizer9import com.google.zxing.common.HybridBinarizer10import com.google.zxing.qrcode.QRCodeReader1112/** QR-only CameraX decoder for reachability invitations. */13internal object PairingQrFrame {14    fun decode(image: ImageProxy): String? {15        val plane = image.planes.getOrNull(0) ?: return null16        val buffer = plane.buffer17        val data = ByteArray(buffer.remaining())18        buffer.get(data)19        val width = image.width20        val height = image.height21        val luma = if (plane.rowStride == width) {22            data23        } else {24            ByteArray(width * height).also { packed ->25                for (row in 0 until height) {26                    System.arraycopy(27                        data,28                        row * plane.rowStride,29                        packed,30                        row * width,31                        width,32                    )33                }34            }35        }36        val source = planar(luma, width, height)3738        val hints = mapOf(39            DecodeHintType.TRY_HARDER to true,40            DecodeHintType.POSSIBLE_FORMATS to listOf(com.google.zxing.BarcodeFormat.QR_CODE),41        )42        // Lazily, one strategy at a time. Building these eagerly meant a strategy43        // that threw during CONSTRUCTION escaped the per-attempt catch below and44        // killed the analyzer thread — which is how the rotated attempt used to45        // take the whole app down on the first camera frame.46        val attempts = sequenceOf<() -> BinaryBitmap>(47            { BinaryBitmap(HybridBinarizer(source)) },48            { BinaryBitmap(GlobalHistogramBinarizer(source)) },49            { BinaryBitmap(HybridBinarizer(rotatedCounterClockwise(luma, width, height))) },50        )51        for (attempt in attempts) {52            try {53                return QRCodeReader().decode(attempt(), hints).text54            } catch (_: Exception) {55                // Try the next lighting/orientation strategy.56            }57        }58        return null59    }6061    private fun planar(luma: ByteArray, width: Int, height: Int): LuminanceSource =62        PlanarYUVLuminanceSource(luma, width, height, 0, 0, width, height, false)6364    /**65     * `PlanarYUVLuminanceSource` does not implement rotation — the base66     * `LuminanceSource.rotateCounterClockwise()` throws67     * `UnsupportedOperationException` — so rotate the luma plane directly.68     * Counter-clockwise sends source (x, y) to (y, width - 1 - x) in an image69     * that is height wide and width tall.70     */71    private fun rotatedCounterClockwise(72        luma: ByteArray,73        width: Int,74        height: Int,75    ): LuminanceSource {76        val rotated = ByteArray(width * height)77        for (y in 0 until height) {78            val row = y * width79            for (x in 0 until width) {80                rotated[(width - 1 - x) * height + y] = luma[row + x]81            }82        }83        return planar(rotated, height, width)84    }85}