import { deflateSync } from "node:zlib"; import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js"; import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js"; type QRCodeConstructor = new ( typeNumber: number, errorCorrectLevel: unknown, ) => { addData: (data: string) => void; make: () => void; getModuleCount: () => number; isDark: (row: number, col: number) => boolean; }; const QRCode = QRCodeModule as unknown as QRCodeConstructor; const QRErrorCorrectLevel = QRErrorCorrectLevelModule as Record< string, unknown >; function createQrMatrix(input: string) { const qr = new QRCode(-1, QRErrorCorrectLevel.L); qr.addData(input); qr.make(); return qr; } function fillPixel( buf: Buffer, x: number, y: number, width: number, r: number, g: number, b: number, a = 255, ) { const idx = (y * width + x) * 4; buf[idx] = r; buf[idx + 1] = g; buf[idx + 2] = b; buf[idx + 3] = a; } function crcTable() { const table = new Uint32Array(256); for (let i = 0; i < 256; i += 1) { let c = i; for (let k = 0; k < 8; k += 1) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } table[i] = c >>> 0; } return table; } const CRC_TABLE = crcTable(); function crc32(buf: Buffer) { let crc = 0xffffffff; for (let i = 0; i < buf.length; i += 1) { crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); } return (crc ^ 0xffffffff) >>> 0; } function pngChunk(type: string, data: Buffer) { const typeBuf = Buffer.from(type, "ascii"); const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0); const crc = crc32(Buffer.concat([typeBuf, data])); const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0); return Buffer.concat([len, typeBuf, data, crcBuf]); } function encodePngRgba(buffer: Buffer, width: number, height: number) { const stride = width * 4; const raw = Buffer.alloc((stride + 1) * height); for (let row = 0; row < height; row += 1) { const rawOffset = row * (stride + 1); raw[rawOffset] = 0; // filter: none buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride); } const compressed = deflateSync(raw); const signature = Buffer.from([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, ]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // color type RGBA ihdr[10] = 0; // compression ihdr[11] = 0; // filter ihdr[12] = 0; // interlace return Buffer.concat([ signature, pngChunk("IHDR", ihdr), pngChunk("IDAT", compressed), pngChunk("IEND", Buffer.alloc(0)), ]); } export async function renderQrPngBase64( input: string, opts: { scale?: number; marginModules?: number } = {}, ): Promise { const { scale = 6, marginModules = 4 } = opts; const qr = createQrMatrix(input); const modules = qr.getModuleCount(); const size = (modules + marginModules * 2) * scale; const buf = Buffer.alloc(size * size * 4, 255); for (let row = 0; row < modules; row += 1) { for (let col = 0; col < modules; col += 1) { if (!qr.isDark(row, col)) continue; const startX = (col + marginModules) * scale; const startY = (row + marginModules) * scale; for (let y = 0; y < scale; y += 1) { const pixelY = startY + y; for (let x = 0; x < scale; x += 1) { const pixelX = startX + x; fillPixel(buf, pixelX, pixelY, size, 0, 0, 0, 255); } } } } const png = encodePngRgba(buf, size, size); return png.toString("base64"); }