├── .gitignore ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── constants.js └── qr.js ├── test └── index.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | types -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # headless-qr 2 | 3 | A simple, modern QR code generator. Adapted from https://github.com/kazuhikoarase/qrcode-generator but without all the junk that was necessary 10 years ago. 4 | 5 | ## Usage 6 | 7 | ```js 8 | import { qr } from 'headless-qr'; 9 | 10 | // generate an n x n array of booleans, 11 | // where `true` is a dark pixel 12 | const modules = qr('https://example.com'); 13 | 14 | // specify version and error correction 15 | const modules = qr('https://example.com', { 16 | version: 40, // 1 - 40, will select the best version if unspecified 17 | correction: 'Q' // L, M, Q or H 18 | }); 19 | ``` 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "headless-qr", 3 | "type": "module", 4 | "version": "1.0.3", 5 | "exports": { 6 | ".": { 7 | "types": "./types/qr.d.ts", 8 | "import": "./src/qr.js" 9 | } 10 | }, 11 | "files": [ 12 | "src", 13 | "types" 14 | ], 15 | "scripts": { 16 | "build": "tsc", 17 | "prepublishOnly": "npm run build" 18 | }, 19 | "devDependencies": { 20 | "typescript": "^5.0.4" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Rich-Harris/headless-qr.git" 25 | } 26 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | typescript: ^5.0.4 5 | 6 | devDependencies: 7 | typescript: 5.0.4 8 | 9 | packages: 10 | 11 | /typescript/5.0.4: 12 | resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} 13 | engines: {node: '>=12.20'} 14 | hasBin: true 15 | dev: true 16 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const QRErrorCorrectionLevel = { 2 | L: 1, 3 | M: 0, 4 | Q: 3, 5 | H: 2 6 | }; 7 | 8 | export const QRMaskPattern = { 9 | PATTERN000: 0, 10 | PATTERN001: 1, 11 | PATTERN010: 2, 12 | PATTERN011: 3, 13 | PATTERN100: 4, 14 | PATTERN101: 5, 15 | PATTERN110: 6, 16 | PATTERN111: 7 17 | }; 18 | -------------------------------------------------------------------------------- /src/qr.js: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/kazuhikoarase/qrcode-generator 2 | // License reproduced below 3 | 4 | //--------------------------------------------------------------------- 5 | // 6 | // QR Code Generator for JavaScript 7 | // 8 | // Copyright (c) 2009 Kazuhiko Arase 9 | // 10 | // URL: http://www.d-project.com/ 11 | // 12 | // Licensed under the MIT license: 13 | // http://www.opensource.org/licenses/mit-license.php 14 | // 15 | // The word 'QR Code' is registered trademark of 16 | // DENSO WAVE INCORPORATED 17 | // http://www.denso-wave.com/qrcode/faqpatent-e.html 18 | // 19 | //--------------------------------------------------------------------- 20 | 21 | import { QRErrorCorrectionLevel, QRMaskPattern } from './constants.js'; 22 | 23 | /** @typedef {boolean | null} Module */ 24 | 25 | const encoder = new TextEncoder(); 26 | 27 | const PAD0 = 0xec; 28 | const PAD1 = 0x11; 29 | 30 | /** 31 | * @param {string} input 32 | * @param {{ 33 | * version?: number; 34 | * correction?: 'L' | 'M' | 'Q' | 'H'; 35 | * }} opts 36 | */ 37 | export function qr(input, { version = -1, correction = 'M' } = {}) { 38 | const error_correction_level = QRErrorCorrectionLevel[correction]; 39 | const data = encoder.encode(input); 40 | 41 | if (version < 1) { 42 | for (version = 1; version < 40; version++) { 43 | const rs_blocks = QRRSBlock.get_rs_blocks( 44 | version, 45 | error_correction_level 46 | ); 47 | const buffer = new QrBitBuffer(); 48 | 49 | buffer.put(4, 4); 50 | buffer.put(data.length, QRUtil.get_length_in_bits(version)); 51 | buffer.put_bytes(data); 52 | 53 | let total_data_count = 0; 54 | for (let i = 0; i < rs_blocks.length; i++) { 55 | total_data_count += rs_blocks[i].data_count; 56 | } 57 | 58 | if (buffer.get_length_in_bits() <= total_data_count * 8) { 59 | break; 60 | } 61 | } 62 | } 63 | 64 | const size = version * 4 + 17; 65 | /** @type {Module[][]} */ 66 | const modules = new Array(size); 67 | for (let row = 0; row < size; row += 1) { 68 | modules[row] = new Array(size); 69 | } 70 | 71 | let min_lost_point = 0; 72 | let best_pattern = 0; 73 | 74 | /** @type {number[]} */ 75 | let cache = create_data(version, error_correction_level, data); 76 | 77 | for (let i = 0; i < 8; i += 1) { 78 | const modules = make(true, i); 79 | 80 | const lost_point = get_lost_point(modules); 81 | 82 | if (i == 0 || min_lost_point > lost_point) { 83 | min_lost_point = lost_point; 84 | best_pattern = i; 85 | } 86 | } 87 | 88 | return make(false, best_pattern); 89 | 90 | /** 91 | * @param {boolean} test 92 | * @param {number} mask_pattern 93 | */ 94 | function make(test, mask_pattern) { 95 | for (let row = 0; row < size; row += 1) { 96 | for (let col = 0; col < size; col += 1) { 97 | modules[row][col] = null; 98 | } 99 | } 100 | 101 | setup_position_probe_pattern(modules, 0, 0); 102 | setup_position_probe_pattern(modules, size - 7, 0); 103 | setup_position_probe_pattern(modules, 0, size - 7); 104 | setup_position_adjust_pattern(modules, version); 105 | setup_timing_pattern(modules); 106 | setup_type_info(modules, test, mask_pattern, error_correction_level); 107 | 108 | if (version >= 7) { 109 | setup_version_number(modules, version, test); 110 | } 111 | 112 | map_data(modules, cache, mask_pattern); 113 | 114 | return modules; 115 | } 116 | } 117 | 118 | /** 119 | * @param {Module[][]} modules 120 | * @param {number} row 121 | * @param {number} col 122 | */ 123 | function setup_position_probe_pattern(modules, row, col) { 124 | for (let r = -1; r <= 7; r += 1) { 125 | if (row + r <= -1 || modules.length <= row + r) continue; 126 | 127 | for (let c = -1; c <= 7; c += 1) { 128 | if (col + c <= -1 || modules.length <= col + c) continue; 129 | 130 | if ( 131 | (0 <= r && r <= 6 && (c == 0 || c == 6)) || 132 | (0 <= c && c <= 6 && (r == 0 || r == 6)) || 133 | (2 <= r && r <= 4 && 2 <= c && c <= 4) 134 | ) { 135 | modules[row + r][col + c] = true; 136 | } else { 137 | modules[row + r][col + c] = false; 138 | } 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * @param {Module[][]} modules 145 | * @param {number} version 146 | */ 147 | function setup_position_adjust_pattern(modules, version) { 148 | const pos = QRUtil.get_pattern_position(version); 149 | 150 | for (let i = 0; i < pos.length; i += 1) { 151 | for (let j = 0; j < pos.length; j += 1) { 152 | const row = pos[i]; 153 | const col = pos[j]; 154 | 155 | if (modules[row][col] != null) { 156 | continue; 157 | } 158 | 159 | for (let r = -2; r <= 2; r += 1) { 160 | for (let c = -2; c <= 2; c += 1) { 161 | if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) { 162 | modules[row + r][col + c] = true; 163 | } else { 164 | modules[row + r][col + c] = false; 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * @param {Module[][]} modules 174 | */ 175 | function setup_timing_pattern(modules) { 176 | for (let r = 8; r < modules.length - 8; r += 1) { 177 | if (modules[r][6] != null) { 178 | continue; 179 | } 180 | modules[r][6] = r % 2 == 0; 181 | } 182 | 183 | for (let c = 8; c < modules.length - 8; c += 1) { 184 | if (modules[6][c] != null) { 185 | continue; 186 | } 187 | modules[6][c] = c % 2 == 0; 188 | } 189 | } 190 | 191 | /** 192 | * @param {Module[][]} modules 193 | * @param {boolean} test 194 | * @param {number} mask_pattern 195 | * @param {number} error_correction_level 196 | */ 197 | function setup_type_info(modules, test, mask_pattern, error_correction_level) { 198 | const data = (error_correction_level << 3) | mask_pattern; 199 | const bits = QRUtil.get_bch_type_info(data); 200 | 201 | // vertical 202 | for (let i = 0; i < 15; i += 1) { 203 | const mod = !test && ((bits >> i) & 1) == 1; 204 | 205 | if (i < 6) { 206 | modules[i][8] = mod; 207 | } else if (i < 8) { 208 | modules[i + 1][8] = mod; 209 | } else { 210 | modules[modules.length - 15 + i][8] = mod; 211 | } 212 | } 213 | 214 | // horizontal 215 | for (let i = 0; i < 15; i += 1) { 216 | const mod = !test && ((bits >> i) & 1) == 1; 217 | 218 | if (i < 8) { 219 | modules[8][modules.length - i - 1] = mod; 220 | } else if (i < 9) { 221 | modules[8][15 - i - 1 + 1] = mod; 222 | } else { 223 | modules[8][15 - i - 1] = mod; 224 | } 225 | } 226 | 227 | // fixed module 228 | modules[modules.length - 8][8] = !test; 229 | } 230 | 231 | /** 232 | * @param {Module[][]} modules 233 | * @param {number} version 234 | * @param {boolean} test 235 | */ 236 | function setup_version_number(modules, version, test) { 237 | const bits = QRUtil.get_bch_type_number(version); 238 | 239 | for (let i = 0; i < 18; i += 1) { 240 | const mod = !test && ((bits >> i) & 1) == 1; 241 | modules[Math.floor(i / 3)][(i % 3) + modules.length - 8 - 3] = mod; 242 | } 243 | 244 | for (let i = 0; i < 18; i += 1) { 245 | const mod = !test && ((bits >> i) & 1) == 1; 246 | modules[(i % 3) + modules.length - 8 - 3][Math.floor(i / 3)] = mod; 247 | } 248 | } 249 | 250 | /** 251 | * @param {Module[][]} modules 252 | * @param {number[]} data 253 | * @param {number} mask_pattern 254 | */ 255 | function map_data(modules, data, mask_pattern) { 256 | let inc = -1; 257 | let row = modules.length - 1; 258 | let bit_index = 7; 259 | let byte_index = 0; 260 | const mask_func = QRUtil.get_mask_function(mask_pattern); 261 | 262 | for (let col = modules.length - 1; col > 0; col -= 2) { 263 | if (col == 6) col -= 1; 264 | 265 | while (true) { 266 | for (let c = 0; c < 2; c += 1) { 267 | if (modules[row][col - c] == null) { 268 | let dark = false; 269 | 270 | if (byte_index < data.length) { 271 | dark = ((data[byte_index] >>> bit_index) & 1) == 1; 272 | } 273 | 274 | const mask = mask_func(row, col - c); 275 | 276 | if (mask) { 277 | dark = !dark; 278 | } 279 | 280 | modules[row][col - c] = dark; 281 | bit_index -= 1; 282 | 283 | if (bit_index == -1) { 284 | byte_index += 1; 285 | bit_index = 7; 286 | } 287 | } 288 | } 289 | 290 | row += inc; 291 | 292 | if (row < 0 || modules.length <= row) { 293 | row -= inc; 294 | inc = -inc; 295 | break; 296 | } 297 | } 298 | } 299 | } 300 | 301 | /** 302 | * @param {Module[][]} modules 303 | */ 304 | function get_lost_point(modules) { 305 | const size = modules.length; 306 | let lost_point = 0; 307 | 308 | /** 309 | * @param {number} row 310 | * @param {number} col 311 | */ 312 | const is_dark = (row, col) => modules[row][col]; 313 | 314 | // LEVEL1 315 | for (let row = 0; row < size; row += 1) { 316 | for (let col = 0; col < size; col += 1) { 317 | const dark = is_dark(row, col); 318 | let same_count = 0; 319 | 320 | for (let r = -1; r <= 1; r += 1) { 321 | if (row + r < 0 || size <= row + r) { 322 | continue; 323 | } 324 | 325 | for (let c = -1; c <= 1; c += 1) { 326 | if (col + c < 0 || size <= col + c) { 327 | continue; 328 | } 329 | 330 | if (r == 0 && c == 0) { 331 | continue; 332 | } 333 | 334 | if (dark == is_dark(row + r, col + c)) { 335 | same_count += 1; 336 | } 337 | } 338 | } 339 | 340 | if (same_count > 5) { 341 | lost_point += 3 + same_count - 5; 342 | } 343 | } 344 | } 345 | 346 | // LEVEL2 347 | for (let row = 0; row < size - 1; row += 1) { 348 | for (let col = 0; col < size - 1; col += 1) { 349 | let count = 0; 350 | if (is_dark(row, col)) count += 1; 351 | if (is_dark(row + 1, col)) count += 1; 352 | if (is_dark(row, col + 1)) count += 1; 353 | if (is_dark(row + 1, col + 1)) count += 1; 354 | if (count == 0 || count == 4) { 355 | lost_point += 3; 356 | } 357 | } 358 | } 359 | 360 | // LEVEL3 361 | for (let row = 0; row < size; row += 1) { 362 | for (let col = 0; col < size - 6; col += 1) { 363 | if ( 364 | is_dark(row, col) && 365 | !is_dark(row, col + 1) && 366 | is_dark(row, col + 2) && 367 | is_dark(row, col + 3) && 368 | is_dark(row, col + 4) && 369 | !is_dark(row, col + 5) && 370 | is_dark(row, col + 6) 371 | ) { 372 | lost_point += 40; 373 | } 374 | } 375 | } 376 | 377 | for (let col = 0; col < size; col += 1) { 378 | for (let row = 0; row < size - 6; row += 1) { 379 | if ( 380 | is_dark(row, col) && 381 | !is_dark(row + 1, col) && 382 | is_dark(row + 2, col) && 383 | is_dark(row + 3, col) && 384 | is_dark(row + 4, col) && 385 | !is_dark(row + 5, col) && 386 | is_dark(row + 6, col) 387 | ) { 388 | lost_point += 40; 389 | } 390 | } 391 | } 392 | 393 | // LEVEL4 394 | let dark_count = 0; 395 | 396 | for (let col = 0; col < size; col += 1) { 397 | for (let row = 0; row < size; row += 1) { 398 | if (is_dark(row, col)) { 399 | dark_count += 1; 400 | } 401 | } 402 | } 403 | 404 | const ratio = Math.abs((100 * dark_count) / size / size - 50) / 5; 405 | lost_point += ratio * 10; 406 | 407 | return lost_point; 408 | } 409 | 410 | /** 411 | * @param {QrBitBuffer} buffer 412 | * @param {Array<{ data_count: number, total_count: number }>} rs_blocks 413 | */ 414 | function create_bytes(buffer, rs_blocks) { 415 | let offset = 0; 416 | 417 | let max_dc_count = 0; 418 | let max_ec_count = 0; 419 | 420 | /** @type {number[][]} */ 421 | const dcdata = new Array(rs_blocks.length); 422 | 423 | /** @type {number[][]} */ 424 | const ecdata = new Array(rs_blocks.length); 425 | 426 | for (let r = 0; r < rs_blocks.length; r += 1) { 427 | const dc_count = rs_blocks[r].data_count; 428 | const ec_count = rs_blocks[r].total_count - dc_count; 429 | 430 | max_dc_count = Math.max(max_dc_count, dc_count); 431 | max_ec_count = Math.max(max_ec_count, ec_count); 432 | 433 | dcdata[r] = new Array(dc_count); 434 | 435 | for (let i = 0; i < dcdata[r].length; i += 1) { 436 | dcdata[r][i] = 0xff & buffer.get_buffer()[i + offset]; 437 | } 438 | offset += dc_count; 439 | 440 | const rs_poly = QRUtil.get_error_correct_polynominal(ec_count); 441 | const raw_poly = new QrPolynomial(dcdata[r], rs_poly.get_length() - 1); 442 | 443 | const mod_poly = raw_poly.mod(rs_poly); 444 | ecdata[r] = new Array(rs_poly.get_length() - 1); 445 | for (let i = 0; i < ecdata[r].length; i += 1) { 446 | const mod_index = i + mod_poly.get_length() - ecdata[r].length; 447 | ecdata[r][i] = mod_index >= 0 ? mod_poly.get_at(mod_index) : 0; 448 | } 449 | } 450 | 451 | let total_code_count = 0; 452 | for (let i = 0; i < rs_blocks.length; i += 1) { 453 | total_code_count += rs_blocks[i].total_count; 454 | } 455 | 456 | /** @type {number[]} */ 457 | const data = new Array(total_code_count); 458 | let index = 0; 459 | 460 | for (let i = 0; i < max_dc_count; i += 1) { 461 | for (let r = 0; r < rs_blocks.length; r += 1) { 462 | if (i < dcdata[r].length) { 463 | data[index] = dcdata[r][i]; 464 | index += 1; 465 | } 466 | } 467 | } 468 | 469 | for (let i = 0; i < max_ec_count; i += 1) { 470 | for (let r = 0; r < rs_blocks.length; r += 1) { 471 | if (i < ecdata[r].length) { 472 | data[index] = ecdata[r][i]; 473 | index += 1; 474 | } 475 | } 476 | } 477 | 478 | return data; 479 | } 480 | 481 | /** 482 | * @param {number} version 483 | * @param {number} error_correction_level 484 | * @param {Uint8Array} data 485 | */ 486 | function create_data(version, error_correction_level, data) { 487 | const rs_blocks = QRRSBlock.get_rs_blocks(version, error_correction_level); 488 | 489 | const buffer = new QrBitBuffer(); 490 | 491 | buffer.put(4, 4); 492 | buffer.put(data.length, QRUtil.get_length_in_bits(version)); 493 | buffer.put_bytes(data); 494 | 495 | // calc num max data. 496 | let total_data_count = 0; 497 | for (let i = 0; i < rs_blocks.length; i += 1) { 498 | total_data_count += rs_blocks[i].data_count; 499 | } 500 | 501 | if (buffer.get_length_in_bits() > total_data_count * 8) { 502 | throw ( 503 | 'code length overflow. (' + 504 | buffer.get_length_in_bits() + 505 | '>' + 506 | total_data_count * 8 + 507 | ')' 508 | ); 509 | } 510 | 511 | // end code 512 | if (buffer.get_length_in_bits() + 4 <= total_data_count * 8) { 513 | buffer.put(0, 4); 514 | } 515 | 516 | // padding 517 | while (buffer.get_length_in_bits() % 8 != 0) { 518 | buffer.put_bit(false); 519 | } 520 | 521 | // padding 522 | while (true) { 523 | if (buffer.get_length_in_bits() >= total_data_count * 8) { 524 | break; 525 | } 526 | buffer.put(PAD0, 8); 527 | 528 | if (buffer.get_length_in_bits() >= total_data_count * 8) { 529 | break; 530 | } 531 | buffer.put(PAD1, 8); 532 | } 533 | 534 | return create_bytes(buffer, rs_blocks); 535 | } 536 | 537 | const QRUtil = (function () { 538 | const PATTERN_POSITION_TABLE = [ 539 | [], 540 | [6, 18], 541 | [6, 22], 542 | [6, 26], 543 | [6, 30], 544 | [6, 34], 545 | [6, 22, 38], 546 | [6, 24, 42], 547 | [6, 26, 46], 548 | [6, 28, 50], 549 | [6, 30, 54], 550 | [6, 32, 58], 551 | [6, 34, 62], 552 | [6, 26, 46, 66], 553 | [6, 26, 48, 70], 554 | [6, 26, 50, 74], 555 | [6, 30, 54, 78], 556 | [6, 30, 56, 82], 557 | [6, 30, 58, 86], 558 | [6, 34, 62, 90], 559 | [6, 28, 50, 72, 94], 560 | [6, 26, 50, 74, 98], 561 | [6, 30, 54, 78, 102], 562 | [6, 28, 54, 80, 106], 563 | [6, 32, 58, 84, 110], 564 | [6, 30, 58, 86, 114], 565 | [6, 34, 62, 90, 118], 566 | [6, 26, 50, 74, 98, 122], 567 | [6, 30, 54, 78, 102, 126], 568 | [6, 26, 52, 78, 104, 130], 569 | [6, 30, 56, 82, 108, 134], 570 | [6, 34, 60, 86, 112, 138], 571 | [6, 30, 58, 86, 114, 142], 572 | [6, 34, 62, 90, 118, 146], 573 | [6, 30, 54, 78, 102, 126, 150], 574 | [6, 24, 50, 76, 102, 128, 154], 575 | [6, 28, 54, 80, 106, 132, 158], 576 | [6, 32, 58, 84, 110, 136, 162], 577 | [6, 26, 54, 82, 110, 138, 166], 578 | [6, 30, 58, 86, 114, 142, 170] 579 | ]; 580 | 581 | const G15 = 582 | (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0); 583 | const G18 = 584 | (1 << 12) | 585 | (1 << 11) | 586 | (1 << 10) | 587 | (1 << 9) | 588 | (1 << 8) | 589 | (1 << 5) | 590 | (1 << 2) | 591 | (1 << 0); 592 | const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1); 593 | 594 | /** 595 | * @param {number} data 596 | */ 597 | function get_bch_digit(data) { 598 | let digit = 0; 599 | while (data != 0) { 600 | digit += 1; 601 | data >>>= 1; 602 | } 603 | return digit; 604 | } 605 | 606 | return { 607 | /** 608 | * @param {number} data 609 | */ 610 | get_bch_type_info(data) { 611 | let d = data << 10; 612 | while (get_bch_digit(d) - get_bch_digit(G15) >= 0) { 613 | d ^= G15 << (get_bch_digit(d) - get_bch_digit(G15)); 614 | } 615 | return ((data << 10) | d) ^ G15_MASK; 616 | }, 617 | 618 | /** 619 | * @param {number} data 620 | */ 621 | get_bch_type_number(data) { 622 | let d = data << 12; 623 | while (get_bch_digit(d) - get_bch_digit(G18) >= 0) { 624 | d ^= G18 << (get_bch_digit(d) - get_bch_digit(G18)); 625 | } 626 | return (data << 12) | d; 627 | }, 628 | 629 | /** 630 | * @param {number} version 631 | */ 632 | get_pattern_position(version) { 633 | return PATTERN_POSITION_TABLE[version - 1]; 634 | }, 635 | 636 | /** 637 | * 638 | * @param {number} mask_pattern 639 | * @returns {(i: number, j: number) => boolean} 640 | */ 641 | get_mask_function(mask_pattern) { 642 | switch (mask_pattern) { 643 | case QRMaskPattern.PATTERN000: 644 | return function (i, j) { 645 | return (i + j) % 2 == 0; 646 | }; 647 | case QRMaskPattern.PATTERN001: 648 | return function (i, j) { 649 | return i % 2 == 0; 650 | }; 651 | case QRMaskPattern.PATTERN010: 652 | return function (i, j) { 653 | return j % 3 == 0; 654 | }; 655 | case QRMaskPattern.PATTERN011: 656 | return function (i, j) { 657 | return (i + j) % 3 == 0; 658 | }; 659 | case QRMaskPattern.PATTERN100: 660 | return function (i, j) { 661 | return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0; 662 | }; 663 | case QRMaskPattern.PATTERN101: 664 | return function (i, j) { 665 | return ((i * j) % 2) + ((i * j) % 3) == 0; 666 | }; 667 | case QRMaskPattern.PATTERN110: 668 | return function (i, j) { 669 | return (((i * j) % 2) + ((i * j) % 3)) % 2 == 0; 670 | }; 671 | case QRMaskPattern.PATTERN111: 672 | return function (i, j) { 673 | return (((i * j) % 3) + ((i + j) % 2)) % 2 == 0; 674 | }; 675 | 676 | default: 677 | throw 'bad mask_pattern:' + mask_pattern; 678 | } 679 | }, 680 | 681 | /** @param {number} error_correct_length */ 682 | get_error_correct_polynominal(error_correct_length) { 683 | let a = new QrPolynomial([1], 0); 684 | for (let i = 0; i < error_correct_length; i += 1) { 685 | a = a.multiply(new QrPolynomial([1, QRMath.gexp(i)], 0)); 686 | } 687 | return a; 688 | }, 689 | 690 | /** 691 | * @param {number} type 692 | */ 693 | get_length_in_bits(type) { 694 | if (1 <= type && type < 10) { 695 | // 1 - 9 696 | return 8; 697 | } else if (type < 27) { 698 | // 10 - 26 699 | return 16; 700 | } else if (type < 41) { 701 | // 27 - 40 702 | return 16; 703 | } else { 704 | throw 'type:' + type; 705 | } 706 | } 707 | }; 708 | })(); 709 | 710 | const QRMath = (function () { 711 | const EXP_TABLE = new Array(256); 712 | const LOG_TABLE = new Array(256); 713 | 714 | // initialize tables 715 | for (let i = 0; i < 8; i += 1) { 716 | EXP_TABLE[i] = 1 << i; 717 | } 718 | for (let i = 8; i < 256; i += 1) { 719 | EXP_TABLE[i] = 720 | EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8]; 721 | } 722 | for (let i = 0; i < 255; i += 1) { 723 | LOG_TABLE[EXP_TABLE[i]] = i; 724 | } 725 | 726 | return { 727 | /** @param {number} n */ 728 | glog(n) { 729 | if (n < 1) { 730 | throw 'glog(' + n + ')'; 731 | } 732 | 733 | return LOG_TABLE[n]; 734 | }, 735 | 736 | /** @param {number} n */ 737 | gexp(n) { 738 | while (n < 0) { 739 | n += 255; 740 | } 741 | 742 | while (n >= 256) { 743 | n -= 255; 744 | } 745 | 746 | return EXP_TABLE[n]; 747 | } 748 | }; 749 | })(); 750 | 751 | class QrPolynomial { 752 | #num; 753 | 754 | /** 755 | * @param {number[]} num 756 | * @param {number} shift 757 | */ 758 | constructor(num, shift) { 759 | if (typeof num.length == 'undefined') { 760 | throw num.length + '/' + shift; 761 | } 762 | 763 | let offset = 0; 764 | while (offset < num.length && num[offset] == 0) { 765 | offset += 1; 766 | } 767 | 768 | this.#num = new Array(num.length - offset + shift); 769 | for (let i = 0; i < num.length - offset; i += 1) { 770 | this.#num[i] = num[i + offset]; 771 | } 772 | } 773 | 774 | /** 775 | * @param {number} index 776 | */ 777 | get_at(index) { 778 | return this.#num[index]; 779 | } 780 | 781 | get_length() { 782 | return this.#num.length; 783 | } 784 | 785 | /** 786 | * @param {QrPolynomial} e 787 | */ 788 | multiply(e) { 789 | const num = new Array(this.get_length() + e.get_length() - 1); 790 | 791 | for (let i = 0; i < this.get_length(); i += 1) { 792 | for (let j = 0; j < e.get_length(); j += 1) { 793 | num[i + j] ^= QRMath.gexp( 794 | QRMath.glog(this.get_at(i)) + QRMath.glog(e.get_at(j)) 795 | ); 796 | } 797 | } 798 | 799 | return new QrPolynomial(num, 0); 800 | } 801 | 802 | /** 803 | * @param {QrPolynomial} e 804 | * @returns {QrPolynomial} 805 | */ 806 | mod(e) { 807 | if (this.get_length() - e.get_length() < 0) { 808 | return this; 809 | } 810 | 811 | const ratio = QRMath.glog(this.get_at(0)) - QRMath.glog(e.get_at(0)); 812 | 813 | const num = new Array(this.get_length()); 814 | for (let i = 0; i < this.get_length(); i += 1) { 815 | num[i] = this.get_at(i); 816 | } 817 | 818 | for (let i = 0; i < e.get_length(); i += 1) { 819 | num[i] ^= QRMath.gexp(QRMath.glog(e.get_at(i)) + ratio); 820 | } 821 | 822 | // recursive call 823 | return new QrPolynomial(num, 0).mod(e); 824 | } 825 | } 826 | 827 | const QRRSBlock = (function () { 828 | const RS_BLOCK_TABLE = [ 829 | // L 830 | // M 831 | // Q 832 | // H 833 | 834 | // 1 835 | [1, 26, 19], 836 | [1, 26, 16], 837 | [1, 26, 13], 838 | [1, 26, 9], 839 | 840 | // 2 841 | [1, 44, 34], 842 | [1, 44, 28], 843 | [1, 44, 22], 844 | [1, 44, 16], 845 | 846 | // 3 847 | [1, 70, 55], 848 | [1, 70, 44], 849 | [2, 35, 17], 850 | [2, 35, 13], 851 | 852 | // 4 853 | [1, 100, 80], 854 | [2, 50, 32], 855 | [2, 50, 24], 856 | [4, 25, 9], 857 | 858 | // 5 859 | [1, 134, 108], 860 | [2, 67, 43], 861 | [2, 33, 15, 2, 34, 16], 862 | [2, 33, 11, 2, 34, 12], 863 | 864 | // 6 865 | [2, 86, 68], 866 | [4, 43, 27], 867 | [4, 43, 19], 868 | [4, 43, 15], 869 | 870 | // 7 871 | [2, 98, 78], 872 | [4, 49, 31], 873 | [2, 32, 14, 4, 33, 15], 874 | [4, 39, 13, 1, 40, 14], 875 | 876 | // 8 877 | [2, 121, 97], 878 | [2, 60, 38, 2, 61, 39], 879 | [4, 40, 18, 2, 41, 19], 880 | [4, 40, 14, 2, 41, 15], 881 | 882 | // 9 883 | [2, 146, 116], 884 | [3, 58, 36, 2, 59, 37], 885 | [4, 36, 16, 4, 37, 17], 886 | [4, 36, 12, 4, 37, 13], 887 | 888 | // 10 889 | [2, 86, 68, 2, 87, 69], 890 | [4, 69, 43, 1, 70, 44], 891 | [6, 43, 19, 2, 44, 20], 892 | [6, 43, 15, 2, 44, 16], 893 | 894 | // 11 895 | [4, 101, 81], 896 | [1, 80, 50, 4, 81, 51], 897 | [4, 50, 22, 4, 51, 23], 898 | [3, 36, 12, 8, 37, 13], 899 | 900 | // 12 901 | [2, 116, 92, 2, 117, 93], 902 | [6, 58, 36, 2, 59, 37], 903 | [4, 46, 20, 6, 47, 21], 904 | [7, 42, 14, 4, 43, 15], 905 | 906 | // 13 907 | [4, 133, 107], 908 | [8, 59, 37, 1, 60, 38], 909 | [8, 44, 20, 4, 45, 21], 910 | [12, 33, 11, 4, 34, 12], 911 | 912 | // 14 913 | [3, 145, 115, 1, 146, 116], 914 | [4, 64, 40, 5, 65, 41], 915 | [11, 36, 16, 5, 37, 17], 916 | [11, 36, 12, 5, 37, 13], 917 | 918 | // 15 919 | [5, 109, 87, 1, 110, 88], 920 | [5, 65, 41, 5, 66, 42], 921 | [5, 54, 24, 7, 55, 25], 922 | [11, 36, 12, 7, 37, 13], 923 | 924 | // 16 925 | [5, 122, 98, 1, 123, 99], 926 | [7, 73, 45, 3, 74, 46], 927 | [15, 43, 19, 2, 44, 20], 928 | [3, 45, 15, 13, 46, 16], 929 | 930 | // 17 931 | [1, 135, 107, 5, 136, 108], 932 | [10, 74, 46, 1, 75, 47], 933 | [1, 50, 22, 15, 51, 23], 934 | [2, 42, 14, 17, 43, 15], 935 | 936 | // 18 937 | [5, 150, 120, 1, 151, 121], 938 | [9, 69, 43, 4, 70, 44], 939 | [17, 50, 22, 1, 51, 23], 940 | [2, 42, 14, 19, 43, 15], 941 | 942 | // 19 943 | [3, 141, 113, 4, 142, 114], 944 | [3, 70, 44, 11, 71, 45], 945 | [17, 47, 21, 4, 48, 22], 946 | [9, 39, 13, 16, 40, 14], 947 | 948 | // 20 949 | [3, 135, 107, 5, 136, 108], 950 | [3, 67, 41, 13, 68, 42], 951 | [15, 54, 24, 5, 55, 25], 952 | [15, 43, 15, 10, 44, 16], 953 | 954 | // 21 955 | [4, 144, 116, 4, 145, 117], 956 | [17, 68, 42], 957 | [17, 50, 22, 6, 51, 23], 958 | [19, 46, 16, 6, 47, 17], 959 | 960 | // 22 961 | [2, 139, 111, 7, 140, 112], 962 | [17, 74, 46], 963 | [7, 54, 24, 16, 55, 25], 964 | [34, 37, 13], 965 | 966 | // 23 967 | [4, 151, 121, 5, 152, 122], 968 | [4, 75, 47, 14, 76, 48], 969 | [11, 54, 24, 14, 55, 25], 970 | [16, 45, 15, 14, 46, 16], 971 | 972 | // 24 973 | [6, 147, 117, 4, 148, 118], 974 | [6, 73, 45, 14, 74, 46], 975 | [11, 54, 24, 16, 55, 25], 976 | [30, 46, 16, 2, 47, 17], 977 | 978 | // 25 979 | [8, 132, 106, 4, 133, 107], 980 | [8, 75, 47, 13, 76, 48], 981 | [7, 54, 24, 22, 55, 25], 982 | [22, 45, 15, 13, 46, 16], 983 | 984 | // 26 985 | [10, 142, 114, 2, 143, 115], 986 | [19, 74, 46, 4, 75, 47], 987 | [28, 50, 22, 6, 51, 23], 988 | [33, 46, 16, 4, 47, 17], 989 | 990 | // 27 991 | [8, 152, 122, 4, 153, 123], 992 | [22, 73, 45, 3, 74, 46], 993 | [8, 53, 23, 26, 54, 24], 994 | [12, 45, 15, 28, 46, 16], 995 | 996 | // 28 997 | [3, 147, 117, 10, 148, 118], 998 | [3, 73, 45, 23, 74, 46], 999 | [4, 54, 24, 31, 55, 25], 1000 | [11, 45, 15, 31, 46, 16], 1001 | 1002 | // 29 1003 | [7, 146, 116, 7, 147, 117], 1004 | [21, 73, 45, 7, 74, 46], 1005 | [1, 53, 23, 37, 54, 24], 1006 | [19, 45, 15, 26, 46, 16], 1007 | 1008 | // 30 1009 | [5, 145, 115, 10, 146, 116], 1010 | [19, 75, 47, 10, 76, 48], 1011 | [15, 54, 24, 25, 55, 25], 1012 | [23, 45, 15, 25, 46, 16], 1013 | 1014 | // 31 1015 | [13, 145, 115, 3, 146, 116], 1016 | [2, 74, 46, 29, 75, 47], 1017 | [42, 54, 24, 1, 55, 25], 1018 | [23, 45, 15, 28, 46, 16], 1019 | 1020 | // 32 1021 | [17, 145, 115], 1022 | [10, 74, 46, 23, 75, 47], 1023 | [10, 54, 24, 35, 55, 25], 1024 | [19, 45, 15, 35, 46, 16], 1025 | 1026 | // 33 1027 | [17, 145, 115, 1, 146, 116], 1028 | [14, 74, 46, 21, 75, 47], 1029 | [29, 54, 24, 19, 55, 25], 1030 | [11, 45, 15, 46, 46, 16], 1031 | 1032 | // 34 1033 | [13, 145, 115, 6, 146, 116], 1034 | [14, 74, 46, 23, 75, 47], 1035 | [44, 54, 24, 7, 55, 25], 1036 | [59, 46, 16, 1, 47, 17], 1037 | 1038 | // 35 1039 | [12, 151, 121, 7, 152, 122], 1040 | [12, 75, 47, 26, 76, 48], 1041 | [39, 54, 24, 14, 55, 25], 1042 | [22, 45, 15, 41, 46, 16], 1043 | 1044 | // 36 1045 | [6, 151, 121, 14, 152, 122], 1046 | [6, 75, 47, 34, 76, 48], 1047 | [46, 54, 24, 10, 55, 25], 1048 | [2, 45, 15, 64, 46, 16], 1049 | 1050 | // 37 1051 | [17, 152, 122, 4, 153, 123], 1052 | [29, 74, 46, 14, 75, 47], 1053 | [49, 54, 24, 10, 55, 25], 1054 | [24, 45, 15, 46, 46, 16], 1055 | 1056 | // 38 1057 | [4, 152, 122, 18, 153, 123], 1058 | [13, 74, 46, 32, 75, 47], 1059 | [48, 54, 24, 14, 55, 25], 1060 | [42, 45, 15, 32, 46, 16], 1061 | 1062 | // 39 1063 | [20, 147, 117, 4, 148, 118], 1064 | [40, 75, 47, 7, 76, 48], 1065 | [43, 54, 24, 22, 55, 25], 1066 | [10, 45, 15, 67, 46, 16], 1067 | 1068 | // 40 1069 | [19, 148, 118, 6, 149, 119], 1070 | [18, 75, 47, 31, 76, 48], 1071 | [34, 54, 24, 34, 55, 25], 1072 | [20, 45, 15, 61, 46, 16] 1073 | ]; 1074 | 1075 | /** 1076 | * @param {number} version 1077 | * @param {number} error_correction_level 1078 | */ 1079 | function get_rs_block_table(version, error_correction_level) { 1080 | switch (error_correction_level) { 1081 | case QRErrorCorrectionLevel.L: 1082 | return RS_BLOCK_TABLE[(version - 1) * 4 + 0]; 1083 | case QRErrorCorrectionLevel.M: 1084 | return RS_BLOCK_TABLE[(version - 1) * 4 + 1]; 1085 | case QRErrorCorrectionLevel.Q: 1086 | return RS_BLOCK_TABLE[(version - 1) * 4 + 2]; 1087 | case QRErrorCorrectionLevel.H: 1088 | return RS_BLOCK_TABLE[(version - 1) * 4 + 3]; 1089 | default: 1090 | return undefined; 1091 | } 1092 | } 1093 | 1094 | return { 1095 | /** 1096 | * @param {number} version 1097 | * @param {number} error_correction_level 1098 | */ 1099 | get_rs_blocks(version, error_correction_level) { 1100 | const rs_block = get_rs_block_table(version, error_correction_level); 1101 | 1102 | if (typeof rs_block == 'undefined') { 1103 | throw ( 1104 | 'bad rs block @ version:' + 1105 | version + 1106 | '/error_correction_level:' + 1107 | error_correction_level 1108 | ); 1109 | } 1110 | 1111 | const length = rs_block.length / 3; 1112 | 1113 | const list = []; 1114 | 1115 | for (let i = 0; i < length; i += 1) { 1116 | const count = rs_block[i * 3 + 0]; 1117 | const total_count = rs_block[i * 3 + 1]; 1118 | const data_count = rs_block[i * 3 + 2]; 1119 | 1120 | for (let j = 0; j < count; j += 1) { 1121 | list.push({ total_count, data_count }); 1122 | } 1123 | } 1124 | 1125 | return list; 1126 | } 1127 | }; 1128 | })(); 1129 | 1130 | class QrBitBuffer { 1131 | /** @type {number[]} */ 1132 | #buffer = []; 1133 | #length = 0; 1134 | 1135 | get_buffer() { 1136 | return this.#buffer; 1137 | } 1138 | 1139 | /** 1140 | * @param {number} num 1141 | * @param {number} length 1142 | */ 1143 | put(num, length) { 1144 | for (let i = 0; i < length; i += 1) { 1145 | this.put_bit(((num >>> (length - i - 1)) & 1) == 1); 1146 | } 1147 | } 1148 | 1149 | get_length_in_bits() { 1150 | return this.#length; 1151 | } 1152 | 1153 | /** 1154 | * @param {boolean} bit 1155 | */ 1156 | put_bit(bit) { 1157 | const buf_index = Math.floor(this.#length / 8); 1158 | if (this.#buffer.length <= buf_index) { 1159 | this.#buffer.push(0); 1160 | } 1161 | 1162 | if (bit) { 1163 | this.#buffer[buf_index] |= 0x80 >>> this.#length % 8; 1164 | } 1165 | 1166 | this.#length += 1; 1167 | } 1168 | 1169 | /** @param {Uint8Array} bytes */ 1170 | put_bytes(bytes) { 1171 | for (let i = 0; i < bytes.length; i += 1) { 1172 | this.put(bytes[i], 8); 1173 | } 1174 | } 1175 | } 1176 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { qr } from '../src/qr.js'; 2 | import test from 'node:test'; 3 | import * as assert from 'node:assert'; 4 | 5 | const input = 'http://www.example.com/ążśźęćńół'; 6 | 7 | const encoded = encodeURI(input); 8 | const unescaped = unescape(encoded); 9 | 10 | const output = ` 11 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 12 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 13 | xx x xx xx x xx xx 14 | xx xxxxx x xx xxx x xxx xxxxx xx 15 | xx x x xx xx xxxx x x x xx 16 | xx x x x x xx xxx xx x x xx 17 | xx x x xxx x x x x xx x x xx 18 | xx xxxxx xxx xx xx x xxxxx xx 19 | xx x x x x x x x x xx 20 | xxxxxxxxxx xxx x xx xxxxxxxxxxx 21 | xx x x x xxxxx xxxxx xx x xx 22 | xxxx xxxxxxx x xx x xxx xx 23 | xxxx x x x x x xxx xxx xxx 24 | xx x xxxx xxxx xx xxx x xx xx 25 | xx xx x x x xxxxxxxxx xxxx 26 | xx x xx x x x x x x xx xxxx xx 27 | xx xx xx xx xx xx x xx 28 | xx x xx x x x xx xxx xxx 29 | xx xx x x xx x xx xxx 30 | xxxxx xxxxxx xx xxxxxxx xxx 31 | xx x xx x x xxxxxx x x xxxx 32 | xxxxxxxxxx x xxx x xxxx xxxx xxxx 33 | xxx xx xx x xx x xxxx 34 | xxxxxxxxxx xx x x x xxx xx 35 | xx x x xx x x x x x xxx 36 | xx xxxxx x x xxx x xxx xx xx 37 | xx x x xx x x x x xx 38 | xx x x x xx x xx x xx x xx 39 | xx x x x x xx x x xx x xx x xx 40 | xx xxxxx xxxx x xx x x x xxx 41 | xx x x x xxx xxxx x xxx 42 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 43 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 44 | `.trim(); 45 | 46 | test('QRCode is correct', () => { 47 | // const qr = qr(-1, 'M'); 48 | // qr.addData(input); 49 | // // qr.addData('漢字'); 50 | // qr.make(); 51 | 52 | const modules = qr(input); 53 | 54 | const lines = output 55 | .split('\n') 56 | .slice(2, -2) 57 | .map((line) => line.slice(2, -2)); 58 | 59 | for (let r = 0; r < lines.length; r++) { 60 | for (let c = 0; c < lines[r].length; c++) { 61 | // const module = qr.isDark(r, c) ? ' ' : 'x'; 62 | assert.strictEqual( 63 | modules[r][c] ? ' ' : 'x', 64 | lines[r][c], 65 | `Module ${r}, ${c} is incorrect` 66 | ); 67 | } 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "strict": true, 6 | "emitDeclarationOnly": true, 7 | "declaration": true, 8 | "declarationDir": "types", 9 | "target": "ES2022" 10 | }, 11 | "include": ["src"] 12 | } --------------------------------------------------------------------------------