// 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);