/* A small QR code encoder for share links: byte mode, error correction level M. * Follows the structure of Project Nayuki's QR Code generator (MIT licence). * QR.svg(text) returns an SVG string; QR.matrix(text, mask) the modules (true = dark). */ (function () { // Per version (index 1 to 40), for level M: error correction codewords per block, and the number of blocks. const ECC = [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28]; const BLOCKS = [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49]; const rawModules = (v) => { let r = (16 * v + 128) * v + 64; if (v >= 2) { const n = Math.floor(v / 7) + 2; r -= (25 * n - 10) * n - 55; if (v >= 7) r -= 36; } return r; }; const dataCodewords = (v) => Math.floor(rawModules(v) / 8) - ECC[v] * BLOCKS[v]; const bit = (x, i) => ((x >>> i) & 1) !== 0; // Reed-Solomon over GF(2^8) with the polynomial 0x11D. function gfMul(x, y) { let z = 0; for (let i = 7; i >= 0; i--) { z = (z << 1) ^ ((z >>> 7) * 0x11d); z ^= ((y >>> i) & 1) * x; } return z; } function rsDivisor(degree) { const r = new Array(degree).fill(0); r[degree - 1] = 1; let root = 1; for (let i = 0; i < degree; i++) { for (let j = 0; j < r.length; j++) { r[j] = gfMul(r[j], root); if (j + 1 < r.length) r[j] ^= r[j + 1]; } root = gfMul(root, 0x02); } return r; } function rsRemainder(data, divisor) { const r = divisor.map(() => 0); for (const b of data) { const f = b ^ r.shift(); r.push(0); divisor.forEach((c, i) => (r[i] ^= gfMul(c, f))); } return r; } function encodeData(bytes) { let ver = 1; for (; ver <= 40; ver++) if (4 + (ver < 10 ? 8 : 16) + bytes.length * 8 <= dataCodewords(ver) * 8) break; if (ver > 40) throw new Error("Text too long for a QR code"); const bits = []; const put = (val, len) => { for (let i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1); }; put(4, 4); // byte mode put(bytes.length, ver < 10 ? 8 : 16); for (const b of bytes) put(b, 8); const cap = dataCodewords(ver) * 8; put(0, Math.min(4, cap - bits.length)); put(0, (8 - (bits.length % 8)) % 8); for (let pad = 0xec; bits.length < cap; pad ^= 0xec ^ 0x11) put(pad, 8); const data = []; for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((a, b2) => (a << 1) | b2, 0)); // split into blocks, add error correction, interleave const nBlocks = BLOCKS[ver], eccLen = ECC[ver], raw = Math.floor(rawModules(ver) / 8); const nShort = nBlocks - (raw % nBlocks), shortLen = Math.floor(raw / nBlocks), div = rsDivisor(eccLen); const blocks = []; for (let i = 0, k = 0; i < nBlocks; i++) { const dat = data.slice(k, k + shortLen - eccLen + (i < nShort ? 0 : 1)); k += dat.length; const ecc = rsRemainder(dat, div); if (i < nShort) dat.push(0); blocks.push(dat.concat(ecc)); } const out = []; for (let i = 0; i < blocks[0].length; i++) blocks.forEach((b, j) => { if (i !== shortLen - eccLen || j >= nShort) out.push(b[i]); }); return { ver, codewords: out }; } function build(ver, codewords, mask) { const size = ver * 4 + 17; const mod = Array.from({ length: size }, () => new Array(size).fill(false)); const fn = Array.from({ length: size }, () => new Array(size).fill(false)); const set = (x, y, dark) => { mod[y][x] = dark; fn[y][x] = true; }; for (let i = 0; i < size; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); } const finder = (x, y) => { for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) { const d = Math.max(Math.abs(dx), Math.abs(dy)), xx = x + dx, yy = y + dy; if (xx >= 0 && xx < size && yy >= 0 && yy < size) set(xx, yy, d !== 2 && d !== 4); } }; finder(3, 3); finder(size - 4, 3); finder(3, size - 4); const align = []; if (ver > 1) { const n = Math.floor(ver / 7) + 2, step = ver === 32 ? 26 : Math.ceil((ver * 4 + 4) / (n * 2 - 2)) * 2; align.push(6); for (let pos = size - 7; align.length < n; pos -= step) align.splice(1, 0, pos); } align.forEach((ax, i) => align.forEach((ay, j) => { if ((i === 0 && j === 0) || (i === 0 && j === align.length - 1) || (i === align.length - 1 && j === 0)) return; for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1); })); const format = (m) => { const d = (0 << 3) | m; // level M has format bits 00 let rem = d; for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537); const b = ((d << 10) | rem) ^ 0x5412; for (let i = 0; i <= 5; i++) set(8, i, bit(b, i)); set(8, 7, bit(b, 6)); set(8, 8, bit(b, 7)); set(7, 8, bit(b, 8)); for (let i = 9; i < 15; i++) set(14 - i, 8, bit(b, i)); for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(b, i)); for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(b, i)); set(8, size - 8, true); }; format(0); if (ver >= 7) { let rem = ver; for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25); const b = (ver << 12) | rem; for (let i = 0; i < 18; i++) { const a = size - 11 + (i % 3), c = Math.floor(i / 3); set(a, c, bit(b, i)); set(c, a, bit(b, i)); } } // the data, in the zigzag order let i = 0; for (let right = size - 1; right >= 1; right -= 2) { if (right === 6) right = 5; for (let vert = 0; vert < size; vert++) for (let j = 0; j < 2; j++) { const x = right - j, up = ((right + 1) & 2) === 0, y = up ? size - 1 - vert : vert; if (!fn[y][x] && i < codewords.length * 8) { mod[y][x] = bit(codewords[i >>> 3], 7 - (i & 7)); i++; } } } const masks = [(x, y) => (x + y) % 2 === 0, (x, y) => y % 2 === 0, (x) => x % 3 === 0, (x, y) => (x + y) % 3 === 0, (x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0, (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0, (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0, (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0]; for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (!fn[y][x] && masks[mask](x, y)) mod[y][x] = !mod[y][x]; format(mask); return mod; } /** How hard a pattern is to scan (lower is better): runs, blocks, finder look-alikes and balance. */ function penalty(m) { const n = m.length; let p = 0, dark = 0; const lines = [...m, ...m[0].map((_, x) => m.map((row) => row[x]))]; for (const line of lines) { let run = 1; for (let i = 1; i <= n; i++) { if (i < n && line[i] === line[i - 1]) run++; else { if (run >= 5) p += run - 2; run = 1; } } const s = line.map((v) => (v ? 1 : 0)).join(""); for (const pat of ["10111010000", "00001011101"]) for (let k = s.indexOf(pat); k !== -1; k = s.indexOf(pat, k + 1)) p += 40; } for (let y = 0; y < n - 1; y++) for (let x = 0; x < n - 1; x++) { const c = m[y][x]; if (c === m[y][x + 1] && c === m[y + 1][x] && c === m[y + 1][x + 1]) p += 3; } for (const row of m) for (const v of row) if (v) dark++; p += Math.floor(Math.abs(dark * 20 - n * n * 10) / (n * n)) * 10; return p; } function matrix(text, mask) { const { ver, codewords } = encodeData([...new TextEncoder().encode(text)]); if (mask != null) return build(ver, codewords, mask); let best = null, bestScore = Infinity; for (let m = 0; m < 8; m++) { const mm = build(ver, codewords, m), sc = penalty(mm); if (sc < bestScore) { best = mm; bestScore = sc; } } return best; } function svg(text, { border = 3, label = "QR code" } = {}) { const m = matrix(text), n = m.length + border * 2; let d = ""; m.forEach((row, y) => row.forEach((v, x) => { if (v) d += `M${x + border} ${y + border}h1v1h-1z`; })); return ``; } const api = { matrix, svg }; if (typeof window !== "undefined") window.QR = api; if (typeof module !== "undefined") module.exports = api; })();