CodexRestore.kt

4.0 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 grid cell directly into a high-res bitmap via a per-cell46     * transform (CodexPdf geometry), then decode. Per-cell rendering keeps each47     * Aztec at ~6 px/module without allocating a giant full-page bitmap.48     */49    private fun decodeGrid(page: PdfRenderer.Page): List<ChunkEnvelope> {50        val margin = 36f51        val gridTop = margin + 54f52        val cell = minOf((595f - 2 * margin) / CODES_PER_ROW, (842f - gridTop - margin - 28f) / CODES_PER_COL)53        val pad = cell * 0.05f // must match CodexPdf54        val sizePt = cell - 2 * pad55        val bleed = sizePt * 0.10f56        val region = sizePt + 2 * bleed57        val target = 64058        val scale = target / region5960        val out = ArrayList<ChunkEnvelope>()61        for (row in 0 until CODES_PER_COL) for (col in 0 until CODES_PER_ROW) {62            val leftPt = margin + col * cell + pad - bleed63            val topPt = gridTop + row * cell + pad - bleed64            val cellBmp = Bitmap.createBitmap(target, target, Bitmap.Config.ARGB_8888)65            cellBmp.eraseColor(Color.WHITE)66            val m = android.graphics.Matrix().apply {67                postTranslate(-leftPt, -topPt)68                postScale(scale, scale)69            }70            page.render(cellBmp, null, m, PdfRenderer.Page.RENDER_MODE_FOR_PRINT)71            val text = CodexAztec.decode(cellBmp)72            cellBmp.recycle()73            if (text != null) parseEnvelope(text)?.let(out::add)74        }75        return out76    }7778    fun parseEnvelope(text: String): ChunkEnvelope? = try {79        CodexCodec.json.decodeFromString(ChunkEnvelope.serializer(), text)80    } catch (_: Exception) {81        null82    }8384    /** Rebuild and validate the payload from decoded envelopes. */85    fun reconstructPayload(envelopes: List<ChunkEnvelope>): CodexPayload {86        if (envelopes.isEmpty()) throw CodexException(CodexErrorCode.INSUFFICIENT_CHUNKS, "No codes decoded")87        val compressed = CodexCodec.reconstructCompressed(envelopes)88        val payload = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(compressed)))89        val fp = CodexCodec.sha256Hex(payload.vaultBlob.toByteArray(Charsets.UTF_8)).take(32)90        if (fp != payload.vaultFingerprint) {91            throw CodexException(CodexErrorCode.RECONSTRUCTION_FINGERPRINT_MISMATCH, "Fingerprint mismatch — codes corrupted")92        }93        return payload94    }95}