package com.peachpasswords.codex import android.graphics.Color import android.graphics.Paint import android.graphics.RectF import android.graphics.pdf.PdfDocument import com.google.zxing.common.BitMatrix import java.io.ByteArrayOutputStream /** * Renders the Aztec codes onto an A4 PDF in a 6×6 grid — the printed Codex. * A4 at 72pt/in is 595×842pt. Header carries the verify code; the footer * carries the generation date and page counter so a re-collated stack stays * unambiguous. * * Each code is drawn as individual filled module rectangles (vector fills), * not an embedded raster bitmap. `PdfDocument.Canvas.drawBitmap()` was found * to corrupt crisp barcode edges when the PDF gets rasterized back out by * `PdfRenderer` (confirmed: a full, uncropped render of a generated PDF page * showed the same speckled corruption as a per-cell crop, and the identical * source bitmap decodes perfectly in an in-memory encode→decode round trip — * so the corruption is specific to the raster-image PDF round trip, not the * encoder or the restore-side crop math). Vector rect fills go through PDF's * lossless path instead. */ object CodexPdf { // Canonical shared page format — see CodexPageLayout. These used to be this // file's own private geometry, which made an Android Codex readable only by // Android. private val PAGE_W = CodexPageLayout.PAGE_WIDTH.toInt() private val PAGE_H = CodexPageLayout.PAGE_HEIGHT.toInt() private const val MARGIN = CodexPageLayout.MARGIN fun render(aztecs: List, manifest: CodexManifest): ByteArray { val doc = PdfDocument() val title = Paint().apply { color = Color.BLACK; textSize = 16f; isAntiAlias = true; isFakeBoldText = true } val mono = Paint().apply { color = Color.rgb(0x55, 0x55, 0x55); textSize = 9f; isAntiAlias = true } // No anti-aliasing on module fills: soft edges between adjacent // black/white modules are exactly what a barcode decoder's binarizer // struggles with at low module-to-pixel ratios. val modulePaint = Paint().apply { color = Color.BLACK; isAntiAlias = false } val pageCount = Math.ceil(aztecs.size.toDouble() / CODES_PER_PAGE).toInt().coerceAtLeast(1) for (page in 0 until pageCount) { val info = PdfDocument.PageInfo.Builder(PAGE_W, PAGE_H, page + 1).create() val pg = doc.startPage(info) val canvas = pg.canvas canvas.drawText("Peach Codex", MARGIN, MARGIN + 18f, title) canvas.drawText(manifest.verifyCode, MARGIN, MARGIN + 36f, mono) canvas.drawText( "${manifest.entryCount} entries · ${manifest.totalChunks} codes · keep with your recovery words", MARGIN + 120f, MARGIN + 36f, mono, ) val first = page * CODES_PER_PAGE val last = minOf(first + CODES_PER_PAGE, aztecs.size) for (idx in first until last) { val slot = idx - first val row = slot / CODES_PER_ROW val col = slot % CODES_PER_ROW drawAztecVector( canvas, aztecs[idx], CodexPageLayout.codeLeft(col), CodexPageLayout.codeTop(row), CodexPageLayout.CODE_SIZE, modulePaint, ) } canvas.drawText( "page ${page + 1} of $pageCount · ${manifest.verifyCode}", MARGIN, PAGE_H - MARGIN + 4f, mono, ) doc.finishPage(pg) } val out = ByteArrayOutputStream() doc.writeTo(out) doc.close() return out.toByteArray() } /** * Draw one Aztec code as individual filled module rectangles inside a * [sizePt]×[sizePt] square at ([left], [top]). Draws each row's "on" * modules as merged horizontal spans (not one rect per module) to keep * the PDF's content stream compact — a 76-code Codex would otherwise * emit tens of thousands of individual fill operators. */ private fun drawAztecVector( canvas: android.graphics.Canvas, matrix: BitMatrix, left: Float, top: Float, sizePt: Float, paint: Paint, ) { val n = matrix.width // square matrix: width == height val moduleSize = sizePt / n for (y in 0 until n) { var x = 0 while (x < n) { if (!matrix.get(x, y)) { x++ continue } val spanStart = x while (x < n && matrix.get(x, y)) x++ val rectLeft = left + spanStart * moduleSize val rectRight = left + x * moduleSize val rectTop = top + y * moduleSize canvas.drawRect(rectLeft, rectTop, rectRight, rectTop + moduleSize, paint) } } } }