CodexPdf.kt

5.1 KB · raw

1package com.peachpasswords.codex23import android.graphics.Color4import android.graphics.Paint5import android.graphics.RectF6import android.graphics.pdf.PdfDocument7import com.google.zxing.common.BitMatrix8import 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 *16 * Each code is drawn as individual filled module rectangles (vector fills),17 * not an embedded raster bitmap. `PdfDocument.Canvas.drawBitmap()` was found18 * to corrupt crisp barcode edges when the PDF gets rasterized back out by19 * `PdfRenderer` (confirmed: a full, uncropped render of a generated PDF page20 * showed the same speckled corruption as a per-cell crop, and the identical21 * source bitmap decodes perfectly in an in-memory encode→decode round trip —22 * so the corruption is specific to the raster-image PDF round trip, not the23 * encoder or the restore-side crop math). Vector rect fills go through PDF's24 * lossless path instead.25 */26object CodexPdf {27    private const val PAGE_W = 59528    private const val PAGE_H = 84229    private const val MARGIN = 36f30    private const val HEADER_H = 54f31    private const val FOOTER_H = 28f3233    fun render(aztecs: List<BitMatrix>, manifest: CodexManifest): ByteArray {34        val doc = PdfDocument()35        val gridTop = MARGIN + HEADER_H36        val gridW = PAGE_W - 2 * MARGIN37        val gridH = PAGE_H - gridTop - MARGIN - FOOTER_H38        val cellW = gridW / CODES_PER_ROW39        val cellH = gridH / CODES_PER_COL40        val cell = minOf(cellW, cellH)41        // Tighter padding → larger codes (~78pt, close to the browser's 80pt),42        // which keeps modules above the camera/scan decode threshold.43        val pad = cell * 0.05f4445        val title = Paint().apply { color = Color.BLACK; textSize = 16f; isAntiAlias = true; isFakeBoldText = true }46        val mono = Paint().apply { color = Color.rgb(0x55, 0x55, 0x55); textSize = 9f; isAntiAlias = true }47        // No anti-aliasing on module fills: soft edges between adjacent48        // black/white modules are exactly what a barcode decoder's binarizer49        // struggles with at low module-to-pixel ratios.50        val modulePaint = Paint().apply { color = Color.BLACK; isAntiAlias = false }5152        val pageCount = Math.ceil(aztecs.size.toDouble() / CODES_PER_PAGE).toInt().coerceAtLeast(1)53        for (page in 0 until pageCount) {54            val info = PdfDocument.PageInfo.Builder(PAGE_W, PAGE_H, page + 1).create()55            val pg = doc.startPage(info)56            val canvas = pg.canvas5758            canvas.drawText("Peach Codex", MARGIN, MARGIN + 18f, title)59            canvas.drawText(manifest.verifyCode, MARGIN, MARGIN + 36f, mono)60            canvas.drawText(61                "${manifest.entryCount} entries · ${manifest.totalChunks} codes · keep with your recovery words",62                MARGIN + 120f, MARGIN + 36f, mono,63            )6465            val first = page * CODES_PER_PAGE66            val last = minOf(first + CODES_PER_PAGE, aztecs.size)67            for (idx in first until last) {68                val slot = idx - first69                val row = slot / CODES_PER_ROW70                val col = slot % CODES_PER_ROW71                val left = MARGIN + col * cell + pad72                val top = gridTop + row * cell + pad73                val sizePt = cell - 2 * pad74                drawAztecVector(canvas, aztecs[idx], left, top, sizePt, modulePaint)75            }7677            canvas.drawText(78                "page ${page + 1} of $pageCount · ${manifest.verifyCode}",79                MARGIN, PAGE_H - MARGIN + 4f, mono,80            )81            doc.finishPage(pg)82        }8384        val out = ByteArrayOutputStream()85        doc.writeTo(out)86        doc.close()87        return out.toByteArray()88    }8990    /**91     * Draw one Aztec code as individual filled module rectangles inside a92     * [sizePt]×[sizePt] square at ([left], [top]). Draws each row's "on"93     * modules as merged horizontal spans (not one rect per module) to keep94     * the PDF's content stream compact — a 76-code Codex would otherwise95     * emit tens of thousands of individual fill operators.96     */97    private fun drawAztecVector(98        canvas: android.graphics.Canvas,99        matrix: BitMatrix,100        left: Float,101        top: Float,102        sizePt: Float,103        paint: Paint,104    ) {105        val n = matrix.width // square matrix: width == height106        val moduleSize = sizePt / n107        for (y in 0 until n) {108            var x = 0109            while (x < n) {110                if (!matrix.get(x, y)) {111                    x++112                    continue113                }114                val spanStart = x115                while (x < n && matrix.get(x, y)) x++116                val rectLeft = left + spanStart * moduleSize117                val rectRight = left + x * moduleSize118                val rectTop = top + y * moduleSize119                canvas.drawRect(rectLeft, rectTop, rectRight, rectTop + moduleSize, paint)120            }121        }122    }123}