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 4546474849 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 54 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 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}