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 4546474849505152 private fun decodeGrid(page: PdfRenderer.Page): List<ChunkEnvelope> {53 val L = CodexPageLayout54 55 56 57 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 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}