package io.flamenet.e2e import java.awt.image.BufferedImage import java.io.File import javax.imageio.ImageIO import kotlin.test.Test /** * Renders QR codes to PNG so an INDEPENDENT decoder can read them back. * * 🔴 **The only honest way to test an encoder we wrote ourselves.** A wrong QR * still looks like a QR; asserting the matrix against values this same code * produced proves nothing except that it is deterministic. So this writes real * images and `Scripts/verify-qr.swift` decodes them with CoreImage's detector, * which shares no line of code with `QrCode`. * * ⚠️ Not an assertion by itself — it is the fixture half. It passes as long as * the encoder does not throw; the verdict comes from the decoder. */ class QrCodeRenderForDecodingTest { @Test fun `render the corpus`() { val out = File(System.getProperty("fnmsg.qr.out") ?: "build/qr-corpus") out.mkdirs() CORPUS.forEachIndexed { index, text -> ImageIO.write(render(text, scale = 8, quiet = 4), "png", File(out, "qr-$index.png")) File(out, "qr-$index.txt").writeText(text) } println("wrote ${CORPUS.size} QR images to ${out.absolutePath}") } private fun render(text: String, scale: Int, quiet: Int): BufferedImage { val m = QrCode.encode(text) val side = (m.size + quiet * 2) * scale val image = BufferedImage(side, side, BufferedImage.TYPE_INT_RGB) val g = image.createGraphics() g.color = java.awt.Color.WHITE g.fillRect(0, 0, side, side) g.color = java.awt.Color.BLACK for (r in 0 until m.size) for (c in 0 until m.size) { if (m[r, c]) g.fillRect((c + quiet) * scale, (r + quiet) * scale, scale, scale) } g.dispose() return image } companion object { /** * Chosen to cross every version boundary the encoder supports, because * the block layout, the alignment patterns and the version-information * block all change with version and each is its own chance to be wrong. */ val CORPUS: List = listOf( "flamenet:fnid:kx7q2m", // v1 "flamenet:fnid:" + "a".repeat(52) + "@relay1.flamenet.io", // a real address "flamenet:fnid:" + "b".repeat(52) + "@" + "long".repeat(8) + ".example.com", // ⚠️ Exactly full, version by version: data codewords minus the // mode nibble and character count. The boundary is where an // off-by-one in the terminator or the padding shows up, and it is // the one place a wrong encoder still produces a scannable-looking // square. "x".repeat(14), // v1 "x".repeat(26), // v2 "x".repeat(42), // v3 "x".repeat(62), // v4 "x".repeat(84), // v5 "x".repeat(106), // v6 "x".repeat(122), // v7 — first version with a version-information block "x".repeat(152), // v8 — two block groups of different sizes "x".repeat(180), // v9 "x".repeat(213), // v10 — the largest this encoder makes ) } }