79d4a0f011
- Seven new cards: parcels, air quality, water and gas usage, solar forecast, garage and gate, sports, Formula One - Kiosk: announcements from automations, tab rotation, guest mode, click sound - Editor: templates, widget presets, undo history, alignment guides, Lovelace import, share link and QR code, theme variants, suggestions, touch and hold actions - Phones: installable app with shortcuts, offline states, tab swipe, pull to refresh, bottom sheets, haptics, phone/large visibility, landscape layout, editing on the phone, away tab - Fixed: the weather forecast and to-do list could stay on Loading… Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
8.3 KiB
JavaScript
175 lines
8.3 KiB
JavaScript
/* 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 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n} ${n}" shape-rendering="crispEdges" role="img" aria-label="${label}"><rect width="${n}" height="${n}" fill="#fff"/><path d="${d}" fill="#000"/></svg>`;
|
|
}
|
|
|
|
const api = { matrix, svg };
|
|
if (typeof window !== "undefined") window.QR = api;
|
|
if (typeof module !== "undefined") module.exports = api;
|
|
})();
|