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