239 lines
7.1 KiB
JavaScript
239 lines
7.1 KiB
JavaScript
// 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}`);
|