package com.peachpasswords.codex import android.content.Context import android.graphics.Bitmap import android.graphics.Color import android.graphics.pdf.PdfRenderer import android.net.Uri import android.os.ParcelFileDescriptor /** * Restore side: render a Codex PDF back to bitmaps, crop the 6×6 grid using the * same geometry [CodexPdf] laid down, decode each Aztec, then Reed-Solomon * reconstruct the payload — surviving missing or unreadable codes up to the * parity budget. */ object CodexRestore { data class ScanResult(val envelopes: List, val cellsTried: Int, val decoded: Int) fun decodeFromUri(context: Context, uri: Uri, onPage: (Int, Int) -> Unit = { _, _ -> }): ScanResult { val pfd: ParcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r") ?: throw CodexException(CodexErrorCode.RESTORE_FAILED, "Cannot open file") val found = LinkedHashMap() var cells = 0 var decoded = 0 pfd.use { descriptor -> PdfRenderer(descriptor).use { renderer -> val pages = renderer.pageCount for (p in 0 until pages) { onPage(p + 1, pages) val page = renderer.openPage(p) for (env in decodeGrid(page)) { cells++ found.putIfAbsent("${env.s}:${env.i}", env) decoded++ } page.close() } } } return ScanResult(found.values.toList(), cells, decoded) } /** * Render each grid cell directly into a high-res bitmap via a per-cell * transform (CodexPdf geometry), then decode. Per-cell rendering keeps each * Aztec at ~6 px/module without allocating a giant full-page bitmap. */ private fun decodeGrid(page: PdfRenderer.Page): List { val margin = 36f val gridTop = margin + 54f val cell = minOf((595f - 2 * margin) / CODES_PER_ROW, (842f - gridTop - margin - 28f) / CODES_PER_COL) val pad = cell * 0.05f // must match CodexPdf val sizePt = cell - 2 * pad val bleed = sizePt * 0.10f val region = sizePt + 2 * bleed val target = 640 val scale = target / region val out = ArrayList() for (row in 0 until CODES_PER_COL) for (col in 0 until CODES_PER_ROW) { val leftPt = margin + col * cell + pad - bleed val topPt = gridTop + row * cell + pad - bleed val cellBmp = Bitmap.createBitmap(target, target, Bitmap.Config.ARGB_8888) cellBmp.eraseColor(Color.WHITE) val m = android.graphics.Matrix().apply { postTranslate(-leftPt, -topPt) postScale(scale, scale) } page.render(cellBmp, null, m, PdfRenderer.Page.RENDER_MODE_FOR_PRINT) val text = CodexAztec.decode(cellBmp) cellBmp.recycle() if (text != null) parseEnvelope(text)?.let(out::add) } return out } fun parseEnvelope(text: String): ChunkEnvelope? = try { CodexCodec.json.decodeFromString(ChunkEnvelope.serializer(), text) } catch (_: Exception) { null } /** Rebuild and validate the payload from decoded envelopes. */ fun reconstructPayload(envelopes: List): CodexPayload { if (envelopes.isEmpty()) throw CodexException(CodexErrorCode.INSUFFICIENT_CHUNKS, "No codes decoded") val compressed = CodexCodec.reconstructCompressed(envelopes) val payload = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(compressed))) val fp = CodexCodec.sha256Hex(payload.vaultBlob.toByteArray(Charsets.UTF_8)).take(32) if (fp != payload.vaultFingerprint) { throw CodexException(CodexErrorCode.RECONSTRUCTION_FINGERPRINT_MISMATCH, "Fingerprint mismatch — codes corrupted") } return payload } }