chore: add topic candidates for 2026-04-25
This commit is contained in:
230
gen-qr-png.cjs
Normal file
230
gen-qr-png.cjs
Normal file
@@ -0,0 +1,230 @@
|
||||
// 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}`);
|
||||
Reference in New Issue
Block a user