// QR codes made on this server (ISO/IEC 18004, byte mode, error correction M), so the authenticator
// secret in an otpauth:// address never goes to another service to be drawn (ACC-7). Pure: the
// same text always gives the same SVG. Follows Project Nayuki's reference QR generator (MIT).
const ECC_PER_BLOCK_M = [-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_M = [-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];
/** The format bits name the level: M is 0. */
const LEVEL_M = 0;
export type QrModules = boolean[][];
function rawDataModules(version: number): number {
let result = (16 * version + 128) * version + 64;
if (version >= 2) {
const align = Math.floor(version / 7) + 2;
result -= (25 * align - 10) * align - 55;
if (version >= 7) result -= 36;
}
return result;
}
const dataCodewords = (version: number) => Math.floor(rawDataModules(version) / 8) - ECC_PER_BLOCK_M[version] * BLOCKS_M[version];
// Reed-Solomon over GF(2^8) with the polynomial 0x11D.
function gfMultiply(x: number, y: number): number {
let z = 0;
for (let i = 7; i >= 0; i--) {
z = (z << 1) ^ ((z >>> 7) * 0x11d);
z ^= ((y >>> i) & 1) * x;
}
return z & 0xff;
}
function rsDivisor(degree: number): number[] {
const result = new Array<number>(degree).fill(0);
result[degree - 1] = 1;
let root = 1;
for (let i = 0; i < degree; i++) {
for (let j = 0; j < degree; j++) {
result[j] = gfMultiply(result[j], root);
if (j + 1 < degree) result[j] ^= result[j + 1];
}
root = gfMultiply(root, 0x02);
}
return result;
}
function rsRemainder(data: number[], divisor: number[]): number[] {
const result = new Array<number>(divisor.length).fill(0);
for (const byte of data) {
const factor = byte ^ result.shift()!;
result.push(0);
divisor.forEach((coefficient, i) => { result[i] ^= gfMultiply(coefficient, factor); });
}
return result;
}
function codewordsFor(bytes: Uint8Array): { version: number; codewords: number[] } {
let version = 1;
const bitsFor = (v: number) => 4 + (v <= 9 ? 8 : 16) + bytes.length * 8;
while (version <= 40 && bitsFor(version) > dataCodewords(version) * 8) version++;
if (version > 40) throw new Error('Too long for a QR code');
const bits: number[] = [];
const push = (value: number, length: number) => { for (let i = length - 1; i >= 0; i--) bits.push((value >>> i) & 1); };
push(0b0100, 4);
push(bytes.length, version <= 9 ? 8 : 16);
bytes.forEach((byte) => push(byte, 8));
const capacity = dataCodewords(version) * 8;
push(0, Math.min(4, capacity - bits.length));
push(0, (8 - (bits.length % 8)) % 8);
for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11) push(pad, 8);
const data: number[] = [];
for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((byte, bit) => (byte << 1) | bit, 0));
// Split into blocks, add each block's error correction, then interleave.
const blocks = BLOCKS_M[version];
const eccLength = ECC_PER_BLOCK_M[version];
const raw = Math.floor(rawDataModules(version) / 8);
const shortBlocks = blocks - (raw % blocks);
const shortLength = Math.floor(raw / blocks);
const divisor = rsDivisor(eccLength);
const parts: number[][] = [];
for (let i = 0, k = 0; i < blocks; i++) {
const block = data.slice(k, k + shortLength - eccLength + (i < shortBlocks ? 0 : 1));
k += block.length;
const ecc = rsRemainder(block, divisor);
if (i < shortBlocks) block.push(0);
parts.push([...block, ...ecc]);
}
const codewords: number[] = [];
for (let i = 0; i < parts[0].length; i++) {
parts.forEach((part, j) => { if (i !== shortLength - eccLength || j >= shortBlocks) codewords.push(part[i]); });
}
return { version, codewords };
}
function alignmentPositions(version: number): number[] {
if (version === 1) return [];
const count = Math.floor(version / 7) + 2;
const step = version === 32 ? 26 : Math.ceil((version * 4 + 4) / (count * 2 - 2)) * 2;
const result = [6];
for (let pos = version * 4 + 17 - 7; result.length < count; pos -= step) result.splice(1, 0, pos);
return result;
}
const MASKS: Array<(x: number, y: number) => boolean> = [
(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,
];
class Grid {
readonly size: number;
readonly modules: QrModules;
readonly fixed: boolean[][];
constructor(readonly version: number) {
this.size = version * 4 + 17;
this.modules = Array.from({ length: this.size }, () => new Array<boolean>(this.size).fill(false));
this.fixed = Array.from({ length: this.size }, () => new Array<boolean>(this.size).fill(false));
}
set(x: number, y: number, dark: boolean) { this.modules[y][x] = dark; this.fixed[y][x] = true; }
drawFunctionPatterns() {
for (let i = 0; i < this.size; i++) { this.set(6, i, i % 2 === 0); this.set(i, 6, i % 2 === 0); }
for (const [cx, cy] of [[3, 3], [this.size - 4, 3], [3, this.size - 4]]) {
for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) {
const x = cx + dx; const y = cy + dy;
const distance = Math.max(Math.abs(dx), Math.abs(dy));
if (x >= 0 && x < this.size && y >= 0 && y < this.size) this.set(x, y, distance !== 2 && distance !== 4);
}
}
const positions = alignmentPositions(this.version);
const last = positions.length - 1;
positions.forEach((px, i) => positions.forEach((py, j) => {
if ((i === 0 && j === 0) || (i === 0 && j === last) || (i === last && j === 0)) return;
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) this.set(px + dx, py + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
}));
this.drawFormat(0);
if (this.version >= 7) {
let rem = this.version;
for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
const bits = (this.version << 12) | rem;
for (let i = 0; i < 18; i++) {
const dark = ((bits >>> i) & 1) !== 0;
const a = this.size - 11 + (i % 3); const b = Math.floor(i / 3);
this.set(a, b, dark); this.set(b, a, dark);
}
}
}
drawFormat(mask: number) {
const data = (LEVEL_M << 3) | mask;
let rem = data;
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
const bits = ((data << 10) | rem) ^ 0x5412;
const bit = (i: number) => ((bits >>> i) & 1) !== 0;
for (let i = 0; i <= 5; i++) this.set(8, i, bit(i));
this.set(8, 7, bit(6)); this.set(8, 8, bit(7)); this.set(7, 8, bit(8));
for (let i = 9; i < 15; i++) this.set(14 - i, 8, bit(i));
for (let i = 0; i < 8; i++) this.set(this.size - 1 - i, 8, bit(i));
for (let i = 8; i < 15; i++) this.set(8, this.size - 15 + i, bit(i));
this.set(8, this.size - 8, true);
}
drawCodewords(codewords: number[]) {
let i = 0;
for (let right = this.size - 1; right >= 1; right -= 2) {
if (right === 6) right = 5;
for (let vert = 0; vert < this.size; vert++) for (let j = 0; j < 2; j++) {
const x = right - j;
const y = ((right + 1) & 2) === 0 ? this.size - 1 - vert : vert;
if (!this.fixed[y][x] && i < codewords.length * 8) {
this.modules[y][x] = ((codewords[i >>> 3] >>> (7 - (i & 7))) & 1) !== 0;
i++;
}
}
}
}
applyMask(mask: number) {
for (let y = 0; y < this.size; y++) for (let x = 0; x < this.size; x++) {
if (!this.fixed[y][x] && MASKS[mask](x, y)) this.modules[y][x] = !this.modules[y][x];
}
}
/** The standard's penalty (runs, finder-like patterns, 2x2 blocks, dark balance): lower is easier to read. */
penalty(): number {
let score = 0;
const lines = [...this.modules, ...this.modules.map((_row, x) => this.modules.map((row) => row[x]))];
for (const line of lines) {
let color = false;
let run = 0;
const history = [0, 0, 0, 0, 0, 0, 0];
for (const dark of line) {
if (dark === color) {
run++;
if (run === 5) score += 3;
else if (run > 5) score++;
} else {
this.addHistory(run, history);
if (!color) score += this.finderLike(history) * 40;
color = dark;
run = 1;
}
}
if (color) { this.addHistory(run, history); run = 0; }
this.addHistory(run + this.size, history);
score += this.finderLike(history) * 40;
}
for (let y = 0; y + 1 < this.size; y++) for (let x = 0; x + 1 < this.size; x++) {
const dark = this.modules[y][x];
if (dark === this.modules[y][x + 1] && dark === this.modules[y + 1][x] && dark === this.modules[y + 1][x + 1]) score += 3;
}
const total = this.size * this.size;
const darkCount = this.modules.reduce((sum, row) => sum + row.filter(Boolean).length, 0);
score += (Math.ceil(Math.abs(darkCount * 20 - total * 10) / total) - 1) * 10;
return score;
}
/** Run lengths, newest first; the light border counts before the first run and after the last. */
private addHistory(run: number, history: number[]) {
const first = history[0] === 0;
history.pop();
history.unshift(first ? run + this.size : run);
}
private finderLike(history: number[]): number {
const n = history[1];
const core = n > 0 && history[2] === n && history[3] === n * 3 && history[4] === n && history[5] === n;
return (core && history[0] >= n * 4 && history[6] >= n ? 1 : 0) + (core && history[6] >= n * 4 && history[0] >= n ? 1 : 0);
}
}
/** The modules of a QR code for this text, true for dark, without the quiet zone. */
export function qrModules(text: string): QrModules {
const { version, codewords } = codewordsFor(new TextEncoder().encode(text));
const grid = new Grid(version);
grid.drawFunctionPatterns();
grid.drawCodewords(codewords);
let best = 0;
let bestScore = Infinity;
for (let mask = 0; mask < 8; mask++) {
grid.applyMask(mask);
grid.drawFormat(mask);
const score = grid.penalty();
if (score < bestScore) { best = mask; bestScore = score; }
grid.applyMask(mask);
}
grid.applyMask(best);
grid.drawFormat(best);
return grid.modules;
}
/** An SVG of the QR code with a four-module quiet zone, black on white. */
export function qrSvg(text: string): string {
const modules = qrModules(text);
const size = modules.length + 8;
const path = modules.flatMap((row, y) => row.map((dark, x) => (dark ? `M${x + 4},${y + 4}h1v1h-1z` : ''))).join('');
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges">`
+ `<rect width="${size}" height="${size}" fill="#fff"/><path d="${path}" fill="#000"/></svg>`;
}