1package com.peachpasswords.codex23import android.content.Context4import android.graphics.Bitmap5import android.graphics.Color6import android.graphics.pdf.PdfRenderer7import android.net.Uri8import android.os.ParcelFileDescriptor910111213141516object 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 4546474849505152535455565758 private fun decodeGrid(page: PdfRenderer.Page): List<ChunkEnvelope> {59 val margin = 36f60 val gridTop = margin + 54f61 val cell = minOf((595f - 2 * margin) / CODES_PER_ROW, (842f - gridTop - margin - 28f) / CODES_PER_COL)62 val pad = cell * 0.05f 63 val sizePt = cell - 2 * pad64 val bleed = sizePt * 0.10f65 val region = sizePt + 2 * bleed66 val target = 160067 val scale = target / region6869 val out = ArrayList<ChunkEnvelope>()70 for (row in 0 until CODES_PER_COL) for (col in 0 until CODES_PER_ROW) {71 val leftPt = margin + col * cell + pad - bleed72 val topPt = gridTop + row * cell + pad - bleed73 val cellBmp = Bitmap.createBitmap(target, target, Bitmap.Config.ARGB_8888)74 cellBmp.eraseColor(Color.WHITE)75 val m = android.graphics.Matrix().apply {76 postTranslate(-leftPt, -topPt)77 postScale(scale, scale)78 }79 page.render(cellBmp, null, m, PdfRenderer.Page.RENDER_MODE_FOR_PRINT)80 val text = CodexAztec.decode(cellBmp)81 cellBmp.recycle()82 if (text != null) parseEnvelope(text)?.let(out::add)83 }84 return out85 }8687 fun parseEnvelope(text: String): ChunkEnvelope? = try {88 CodexCodec.json.decodeFromString(ChunkEnvelope.serializer(), text)89 } catch (_: Exception) {90 null91 }9293 94 fun reconstructPayload(envelopes: List<ChunkEnvelope>): CodexPayload {95 if (envelopes.isEmpty()) throw CodexException(CodexErrorCode.INSUFFICIENT_CHUNKS, "No codes decoded")96 val compressed = CodexCodec.reconstructCompressed(envelopes)97 val payload = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(compressed)))98 val fp = CodexCodec.sha256Hex(payload.vaultBlob.toByteArray(Charsets.UTF_8)).take(32)99 if (fp != payload.vaultFingerprint) {100 throw CodexException(CodexErrorCode.RECONSTRUCTION_FINGERPRINT_MISMATCH, "Fingerprint mismatch — codes corrupted")101 }102 return payload103 }104}