CodexAztecFrame.kt

2.6 KB · raw

1package com.peachpasswords.codex23import androidx.camera.core.ImageProxy4import com.google.zxing.BinaryBitmap5import com.google.zxing.DecodeHintType6import com.google.zxing.PlanarYUVLuminanceSource7import com.google.zxing.aztec.AztecReader8import com.google.zxing.common.GlobalHistogramBinarizer9import com.google.zxing.common.HybridBinarizer1011/**12 * Live-camera Aztec decode for the Codex restore camera path (D10).13 *14 * Distinct from [CodexAztec.decode], which decodes a tightly-cropped render of a15 * single PDF cell. A camera frame is a full scene that may contain one Aztec among16 * the printed 6×6 grid, so we use the scene detector (no PURE_BARCODE) and read the17 * Y plane directly — converting the YUV_420_888 frame to an ARGB bitmap every frame18 * would be wasteful at preview frame rates.19 */20object CodexAztecFrame {2122    /**23     * Decode the first readable Aztec code from a CameraX [ImageProxy]24     * (YUV_420_888). Returns the raw text, or null if no code reads. Does NOT25     * close the proxy — the caller owns its lifecycle.26     */27    fun decode(image: ImageProxy): String? {28        val plane = image.planes.getOrNull(0) ?: return null29        val buffer = plane.buffer30        val data = ByteArray(buffer.remaining())31        buffer.get(data)3233        val width = image.width34        val height = image.height35        val rowStride = plane.rowStride3637        // PlanarYUVLuminanceSource expects a tightly-packed Y plane; when the38        // camera reports row padding (rowStride > width) we repack row by row.39        val luma: ByteArray = if (rowStride == width) {40            data41        } else {42            val packed = ByteArray(width * height)43            for (row in 0 until height) {44                System.arraycopy(data, row * rowStride, packed, row * width, width)45            }46            packed47        }4849        val source = PlanarYUVLuminanceSource(50            luma, width, height, 0, 0, width, height, false,51        )52        val hints = mapOf(DecodeHintType.TRY_HARDER to true)53        // Cross both binarizers: Hybrid adapts to uneven lighting, Global handles54        // flat well-lit prints. Rotating the source covers codes shot sideways.55        val attempts = listOf(56            BinaryBitmap(HybridBinarizer(source)),57            BinaryBitmap(GlobalHistogramBinarizer(source)),58            BinaryBitmap(HybridBinarizer(source.rotateCounterClockwise())),59        )60        for (binary in attempts) {61            try {62                return AztecReader().decode(binary, hints).text63            } catch (_: Exception) {64                // try next strategy65            }66        }67        return null68    }69}