/** * 极简二维码编码器:字节模式、纠错等级 M、版本 1–10(最多 213 字节)。 * 足够容纳频道链接。结构参照 ISO/IEC 18004 与 Nayuki 的公开实现思路。 * 为什么自写:避免为一个 100 行量级的功能引入依赖(frontend.md 依赖策略)。 */ const ECC_PER_BLOCK_M = [0, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26] as const; const NUM_BLOCKS_M = [0, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5] as const; const MAX_VERSION = 10; const FORMAT_M = 0; // 纠错等级 M 的格式位值 type Grid = boolean[][]; function rawModules(ver: number): number { let n = (16 * ver + 128) * ver + 64; if (ver >= 2) { const align = Math.floor(ver / 7) + 2; n -= (25 * align - 10) * align - 55; if (ver >= 7) n -= 36; } return n; } const rawCodewords = (ver: number) => Math.floor(rawModules(ver) / 8); const dataCodewords = (ver: number) => rawCodewords(ver) - ECC_PER_BLOCK_M[ver]! * NUM_BLOCKS_M[ver]!; // ---- GF(256) 与 Reed-Solomon ---- function gfMul(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; } function rsDivisor(degree: number): number[] { const result = new Array(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] = gfMul(result[j]!, root); if (j + 1 < degree) result[j]! ^= result[j + 1]!; } root = gfMul(root, 2); } return result; } function rsRemainder(data: number[], divisor: number[]): number[] { const result = divisor.map(() => 0); for (const b of data) { const factor = b ^ result.shift()!; result.push(0); divisor.forEach((coef, i) => { result[i]! ^= gfMul(coef, factor); }); } return result; } // ---- 数据编码 ---- function encodeData(bytes: Uint8Array): { ver: number; codewords: number[] } { let ver = 1; const bitsFor = (v: number) => 4 + (v < 10 ? 8 : 16) + bytes.length * 8; while (ver <= MAX_VERSION && bitsFor(ver) > dataCodewords(ver) * 8) ver++; if (ver > MAX_VERSION) throw new RangeError('内容太长,无法编码为二维码'); const bits: number[] = []; const push = (value: number, len: number) => { for (let i = len - 1; i >= 0; i--) bits.push((value >>> i) & 1); }; push(0b0100, 4); push(bytes.length, ver < 10 ? 8 : 16); for (const b of bytes) push(b, 8); const capacity = dataCodewords(ver) * 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((a, b) => (a << 1) | b, 0)); } return { ver, codewords: addEccAndInterleave(data, ver) }; } function addEccAndInterleave(data: number[], ver: number): number[] { const blocks = NUM_BLOCKS_M[ver]!; const eccLen = ECC_PER_BLOCK_M[ver]!; const raw = rawCodewords(ver); const shortLen = Math.floor(raw / blocks); const numShort = blocks - (raw % blocks); const divisor = rsDivisor(eccLen); const parts: number[][] = []; for (let i = 0, k = 0; i < blocks; i++) { const len = shortLen - eccLen + (i < numShort ? 0 : 1); const dat = data.slice(k, k + len); k += len; const ecc = rsRemainder(dat, divisor); if (i < numShort) dat.push(0); parts.push(dat.concat(ecc)); } const out: number[] = []; for (let i = 0; i < parts[0]!.length; i++) { parts.forEach((block, j) => { if (i !== shortLen - eccLen || j >= numShort) out.push(block[i]!); }); } return out; } // ---- 矩阵 ---- class Matrix { readonly size: number; readonly modules: Grid; readonly isFn: Grid; constructor(readonly ver: number) { this.size = ver * 4 + 17; this.modules = Array.from({ length: this.size }, () => new Array(this.size).fill(false)); this.isFn = Array.from({ length: this.size }, () => new Array(this.size).fill(false)); this.drawFunctionPatterns(); } private setFn(x: number, y: number, dark: boolean) { this.modules[y]![x] = dark; this.isFn[y]![x] = true; } private drawFunctionPatterns() { const { size, ver } = this; for (let i = 0; i < size; i++) { this.setFn(6, i, i % 2 === 0); this.setFn(i, 6, i % 2 === 0); } this.finder(3, 3); this.finder(size - 4, 3); this.finder(3, size - 4); const pos = alignPositions(ver); pos.forEach((cy, i) => pos.forEach((cx, j) => { const corner = (i === 0 && j === 0) || (i === 0 && j === pos.length - 1) || (i === pos.length - 1 && j === 0); if (!corner) this.align(cx, cy); }), ); this.drawFormat(0); this.drawVersion(); } private finder(cx: number, cy: number) { for (let dy = -4; dy <= 4; dy++) { for (let dx = -4; dx <= 4; dx++) { const dist = Math.max(Math.abs(dx), Math.abs(dy)); const x = cx + dx; const y = cy + dy; if (x >= 0 && x < this.size && y >= 0 && y < this.size) this.setFn(x, y, dist !== 2 && dist !== 4); } } } private align(cx: number, cy: number) { for (let dy = -2; dy <= 2; dy++) { for (let dx = -2; dx <= 2; dx++) this.setFn(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1); } } drawFormat(mask: number) { const data = (FORMAT_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; const { size } = this; for (let i = 0; i <= 5; i++) this.setFn(8, i, bit(i)); this.setFn(8, 7, bit(6)); this.setFn(8, 8, bit(7)); this.setFn(7, 8, bit(8)); for (let i = 9; i < 15; i++) this.setFn(14 - i, 8, bit(i)); for (let i = 0; i < 8; i++) this.setFn(size - 1 - i, 8, bit(i)); for (let i = 8; i < 15; i++) this.setFn(8, size - 15 + i, bit(i)); this.setFn(8, size - 8, true); } private drawVersion() { if (this.ver < 7) return; let rem = this.ver; for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25); const bits = (this.ver << 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.setFn(a, b, dark); this.setFn(b, a, dark); } } drawCodewords(data: number[]) { const { size } = this; 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; const upward = ((right + 1) & 2) === 0; const y = upward ? size - 1 - vert : vert; if (!this.isFn[y]![x] && i < data.length * 8) { this.modules[y]![x] = ((data[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.isFn[y]![x] && maskCondition(mask, x, y)) this.modules[y]![x] = !this.modules[y]![x]; } } } /** 简化的惩罚分:同色连续、同色 2×2 块、深浅失衡。只影响掩码选择,不影响可读性 */ penalty(): number { const { size, modules } = this; let score = 0; for (let y = 0; y < size; y++) { for (const horizontal of [true, false]) { let run = 1; for (let i = 1; i < size; i++) { const cur = horizontal ? modules[y]![i] : modules[i]![y]; const prev = horizontal ? modules[y]![i - 1] : modules[i - 1]![y]; run = cur === prev ? run + 1 : 1; if (run === 5) score += 3; else if (run > 5) score += 1; } } } for (let y = 0; y < size - 1; y++) { for (let x = 0; x < size - 1; x++) { const c = modules[y]![x]; if (c === modules[y]![x + 1] && c === modules[y + 1]![x] && c === modules[y + 1]![x + 1]) score += 3; } } const dark = modules.reduce((n, row) => n + row.filter(Boolean).length, 0); return score + Math.floor(Math.abs(dark * 20 - size * size * 10) / (size * size)) * 10; } } function alignPositions(ver: number): number[] { if (ver === 1) return []; const n = Math.floor(ver / 7) + 2; const step = Math.ceil((ver * 4 + 4) / (n * 2 - 2)) * 2; const result = [6]; for (let pos = ver * 4 + 10; result.length < n; pos -= step) result.splice(1, 0, pos); return result; } function maskCondition(mask: number, x: number, y: number): boolean { switch (mask) { case 0: return (x + y) % 2 === 0; case 1: return y % 2 === 0; case 2: return x % 3 === 0; case 3: return (x + y) % 3 === 0; case 4: return (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0; case 5: return ((x * y) % 2) + ((x * y) % 3) === 0; case 6: return ((((x * y) % 2) + ((x * y) % 3)) % 2) === 0; default: return ((((x + y) % 2) + ((x * y) % 3)) % 2) === 0; } } /** 编码为深浅矩阵(true = 深色)。mask 仅供测试固定掩码,默认自动选择 */ export function encodeQr(text: string, mask?: number): Grid { const { ver, codewords } = encodeData(new TextEncoder().encode(text)); const m = new Matrix(ver); m.drawCodewords(codewords); let best = mask ?? 0; if (mask === undefined) { let bestScore = Infinity; for (let candidate = 0; candidate < 8; candidate++) { m.applyMask(candidate); m.drawFormat(candidate); const score = m.penalty(); if (score < bestScore) { bestScore = score; best = candidate; } m.applyMask(candidate); // 还原 } } m.applyMask(best); m.drawFormat(best); return m.modules; }