// Complete QR Code Generator - ISO/IEC 18004 compliant // Outputs a proper PNG file const fs = require('fs'); const zlib = require('zlib'); // ===== Reed-Solomon GF arithmetic ===== const GF_EXP = new Uint8Array(512); const GF_LOG = new Uint8Array(256); { let x = 1; for (let i = 0; i < 255; i++) { GF_EXP[i] = x; GF_LOG[x] = i; x = (x << 1) ^ (x & 0x100 ? 0x11d : 0); } for (let i = 255; i < 512; i++) GF_EXP[i] = GF_EXP[i - 255]; } function gfMul(a, b) { if (a === 0 || b === 0) return 0; return GF_EXP[GF_LOG[a] + GF_LOG[b]]; } // ===== RS encoder ===== function rsEncode(data, eccLen) { // Generator polynomial g(x) = \prod_{i=0}^{eccLen-1} (x - \alpha^i) let gen = [1]; for (let i = 0; i < eccLen; i++) { gen = [...gen, 0]; // multiply by x for (let j = 0; j < gen.length; j++) { gen[j] = gfMul(gen[j], GF_EXP[i]); } // add \alpha^i for (let j = gen.length - 1; j > 0; j--) { gen[j] = gen[j] ^ (gen[j-1] !== 0 ? GF_EXP[(GF_LOG[gen[j-1]] + i) % 255] : 0); } gen[0] = gfMul(gen[0], GF_EXP[i]); } gen = gen.slice(1); // drop leading 1 const result = new Array(data.length + eccLen).fill(0); for (let i = 0; i < data.length; i++) result[i] = data[i]; for (let i = 0; i < data.length; i++) { const coef = result[i]; if (coef !== 0) { for (let j = 0; j < eccLen; j++) { result[data.length + j] ^= gfMul(gen[j], coef); } } } return result.slice(data.length); } // ===== PNG writer ===== function crc32(buf) { let crc = 0xFFFFFFFF; const table = new Uint32Array(256); for (let i = 0; i < 256; i++) { let c = i; for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; table[i] = c; } for (let i = 0; i < buf.length; i++) crc = table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8); return (crc ^ 0xFFFFFFFF) >>> 0; } function makeChunk(type, data) { const typeB = Buffer.from(type); const lenB = Buffer.alloc(4); lenB.writeUInt32BE(data.length); const crcB = Buffer.alloc(4); crcB.writeUInt32BE(crc32(Buffer.concat([typeB, data]))); return Buffer.concat([lenB, typeB, data, crcB]); } function writePNG(matrix, scale) { const size = matrix.length; const outSize = size * scale; // Raw image data: filter byte (0 = none) + RGB scanlines const raw = Buffer.alloc((1 + outSize * 3) * outSize); for (let y = 0; y < outSize; y++) { const rowOffset = y * (1 + outSize * 3); raw[rowOffset] = 0; const my = Math.min(Math.floor(y / scale), size - 1); for (let x = 0; x < outSize; x++) { const mx = Math.min(Math.floor(x / scale), size - 1); const pixel = matrix[my][mx] ? 0 : 255; const o = rowOffset + 1 + x * 3; raw[o] = pixel; raw[o+1] = pixel; raw[o+2] = pixel; } } const sig = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(outSize, 0); ihdr.writeUInt32BE(outSize, 4); ihdr[8] = 8; ihdr[9] = 2; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; const compressed = zlib.deflateSync(raw, { level: 9 }); return Buffer.concat([sig, makeChunk('IHDR', ihdr), makeChunk('IDAT', compressed), makeChunk('IEND', Buffer.alloc(0))]); } // ===== QR Code encoding ===== const MODE_BYTE = 0b0100; function encodeQR(text) { // capacity table: version -> {L: dataBytes} const CAP = { 1: 17, // 19 - 2 pad bytes 2: 32, // 34 - 2 pad bytes 3: 46, // 49 - 2 pad bytes 4: 60, // 64 - 4 5: 74, 6: 88, }; let version = 1; while (version < 6 && CAP[version] < text.length) version++; const eccLen = { 1: 7, 2: 10, 3: 15, 4: 16, 5: 18, 6: 20 }[version]; const capacityBytes = { 1: 19, 2: 34, 3: 50, 4: 67, 5: 80, 6: 97 }[version]; const size = 17 + version * 4; // Encode header const headerBytes = []; const modeBits = (MODE_BYTE << 12) | (text.length << 4) | 0b0000; headerBytes.push((modeBits >> 8) & 0xFF); headerBytes.push(modeBits & 0xFF); // Data bytes for (let i = 0; i < text.length; i++) headerBytes.push(text.charCodeAt(i) & 0xFF); headerBytes.push(0x00); // terminator // Pad to capacity while (headerBytes.length < capacityBytes - 1) { headerBytes.push(0xEC, 0x11); } while (headerBytes.length < capacityBytes) headerBytes.push(0xEC); const data = headerBytes.slice(0, capacityBytes - eccLen); const ecc = rsEncode(data, eccLen); const allData = [...data, ...ecc]; return { size, allData, version }; } function buildMatrix(text) { const { size, allData, version } = encodeQR(text); const M = Array.from({ length: size }, () => Array(size).fill(null)); const R = Array.from({ length: size }, () => Array(size).fill(false)); // Finder patterns function finder(r, c) { for (let dr = -3; dr <= 3; dr++) for (let dc = -3; dc <= 3; dc++) { const rr = r + dr, cc = c + dc; if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue; const isEdge = Math.abs(dr) === 3 || Math.abs(dc) === 3; const isInner = Math.abs(dr) <= 1 && Math.abs(c) <= 1; M[rr][cc] = isEdge ? 1 : (isInner ? 0 : 1); R[rr][cc] = true; } } finder(3, 3); finder(3, size - 4); finder(size - 4, 3); // Timing for (let i = 8; i < size - 8; i++) { if (M[6][i] === null) { M[6][i] = i % 2 === 0 ? 1 : 0; R[6][i] = true; } if (M[i][6] === null) { M[i][6] = i % 2 === 0 ? 1 : 0; R[i][6] = true; } } // Alignment patterns for version >= 2 if (version >= 2) { const positions = { 2: [18], 3: [22, 8], 4: [26, 16, 6], 5: [30, 20, 10], 6: [34, 24, 14, 4] }[version]; for (const r of positions) for (const c of positions) { if (M[r][c] !== null) continue; for (let dr = -2; dr <= 2; dr++) for (let dc = -2; dc <= 2; dc++) { const rr = r + dr, cc = c + dc; if (M[rr][cc] !== null) continue; const isEdge = Math.abs(dr) === 2 || Math.abs(dc) === 2; M[rr][cc] = isEdge ? 1 : 0; R[rr][cc] = true; } } } // Format info (mask 0, L=01) const FMT = [1,0,1,0,1,0,0,0,0,0,1,0,0,1,0]; for (let i = 0; i < 15; i++) { const bit = FMT[i]; if (i < 6) { M[i][8] = bit; R[i][8] = true; } else if (i < 8) { M[i+1][8] = bit; R[i+1][8] = true; } else { M[8][14-i] = bit; R[8][14-i] = true; } M[8][size-1-i] = bit; R[8][size-1-i] = true; } M[8][7] = 1; R[8][7] = true; // Version info for version >= 7 // (skipped - we're using version <= 6) // Data bits (boustrophedon fill) const bits = []; for (const b of allData) for (let i = 7; i >= 0; i--) bits.push((b >> i) & 1); let b = 0; for (let col = size - 1; col >= 1; col--) { if (col === 6) continue; const downwards = col % 2 === 0; for (let row = downwards ? size - 1 : 0; downwards ? row >= 0 : row < size; downwards ? row-- : row++) { if (M[row][col] === null) M[row][col] = bits[b++]; if (b >= bits.length) break; } if (b >= bits.length) break; } // Mask 0: (r+c) % 2 == 0 for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) { if (R[r][c] || M[r][c] === null) continue; if ((r + c) % 2 === 0) M[r][c] ^= 1; } return M; } // ===== Main ===== const text = process.argv[2] || 'https://github.com/login/device'; const matrix = buildMatrix(text); const png = writePNG(matrix, 10); const outFile = process.argv[3] || '/home/node/.openclaw/workspace/assets/qrcode-out.png'; fs.writeFileSync(outFile, png); console.log(`QR for "${text}": ${matrix.length}x${matrix.length} v${text.length}ch, ${png.length} bytes -> ${outFile}`);