/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** Error-correction level ordinals used as array indices. */ const EC_ORDINALS: Record = { L: 0, M: 1, Q: 2, H: 3 }; /** * Maximum number of *data* codewords for each (version, ecl) combination. * Index: [version-1][ecl ordinal] (versions 1–40). * Source: ISO/IEC 18004 Table 7. */ const DATA_CODEWORDS: number[][] = [ [19, 16, 13, 9], [34, 28, 22, 16], [55, 44, 34, 26], [80, 64, 48, 36], [108, 86, 62, 46], [136, 108, 76, 60], [156, 124, 88, 66], [194, 154, 110, 86], [232, 182, 132, 100], [274, 216, 154, 122], [324, 254, 180, 140], [370, 290, 206, 158], [428, 334, 244, 180], [461, 365, 261, 197], [523, 415, 295, 223], [589, 453, 325, 253], [647, 507, 367, 283], [721, 563, 397, 313], [795, 627, 445, 341], [861, 669, 485, 385], [932, 714, 512, 406], [1006, 782, 568, 442], [1094, 860, 614, 464], [1174, 914, 664, 514], [1276, 1000, 718, 538], [1370, 1062, 754, 596], [1468, 1128, 808, 628], [1531, 1193, 871, 661], [1631, 1267, 911, 701], [1735, 1373, 985, 745], [1843, 1455, 1033, 793], [1955, 1541, 1115, 845], [2071, 1631, 1171, 901], [2191, 1725, 1231, 961], [2306, 1812, 1286, 986], [2434, 1914, 1354, 1054], [2566, 1992, 1426, 1096], [2702, 2102, 1502, 1142], [2812, 2216, 1582, 1222], [2956, 2334, 1666, 1276], ]; /** * Total codewords per version (data + EC). */ const TOTAL_CODEWORDS: number[] = [ 26, 44, 70, 100, 134, 172, 196, 242, 292, 346, 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085, 1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185, 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706, ]; /** * Number of EC blocks (group-1 count, group-1 data-cw, group-2 count, group-2 data-cw) * for each (version, ecl). * Index: [version-1][ecl ordinal] → [g1Count, g1DataCw, g2Count, g2DataCw] */ const EC_BLOCKS: number[][][] = [ /* v1 */ [[1,19,0,0],[1,16,0,0],[1,13,0,0],[1,9,0,0]], /* v2 */ [[1,34,0,0],[1,28,0,0],[1,22,0,0],[1,16,0,0]], /* v3 */ [[1,55,0,0],[1,44,0,0],[2,17,0,0],[2,13,0,0]], /* v4 */ [[1,80,0,0],[2,32,0,0],[2,24,0,0],[4,9,0,0]], /* v5 */ [[1,108,0,0],[2,43,0,0],[2,15,2,16],[2,11,2,12]], /* v6 */ [[2,68,0,0],[4,27,0,0],[4,19,0,0],[4,15,0,0]], /* v7 */ [[2,78,0,0],[4,31,0,0],[2,14,4,15],[4,13,1,14]], /* v8 */ [[2,97,0,0],[2,38,2,39],[4,18,2,19],[4,14,2,15]], /* v9 */ [[2,116,0,0],[3,36,2,37],[4,16,4,17],[4,12,4,13]], /* v10 */ [[2,68,2,69],[4,43,1,44],[6,19,2,20],[6,15,2,16]], /* v11 */ [[4,81,0,0],[1,50,4,51],[4,22,4,23],[3,12,8,13]], /* v12 */ [[2,92,2,93],[6,36,2,37],[4,20,6,21],[7,14,4,15]], /* v13 */ [[4,107,0,0],[8,37,1,38],[8,20,4,21],[12,11,4,12]], /* v14 */ [[3,115,1,116],[4,40,5,41],[11,16,5,17],[11,12,5,13]], /* v15 */ [[5,87,1,88],[5,41,5,42],[5,24,7,25],[11,12,7,13]], /* v16 */ [[5,98,1,99],[7,45,3,46],[15,19,2,20],[3,15,13,16]], /* v17 */ [[1,107,5,108],[10,46,1,47],[1,22,15,23],[2,14,17,15]], /* v18 */ [[5,120,1,121],[9,43,4,44],[17,22,1,23],[2,14,19,15]], /* v19 */ [[3,113,4,114],[3,44,11,45],[17,21,4,22],[9,13,16,14]], /* v20 */ [[3,107,5,108],[3,41,13,42],[15,24,5,25],[15,15,10,16]], /* v21 */ [[4,116,4,117],[17,42,0,0],[17,22,6,23],[19,16,6,17]], /* v22 */ [[2,111,7,112],[17,46,0,0],[7,24,16,25],[34,13,0,0]], /* v23 */ [[4,121,5,122],[4,47,14,48],[11,24,14,25],[16,15,14,16]], /* v24 */ [[6,117,4,118],[6,45,14,46],[11,24,16,25],[30,16,2,17]], /* v25 */ [[8,106,4,107],[8,47,13,48],[7,24,22,25],[22,15,13,16]], /* v26 */ [[10,114,2,115],[19,46,4,47],[28,22,6,23],[33,16,4,17]], /* v27 */ [[8,122,4,123],[22,45,3,46],[8,23,26,24],[12,15,28,16]], /* v28 */ [[3,117,10,118],[3,45,23,46],[4,24,31,25],[11,15,31,16]], /* v29 */ [[7,116,7,117],[21,45,7,46],[1,23,37,24],[19,15,26,16]], /* v30 */ [[5,115,10,116],[19,47,10,48],[15,24,25,25],[23,15,25,16]], /* v31 */ [[13,115,3,116],[2,46,29,47],[42,24,1,25],[23,15,28,16]], /* v32 */ [[17,115,0,0],[10,46,23,47],[10,24,35,25],[19,15,35,16]], /* v33 */ [[17,115,1,116],[14,46,21,47],[29,24,19,25],[11,15,46,16]], /* v34 */ [[13,115,6,116],[14,46,23,47],[44,24,7,25],[59,16,1,17]], /* v35 */ [[12,121,7,122],[12,47,26,48],[39,24,14,25],[22,15,41,16]], /* v36 */ [[6,121,14,122],[6,47,34,48],[46,24,10,25],[2,15,64,16]], /* v37 */ [[17,122,4,123],[29,46,14,47],[49,24,10,25],[24,15,46,16]], /* v38 */ [[4,122,18,123],[13,46,32,47],[48,24,14,25],[42,15,32,16]], /* v39 */ [[20,117,4,118],[40,47,7,48],[43,24,22,25],[10,15,67,16]], /* v40 */ [[19,118,6,119],[18,47,31,48],[34,24,34,25],[20,15,61,16]], ]; /** * Alignment-pattern center coordinates per version (empty for v1). */ const ALIGNMENT_COORDS: number[][] = [ [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170], ]; /** Format information bits (15-bit BCH) for each (ecl, mask) pair. */ const FORMAT_INFO: number[][] = [ /* L */ [0x77C4, 0x72F3, 0x7DAA, 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976], /* M */ [0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0], /* Q */ [0x355F, 0x3068, 0x3F31, 0x3A06, 0x24B4, 0x2183, 0x2EDA, 0x2BED], /* H */ [0x1689, 0x13BE, 0x1CE7, 0x19D0, 0x0762, 0x0255, 0x0D0C, 0x083B], ]; /** Version information bits (18-bit BCH) for versions 7–40. */ const VERSION_INFO: number[] = [ 0x07C94, 0x085BC, 0x09A99, 0x0A4D3, 0x0BBF6, 0x0C762, 0x0D847, 0x0E60D, 0x0F928, 0x10B78, 0x1145D, 0x12A17, 0x13532, 0x149A6, 0x15683, 0x168C9, 0x177EC, 0x18EC4, 0x191E1, 0x1AFAB, 0x1B08E, 0x1CC1A, 0x1D33F, 0x1ED75, 0x1F250, 0x209D5, 0x216F0, 0x228BA, 0x2379F, 0x24B0B, 0x2542E, 0x26A64, 0x27541, 0x28C69, ]; // --------------------------------------------------------------------------- // GF(256) arithmetic for Reed-Solomon // --------------------------------------------------------------------------- const GF_EXP = new Uint8Array(512); const GF_LOG = new Uint8Array(256); (function initGF() { let x = 1; for (let i = 0; i < 255; i++) { GF_EXP[i] = x; GF_LOG[x] = i; x <<= 1; if (x >= 256) x ^= 0x11D; // primitive polynomial } for (let i = 255; i < 512; i++) { GF_EXP[i] = GF_EXP[i - 255]; } })(); function gfMul(a: number, b: number): number { if (a === 0 || b === 0) return 0; return GF_EXP[GF_LOG[a] + GF_LOG[b]]; } // --------------------------------------------------------------------------- // Reed-Solomon EC codeword generation // --------------------------------------------------------------------------- function rsGeneratorPoly(degree: number): Uint8Array { const gen = new Uint8Array(degree + 1); gen[0] = 1; for (let i = 0; i < degree; i++) { for (let j = degree; j >= 1; j--) { gen[j] = gen[j] ^ gfMul(gen[j - 1], GF_EXP[i]); } // gen[0] stays 1 (leading coeff) } return gen; } function rsEncode(data: Uint8Array, ecCount: number): Uint8Array { const gen = rsGeneratorPoly(ecCount); const result = new Uint8Array(ecCount); for (let i = 0; i < data.length; i++) { const coeff = data[i] ^ result[0]; // shift result left for (let j = 0; j < ecCount - 1; j++) { result[j] = result[j + 1] ^ gfMul(gen[j + 1], coeff); } result[ecCount - 1] = gfMul(gen[ecCount], coeff); } return result; } // --------------------------------------------------------------------------- // Data encoding (Byte mode only) // --------------------------------------------------------------------------- function encodeData(text: string, version: number, ecl: ErrorCorrectionLevel): Uint8Array { const eclOrd = EC_ORDINALS[ecl]; const totalDataCw = DATA_CODEWORDS[version - 1][eclOrd]; const bytes = new TextEncoder().encode(text); // Build bit stream const bits: number[] = []; const pushBits = (value: number, length: number) => { for (let i = length - 1; i >= 0; i--) { bits.push((value >>> i) & 1); } }; // Mode indicator: byte = 0100 pushBits(0b0100, 4); // Character count indicator const ccBits = version <= 9 ? 8 : 16; pushBits(bytes.length, ccBits); // Data for (const b of bytes) { pushBits(b, 8); } // Terminator (up to 4 bits of zeros) const totalDataBits = totalDataCw * 8; const terminatorLen = Math.min(4, totalDataBits - bits.length); pushBits(0, terminatorLen); // Pad to byte boundary while (bits.length % 8 !== 0) { bits.push(0); } // Pad codewords const padBytes = [0xEC, 0x11]; let padIdx = 0; while (bits.length < totalDataBits) { pushBits(padBytes[padIdx], 8); padIdx ^= 1; } // Convert to byte array const codewords = new Uint8Array(totalDataCw); for (let i = 0; i < totalDataCw; i++) { let byte = 0; for (let b = 0; b < 8; b++) { byte = (byte << 1) | bits[i * 8 + b]; } codewords[i] = byte; } return codewords; } // --------------------------------------------------------------------------- // Interleave data + EC blocks // --------------------------------------------------------------------------- function buildFinalMessage(data: Uint8Array, version: number, ecl: ErrorCorrectionLevel): Uint8Array { const eclOrd = EC_ORDINALS[ecl]; const [g1Count, g1DataCw, g2Count, g2DataCw] = EC_BLOCKS[version - 1][eclOrd]; const totalCw = TOTAL_CODEWORDS[version - 1]; const totalBlocks = g1Count + g2Count; const ecCwPerBlock = Math.floor((totalCw - DATA_CODEWORDS[version - 1][eclOrd]) / totalBlocks); // Split data into blocks const dataBlocks: Uint8Array[] = []; const ecBlocks: Uint8Array[] = []; let offset = 0; for (let i = 0; i < g1Count; i++) { const block = data.slice(offset, offset + g1DataCw); dataBlocks.push(block); ecBlocks.push(rsEncode(block, ecCwPerBlock)); offset += g1DataCw; } for (let i = 0; i < g2Count; i++) { const block = data.slice(offset, offset + g2DataCw); dataBlocks.push(block); ecBlocks.push(rsEncode(block, ecCwPerBlock)); offset += g2DataCw; } // Interleave data codewords const result: number[] = []; const maxDataCw = Math.max(g1DataCw, g2DataCw); for (let i = 0; i < maxDataCw; i++) { for (let j = 0; j < totalBlocks; j++) { if (i < dataBlocks[j].length) { result.push(dataBlocks[j][i]); } } } // Interleave EC codewords for (let i = 0; i < ecCwPerBlock; i++) { for (let j = 0; j < totalBlocks; j++) { result.push(ecBlocks[j][i]); } } return new Uint8Array(result); } // --------------------------------------------------------------------------- // QR matrix construction // --------------------------------------------------------------------------- /** Module size = 4 * version + 17 */ function getModuleCount(version: number): number { return version * 4 + 17; } /** Create a blank matrix (-1 = unset). */ function createMatrix(size: number): number[][] { return Array.from({ length: size }, () => Array(size).fill(-1)); } function setModule(matrix: number[][], row: number, col: number, value: number): void { if (row >= 0 && row < matrix.length && col >= 0 && col < matrix.length) { matrix[row][col] = value; } } /** Place a finder pattern with its separator. */ function placeFinderPattern(matrix: number[][], row: number, col: number): void { for (let r = -1; r <= 7; r++) { for (let c = -1; c <= 7; c++) { const isPattern = r >= 0 && r <= 6 && c >= 0 && c <= 6; let value = 0; if (isPattern) { if (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4)) { value = 1; } } setModule(matrix, row + r, col + c, value); } } } /** Place alignment patterns. */ function placeAlignmentPatterns(matrix: number[][], version: number): void { if (version < 2) return; const coords = ALIGNMENT_COORDS[version - 1]; for (const r of coords) { for (const c of coords) { // Skip if overlapping finder patterns if (matrix[r][c] !== -1) continue; for (let dr = -2; dr <= 2; dr++) { for (let dc = -2; dc <= 2; dc++) { const value = (Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0)) ? 1 : 0; setModule(matrix, r + dr, c + dc, value); } } } } } /** Place timing patterns. */ function placeTimingPatterns(matrix: number[][]): void { const size = matrix.length; for (let i = 8; i < size - 8; i++) { if (matrix[6][i] === -1) matrix[6][i] = (i + 1) % 2; if (matrix[i][6] === -1) matrix[i][6] = (i + 1) % 2; } } /** Reserve format info areas (filled later). */ function reserveFormatAreas(matrix: number[][]): void { const size = matrix.length; // Around top-left finder for (let i = 0; i <= 8; i++) { if (matrix[8][i] === -1) matrix[8][i] = 0; if (matrix[i][8] === -1) matrix[i][8] = 0; } // Around top-right finder for (let i = 0; i <= 7; i++) { if (matrix[8][size - 1 - i] === -1) matrix[8][size - 1 - i] = 0; } // Around bottom-left finder for (let i = 0; i <= 7; i++) { if (matrix[size - 1 - i][8] === -1) matrix[size - 1 - i][8] = 0; } // Dark module matrix[size - 8][8] = 1; } /** Reserve version info areas (versions >= 7). */ function reserveVersionAreas(matrix: number[][], version: number): void { if (version < 7) return; const size = matrix.length; for (let i = 0; i < 6; i++) { for (let j = 0; j < 3; j++) { if (matrix[i][size - 11 + j] === -1) matrix[i][size - 11 + j] = 0; if (matrix[size - 11 + j][i] === -1) matrix[size - 11 + j][i] = 0; } } } /** Place data bits in the matrix using the zigzag pattern. */ function placeDataBits(matrix: number[][], data: Uint8Array): void { const size = matrix.length; let bitIndex = 0; const totalBits = data.length * 8; let upward = true; for (let right = size - 1; right >= 1; right -= 2) { // Skip timing pattern column if (right === 6) right = 5; for (let vert = 0; vert < size; vert++) { const row = upward ? size - 1 - vert : vert; for (let colOffset = 0; colOffset <= 1; colOffset++) { const col = right - colOffset; if (matrix[row][col] !== -1) continue; if (bitIndex < totalBits) { const byteIdx = bitIndex >>> 3; const bitIdx = 7 - (bitIndex & 7); matrix[row][col] = (data[byteIdx] >>> bitIdx) & 1; bitIndex++; } else { matrix[row][col] = 0; } } } upward = !upward; } } // --------------------------------------------------------------------------- // Masking // --------------------------------------------------------------------------- type MaskFn = (row: number, col: number) => boolean; const MASK_FUNCTIONS: MaskFn[] = [ (r, c) => (r + c) % 2 === 0, (r, _c) => r % 2 === 0, (_r, c) => c % 3 === 0, (r, c) => (r + c) % 3 === 0, (r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0, (r, c) => ((r * c) % 2 + (r * c) % 3) === 0, (r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0, (r, c) => ((r + c) % 2 + (r * c) % 3) % 2 === 0, ]; /** Check if a module is a function pattern (not data). */ function isFunctionModule(matrix: number[][], reserved: number[][], row: number, col: number): boolean { return reserved[row][col] !== -1; } function applyMask(matrix: number[][], reserved: number[][], maskIdx: number): number[][] { const size = matrix.length; const result = matrix.map(row => [...row]); const fn = MASK_FUNCTIONS[maskIdx]; for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (!isFunctionModule(result, reserved, r, c) && fn(r, c)) { result[r][c] ^= 1; } } } return result; } // --------------------------------------------------------------------------- // Format & version info writing // --------------------------------------------------------------------------- function writeFormatInfo(matrix: number[][], ecl: ErrorCorrectionLevel, maskIdx: number): void { const eclOrd = EC_ORDINALS[ecl]; const bits = FORMAT_INFO[eclOrd][maskIdx]; const size = matrix.length; for (let i = 0; i <= 5; i++) { matrix[8][i] = (bits >>> (14 - i)) & 1; } matrix[8][7] = (bits >>> 8) & 1; matrix[8][8] = (bits >>> 7) & 1; matrix[7][8] = (bits >>> 6) & 1; for (let i = 0; i <= 5; i++) { matrix[5 - i][8] = (bits >>> (i)) & 1; } for (let i = 0; i <= 7; i++) { matrix[8][size - 1 - i] = (bits >>> i) & 1; } for (let i = 0; i <= 6; i++) { matrix[size - 1 - i][8] = (bits >>> (14 - i)) & 1; } matrix[size - 8][8] = 1; // dark module } function writeVersionInfo(matrix: number[][], version: number): void { if (version < 7) return; const bits = VERSION_INFO[version - 7]; const size = matrix.length; for (let i = 0; i < 6; i++) { for (let j = 0; j < 3; j++) { const bitIdx = i * 3 + j; const bit = (bits >>> bitIdx) & 1; matrix[i][size - 11 + j] = bit; matrix[size - 11 + j][i] = bit; } } } // --------------------------------------------------------------------------- // Penalty scoring // --------------------------------------------------------------------------- function penaltyScore(matrix: number[][]): number { const size = matrix.length; let score = 0; // Rule 1: 5+ consecutive same-color modules in row/col for (let r = 0; r < size; r++) { let count = 1; for (let c = 1; c < size; c++) { if (matrix[r][c] === matrix[r][c - 1]) { count++; } else { if (count >= 5) score += count - 2; count = 1; } } if (count >= 5) score += count - 2; } for (let c = 0; c < size; c++) { let count = 1; for (let r = 1; r < size; r++) { if (matrix[r][c] === matrix[r - 1][c]) { count++; } else { if (count >= 5) score += count - 2; count = 1; } } if (count >= 5) score += count - 2; } // Rule 2: 2x2 blocks of same color for (let r = 0; r < size - 1; r++) { for (let c = 0; c < size - 1; c++) { const val = matrix[r][c]; if (val === matrix[r][c + 1] && val === matrix[r + 1][c] && val === matrix[r + 1][c + 1]) { score += 3; } } } // Rule 3: Finder-like patterns const pattern1 = [1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]; const pattern2 = [0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1]; for (let r = 0; r < size; r++) { for (let c = 0; c <= size - 11; c++) { let match1 = true; let match2 = true; for (let i = 0; i < 11; i++) { if (matrix[r][c + i] !== pattern1[i]) match1 = false; if (matrix[r][c + i] !== pattern2[i]) match2 = false; } if (match1 || match2) score += 40; } } for (let c = 0; c < size; c++) { for (let r = 0; r <= size - 11; r++) { let match1 = true; let match2 = true; for (let i = 0; i < 11; i++) { if (matrix[r + i][c] !== pattern1[i]) match1 = false; if (matrix[r + i][c] !== pattern2[i]) match2 = false; } if (match1 || match2) score += 40; } } // Rule 4: Proportion of dark modules let dark = 0; for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (matrix[r][c] === 1) dark++; } } const total = size * size; const percent = (dark / total) * 100; const prev5 = Math.floor(percent / 5) * 5; const next5 = prev5 + 5; score += Math.min(Math.abs(prev5 - 50) / 5, Math.abs(next5 - 50) / 5) * 10; return score; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /** * Find the minimum version that can hold the given data. */ function findMinVersion(text: string, ecl: ErrorCorrectionLevel): number { const bytes = new TextEncoder().encode(text); const dataLen = bytes.length; const eclOrd = EC_ORDINALS[ecl]; for (let v = 1; v <= 40; v++) { const ccBits = v <= 9 ? 8 : 16; const totalBits = 4 + ccBits + dataLen * 8; const capacity = DATA_CODEWORDS[v - 1][eclOrd] * 8; if (totalBits <= capacity) return v; } throw new Error('Data too long for any QR version'); } /** * Generate a QR code matrix from a string. * * @param text The text to encode. * @param ecl Error-correction level (L, M, Q, H). * @returns A 2-D boolean-like matrix where `1` = dark module. */ export function generateQR(text: string, ecl: ErrorCorrectionLevel = 'H'): number[][] { if (!text) { // Return a minimal empty 21x21 (version 1) blank grid return Array.from({ length: 21 }, () => Array(21).fill(0)); } const version = findMinVersion(text, ecl); const moduleCount = getModuleCount(version); // 1. Build function-pattern matrix (reserved areas) const reserved = createMatrix(moduleCount); placeFinderPattern(reserved, 0, 0); placeFinderPattern(reserved, 0, moduleCount - 7); placeFinderPattern(reserved, moduleCount - 7, 0); placeAlignmentPatterns(reserved, version); placeTimingPatterns(reserved); reserveFormatAreas(reserved); reserveVersionAreas(reserved, version); // 2. Build actual matrix with function patterns const matrix = createMatrix(moduleCount); placeFinderPattern(matrix, 0, 0); placeFinderPattern(matrix, 0, moduleCount - 7); placeFinderPattern(matrix, moduleCount - 7, 0); placeAlignmentPatterns(matrix, version); placeTimingPatterns(matrix); reserveFormatAreas(matrix); reserveVersionAreas(matrix, version); // 3. Encode & place data const dataCw = encodeData(text, version, ecl); const finalMsg = buildFinalMessage(dataCw, version, ecl); placeDataBits(matrix, finalMsg); // 4. Try all 8 masks, pick the best let bestMask = 0; let bestScore = Infinity; let bestMatrix = matrix; for (let mask = 0; mask < 8; mask++) { const candidate = applyMask(matrix, reserved, mask); writeFormatInfo(candidate, ecl, mask); writeVersionInfo(candidate, version); const score = penaltyScore(candidate); if (score < bestScore) { bestScore = score; bestMask = mask; bestMatrix = candidate; } } // Re-apply best mask (already stored) writeFormatInfo(bestMatrix, ecl, bestMask); writeVersionInfo(bestMatrix, version); return bestMatrix; }