CodexRestore.kt

4.2 KB · raw

1package com.peachpasswords.codex23import android.content.Context4import android.graphics.Bitmap5import android.graphics.Color6import android.graphics.pdf.PdfRenderer7import android.net.Uri8import android.os.ParcelFileDescriptor910/**11 * Restore side: render a Codex PDF back to bitmaps, crop the 6×6 grid using the12 * same geometry [CodexPdf] laid down, decode each Aztec, then Reed-Solomon13 * reconstruct the payload — surviving missing or unreadable codes up to the14 * parity budget.15 */16object CodexRestore {1718    data class ScanResult(val envelopes: List<ChunkEnvelope>, val cellsTried: Int, val decoded: Int)1920    fun decodeFromUri(context: Context, uri: Uri, onPage: (Int, Int) -> Unit = { _, _ -> }): ScanResult {21        val pfd: ParcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")22            ?: throw CodexException(CodexErrorCode.RESTORE_FAILED, "Cannot open file")23        val found = LinkedHashMap<String, ChunkEnvelope>()24        var cells = 025        var decoded = 026        pfd.use { descriptor ->27            PdfRenderer(descriptor).use { renderer ->28                val pages = renderer.pageCount29                for (p in 0 until pages) {30                    onPage(p + 1, pages)31                    val page = renderer.openPage(p)32                    for (env in decodeGrid(page)) {33                        cells++34                        found.putIfAbsent("${env.s}:${env.i}", env)35                        decoded++36                    }37                    page.close()38                }39            }40        }41        return ScanResult(found.values.toList(), cells, decoded)42    }4344    /**45     * Render each code's region into a high-res bitmap via a per-cell46     * transform, then decode. Per-cell rendering keeps each Aztec at high47     * fidelity without allocating a giant full-page bitmap.48     *49     * Positions come from [CodexPageLayout] — the shared browser/Android page50     * format — not from whatever grid this app's own generator happens to use.51     */52    private fun decodeGrid(page: PdfRenderer.Page): List<ChunkEnvelope> {53        val L = CodexPageLayout54        // Crop each code plus its quiet zone, at the canonical positions —55        // mirrors the browser importer's cellRectFor. Aztec cannot tolerate a56        // clipped finder pattern, so the region must match the generator's57        // geometry exactly rather than being an evenly-sliced grid.58        val quiet = L.CODE_SIZE * L.QUIET_FRACTION59        val region = L.CODE_SIZE + 2 * quiet60        val target = 160061        val scale = target / region6263        val out = ArrayList<ChunkEnvelope>()64        for (row in 0 until CODES_PER_COL) for (col in 0 until CODES_PER_ROW) {65            val leftPt = L.codeLeft(col) - quiet66            val topPt = L.codeTop(row) - quiet67            val cellBmp = Bitmap.createBitmap(target, target, Bitmap.Config.ARGB_8888)68            cellBmp.eraseColor(Color.WHITE)69            val m = android.graphics.Matrix().apply {70                postTranslate(-leftPt, -topPt)71                postScale(scale, scale)72            }73            page.render(cellBmp, null, m, PdfRenderer.Page.RENDER_MODE_FOR_PRINT)74            val text = CodexAztec.decode(cellBmp)75            cellBmp.recycle()76            if (text != null) parseEnvelope(text)?.let(out::add)77        }78        return out79    }8081    fun parseEnvelope(text: String): ChunkEnvelope? = try {82        CodexCodec.json.decodeFromString(ChunkEnvelope.serializer(), text)83    } catch (_: Exception) {84        null85    }8687    /** Rebuild and validate the payload from decoded envelopes. */88    fun reconstructPayload(envelopes: List<ChunkEnvelope>): CodexPayload {89        if (envelopes.isEmpty()) throw CodexException(CodexErrorCode.INSUFFICIENT_CHUNKS, "No codes decoded")90        val compressed = CodexCodec.reconstructCompressed(envelopes)91        val payload = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(compressed)))92        val fp = CodexCodec.sha256Hex(payload.vaultBlob.toByteArray(Charsets.UTF_8)).take(32)93        if (fp != payload.vaultFingerprint) {94            throw CodexException(CodexErrorCode.RECONSTRUCTION_FINGERPRINT_MISMATCH, "Fingerprint mismatch — codes corrupted")95        }96        return payload97    }98}