chore: add topic candidates for 2026-04-25

This commit is contained in:
小橙
2026-04-25 01:38:03 +00:00
parent fbe1d70c32
commit c2981264c6
72 changed files with 2143 additions and 21 deletions

230
gen-qr-png.cjs Normal file
View 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}`);