Files
NASOpenClawRunTime/gen-qr-full.cjs
2026-04-25 01:38:03 +00:00

239 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Complete QR Code Generator - Version 1, Byte mode
// Based on ISO/IEC 18004
const fs = require('fs');
// GF setup
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]];
}
// Reed-Solomon generator polynomial for ECLevel 1 (L), 7 ECC for version 1
// g(x) = (x - α^0)(x - α^1)...(x - α^6) = x^7 + 87x^6 + 229x^5 + 146x^4 + 149x^3 + 238x^2 + 102x + 21
function rsGeneratorPolynomial(eccLen) {
let poly = [1];
for (let i = 0; i < eccLen; i++) {
poly = [...poly, 0];
for (let j = 0; j < poly.length; j++) {
poly[j] = gfMul(poly[j], GF_EXP[i]);
}
// Multiply by x+α^i
const newPoly = [1];
for (let j = 1; j < poly.length; j++) {
const a = poly[j - 1];
const b = poly[j];
newPoly[j] = b ^ (a !== 0 ? GF_EXP[(GF_LOG[a] + i) % 255] : 0);
}
poly = newPoly;
}
return poly.slice(1); // return coefficient array
}
function rsEncode(data, eccLen) {
const gen = rsGeneratorPolynomial(eccLen);
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);
}
// Mode: byte = 0100
const MODE_BYTE = 0b0100;
// Version 1: 21x21 modules, capacity: 19 bytes L, 16 bytes M, 13 bytes Q, 9 bytes H
// For "https://github.com/login/device" (29 chars) - need Version 2
// But let's try with a shorter URL that fits in Version 1
function encodeData(text) {
const capacityTable = {
1: { L: 19, M: 16, Q: 13, H: 9 },
2: { L: 34, M: 28, Q: 22, H: 16 },
};
// Find minimum version that fits
let version = 1;
while (version <= 2) {
const cap = capacityTable[version];
const byteCount = text.length + 3; // mode (1) + char count (1) + data + terminator
if (byteCount <= cap.L) break;
version++;
}
if (version > 2) version = 2;
// For Version 1-L, 19 data bytes
const dataBytes = [];
// Mode indicator (4 bits) + character count (9 bits for version 1-9)
const modeBits = (MODE_BYTE << 12) | (text.length << 4) | (0b0000);
dataBytes.push(modeBits >> 8);
dataBytes.push(modeBits & 0xff);
// Character data
for (let i = 0; i < text.length; i++) {
dataBytes.push(text.charCodeAt(i));
}
// Terminator (4 zeros)
dataBytes.push(0x00);
// Pad to fill capacity
while (dataBytes.length < 19) {
dataBytes.push(0xec);
if (dataBytes.length < 19) dataBytes.push(0x11);
}
const data = dataBytes.slice(0, 19);
const ecc = rsEncode(data, 7);
return { version, data: [...data, ...ecc], modules: version === 1 ? 21 : 25 };
}
function createQRMatrix(text) {
const result = encodeData(text);
const size = result.modules;
const matrix = Array.from({ length: size }, () => Array(size).fill(null));
const reserved = Array.from({ length: size }, () => Array(size).fill(false));
// Add finder pattern
function addFinder(mrow, mcol) {
for (let r = -3; r <= 3; r++) {
for (let c = -3; c <= 3; c++) {
const rr = mrow + r, cc = mcol + c;
if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue;
const isOuter = Math.abs(r) === 3 || Math.abs(c) === 3;
const isInner = Math.abs(r) <= 1 && Math.abs(c) <= 1;
matrix[rr][cc] = isOuter ? 1 : (isInner ? 0 : 1);
reserved[rr][cc] = true;
}
}
}
addFinder(3, 3);
addFinder(3, size - 4);
addFinder(size - 4, 3);
// Timing patterns
for (let i = 8; i < size - 8; i++) {
if (matrix[6][i] === null) { matrix[6][i] = i % 2 === 0 ? 1 : 0; reserved[6][i] = true; }
if (matrix[i][6] === null) { matrix[i][6] = i % 2 === 0 ? 1 : 0; reserved[i][6] = true; }
}
// Alignment pattern (Version 2 = center + one)
if (size === 25) {
const pos = [22];
for (const r of pos) {
for (const c of pos) {
if (matrix[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 (matrix[rr][cc] !== null) continue;
const isEdge = Math.abs(dr) === 2 || Math.abs(dc) === 2;
matrix[rr][cc] = isEdge ? 1 : 0;
reserved[rr][cc] = true;
}
}
}
}
}
// Format info (mask 0, L=01)
// Format string: 15 bits: 5 data + 10 ECC for L level
// Simplified: just use correct bits for format
const FORMAT_BITS = [
1,0,1,0,1,0,0,0,0,0,1,0,0,1,0 // mask 0, L
];
// Place format info
for (let i = 0; i < 15; i++) {
const bit = FORMAT_BITS[i];
// Above top finder
if (i < 6) { matrix[i][8] = bit; reserved[i][8] = true; }
else if (i < 8) { matrix[i + 1][8] = bit; reserved[i + 1][8] = true; }
else { matrix[8][14 - i] = bit; reserved[8][14 - i] = true; }
// Left of left finder
if (i < 8) { matrix[8][size - 1 - i] = bit; reserved[8][size - 1 - i] = true; }
}
matrix[8][7] = 1; reserved[8][7] = true; // dark module
// Fill data (boustrophedon)
const bits = [];
for (const b of result.data) {
for (let i = 7; i >= 0; i--) bits.push((b >> i) & 1);
}
let bitIdx = 0;
for (let col = size - 1; col >= 1; col--) {
if (col === 6) continue;
if (col % 2 === 0) {
for (let row = size - 1; row >= 0; row--) {
if (matrix[row][col] === null) {
matrix[row][col] = bits[bitIdx++];
if (bitIdx >= bits.length) break;
}
}
} else {
for (let row = 0; row < size; row++) {
if (matrix[row][col] === null) {
matrix[row][col] = bits[bitIdx++];
if (bitIdx >= bits.length) break;
}
}
}
if (bitIdx >= bits.length) break;
}
// Mask 0: (r+c) % 2 == 0
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
if (reserved[r][c] || matrix[r][c] === null) continue;
if ((r + c) % 2 === 0) matrix[r][c] ^= 1;
}
}
return { size, matrix, version: result.version };
}
function matrixToSVG(matrix, size) {
const MOD = 10;
const total = size * MOD;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${total}" height="${total}" viewBox="0 0 ${size} ${size}">`;
svg += `<rect width="${size}" height="${size}" fill="white"/>`;
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
if (matrix[r][c] === 1) {
svg += `<rect x="${c}" y="${r}" width="1" height="1" fill="black"/>`;
}
}
}
svg += `</svg>`;
return svg;
}
const text = process.argv[2] || 'TEST';
const { size, matrix } = createQRMatrix(text);
const svg = matrixToSVG(matrix, size);
const outFile = process.argv[3] || '/home/node/.openclaw/workspace/assets/qr-output.svg';
fs.writeFileSync(outFile, svg);
console.log(`QR for "${text}": ${size}x${size} Version ${text.length} chars, saved to ${outFile}`);