// Tiny self-contained QR encoder for the short-lived refund upload URL. // It intentionally supports QR version 4-L (up to 78 UTF-8 bytes), which is // enough for the local upload route and keeps the admin bundle dependency-free. const EXP = new Array(512); const LOG = new Array(256); let value = 1; for (let i = 0; i < 255; i += 1) { EXP[i] = value; LOG[value] = i; value <<= 1; if (value & 0x100) value ^= 0x11d; } for (let i = 255; i < 512; i += 1) EXP[i] = EXP[i - 255]; function multiply(a, b) { return a && b ? EXP[LOG[a] + LOG[b]] : 0; } function reedSolomon(data, count) { let generator = [1]; for (let i = 0; i < count; i += 1) { const next = new Array(generator.length + 1).fill(0); generator.forEach((coefficient, index) => { next[index] ^= coefficient; next[index + 1] ^= multiply(coefficient, EXP[i]); }); generator = next; } const result = new Array(count).fill(0); data.forEach((byte) => { const factor = byte ^ result[0]; result.shift(); result.push(0); for (let i = 0; i < count; i += 1) result[i] ^= multiply(generator[i + 1], factor); }); return result; } function bchTypeInfo(data) { let value = data << 10; while (value && value.toString(2).length >= 11) { value ^= 0x537 << (value.toString(2).length - 11); } return ((data << 10) | value) ^ 0x5412; } function setFinder(matrix, row, col) { for (let r = -1; r <= 7; r += 1) { for (let c = -1; c <= 7; c += 1) { if (row + r < 0 || row + r >= matrix.length || col + c < 0 || col + c >= matrix.length) continue; matrix[row + r][col + c] = r >= 0 && r <= 6 && c >= 0 && c <= 6 && (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4)); } } } function makeMatrix(text) { const bytes = Array.from(new TextEncoder().encode(text)); if (bytes.length > 78) throw new Error("二维码内容过长"); const bits = []; const push = (value, length) => { for (let i = length - 1; i >= 0; i -= 1) bits.push((value >>> i) & 1); }; push(0x4, 4); push(bytes.length, 8); bytes.forEach((byte) => push(byte, 8)); for (let i = 0; i < 4 && bits.length < 640; i += 1) bits.push(0); while (bits.length % 8) bits.push(0); const data = []; for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((sum, bit) => (sum << 1) | bit, 0)); // Version 4-L has 100 total codewords: one block of 80 data bytes and // 20 Reed-Solomon correction bytes. while (data.length < 80) data.push(data.length % 2 ? 0x11 : 0xec); const codewords = data.concat(reedSolomon(data, 20)); const size = 33; const matrix = Array.from({ length: size }, () => Array(size).fill(null)); setFinder(matrix, 0, 0); setFinder(matrix, size - 7, 0); setFinder(matrix, 0, size - 7); for (let i = 8; i < size - 8; i += 1) { if (matrix[i][6] === null) matrix[i][6] = i % 2 === 0; if (matrix[6][i] === null) matrix[6][i] = i % 2 === 0; } // Version 4 has one alignment pattern at (26, 26). for (let r = -2; r <= 2; r += 1) for (let c = -2; c <= 2; c += 1) { if (matrix[26 + r][26 + c] === null) matrix[26 + r][26 + c] = Math.max(Math.abs(r), Math.abs(c)) !== 1; } let bitIndex = 0; const writeFormat = (format) => { for (let i = 0; i < 15; i += 1) { const dark = ((format >>> i) & 1) === 1; if (i < 6) matrix[i][8] = dark; else if (i < 8) matrix[i + 1][8] = dark; else matrix[size - 15 + i][8] = dark; if (i < 8) matrix[8][size - i - 1] = dark; else if (i < 9) matrix[8][15 - i] = dark; else matrix[8][15 - i - 1] = dark; } matrix[size - 8][8] = true; }; // Reserve these cells before data placement. Otherwise the data walk // would overwrite them and the resulting code could not be scanned. writeFormat(0); let upward = true; for (let right = size - 1; right >= 1; right -= 2) { if (right === 6) right -= 1; for (let offset = 0; offset < size; offset += 1) { const row = upward ? size - 1 - offset : offset; for (const col of [right, right - 1]) { if (matrix[row][col] !== null) continue; const byte = codewords[Math.floor(bitIndex / 8)] || 0; const raw = ((byte >>> (7 - (bitIndex % 8))) & 1) === 1; matrix[row][col] = raw !== ((row + col) % 2 === 0); // mask 0 bitIndex += 1; } } upward = !upward; } writeFormat(bchTypeInfo(8)); // error correction L, mask 0 return matrix; } export function qrDataUrl(text, moduleSize = 5) { const matrix = makeMatrix(text); const quiet = 4; const dimension = (matrix.length + quiet * 2) * moduleSize; const paths = []; matrix.forEach((row, r) => row.forEach((dark, c) => { if (dark) paths.push(`M${(c + quiet) * moduleSize} ${(r + quiet) * moduleSize}h${moduleSize}v${moduleSize}h-${moduleSize}z`); })); const svg = ``; return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`; }