Setup 5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643 Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4 Bundled since 0.3.27: 32d4b75 - Aegis (bchwallet) gains its own update card in Settings > General beside Ariadne. Check for updates hits the same signed OTA endpoint the boot timer uses; Restart to apply appears when a signed newer version is staged. Uses the existing addons-check-updates + a new app-restart IPC. New Aegis versions ship without a Theseus release. 32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to the right of the tab (mode: 'right') instead of a detached window. Matches stock Chrome. Users who prefer detached can drag out via the DevTools own toolbar. b71c925 - Search-engine favicons in Settings > Search now use Google's /s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough hosts (Brave, Bing, Yandex, etc.) that half the list was falling through to the emoji placeholder. Deployed. Verified LIVE 0.3.28.
218 lines
9.8 KiB
JavaScript
218 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);
|