CodexPdf.kt

4.9 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    // Canonical shared page format — see CodexPageLayout. These used to be this28    // file's own private geometry, which made an Android Codex readable only by29    // Android.30    private val PAGE_W = CodexPageLayout.PAGE_WIDTH.toInt()31    private val PAGE_H = CodexPageLayout.PAGE_HEIGHT.toInt()32    private const val MARGIN = CodexPageLayout.MARGIN3334    fun render(aztecs: List<BitMatrix>, manifest: CodexManifest): ByteArray {35        val doc = PdfDocument()3637        val title = Paint().apply { color = Color.BLACK; textSize = 16f; isAntiAlias = true; isFakeBoldText = true }38        val mono = Paint().apply { color = Color.rgb(0x55, 0x55, 0x55); textSize = 9f; isAntiAlias = true }39        // No anti-aliasing on module fills: soft edges between adjacent40        // black/white modules are exactly what a barcode decoder's binarizer41        // struggles with at low module-to-pixel ratios.42        val modulePaint = Paint().apply { color = Color.BLACK; isAntiAlias = false }4344        val pageCount = Math.ceil(aztecs.size.toDouble() / CODES_PER_PAGE).toInt().coerceAtLeast(1)45        for (page in 0 until pageCount) {46            val info = PdfDocument.PageInfo.Builder(PAGE_W, PAGE_H, page + 1).create()47            val pg = doc.startPage(info)48            val canvas = pg.canvas4950            canvas.drawText("Peach Codex", MARGIN, MARGIN + 18f, title)51            canvas.drawText(manifest.verifyCode, MARGIN, MARGIN + 36f, mono)52            canvas.drawText(53                "${manifest.entryCount} entries · ${manifest.totalChunks} codes · keep with your recovery words",54                MARGIN + 120f, MARGIN + 36f, mono,55            )5657            val first = page * CODES_PER_PAGE58            val last = minOf(first + CODES_PER_PAGE, aztecs.size)59            for (idx in first until last) {60                val slot = idx - first61                val row = slot / CODES_PER_ROW62                val col = slot % CODES_PER_ROW63                drawAztecVector(64                    canvas,65                    aztecs[idx],66                    CodexPageLayout.codeLeft(col),67                    CodexPageLayout.codeTop(row),68                    CodexPageLayout.CODE_SIZE,69                    modulePaint,70                )71            }7273            canvas.drawText(74                "page ${page + 1} of $pageCount · ${manifest.verifyCode}",75                MARGIN, PAGE_H - MARGIN + 4f, mono,76            )77            doc.finishPage(pg)78        }7980        val out = ByteArrayOutputStream()81        doc.writeTo(out)82        doc.close()83        return out.toByteArray()84    }8586    /**87     * Draw one Aztec code as individual filled module rectangles inside a88     * [sizePt]×[sizePt] square at ([left], [top]). Draws each row's "on"89     * modules as merged horizontal spans (not one rect per module) to keep90     * the PDF's content stream compact — a 76-code Codex would otherwise91     * emit tens of thousands of individual fill operators.92     */93    private fun drawAztecVector(94        canvas: android.graphics.Canvas,95        matrix: BitMatrix,96        left: Float,97        top: Float,98        sizePt: Float,99        paint: Paint,100    ) {101        val n = matrix.width // square matrix: width == height102        val moduleSize = sizePt / n103        for (y in 0 until n) {104            var x = 0105            while (x < n) {106                if (!matrix.get(x, y)) {107                    x++108                    continue109                }110                val spanStart = x111                while (x < n && matrix.get(x, y)) x++112                val rectLeft = left + spanStart * moduleSize113                val rectRight = left + x * moduleSize114                val rectTop = top + y * moduleSize115                canvas.drawRect(rectLeft, rectTop, rectRight, rectTop + moduleSize, paint)116            }117        }118    }119}