219 lines
9.8 KiB
JavaScript
219 lines
9.8 KiB
JavaScript
|
|
// Minimal QR encoder for the receive tab: byte mode, versions 1-10, error
|
||
|
|
// correction M (falls back to L when M won't fit). Returns a boolean matrix.
|
||
|
|
// Loaded as a plain script in panel.html and as a CommonJS module in tests.
|
||
|
|
(function (root) {
|
||
|
|
const EC_LEVELS = { L: 1, M: 0 };
|
||
|
|
// Per version: [totalCodewords, {L:[ecPerBlock, [[blocks, dataCw], ...]], M:[...]}]
|
||
|
|
const TABLE = {
|
||
|
|
1: [26, { L: [7, [[1, 19]]], M: [10, [[1, 16]]] }],
|
||
|
|
2: [44, { L: [10, [[1, 34]]], M: [16, [[1, 28]]] }],
|
||
|
|
3: [70, { L: [15, [[1, 55]]], M: [26, [[1, 44]]] }],
|
||
|
|
4: [100, { L: [20, [[1, 80]]], M: [18, [[2, 32]]] }],
|
||
|
|
5: [134, { L: [26, [[1, 108]]], M: [24, [[2, 43]]] }],
|
||
|
|
6: [172, { L: [18, [[2, 68]]], M: [16, [[4, 27]]] }],
|
||
|
|
7: [196, { L: [20, [[2, 78]]], M: [18, [[4, 31]]] }],
|
||
|
|
8: [242, { L: [24, [[2, 97]]], M: [22, [[2, 38], [2, 39]]] }],
|
||
|
|
9: [292, { L: [30, [[2, 116]]], M: [22, [[3, 36], [2, 37]]] }],
|
||
|
|
10: [346, { L: [18, [[2, 68], [2, 69]]], M: [26, [[4, 43], [1, 44]]] }],
|
||
|
|
};
|
||
|
|
const ALIGN = { 1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30], 6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50] };
|
||
|
|
const REMAINDER = { 1: 0, 2: 7, 3: 7, 4: 7, 5: 7, 6: 7, 7: 0, 8: 0, 9: 0, 10: 0 };
|
||
|
|
|
||
|
|
// GF(256) with the QR polynomial 0x11d.
|
||
|
|
const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
|
||
|
|
(function () {
|
||
|
|
let x = 1;
|
||
|
|
for (let i = 0; i < 255; i++) { EXP[i] = x; LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x11d; }
|
||
|
|
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
|
||
|
|
})();
|
||
|
|
const gmul = (a, b) => (a && b) ? EXP[LOG[a] + LOG[b]] : 0;
|
||
|
|
function generator(n) {
|
||
|
|
let g = [1];
|
||
|
|
for (let i = 0; i < n; i++) {
|
||
|
|
const next = new Array(g.length + 1).fill(0);
|
||
|
|
for (let j = 0; j < g.length; j++) { next[j] ^= g[j]; next[j + 1] ^= gmul(g[j], EXP[i]); }
|
||
|
|
g = next;
|
||
|
|
}
|
||
|
|
return g;
|
||
|
|
}
|
||
|
|
function rsEncode(data, nEc) {
|
||
|
|
const g = generator(nEc);
|
||
|
|
const out = new Uint8Array(data.length + nEc);
|
||
|
|
out.set(data);
|
||
|
|
for (let i = 0; i < data.length; i++) {
|
||
|
|
const c = out[i];
|
||
|
|
if (c) for (let j = 1; j < g.length; j++) out[i + j] ^= gmul(g[j], c);
|
||
|
|
}
|
||
|
|
return out.slice(data.length);
|
||
|
|
}
|
||
|
|
|
||
|
|
function dataCapacity(v, ec) { return TABLE[v][1][ec][1].reduce((a, [b, d]) => a + b * d, 0); }
|
||
|
|
function pickVersion(len) {
|
||
|
|
for (const ec of ["M", "L"]) {
|
||
|
|
for (let v = 1; v <= 10; v++) {
|
||
|
|
const bitsNeeded = 4 + (v <= 9 ? 8 : 16) + len * 8;
|
||
|
|
if (bitsNeeded <= dataCapacity(v, ec) * 8) return { v, ec };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw new Error("qr: text too long");
|
||
|
|
}
|
||
|
|
|
||
|
|
function encodeData(bytes, v, ec) {
|
||
|
|
const cap = dataCapacity(v, ec);
|
||
|
|
const bits = [];
|
||
|
|
const push = (val, n) => { for (let i = n - 1; i >= 0; i--) bits.push((val >> i) & 1); };
|
||
|
|
push(0b0100, 4);
|
||
|
|
push(bytes.length, v <= 9 ? 8 : 16);
|
||
|
|
for (const b of bytes) push(b, 8);
|
||
|
|
for (let i = 0; i < 4 && bits.length < cap * 8; i++) bits.push(0);
|
||
|
|
while (bits.length % 8) bits.push(0);
|
||
|
|
const data = new Uint8Array(cap);
|
||
|
|
for (let i = 0; i < bits.length / 8; i++) data[i] = parseInt(bits.slice(i * 8, i * 8 + 8).join(""), 2);
|
||
|
|
for (let i = bits.length / 8, k = 0; i < cap; i++, k++) data[i] = k % 2 ? 0x11 : 0xec;
|
||
|
|
return data;
|
||
|
|
}
|
||
|
|
|
||
|
|
function interleave(data, v, ec) {
|
||
|
|
const [nEc, groups] = TABLE[v][1][ec];
|
||
|
|
const blocks = []; let o = 0;
|
||
|
|
for (const [count, size] of groups) for (let i = 0; i < count; i++) { blocks.push(data.slice(o, o + size)); o += size; }
|
||
|
|
const ecBlocks = blocks.map((b) => rsEncode(b, nEc));
|
||
|
|
const out = [];
|
||
|
|
const maxLen = Math.max(...blocks.map((b) => b.length));
|
||
|
|
for (let i = 0; i < maxLen; i++) for (const b of blocks) if (i < b.length) out.push(b[i]);
|
||
|
|
for (let i = 0; i < nEc; i++) for (const b of ecBlocks) out.push(b[i]);
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remainder of value·x^degree modulo the generator poly (BCH error correction
|
||
|
|
// for the format / version fields).
|
||
|
|
function bch(value, poly, degree) {
|
||
|
|
let v = value << degree;
|
||
|
|
for (let i = 31 - Math.clz32(v); i >= degree; i--) if ((v >> i) & 1) v ^= poly << (i - degree);
|
||
|
|
return v;
|
||
|
|
}
|
||
|
|
const formatBits = (ec, mask) => { const d = (EC_LEVELS[ec] << 3) | mask; return ((d << 10) | bch(d, 0x537, 10)) ^ 0x5412; };
|
||
|
|
const versionBits = (v) => (v << 12) | bch(v, 0x1f25, 12);
|
||
|
|
|
||
|
|
function build(text) {
|
||
|
|
const bytes = typeof text === "string" ? new TextEncoder().encode(text) : Uint8Array.from(text);
|
||
|
|
const { v, ec } = pickVersion(bytes.length);
|
||
|
|
const size = v * 4 + 17;
|
||
|
|
const codewords = interleave(encodeData(bytes, v, ec), v, ec);
|
||
|
|
const grid = Array.from({ length: size }, () => new Uint8Array(size)); // 1 dark, 0 light
|
||
|
|
const fixed = Array.from({ length: size }, () => new Uint8Array(size)); // function pattern / reserved
|
||
|
|
const set = (r, c, val) => { grid[r][c] = val ? 1 : 0; fixed[r][c] = 1; };
|
||
|
|
const finder = (r0, c0) => {
|
||
|
|
for (let r = -1; r <= 7; r++) for (let c = -1; c <= 7; c++) {
|
||
|
|
const rr = r0 + r, cc = c0 + c;
|
||
|
|
if (rr < 0 || cc < 0 || rr >= size || cc >= size) continue;
|
||
|
|
const inner = r >= 0 && r <= 6 && c >= 0 && c <= 6;
|
||
|
|
const dark = inner && (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4));
|
||
|
|
set(rr, cc, dark);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
finder(0, 0); finder(0, size - 7); finder(size - 7, 0);
|
||
|
|
for (let i = 8; i < size - 8; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
|
||
|
|
const al = ALIGN[v], last = al.length - 1;
|
||
|
|
al.forEach((r, ri) => al.forEach((c, ci) => {
|
||
|
|
// The three corners that would sit on a finder pattern are omitted.
|
||
|
|
if ((ri === 0 && ci === 0) || (ri === 0 && ci === last) || (ri === last && ci === 0)) return;
|
||
|
|
for (let dr = -2; dr <= 2; dr++) for (let dc = -2; dc <= 2; dc++) {
|
||
|
|
set(r + dr, c + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1);
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
set(size - 8, 8, 1); // dark module
|
||
|
|
// Reserve format areas (filled per mask below) and version areas.
|
||
|
|
for (let i = 0; i < 9; i++) { fixed[8][i] = 1; fixed[i][8] = 1; }
|
||
|
|
for (let i = 0; i < 8; i++) { fixed[8][size - 1 - i] = 1; fixed[size - 1 - i][8] = 1; }
|
||
|
|
if (v >= 7) {
|
||
|
|
const vb = versionBits(v);
|
||
|
|
for (let i = 0; i < 18; i++) {
|
||
|
|
const bit = (vb >> i) & 1;
|
||
|
|
set(Math.floor(i / 3), size - 11 + (i % 3), bit);
|
||
|
|
set(size - 11 + (i % 3), Math.floor(i / 3), bit);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Zigzag data placement.
|
||
|
|
const bits = [];
|
||
|
|
for (const cw of codewords) for (let i = 7; i >= 0; i--) bits.push((cw >> i) & 1);
|
||
|
|
for (let i = 0; i < REMAINDER[v]; i++) bits.push(0);
|
||
|
|
let bi = 0, up = true;
|
||
|
|
for (let col = size - 1; col > 0; col -= 2) {
|
||
|
|
if (col === 6) col--;
|
||
|
|
for (let k = 0; k < size; k++) {
|
||
|
|
const r = up ? size - 1 - k : k;
|
||
|
|
for (const c of [col, col - 1]) {
|
||
|
|
if (fixed[r][c]) continue;
|
||
|
|
grid[r][c] = bi < bits.length ? bits[bi] : 0;
|
||
|
|
bi++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
up = !up;
|
||
|
|
}
|
||
|
|
// Try every mask, keep the lowest penalty.
|
||
|
|
const MASKS = [
|
||
|
|
(i, j) => (i + j) % 2 === 0, (i) => i % 2 === 0, (_i, j) => j % 3 === 0, (i, j) => (i + j) % 3 === 0,
|
||
|
|
(i, j) => (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0, (i, j) => ((i * j) % 2) + ((i * j) % 3) === 0,
|
||
|
|
(i, j) => (((i * j) % 2) + ((i * j) % 3)) % 2 === 0, (i, j) => (((i + j) % 2) + ((i * j) % 3)) % 2 === 0,
|
||
|
|
];
|
||
|
|
let best = null;
|
||
|
|
for (let m = 0; m < 8; m++) {
|
||
|
|
const g = grid.map((row) => Uint8Array.from(row));
|
||
|
|
for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) if (!fixed[r][c] && MASKS[m](r, c)) g[r][c] ^= 1;
|
||
|
|
const fb = formatBits(ec, m);
|
||
|
|
for (let i = 0; i < 15; i++) {
|
||
|
|
const bit = (fb >> i) & 1;
|
||
|
|
// Copy 1: down column 8 (bits 0-7, skipping the timing row), then
|
||
|
|
// left along row 8 (bits 8-14). Copy 2: right end of row 8, bottom
|
||
|
|
// of column 8.
|
||
|
|
if (i < 6) g[i][8] = bit; else if (i === 6) g[7][8] = bit; else if (i === 7) g[8][8] = bit;
|
||
|
|
else if (i === 8) g[8][7] = bit; else g[8][14 - i] = bit;
|
||
|
|
if (i < 8) g[8][size - 1 - i] = bit; else g[size - 15 + i][8] = bit;
|
||
|
|
}
|
||
|
|
const p = penalty(g, size);
|
||
|
|
if (!best || p < best.p) best = { g, p, m };
|
||
|
|
}
|
||
|
|
return { size, version: v, ec, mask: best.m, modules: best.g.map((row) => Array.from(row, (x) => !!x)) };
|
||
|
|
}
|
||
|
|
|
||
|
|
function penalty(g, n) {
|
||
|
|
let p = 0;
|
||
|
|
const runs = (get) => {
|
||
|
|
for (let a = 0; a < n; a++) {
|
||
|
|
let run = 1;
|
||
|
|
for (let b = 1; b <= n; b++) {
|
||
|
|
if (b < n && get(a, b) === get(a, b - 1)) run++;
|
||
|
|
else { if (run >= 5) p += 3 + run - 5; run = 1; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
runs((a, b) => g[a][b]); runs((a, b) => g[b][a]);
|
||
|
|
for (let r = 0; r < n - 1; r++) for (let c = 0; c < n - 1; c++) {
|
||
|
|
const s = g[r][c] + g[r][c + 1] + g[r + 1][c] + g[r + 1][c + 1];
|
||
|
|
if (s === 0 || s === 4) p += 3;
|
||
|
|
}
|
||
|
|
const pat = [1, 0, 1, 1, 1, 0, 1];
|
||
|
|
const finderLike = (get, a) => {
|
||
|
|
for (let b = 0; b <= n - 7; b++) {
|
||
|
|
let ok = true;
|
||
|
|
for (let k = 0; k < 7; k++) if (get(a, b + k) !== pat[k]) { ok = false; break; }
|
||
|
|
if (!ok) continue;
|
||
|
|
const before = b >= 4 && [0, 1, 2, 3].every((k) => get(a, b - 1 - k) === 0);
|
||
|
|
const after = b + 10 < n && [0, 1, 2, 3].every((k) => get(a, b + 7 + k) === 0);
|
||
|
|
if (before || after) p += 40;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
for (let a = 0; a < n; a++) { finderLike((x, y) => g[x][y], a); finderLike((x, y) => g[y][x], a); }
|
||
|
|
let dark = 0;
|
||
|
|
for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) dark += g[r][c];
|
||
|
|
const pct = (dark * 100) / (n * n);
|
||
|
|
p += Math.floor(Math.abs(pct - 50) / 5) * 10;
|
||
|
|
return p;
|
||
|
|
}
|
||
|
|
|
||
|
|
const api = { build };
|
||
|
|
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||
|
|
else root.QR = api;
|
||
|
|
})(typeof globalThis !== "undefined" ? globalThis : this);
|