CodexPdf.kt

3.1 KB · raw

1package com.peachpasswords.codex23import android.graphics.Bitmap4import android.graphics.Color5import android.graphics.Paint6import android.graphics.Rect7import android.graphics.pdf.PdfDocument8import java.io.ByteArrayOutputStream910/**11 * Renders the Aztec codes onto an A4 PDF in a 6×6 grid — the printed Codex.12 * A4 at 72pt/in is 595×842pt. Header carries the verify code; the footer13 * carries the generation date and page counter so a re-collated stack stays14 * unambiguous.15 */16object CodexPdf {17    private const val PAGE_W = 59518    private const val PAGE_H = 84219    private const val MARGIN = 36f20    private const val HEADER_H = 54f21    private const val FOOTER_H = 28f2223    fun render(aztecs: List<Bitmap>, manifest: CodexManifest): ByteArray {24        val doc = PdfDocument()25        val gridTop = MARGIN + HEADER_H26        val gridW = PAGE_W - 2 * MARGIN27        val gridH = PAGE_H - gridTop - MARGIN - FOOTER_H28        val cellW = gridW / CODES_PER_ROW29        val cellH = gridH / CODES_PER_COL30        val cell = minOf(cellW, cellH)31        // Tighter padding → larger codes (~78pt, close to the browser's 80pt),32        // which keeps modules above the camera/scan decode threshold.33        val pad = cell * 0.05f3435        val title = Paint().apply { color = Color.BLACK; textSize = 16f; isAntiAlias = true; isFakeBoldText = true }36        val mono = Paint().apply { color = Color.rgb(0x55, 0x55, 0x55); textSize = 9f; isAntiAlias = true }37        val codePaint = Paint().apply { isFilterBitmap = false }3839        val pageCount = Math.ceil(aztecs.size.toDouble() / CODES_PER_PAGE).toInt().coerceAtLeast(1)40        for (page in 0 until pageCount) {41            val info = PdfDocument.PageInfo.Builder(PAGE_W, PAGE_H, page + 1).create()42            val pg = doc.startPage(info)43            val canvas = pg.canvas4445            canvas.drawText("Peach Codex", MARGIN, MARGIN + 18f, title)46            canvas.drawText(manifest.verifyCode, MARGIN, MARGIN + 36f, mono)47            canvas.drawText(48                "${manifest.entryCount} entries · ${manifest.totalChunks} codes · keep with your recovery words",49                MARGIN + 120f, MARGIN + 36f, mono,50            )5152            val first = page * CODES_PER_PAGE53            val last = minOf(first + CODES_PER_PAGE, aztecs.size)54            for (idx in first until last) {55                val slot = idx - first56                val row = slot / CODES_PER_ROW57                val col = slot % CODES_PER_ROW58                val left = MARGIN + col * cell + pad59                val top = gridTop + row * cell + pad60                val dst = Rect(left.toInt(), top.toInt(), (left + cell - 2 * pad).toInt(), (top + cell - 2 * pad).toInt())61                canvas.drawBitmap(aztecs[idx], null, dst, codePaint)62            }6364            canvas.drawText(65                "page ${page + 1} of $pageCount · ${manifest.verifyCode}",66                MARGIN, PAGE_H - MARGIN + 4f, mono,67            )68            doc.finishPage(pg)69        }7071        val out = ByteArrayOutputStream()72        doc.writeTo(out)73        doc.close()74        return out.toByteArray()75    }76}