1<?php 2 3namespace Mpdf\QrCode; 4 5/** 6 * QR Code generator 7 * 8 * @license LGPL 9 */ 10class QrCode 11{ 12 13 /** 14 * Maximal allowed QR code version 15 * 16 * @var int 17 */ 18 private $maxVersion = 40; 19 20 /** 21 * ECC level 22 * 23 * @var string 24 */ 25 private $level; 26 27 /** 28 * QR code contents 29 * @var string 30 */ 31 private $value; 32 33 /** 34 * @var int 35 */ 36 private $length; 37 38 /** 39 * @var int 40 */ 41 private $version = 0; 42 43 /** 44 * Zone data size 45 * 46 * @var int 47 */ 48 private $size = 0; 49 50 /** 51 * QR code dimensions 52 * 53 * @var int 54 */ 55 private $qrSize = 0; 56 57 /** 58 * @var int[] 59 */ 60 private $bitData; // nb de bit de chacune des valeurs 61 62 /** 63 * @var int[] 64 */ 65 private $valData; // liste des valeurs de bit différents 66 67 /** 68 * @var int[] 69 */ 70 private $wordData = []; // liste des valeurs tout ramené à 8bit 71 72 /** 73 * @var int Current position 74 */ 75 private $ptr; 76 77 /** 78 * @var int 79 */ 80 private $dataPtr = 0; 81 82 /** 83 * @var int 84 */ 85 private $bitCount; 86 87 /** 88 * @var int 89 */ 90 private $dataBitLimit = 0; 91 92 /** 93 * @var int 94 */ 95 private $dataWordLimit = 0; 96 97 /** 98 * @var int 99 */ 100 private $totalWordLimit = 0; 101 102 /** 103 * @var int 104 */ 105 private $ec = 0; 106 107 /** 108 * @var int[] 109 */ 110 private $matrix = []; 111 112 /** 113 * @var int 114 */ 115 private $matrixRemain = 0; 116 117 /** 118 * @var int[] 119 */ 120 private $matrixXArray = []; 121 122 /** 123 * @var int[] 124 */ 125 private $matrixYArray = []; 126 127 /** 128 * @var int[] 129 */ 130 private $maskArray = []; 131 132 /** 133 * @var int[] 134 */ 135 private $formatInformationX1 = []; 136 137 /** 138 * @var int[] 139 */ 140 private $formatInformationY1 = []; 141 142 /** 143 * @var int[] 144 */ 145 private $formatInformationX2 = []; 146 147 /** 148 * @var int[] 149 */ 150 private $formatInformationY2 = []; 151 152 /** 153 * @var int[] 154 */ 155 private $rsBlockOrder = []; 156 157 /** 158 * @var int 159 */ 160 private $rsEccCodewords = 0; 161 162 /** 163 * @var int 164 */ 165 private $byteCount = 0; 166 167 /** 168 * @var int[] 169 */ 170 private $final = []; 171 172 /** 173 * @var bool 174 */ 175 private $disableBorder = false; 176 177 /** 178 * @var string 179 */ 180 const ERROR_CORRECTION_LOW = 'L'; 181 182 /** 183 * @var string 184 */ 185 const ERROR_CORRECTION_MEDIUM = 'M'; 186 187 /** 188 * @var string 189 */ 190 const ERROR_CORRECTION_QUARTILE = 'Q'; 191 192 /** 193 * @var string 194 */ 195 const ERROR_CORRECTION_HIGH = 'H'; 196 197 /** 198 * @param string $value Contents of the QR code 199 * @param string $level Level of error correction (ECC) : L, M, Q, H 200 */ 201 public function __construct($value, $level = 'L') 202 { 203 if (!$this->isAllowedErrorCorrectionLevel($level)) { 204 throw new \Mpdf\QrCode\QrCodeException('ECC not recognized; valid values are L, M, Q and H'); 205 } 206 207 $this->length = strlen($value); 208 if (!$this->length) { 209 throw new \Mpdf\QrCode\QrCodeException('No data for QrCode'); 210 } 211 212 $this->level = $level; 213 $this->value = $value; 214 215 $this->bitData = []; 216 $this->valData = []; 217 $this->ptr = 0; 218 $this->bitCount = 0; 219 220 $this->encode(); 221 $this->loadEcc(); 222 $this->makeECC(); 223 $this->makeMatrix(); 224 } 225 226 /** 227 * @return int QR code dimensions concerning disabled border 228 */ 229 public function getQrDimensions() 230 { 231 if ($this->disableBorder) { 232 return $this->qrSize - 8; 233 } 234 235 return $this->qrSize; 236 } 237 238 /** 239 * @return int QR code dimensions 240 */ 241 public function getQrSize() 242 { 243 return $this->qrSize; 244 } 245 246 public function disableBorder() 247 { 248 $this->disableBorder = true; 249 } 250 251 /** 252 * @return bool 253 */ 254 public function isBorderDisabled() 255 { 256 return $this->disableBorder; 257 } 258 259 /** 260 * @return mixed[] 261 */ 262 public function getFinal() 263 { 264 return $this->final; 265 } 266 267 private function addData($val, $bit, $next = true) 268 { 269 $this->valData[$this->ptr] = $val; 270 $this->bitData[$this->ptr] = $bit; 271 272 if ($next) { 273 $this->ptr++; 274 return $this->ptr - 1; 275 } 276 277 return $this->ptr; 278 } 279 280 private function encode() 281 { 282 // conversion des datas 283 if (preg_match('/\D/', $this->value)) { 284 if (preg_match('/[^0-9A-Z \$\*\%\+\-\.\/\:]/', $this->value)) { 285 286 $this->addData(4, 4); 287 288 $this->dataPtr = $this->addData($this->length, 8); /* #version 1-9 */ 289 $dataNumCorrection = [ 290 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 291 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 292 ]; 293 294 // data 295 for ($i = 0; $i < $this->length; $i++) { 296 $this->addData(ord(substr($this->value, $i, 1)), 8); 297 } 298 299 } else { 300 301 $this->addData(2, 4); 302 303 $this->dataPtr = $this->addData($this->length, 9); /* #version 1-9 */ 304 $dataNumCorrection = [ 305 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 306 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 307 ]; 308 309 $alNumHash = [ 310 '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, 311 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15, 'G' => 16, 'H' => 17, 'I' => 18, 312 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, 'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 313 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31, 'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, 314 '$' => 37, '%' => 38, '*' => 39, '+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44 315 ]; 316 317 for ($i = 0; $i < $this->length; $i++) { 318 if (($i % 2) === 0) { 319 $this->addData($alNumHash[$this->value[$i]], 6, false); 320 } else { 321 $this->addData($this->valData[$this->ptr] * 45 + $alNumHash[$this->value[$i]], 11); 322 } 323 } 324 325 unset($alNumHash); 326 327 if (isset($this->bitData[$this->ptr])) { 328 $this->ptr++; 329 } 330 } 331 332 } else { 333 334 $this->addData(1, 4); 335 336 $this->dataPtr = $this->addData($this->length, 10); /* #version 1-9 */ 337 $dataNumCorrection = [ 338 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 339 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 340 ]; 341 342 // data 343 for ($i = 0; $i < $this->length; $i++) { 344 if (($i % 3) === 0) { 345 $this->addData((int) $this->value[$i], 4, false); 346 } elseif (($i % 3) === 1) { 347 $this->addData($this->valData[$this->ptr] * 10 + (int) $this->value[$i], 7, false); 348 } else { 349 $this->addData($this->valData[$this->ptr] * 10 + (int) $this->value[$i], 10); 350 } 351 } 352 353 if (isset($this->bitData[$this->ptr])) { 354 $this->ptr++; 355 } 356 } 357 358 // calculate bit count 359 $this->bitCount = 0; 360 foreach ($this->bitData as $bit) { 361 $this->bitCount += $bit; 362 } 363 364 // code ECC 365 $ecHash = [ 366 static::ERROR_CORRECTION_LOW => 1, 367 static::ERROR_CORRECTION_MEDIUM => 0, 368 static::ERROR_CORRECTION_QUARTILE => 3, 369 static::ERROR_CORRECTION_HIGH => 2 370 ]; 371 372 $this->ec = $ecHash[$this->level]; 373 374 // bit size limit array 375 $maxBits = [ 376 0, 128, 224, 352, 512, 688, 864, 992, 1232, 1456, 1728, 2032, 2320, 2672, 2920, 3320, 3624, 4056, 4504, 5016, 5352, 377 5712, 6256, 6880, 7312, 8000, 8496, 9024, 9544, 10136, 10984, 11640, 12328, 13048, 13800, 14496, 15312, 15936, 16816, 17728, 18672, 378 379 152, 272, 440, 640, 864, 1088, 1248, 1552, 1856, 2192, 2592, 2960, 3424, 3688, 4184, 4712, 5176, 5768, 6360, 6888, 380 7456, 8048, 8752, 9392, 10208, 10960, 11744, 12248, 13048, 13880, 14744, 15640, 16568, 17528, 18448, 19472, 20528, 21616, 22496, 23648, 381 382 72, 128, 208, 288, 368, 480, 528, 688, 800, 976, 1120, 1264, 1440, 1576, 1784, 2024, 2264, 2504, 2728, 3080, 383 3248, 3536, 3712, 4112, 4304, 4768, 5024, 5288, 5608, 5960, 6344, 6760, 7208, 7688, 7888, 8432, 8768, 9136, 9776, 10208, 384 385 104, 176, 272, 384, 496, 608, 704, 880, 1056, 1232, 1440, 1648, 1952, 2088, 2360, 2600, 2936, 3176, 3560, 3880, 386 4096, 4544, 4912, 5312, 5744, 6032, 6464, 6968, 7288, 7880, 8264, 8920, 9368, 9848, 10288, 10832, 11408, 12016, 12656, 13328, 387 ]; 388 389 // version determination 390 $this->version = 1; 391 $i = 1 + 40 * $this->ec; 392 $j = $i + 39; 393 while ($i <= $j) { 394 if ($maxBits[$i] >= $this->bitCount + $dataNumCorrection[$this->version]) { 395 $this->dataBitLimit = $maxBits[$i]; 396 break; 397 } 398 $i++; 399 $this->version++; 400 } 401 402 if ($this->version > $this->maxVersion) { 403 throw new \Mpdf\QrCode\QrCodeException('QrCode version too large'); 404 } 405 406 // strlen bits of the value number fix 407 $this->bitCount += $dataNumCorrection[$this->version]; 408 $this->bitData[$this->dataPtr] += $dataNumCorrection[$this->version]; 409 $this->dataWordLimit = ($this->dataBitLimit / 8); 410 411 // maximal word counts 412 $maxWordCountArray = [ 413 0, 26, 44, 70, 100, 134, 172, 196, 242, 292, 346, 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085, 1156, 414 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185, 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706 415 ]; 416 417 $this->totalWordLimit = $maxWordCountArray[$this->version]; 418 $this->size = 17 + 4 * $this->version; 419 420 unset($maxBits, $dataNumCorrection, $maxWordCountArray, $ecHash); 421 422 // terminator 423 if ($this->bitCount <= $this->dataBitLimit - 4) { 424 $this->addData(0, 4); 425 } elseif ($this->bitCount < $this->dataBitLimit) { 426 $this->addData(0, $this->dataBitLimit - $this->bitCount); 427 } elseif ($this->bitCount > $this->dataBitLimit) { 428 throw new \Mpdf\QrCode\QrCodeException('QrCode data overflow error'); 429 } 430 431 // construction of 8bit words 432 $this->wordData = []; 433 $this->wordData[0] = 0; 434 $wordCount = 0; 435 436 $remainingBit = 8; 437 for ($i = 0; $i < $this->ptr; $i++) { 438 $bufferVal = $this->valData[$i]; 439 $bufferBit = $this->bitData[$i]; 440 441 $flag = true; 442 while ($flag) { 443 if ($remainingBit > $bufferBit) { 444 $this->wordData[$wordCount] = ((@$this->wordData[$wordCount] << $bufferBit) | $bufferVal); 445 $remainingBit -= $bufferBit; 446 $flag = false; 447 } else { 448 $bufferBit -= $remainingBit; 449 $this->wordData[$wordCount] = ((@$this->wordData[$wordCount] << $remainingBit) | ($bufferVal >> $bufferBit)); 450 $wordCount++; 451 452 if ($bufferBit === 0) { 453 $flag = false; 454 } else { 455 $bufferVal &= ((1 << $bufferBit) - 1); 456 } 457 458 if ($wordCount < $this->dataWordLimit - 1) { 459 $this->wordData[$wordCount] = 0; 460 } 461 $remainingBit = 8; 462 } 463 } 464 } 465 466 // completion of the last word if incomplete 467 if ($remainingBit < 8) { 468 $this->wordData[$wordCount] <<= $remainingBit; 469 } else { 470 $wordCount--; 471 } 472 473 // fill the rest 474 if ($wordCount < $this->dataWordLimit - 1) { 475 $flag = true; 476 while ($wordCount < $this->dataWordLimit - 1) { 477 $wordCount++; 478 if ($flag) { 479 $this->wordData[$wordCount] = 236; 480 } else { 481 $this->wordData[$wordCount] = 17; 482 } 483 $flag = !$flag; 484 } 485 } 486 } 487 488 private function loadEcc() 489 { 490 $matrixRemainBits = [0, 0, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0]; 491 492 $this->matrixRemain = $matrixRemainBits[$this->version]; 493 unset($matrixRemainBits); 494 495 // data file of geometry & mask for version V, ecc level N 496 $this->byteCount = $this->matrixRemain + 8 * $this->totalWordLimit; 497 498 $filename = __DIR__ . '/../data/qrv' . $this->version . '_' . $this->ec . '.dat'; 499 500 $fp1 = fopen($filename, 'rb'); 501 502 $this->matrixXArray = unpack('C*', fread($fp1, $this->byteCount)); 503 $this->matrixYArray = unpack('C*', fread($fp1, $this->byteCount)); 504 $this->maskArray = unpack('C*', fread($fp1, $this->byteCount)); 505 $this->formatInformationX2 = unpack('C*', fread($fp1, 15)); 506 $this->formatInformationY2 = unpack('C*', fread($fp1, 15)); 507 $this->rsEccCodewords = ord(fread($fp1, 1)); 508 $this->rsBlockOrder = unpack('C*', fread($fp1, 128)); 509 510 fclose($fp1); 511 512 $this->formatInformationX1 = [0, 1, 2, 3, 4, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8]; 513 $this->formatInformationY1 = [8, 8, 8, 8, 8, 8, 8, 8, 7, 5, 4, 3, 2, 1, 0]; 514 } 515 516 private function makeECC() 517 { 518 // calculating tables for RS encoding 519 $rsTable = []; 520 521 $filename = __DIR__ . '/../data/rsc' . $this->rsEccCodewords . '.dat'; 522 523 $fp0 = fopen($filename, 'rb'); 524 for ($i = 0; $i < 256; $i++) { 525 $rsTable[$i] = fread($fp0, $this->rsEccCodewords); 526 } 527 fclose($fp0); 528 529 // preparation 530 $j = 0; 531 $rsBlockNumber = 0; 532 $rsTemp[0] = ''; 533 534 foreach ($this->wordData as $wordItem) { 535 $rsTemp[$rsBlockNumber] .= chr($wordItem); 536 $j++; 537 if ($j >= $this->rsBlockOrder[$rsBlockNumber + 1] - $this->rsEccCodewords) { 538 $j = 0; 539 $rsBlockNumber++; 540 $rsTemp[$rsBlockNumber] = ''; 541 } 542 } 543 544 $rsBlockOrderNum = count($this->rsBlockOrder); 545 $data = []; 546 547 for ($rsBlockNumber = 0; $rsBlockNumber < $rsBlockOrderNum; $rsBlockNumber++) { 548 $rsCodewords = $this->rsBlockOrder[$rsBlockNumber + 1]; 549 $rsDataCodewords = $rsCodewords - $this->rsEccCodewords; 550 551 $rst = $rsTemp[$rsBlockNumber] . str_repeat(chr(0), $this->rsEccCodewords); 552 $paddingData = str_repeat(chr(0), $rsDataCodewords); 553 554 $j = $rsDataCodewords; 555 while ($j > 0) { 556 $first = ord($rst[0]); 557 558 if ($first) { 559 $leftChr = substr($rst, 1); 560 $cal = $rsTable[$first] . $paddingData; 561 $rst = $leftChr ^ $cal; 562 } else { 563 $rst = substr($rst, 1); 564 } 565 $j--; 566 } 567 568 $data[] = unpack('C*', $rst); 569 } 570 571 572 $this->wordData = array_merge($this->wordData, ...$data); 573 } 574 575 private function makeMatrix() 576 { 577 // preparation 578 $this->matrix = array_fill(0, $this->size, array_fill(0, $this->size, 0)); 579 580 // put in words 581 for ($i = 0; $i < $this->totalWordLimit; $i++) { 582 $word = $this->wordData[$i]; 583 for ($j = 8; $j > 0; $j--) { 584 $bitPos = ($i << 3) + $j; 585 $this->matrix[$this->matrixXArray[$bitPos]][$this->matrixYArray[$bitPos]] = ((255 * ($word & 1)) ^ $this->maskArray[$bitPos]); 586 $word >>= 1; 587 } 588 } 589 590 for ($k = $this->matrixRemain; $k > 0; $k--) { 591 $bitPos = $k + ($this->totalWordLimit << 3); 592 $this->matrix[$this->matrixXArray[$bitPos]][$this->matrixYArray[$bitPos]] = (255 ^ $this->maskArray[$bitPos]); 593 } 594 595 // mask select 596 $minDemeritScore = 0; 597 $hMaster = ''; 598 $vMaster = ''; 599 $k = 0; 600 while ($k < $this->size) { 601 $l = 0; 602 while ($l < $this->size) { 603 $hMaster .= chr($this->matrix[$l][$k]); 604 $vMaster .= chr($this->matrix[$k][$l]); 605 $l++; 606 } 607 $k++; 608 } 609 610 $i = 0; 611 $allMatrix = $this->size * $this->size; 612 613 $maskNumber = 0; 614 while ($i < 8) { 615 616 $demeritN1 = 0; 617 $ptnTemp = []; 618 $bit = 1 << $i; 619 $bitR = (~$bit) & 255; 620 $bitMask = str_repeat(chr($bit), $allMatrix); 621 $hor = $hMaster & $bitMask; 622 $ver = $vMaster & $bitMask; 623 624 $vShift1 = $ver . str_repeat(chr(170), $this->size); 625 $vShift2 = str_repeat(chr(170), $this->size) . $ver; 626 $vShift10 = $ver . str_repeat(chr(0), $this->size); 627 $vShift20 = str_repeat(chr(0), $this->size) . $ver; 628 $verOr = chunk_split(~($vShift1 | $vShift2), $this->size, chr(170)); 629 $verAnd = chunk_split(~($vShift10 & $vShift20), $this->size, chr(170)); 630 631 $hor = chunk_split(~$hor, $this->size, chr(170)); 632 $ver = chunk_split(~$ver, $this->size, chr(170)); 633 $hor = $hor . chr(170) . $ver; 634 635 $n1Search = '/' . str_repeat(chr(255), 5) . '+|' . str_repeat(chr($bitR), 5) . '+/'; 636 $n3Search = chr($bitR) . chr(255) . chr($bitR) . chr($bitR) . chr($bitR) . chr(255) . chr($bitR); 637 638 $demeritN3 = substr_count($hor, $n3Search) * 40; 639 $demeritN4 = floor(abs(((100 * (substr_count($ver, chr($bitR)) / $this->byteCount)) - 50) / 5)) * 10; 640 641 $n2Search1 = '/' . chr($bitR) . chr($bitR) . '+/'; 642 $n2Search2 = '/' . chr(255) . chr(255) . '+/'; 643 $demeritN2 = 0; 644 645 preg_match_all($n2Search1, $verAnd, $ptnTemp); 646 foreach ($ptnTemp[0] as $strTemp) { 647 $demeritN2 += (strlen($strTemp) - 1); 648 } 649 650 $ptnTemp = []; 651 preg_match_all($n2Search2, $verOr, $ptnTemp); 652 foreach ($ptnTemp[0] as $strTemp) { 653 $demeritN2 += (strlen($strTemp) - 1); 654 } 655 $demeritN2 *= 3; 656 657 $ptnTemp = []; 658 659 preg_match_all($n1Search, $hor, $ptnTemp); 660 foreach ($ptnTemp[0] as $strTemp) { 661 $demeritN1 += (strlen($strTemp) - 2); 662 } 663 $demeritScore = $demeritN1 + $demeritN2 + $demeritN3 + $demeritN4; 664 665 if ($demeritScore <= $minDemeritScore || $i === 0) { 666 $maskNumber = $i; 667 $minDemeritScore = $demeritScore; 668 } 669 670 $i++; 671 } 672 673 $maskContent = 1 << $maskNumber; 674 675 $formatInformationValue = (($this->ec << 3) | $maskNumber); 676 $formatInformationArray = [ 677 '101010000010010', '101000100100101', 678 '101111001111100', '101101101001011', '100010111111001', '100000011001110', 679 '100111110010111', '100101010100000', '111011111000100', '111001011110011', 680 '111110110101010', '111100010011101', '110011000101111', '110001100011000', 681 '110110001000001', '110100101110110', '001011010001001', '001001110111110', 682 '001110011100111', '001100111010000', '000011101100010', '000001001010101', 683 '000110100001100', '000100000111011', '011010101011111', '011000001101000', 684 '011111100110001', '011101000000110', '010010010110100', '010000110000011', 685 '010111011011010', '010101111101101' 686 ]; 687 688 for ($i = 0; $i < 15; $i++) { 689 $content = (int) $formatInformationArray[$formatInformationValue][$i]; 690 691 $this->matrix[$this->formatInformationX1[$i]][$this->formatInformationY1[$i]] = $content * 255; 692 $this->matrix[$this->formatInformationX2[$i + 1]][$this->formatInformationY2[$i + 1]] = $content * 255; 693 } 694 695 $this->final = unpack('C*', file_get_contents(__DIR__ . '/../data/modele' . $this->version . '.dat')); 696 $this->qrSize = $this->size + 8; 697 698 for ($x = 0; $x < $this->size; $x++) { 699 for ($y = 0; $y < $this->size; $y++) { 700 if ($this->matrix[$x][$y] & $maskContent) { 701 $this->final[($x + 4) + ($y + 4) * $this->qrSize + 1] = true; 702 } 703 } 704 } 705 } 706 707 private function isAllowedErrorCorrectionLevel($level) 708 { 709 return \in_array($level, [ 710 static::ERROR_CORRECTION_LOW, 711 static::ERROR_CORRECTION_MEDIUM, 712 static::ERROR_CORRECTION_QUARTILE, 713 static::ERROR_CORRECTION_HIGH, 714 ], true); 715 } 716 717} 718