qr.js (15.0 KB)
1 // Self-contained QR encoder (byte mode, EC level M, versions 1-40). 2 // createMatrix(text) returns a square matrix[row][col] of 0/1 modules 3 4 (function () { 5 // Galois Field tables for Reed-Solomon 6 var GF_EXP = new Array(512); 7 var GF_LOG = new Array(256); 8 9 (function initGF() { 10 var x = 1; 11 for (var i = 0; i < 255; i++) { 12 GF_EXP[i] = x; 13 GF_LOG[x] = i; 14 x <<= 1; 15 if (x & 0x100) x ^= 0x11d; 16 } 17 for (var j = 255; j < 512; j++) { 18 GF_EXP[j] = GF_EXP[j - 255]; 19 } 20 })(); 21 22 function gfMul(a, b) { 23 if (a === 0 || b === 0) return 0; 24 return GF_EXP[GF_LOG[a] + GF_LOG[b]]; 25 } 26 27 // Alignment pattern positions lookup table (from QR spec) 28 var ALIGNMENT_POSITIONS = [ 29 null, 30 [], 31 [6, 18], 32 [6, 22], 33 [6, 26], 34 [6, 30], 35 [6, 34], 36 [6, 22, 38], 37 [6, 24, 42], 38 [6, 26, 46], 39 [6, 28, 50], 40 [6, 30, 54], 41 [6, 32, 58], 42 [6, 34, 62], 43 [6, 26, 46, 66], 44 [6, 26, 48, 70], 45 [6, 26, 50, 74], 46 [6, 30, 54, 78], 47 [6, 30, 56, 82], 48 [6, 30, 58, 86], 49 [6, 34, 62, 90], 50 [6, 28, 50, 72, 94], 51 [6, 26, 50, 74, 98], 52 [6, 30, 54, 78, 102], 53 [6, 28, 54, 80, 106], 54 [6, 32, 58, 84, 110], 55 [6, 30, 58, 86, 114], 56 [6, 34, 62, 90, 118], 57 [6, 26, 50, 74, 98, 122], 58 [6, 30, 54, 78, 102, 126], 59 [6, 26, 52, 78, 104, 130], 60 [6, 30, 56, 82, 108, 134], 61 [6, 34, 60, 86, 112, 138], 62 [6, 30, 58, 86, 114, 142], 63 [6, 34, 62, 90, 118, 146], 64 [6, 30, 54, 78, 102, 126, 150], 65 [6, 24, 50, 76, 102, 128, 154], 66 [6, 28, 54, 80, 106, 132, 158], 67 [6, 32, 58, 84, 110, 136, 162], 68 [6, 26, 54, 82, 110, 138, 166], 69 [6, 30, 58, 86, 114, 142, 170] 70 ]; 71 72 // EC_TABLE: [totalDataCodewords, ecPerBlock, group1Blocks, group1Data, group2Blocks, group2Data] 73 var EC_TABLE = [ 74 null, 75 [16, 10, 1, 16, 0, 0], // V1 76 [28, 16, 1, 28, 0, 0], // V2 77 [44, 26, 1, 44, 0, 0], // V3 78 [64, 18, 2, 32, 0, 0], // V4 79 [86, 24, 2, 43, 0, 0], // V5 80 [108, 16, 4, 27, 0, 0], // V6 81 [124, 18, 4, 31, 0, 0], // V7 82 [154, 22, 2, 38, 2, 39], // V8 83 [182, 22, 3, 36, 2, 37], // V9 84 [216, 26, 4, 43, 1, 44], // V10 85 [254, 30, 1, 50, 4, 51], // V11 86 [290, 22, 6, 36, 2, 37], // V12 87 [334, 22, 8, 37, 1, 38], // V13 88 [365, 24, 4, 40, 5, 41], // V14 89 [415, 24, 5, 41, 5, 42], // V15 90 [453, 28, 7, 45, 3, 46], // V16 91 [507, 28, 10, 46, 1, 47], // V17 92 [563, 26, 9, 43, 4, 44], // V18 93 [627, 26, 3, 44, 11, 45], // V19 94 [669, 26, 3, 41, 13, 42], // V20 95 [714, 26, 17, 42, 0, 0], // V21 96 [782, 28, 17, 46, 0, 0], // V22 97 [860, 28, 4, 47, 14, 48], // V23 98 [914, 28, 6, 45, 14, 46], // V24 99 [1000, 28, 8, 47, 13, 48], // V25 100 [1062, 28, 19, 46, 4, 47], // V26 101 [1128, 28, 22, 45, 3, 46], // V27 102 [1193, 28, 3, 45, 23, 46], // V28 103 [1267, 28, 21, 45, 7, 46], // V29 104 [1373, 28, 19, 47, 10, 48],// V30 105 [1455, 28, 2, 46, 29, 47], // V31 106 [1541, 28, 10, 46, 23, 47],// V32 107 [1631, 28, 14, 46, 21, 47],// V33 108 [1725, 28, 14, 46, 23, 47],// V34 109 [1812, 28, 12, 47, 26, 48],// V35 110 [1914, 28, 6, 47, 34, 48], // V36 111 [1992, 28, 29, 46, 14, 47],// V37 112 [2102, 28, 13, 46, 32, 47],// V38 113 [2216, 28, 40, 47, 7, 48], // V39 114 [2334, 28, 18, 47, 31, 48] // V40 115 ]; 116 117 function getVersion(byteLength) { 118 for (var v = 1; v <= 40; v++) { 119 var charCountBits = v <= 9 ? 8 : 16; 120 var dataBits = 4 + charCountBits + byteLength * 8; 121 var dataBytes = Math.ceil(dataBits / 8); 122 if (dataBytes <= EC_TABLE[v][0]) return v; 123 } 124 return 40; 125 } 126 127 function createQR(text) { 128 var bytes = new TextEncoder().encode(text); 129 var version = getVersion(bytes.length); 130 var size = version * 4 + 17; 131 var matrix = Array.from({ length: size }, function () { return Array(size).fill(null); }); 132 133 addFinderPatterns(matrix, size); 134 addSeparators(matrix, size); 135 addTimingPatterns(matrix, size); 136 addAlignmentPatterns(matrix, version, size); 137 addDarkModule(matrix, version); 138 139 reserveFormatAreas(matrix, size); 140 if (version >= 7) reserveVersionAreas(matrix, size); 141 142 var reserved = createReservedMap(matrix, size); 143 144 var data = encodeData(bytes, version); 145 var ecData = addErrorCorrection(data, version); 146 placeData(matrix, ecData, size); 147 148 var mask = applyBestMask(matrix, size, reserved); 149 addFormatInfo(matrix, size, mask); 150 if (version >= 7) addVersionInfo(matrix, version); 151 152 return matrix; 153 } 154 155 // Finder pattern: 7x7 with specific structure 156 // Outer black border, inner white border, center 3x3 black 157 function addFinderPatterns(matrix, size) { 158 var positions = [ 159 [0, 0], // top-left 160 [0, size - 7], // top-right 161 [size - 7, 0] // bottom-left 162 ]; 163 164 for (var p = 0; p < positions.length; p++) { 165 var startRow = positions[p][0], startCol = positions[p][1]; 166 for (var r = 0; r < 7; r++) { 167 for (var c = 0; c < 7; c++) { 168 var isBlack; 169 if (r === 0 || r === 6 || c === 0 || c === 6) { 170 isBlack = true; // outer border 171 } else if (r === 1 || r === 5 || c === 1 || c === 5) { 172 isBlack = false; // inner white border 173 } else { 174 isBlack = true; // center 3x3 175 } 176 matrix[startRow + r][startCol + c] = isBlack ? 1 : 0; 177 } 178 } 179 } 180 } 181 182 // Separators: 1-module white border around finder patterns 183 function addSeparators(matrix, size) { 184 for (var i = 0; i < 8; i++) { 185 matrix[7][i] = 0; 186 matrix[i][7] = 0; 187 } 188 for (var j = 0; j < 8; j++) { 189 matrix[7][size - 8 + j] = 0; 190 matrix[j][size - 8] = 0; 191 } 192 for (var k = 0; k < 8; k++) { 193 matrix[size - 8][k] = 0; 194 matrix[size - 8 + k][7] = 0; 195 } 196 } 197 198 // Timing patterns: alternating modules on row 6 and column 6 199 function addTimingPatterns(matrix, size) { 200 for (var i = 0; i < size; i++) { 201 var bit = i % 2 === 0 ? 1 : 0; 202 if (matrix[6][i] === null) matrix[6][i] = bit; 203 if (matrix[i][6] === null) matrix[i][6] = bit; 204 } 205 } 206 207 // Alignment patterns: 5x5 with black border, white inner, black center 208 function addAlignmentPatterns(matrix, version, size) { 209 if (version < 2) return; 210 211 var positions = ALIGNMENT_POSITIONS[version]; 212 213 for (var ri = 0; ri < positions.length; ri++) { 214 for (var ci = 0; ci < positions.length; ci++) { 215 var row = positions[ri], col = positions[ci]; 216 if (isInFinderPattern(row, col, size)) continue; // skip finder overlap 217 218 for (var r = -2; r <= 2; r++) { 219 for (var c = -2; c <= 2; c++) { 220 var isBlack; 221 if (Math.abs(r) === 2 || Math.abs(c) === 2) { 222 isBlack = true; // outer border 223 } else if (r === 0 && c === 0) { 224 isBlack = true; // center 225 } else { 226 isBlack = false; // inner white ring 227 } 228 matrix[row + r][col + c] = isBlack ? 1 : 0; 229 } 230 } 231 } 232 } 233 } 234 235 function isInFinderPattern(row, col, size) { 236 if (row <= 7 && col <= 7) return true; 237 if (row <= 7 && col >= size - 8) return true; 238 if (row >= size - 8 && col <= 7) return true; 239 return false; 240 } 241 242 // Dark module: always at matrix[4*version+9][8] per spec 243 function addDarkModule(matrix, version) { 244 matrix[4 * version + 9][8] = 1; 245 } 246 247 // Reserve format info areas (filled in later) 248 function reserveFormatAreas(matrix, size) { 249 for (var i = 0; i < 9; i++) { 250 if (matrix[8][i] === null) matrix[8][i] = 0; 251 if (matrix[i][8] === null) matrix[i][8] = 0; 252 } 253 for (var j = 0; j < 8; j++) { 254 if (matrix[8][size - 1 - j] === null) matrix[8][size - 1 - j] = 0; 255 } 256 for (var k = 0; k < 7; k++) { 257 if (matrix[size - 1 - k][8] === null) matrix[size - 1 - k][8] = 0; 258 } 259 } 260 261 // Reserve version info areas (version 7+) 262 function reserveVersionAreas(matrix, size) { 263 for (var i = 0; i < 6; i++) { 264 for (var j = 0; j < 3; j++) { 265 matrix[i][size - 11 + j] = 0; 266 } 267 } 268 for (var k = 0; k < 6; k++) { 269 for (var m = 0; m < 3; m++) { 270 matrix[size - 11 + m][k] = 0; 271 } 272 } 273 } 274 275 function createReservedMap(matrix, size) { 276 var reserved = Array.from({ length: size }, function () { return Array(size).fill(false); }); 277 278 for (var row = 0; row < size; row++) { 279 for (var col = 0; col < size; col++) { 280 if (matrix[row][col] !== null) { 281 reserved[row][col] = true; 282 } 283 } 284 } 285 286 return reserved; 287 } 288 289 function encodeData(bytes, version) { 290 var bits = []; 291 var charCountBits = version <= 9 ? 8 : 16; 292 293 bits.push(0, 1, 0, 0); // mode: byte 294 295 for (var i = charCountBits - 1; i >= 0; i--) { 296 bits.push((bytes.length >> i) & 1); // character count 297 } 298 299 for (var b = 0; b < bytes.length; b++) { 300 for (var k = 7; k >= 0; k--) { 301 bits.push((bytes[b] >> k) & 1); 302 } 303 } 304 305 // terminator (up to 4 zeros) 306 var capacity = EC_TABLE[version][0] * 8; 307 for (var t = 0; t < 4 && bits.length < capacity; t++) { 308 bits.push(0); 309 } 310 311 while (bits.length % 8 !== 0 && bits.length < capacity) { 312 bits.push(0); // pad to byte boundary 313 } 314 315 // pad codewords 316 var padBytes = [0xEC, 0x11]; 317 var padIndex = 0; 318 while (bits.length < capacity) { 319 var pad = padBytes[padIndex++ % 2]; 320 for (var pb = 7; pb >= 0; pb--) { 321 bits.push((pad >> pb) & 1); 322 } 323 } 324 325 return bits; 326 } 327 328 function addErrorCorrection(data, version) { 329 var row = EC_TABLE[version]; 330 var totalCap = row[0], ecPerBlock = row[1]; 331 var g1Count = row[2], g1Data = row[3], g2Count = row[4], g2Data = row[5]; 332 333 var dataBytes = []; 334 for (var i = 0; i < data.length; i += 8) { 335 var byte = 0; 336 for (var j = 0; j < 8; j++) byte = (byte << 1) | (data[i + j] || 0); 337 dataBytes.push(byte); 338 } 339 340 while (dataBytes.length < totalCap) { 341 dataBytes.push(dataBytes.length % 2 === 0 ? 0xEC : 0x11); // pad to capacity 342 } 343 344 var blocks = []; 345 var ecBlocks = []; 346 var offset = 0; 347 348 for (var a = 0; a < g1Count; a++) { 349 var b1 = dataBytes.slice(offset, offset + g1Data); 350 blocks.push(b1); 351 ecBlocks.push(generateECBytes(b1, ecPerBlock)); 352 offset += g1Data; 353 } 354 355 for (var c = 0; c < g2Count; c++) { 356 var b2 = dataBytes.slice(offset, offset + g2Data); 357 blocks.push(b2); 358 ecBlocks.push(generateECBytes(b2, ecPerBlock)); 359 offset += g2Data; 360 } 361 362 var resultBytes = []; 363 var maxDataSize = Math.max(g1Data, g2Data); 364 365 // interleave data byte-by-byte across all blocks 366 for (var d = 0; d < maxDataSize; d++) { 367 for (var e = 0; e < blocks.length; e++) { 368 if (d < blocks[e].length) { 369 resultBytes.push(blocks[e][d]); 370 } 371 } 372 } 373 374 // interleave EC byte-by-byte (EC sizes are always equal) 375 for (var f = 0; f < ecPerBlock; f++) { 376 for (var g = 0; g < ecBlocks.length; g++) { 377 resultBytes.push(ecBlocks[g][f]); 378 } 379 } 380 381 var resultBits = []; 382 for (var rb = 0; rb < resultBytes.length; rb++) { 383 for (var h = 7; h >= 0; h--) resultBits.push((resultBytes[rb] >> h) & 1); 384 } 385 return resultBits; 386 } 387 388 function generateECBytes(data, ecCount) { 389 var gen = [1]; 390 for (var i = 0; i < ecCount; i++) { 391 var next = new Array(gen.length + 1).fill(0); 392 for (var j = 0; j < gen.length; j++) { 393 next[j] ^= gen[j]; 394 next[j + 1] ^= gfMul(gen[j], GF_EXP[i]); 395 } 396 for (var k = 0; k < next.length; k++) gen[k] = next[k]; 397 gen.length = next.length; 398 } 399 400 var remainder = new Array(ecCount).fill(0); 401 for (var d = 0; d < data.length; d++) { 402 var factor = data[d] ^ remainder[0]; 403 remainder.shift(); 404 remainder.push(0); 405 for (var m = 0; m < ecCount; m++) { 406 remainder[m] ^= gfMul(gen[m + 1], factor); 407 } 408 } 409 410 return remainder; 411 } 412 413 // Place data in zigzag pattern from bottom-right, skipping column 6 414 function placeData(matrix, data, size) { 415 var bitIndex = 0; 416 var up = true; 417 418 for (var col = size - 1; col >= 1; col -= 2) { 419 if (col === 6) col = 5; // skip timing column 420 421 for (var i = 0; i < size; i++) { 422 var row = up ? size - 1 - i : i; 423 424 for (var j = 0; j < 2; j++) { 425 var c = col - j; 426 if (matrix[row][c] === null) { 427 matrix[row][c] = bitIndex < data.length ? data[bitIndex++] : 0; 428 } 429 } 430 } 431 up = !up; 432 } 433 } 434 435 function applyBestMask(matrix, size, reserved) { 436 var bestMask = 0; 437 var bestPenalty = Infinity; 438 439 for (var mask = 0; mask < 8; mask++) { 440 var copy = matrix.map(function (r) { return r.slice(); }); 441 applyMask(copy, size, mask, reserved); 442 var penalty = calculatePenalty(copy, size); 443 444 if (penalty < bestPenalty) { 445 bestPenalty = penalty; 446 bestMask = mask; 447 } 448 } 449 450 applyMask(matrix, size, bestMask, reserved); 451 return bestMask; 452 } 453 454 function applyMask(matrix, size, mask, reserved) { 455 for (var row = 0; row < size; row++) { 456 for (var col = 0; col < size; col++) { 457 if (reserved[row][col]) continue; 458 459 var invert = false; 460 switch (mask) { 461 case 0: invert = (row + col) % 2 === 0; break; 462 case 1: invert = row % 2 === 0; break; 463 case 2: invert = col % 3 === 0; break; 464 case 3: invert = (row + col) % 3 === 0; break; 465 case 4: invert = (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0; break; 466 case 5: invert = (row * col) % 2 + (row * col) % 3 === 0; break; 467 case 6: invert = ((row * col) % 2 + (row * col) % 3) % 2 === 0; break; 468 case 7: invert = ((row + col) % 2 + (row * col) % 3) % 2 === 0; break; 469 } 470 471 if (invert) matrix[row][col] ^= 1; 472 } 473 } 474 } 475 476 function calculatePenalty(matrix, size) { 477 var penalty = 0; 478 479 // rule 1: 5+ consecutive same-color modules 480 for (var i = 0; i < size; i++) { 481 var rowRun = 1, colRun = 1; 482 for (var j = 1; j < size; j++) { 483 rowRun = matrix[i][j] === matrix[i][j - 1] ? rowRun + 1 : 1; 484 if (rowRun === 5) penalty += 3; 485 else if (rowRun > 5) penalty += 1; 486 487 colRun = matrix[j][i] === matrix[j - 1][i] ? colRun + 1 : 1; 488 if (colRun === 5) penalty += 3; 489 else if (colRun > 5) penalty += 1; 490 } 491 } 492 493 // rule 2: 2x2 blocks of same color 494 for (var r = 0; r < size - 1; r++) { 495 for (var c = 0; c < size - 1; c++) { 496 var v = matrix[r][c]; 497 if (v === matrix[r][c + 1] && v === matrix[r + 1][c] && v === matrix[r + 1][c + 1]) { 498 penalty += 3; 499 } 500 } 501 } 502 503 return penalty; 504 } 505 506 function addFormatInfo(matrix, size, mask) { 507 var format = (0 << 3) | mask; // Level M (00) + Mask 508 var rem = format; 509 for (var i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537); 510 var bits = ((format << 10) | rem) ^ 0x5412; 511 512 function getBit(i) { return (bits >> i) & 1; } 513 514 var coords = [ 515 [8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8], [7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8] 516 ]; 517 518 for (var c = 0; c < 15; c++) { 519 var r = coords[c][0], col = coords[c][1]; 520 var bit = getBit(c); 521 matrix[r][col] = bit; // first copy 522 // second copy, mirrored around the other finders 523 if (c < 8) { 524 matrix[8][size - 1 - c] = bit; 525 } else { 526 matrix[size - 1 - (14 - c)][8] = bit; 527 } 528 } 529 } 530 531 function addVersionInfo(matrix, version) { 532 if (version < 7) return; 533 534 // 18-bit BCH Error Corrected Version Codes 535 var versionBits = { 536 7: 0x07C94, 8: 0x085BC, 9: 0x09A99, 10: 0x0A4D3, 537 11: 0x0BBF6, 12: 0x0C762, 13: 0x0D847, 14: 0x0E60D, 538 15: 0x0F928, 16: 0x10B78, 17: 0x1145D, 18: 0x12A17, 539 19: 0x13532, 20: 0x149A6, 21: 0x15683, 22: 0x168C9, 540 23: 0x177EC, 24: 0x18EC4, 25: 0x191E1, 26: 0x1AFAB, 541 27: 0x1B08E, 28: 0x1CC1A, 29: 0x1D33F, 30: 0x1ED75, 542 31: 0x1F250, 32: 0x209D5, 33: 0x216F0, 34: 0x228BA, 543 35: 0x2379F, 36: 0x24B0B, 37: 0x2542E, 38: 0x26A64, 544 39: 0x27541, 40: 0x28C69 545 }; 546 547 var bits = versionBits[version]; 548 var size = matrix.length; 549 550 for (var i = 0; i < 18; i++) { 551 var bit = (bits >> i) & 1; 552 var a = Math.floor(i / 3); 553 var b = i % 3; 554 555 matrix[size - 11 + b][a] = bit; // bottom-left block 556 matrix[a][size - 11 + b] = bit; // top-right block 557 } 558 } 559 560 window.qr = { createMatrix: createQR }; 561 })();