import { PNG } from 'pngjs'; import QRCode from 'qrcode'; import type { Fixture } from '../src/db/fixtures'; const WIDTH = 620; const HEIGHT = 840; const QR_MODULE_PX = 6; const QUIET_ZONE_MODULES = 4; /** * Draws a fixture as a PNG "page" with a real, decodable QR where a KUDE prints one. * * SPEC-GAP: CONTRACTS.md section 4 describes rendering these through HTML or canvas. They * are drawn with raw pixels instead, because the only thing any test reads from them is * the QR, and a faithful visual replica would mean carrying a browser or an SVG * rasteriser purely to produce three images nothing looks at. The emitter, date and * totals live in fixtures.ts, which is where tests and the seed read them from. */ export function renderKude(fixture: Fixture): Buffer { const png = new PNG({ width: WIDTH, height: HEIGHT }); fill(png, 0xff, 0xff, 0xff); // A masthead bar and a rule, so the image reads as a document rather than a blank page. rect(png, 0, 0, WIDTH, 72, 0x0f, 0x4c, 0x4c); rect(png, 40, 120, WIDTH - 80, 2, 0xd0, 0xd0, 0xd0); rect(png, 40, 220, WIDTH - 80, 2, 0xd0, 0xd0, 0xd0); if (fixture.qrUrl) drawQr(png, fixture.qrUrl); return PNG.sync.write(png); } function drawQr(png: PNG, payload: string): void { // Level M with an explicit quiet zone: a QR without margin is unreliable to decode. const qr = QRCode.create(payload, { errorCorrectionLevel: 'M' }); const size = qr.modules.size; const data = qr.modules.data; const side = (size + QUIET_ZONE_MODULES * 2) * QR_MODULE_PX; const originX = Math.floor((WIDTH - side) / 2); const originY = HEIGHT - side - 60; rect(png, originX, originY, side, side, 0xff, 0xff, 0xff); for (let row = 0; row < size; row++) { for (let column = 0; column < size; column++) { if (!data[row * size + column]) continue; rect( png, originX + (column + QUIET_ZONE_MODULES) * QR_MODULE_PX, originY + (row + QUIET_ZONE_MODULES) * QR_MODULE_PX, QR_MODULE_PX, QR_MODULE_PX, 0x00, 0x00, 0x00, ); } } } function fill(png: PNG, r: number, g: number, b: number): void { rect(png, 0, 0, png.width, png.height, r, g, b); } function rect( png: PNG, x: number, y: number, width: number, height: number, r: number, g: number, b: number, ): void { for (let row = y; row < y + height && row < png.height; row++) { for (let column = x; column < x + width && column < png.width; column++) { const index = (png.width * row + column) << 2; png.data[index] = r; png.data[index + 1] = g; png.data[index + 2] = b; png.data[index + 3] = 0xff; } } }