Passed
Push — gh-pages ( 20c441...dd59e5 )
by
unknown
02:54 queued 01:00
created

QRMatrix::mask()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 7
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 13
rs 9.6111
1
<?php
2
/**
3
 * Class QRMatrix
4
 *
5
 * @created      15.11.2017
6
 * @author       Smiley <[email protected]>
7
 * @copyright    2017 Smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\QRCode\Data;
12
13
use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
14
15
use SplFixedArray;
16
use function array_fill, array_push, array_unshift, floor, max, min, range;
17
18
/**
19
 * Holds a numerical representation of the final QR Code;
20
 * maps the ECC coded binary data and applies the mask pattern
21
 *
22
 * @see http://www.thonky.com/qr-code-tutorial/format-version-information
23
 */
24
final class QRMatrix{
25
26
	/** @var int */
27
	public const M_NULL       = 0b000000000000;
28
	/** @var int */
29
	public const M_DARKMODULE = 0b000000000001;
30
	/** @var int */
31
	public const M_DATA       = 0b000000000010;
32
	/** @var int */
33
	public const M_FINDER     = 0b000000000100;
34
	/** @var int */
35
	public const M_SEPARATOR  = 0b000000001000;
36
	/** @var int */
37
	public const M_ALIGNMENT  = 0b000000010000;
38
	/** @var int */
39
	public const M_TIMING     = 0b000000100000;
40
	/** @var int */
41
	public const M_FORMAT     = 0b000001000000;
42
	/** @var int */
43
	public const M_VERSION    = 0b000010000000;
44
	/** @var int */
45
	public const M_QUIETZONE  = 0b000100000000;
46
	/** @var int */
47
	public const M_LOGO       = 0b001000000000;
48
	/** @var int */
49
	public const M_FINDER_DOT = 0b010000000000;
50
	/** @var int */
51
	public const M_TEST       = 0b011111111111;
52
	/** @var int */
53
	public const IS_DARK      = 0b100000000000;
54
55
	/**
56
	 * the used mask pattern, set via QRMatrix::mask()
57
	 */
58
	private ?MaskPattern $maskPattern = null;
59
60
	/**
61
	 * the size (side length) of the matrix, including quiet zone (if created)
62
	 */
63
	private int $moduleCount;
64
65
	/**
66
	 * the actual matrix data array
67
	 *
68
	 * @var int[][]
69
	 */
70
	private array $matrix;
71
72
	/**
73
	 * the current ECC level
74
	 */
75
	private EccLevel $eccLevel;
76
77
	/**
78
	 * a Version instance
79
	 */
80
	private Version $version;
81
82
	/**
83
	 * QRMatrix constructor.
84
	 */
85
	public function __construct(Version $version, EccLevel $eccLevel){
86
		$this->version     = $version;
87
		$this->eccLevel    = $eccLevel;
88
		$this->moduleCount = $this->version->getDimension();
89
		$this->matrix      = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
90
	}
91
92
	/**
93
	 * shortcut to initialize the matrix
94
	 */
95
	public function init(MaskPattern $maskPattern, bool $test = null):self{
96
		return $this
97
			->setFinderPattern()
98
			->setSeparators()
99
			->setAlignmentPattern()
100
			->setTimingPattern()
101
			->setVersionNumber($test)
102
			->setFormatInfo($maskPattern, $test)
103
			->setDarkModule()
104
		;
105
	}
106
107
	/**
108
	 * Returns the data matrix, returns a pure boolean representation if $boolean is set to true
109
	 *
110
	 * @return int[][]|bool[][]
111
	 */
112
	public function matrix(bool $boolean = false):array{
113
114
		if(!$boolean){
115
			return $this->matrix;
116
		}
117
118
		$matrix = [];
119
120
		foreach($this->matrix as $y => $row){
121
			$matrix[$y] = [];
122
123
			foreach($row as $x => $val){
124
				$matrix[$y][$x] = ($val & $this::IS_DARK) === $this::IS_DARK;
125
			}
126
		}
127
128
		return $matrix;
129
	}
130
131
	/**
132
	 * Returns the current version number
133
	 */
134
	public function version():Version{
135
		return $this->version;
136
	}
137
138
	/**
139
	 * Returns the current ECC level
140
	 */
141
	public function eccLevel():EccLevel{
142
		return $this->eccLevel;
143
	}
144
145
	/**
146
	 * Returns the current mask pattern
147
	 */
148
	public function maskPattern():?MaskPattern{
149
		return $this->maskPattern;
150
	}
151
152
	/**
153
	 * Returns the absoulute size of the matrix, including quiet zone (after setting it).
154
	 *
155
	 * size = version * 4 + 17 [ + 2 * quietzone size]
156
	 */
157
	public function size():int{
158
		return $this->moduleCount;
159
	}
160
161
	/**
162
	 * Returns the value of the module at position [$x, $y]
163
	 */
164
	public function get(int $x, int $y):int{
165
		return $this->matrix[$y][$x];
166
	}
167
168
	/**
169
	 * Sets the $M_TYPE value for the module at position [$x, $y]
170
	 *
171
	 *   true  => $M_TYPE | 0x800
172
	 *   false => $M_TYPE
173
	 */
174
	public function set(int $x, int $y, bool $value, int $M_TYPE):self{
175
		$this->matrix[$y][$x] = $M_TYPE | ($value ? $this::IS_DARK : 0);
176
177
		return $this;
178
	}
179
180
	/**
181
	 * Flips the value of the module
182
	 */
183
	public function flip(int $x, int $y):self{
184
		$this->matrix[$y][$x] ^= $this::IS_DARK;
185
186
		return $this;
187
	}
188
189
	/**
190
	 * Checks whether a module is of the given $M_TYPE
191
	 *
192
	 *   true => $value & $M_TYPE === $M_TYPE
193
	 */
194
	public function checkType(int $x, int $y, int $M_TYPE):bool{
195
		return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE;
196
	}
197
198
	/**
199
	 * Checks whether a module is true (dark) or false (light)
200
	 *
201
	 *   true  => $value & 0x800 === 0x800
202
	 *   false => $value & 0x800 === 0
203
	 */
204
	public function check(int $x, int $y):bool{
205
		return $this->checkType($x, $y, $this::IS_DARK);
206
	}
207
208
	/**
209
	 * Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder
210
	 */
211
	public function setDarkModule():self{
212
		$this->set(8, 4 * $this->version->getVersionNumber() + 9, true, $this::M_DARKMODULE);
213
214
		return $this;
215
	}
216
217
	/**
218
	 * Draws the 7x7 finder patterns in the corners top left/right and bottom left
219
	 *
220
	 * ISO/IEC 18004:2000 Section 7.3.2
221
	 */
222
	public function setFinderPattern():self{
223
224
		$pos = [
225
			[0, 0], // top left
226
			[$this->moduleCount - 7, 0], // bottom left
227
			[0, $this->moduleCount - 7], // top right
228
		];
229
230
		foreach($pos as $c){
231
			for($y = 0; $y < 7; $y++){
232
				for($x = 0; $x < 7; $x++){
233
					// outer (dark) 7*7 square
234
					if($x === 0 || $x === 6 || $y === 0 || $y === 6){
235
						$this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER);
236
					}
237
					// inner (light) 5*5 square
238
					elseif($x === 1 || $x === 5 || $y === 1 || $y === 5){
239
						$this->set($c[0] + $y, $c[1] + $x, false, $this::M_FINDER);
240
					}
241
					// 3*3 dot
242
					else{
243
						$this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER_DOT);
244
					}
245
				}
246
			}
247
		}
248
249
		return $this;
250
	}
251
252
	/**
253
	 * Draws the separator lines around the finder patterns
254
	 *
255
	 * ISO/IEC 18004:2000 Section 7.3.3
256
	 */
257
	public function setSeparators():self{
258
259
		$h = [
260
			[7, 0],
261
			[$this->moduleCount - 8, 0],
262
			[7, $this->moduleCount - 8],
263
		];
264
265
		$v = [
266
			[7, 7],
267
			[$this->moduleCount - 1, 7],
268
			[7, $this->moduleCount - 8],
269
		];
270
271
		for($c = 0; $c < 3; $c++){
272
			for($i = 0; $i < 8; $i++){
273
				$this->set($h[$c][0]     , $h[$c][1] + $i, false, $this::M_SEPARATOR);
274
				$this->set($v[$c][0] - $i, $v[$c][1]     , false, $this::M_SEPARATOR);
275
			}
276
		}
277
278
		return $this;
279
	}
280
281
282
	/**
283
	 * Draws the 5x5 alignment patterns
284
	 *
285
	 * ISO/IEC 18004:2000 Section 7.3.5
286
	 */
287
	public function setAlignmentPattern():self{
288
		$alignmentPattern = $this->version->getAlignmentPattern();
289
290
		foreach($alignmentPattern as $y){
291
			foreach($alignmentPattern as $x){
292
293
				// skip existing patterns
294
				if($this->matrix[$y][$x] !== $this::M_NULL){
295
					continue;
296
				}
297
298
				for($ry = -2; $ry <= 2; $ry++){
299
					for($rx = -2; $rx <= 2; $rx++){
300
						$v = ($ry === 0 && $rx === 0) || $ry === 2 || $ry === -2 || $rx === 2 || $rx === -2;
301
302
						$this->set($x + $rx, $y + $ry, $v, $this::M_ALIGNMENT);
303
					}
304
				}
305
306
			}
307
		}
308
309
		return $this;
310
	}
311
312
313
	/**
314
	 * Draws the timing pattern (h/v checkered line between the finder patterns)
315
	 *
316
	 * ISO/IEC 18004:2000 Section 7.3.4
317
	 */
318
	public function setTimingPattern():self{
319
320
		foreach(range(8, $this->moduleCount - 8 - 1) as $i){
321
322
			if($this->matrix[6][$i] !== $this::M_NULL || $this->matrix[$i][6] !== $this::M_NULL){
323
				continue;
324
			}
325
326
			$v = $i % 2 === 0;
327
328
			$this->set($i, 6, $v, $this::M_TIMING); // h
329
			$this->set(6, $i, $v, $this::M_TIMING); // v
330
		}
331
332
		return $this;
333
	}
334
335
	/**
336
	 * Draws the version information, 2x 3x6 pixel
337
	 *
338
	 * ISO/IEC 18004:2000 Section 8.10
339
	 */
340
	public function setVersionNumber(bool $test = null):self{
341
		$bits = $this->version->getVersionPattern();
342
343
		if($bits !== null){
344
345
			for($i = 0; $i < 18; $i++){
346
				$a = (int)($i / 3);
347
				$b = $i % 3 + $this->moduleCount - 8 - 3;
348
				$v = !$test && (($bits >> $i) & 1) === 1;
0 ignored issues
show
Bug Best Practice introduced by
The expression $test of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
349
350
				$this->set($b, $a, $v, $this::M_VERSION); // ne
351
				$this->set($a, $b, $v, $this::M_VERSION); // sw
352
			}
353
354
		}
355
356
		return $this;
357
	}
358
359
	/**
360
	 * Draws the format info along the finder patterns
361
	 *
362
	 * ISO/IEC 18004:2000 Section 8.9
363
	 */
364
	public function setFormatInfo(MaskPattern $maskPattern, bool $test = null):self{
365
		$bits = $this->eccLevel->getformatPattern($maskPattern);
366
367
		for($i = 0; $i < 15; $i++){
368
			$v = !$test && (($bits >> $i) & 1) === 1;
0 ignored issues
show
Bug Best Practice introduced by
The expression $test of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
369
370
			if($i < 6){
371
				$this->set(8, $i, $v, $this::M_FORMAT);
372
			}
373
			elseif($i < 8){
374
				$this->set(8, $i + 1, $v, $this::M_FORMAT);
375
			}
376
			else{
377
				$this->set(8, $this->moduleCount - 15 + $i, $v, $this::M_FORMAT);
378
			}
379
380
			if($i < 8){
381
				$this->set($this->moduleCount - $i - 1, 8, $v, $this::M_FORMAT);
382
			}
383
			elseif($i < 9){
384
				$this->set(15 - $i, 8, $v, $this::M_FORMAT);
385
			}
386
			else{
387
				$this->set(15 - $i - 1, 8, $v, $this::M_FORMAT);
388
			}
389
390
		}
391
392
		$this->set(8, $this->moduleCount - 8, !$test, $this::M_FORMAT);
393
394
		return $this;
395
	}
396
397
	/**
398
	 * Draws the "quiet zone" of $size around the matrix
399
	 *
400
	 * ISO/IEC 18004:2000 Section 7.3.7
401
	 *
402
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
403
	 */
404
	public function setQuietZone(int $size = null):self{
405
406
		if($this->matrix[$this->moduleCount - 1][$this->moduleCount - 1] === $this::M_NULL){
407
			throw new QRCodeDataException('use only after writing data');
408
		}
409
410
		$size = $size !== null
411
			? max(0, min($size, floor($this->moduleCount / 2)))
412
			: 4;
413
414
		for($y = 0; $y < $this->moduleCount; $y++){
415
			for($i = 0; $i < $size; $i++){
416
				array_unshift($this->matrix[$y], $this::M_QUIETZONE);
417
				array_push($this->matrix[$y], $this::M_QUIETZONE);
418
			}
419
		}
420
421
		$this->moduleCount += ($size * 2);
422
423
		$r = array_fill(0, $this->moduleCount, $this::M_QUIETZONE);
424
425
		for($i = 0; $i < $size; $i++){
426
			array_unshift($this->matrix, $r);
427
			array_push($this->matrix, $r);
428
		}
429
430
		return $this;
431
	}
432
433
	/**
434
	 * Clears a space of $width * $height in order to add a logo or text.
435
	 *
436
	 * Additionally, the logo space can be positioned within the QR Code - respecting the main functional patterns -
437
	 * using $startX and $startY. If either of these are null, the logo space will be centered in that direction.
438
	 * ECC level "H" (30%) is required.
439
	 *
440
	 * Please note that adding a logo space minimizes the error correction capacity of the QR Code and
441
	 * created images may become unreadable, especially when printed with a chance to receive damage.
442
	 * Please test thoroughly before using this feature in production.
443
	 *
444
	 * This method should be called from within an output module (after the matrix has been filled with data).
445
	 * Note that there is no restiction on how many times this method could be called on the same matrix instance.
446
	 *
447
	 * @link https://github.com/chillerlan/php-qrcode/issues/52
448
	 *
449
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
450
	 */
451
	public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):self{
452
453
		// for logos we operate in ECC H (30%) only
454
		if($this->eccLevel->getLevel() !== EccLevel::H){
455
			throw new QRCodeDataException('ECC level "H" required to add logo space');
456
		}
457
458
		// we need uneven sizes to center the logo space, adjust if needed
459
		if($startX === null && ($width % 2) === 0){
460
			$width++;
461
		}
462
463
		if($startY === null && ($height % 2) === 0){
464
			$height++;
465
		}
466
467
		// $this->moduleCount includes the quiet zone (if created), we need the QR size here
468
		$length = $this->version->getDimension();
469
470
		// throw if the logo space exceeds the maximum error correction capacity
471
		if($width * $height > floor($length * $length * 0.2)){
472
			throw new QRCodeDataException('logo space exceeds the maximum error correction capacity');
473
		}
474
475
		// quiet zone size
476
		$qz    = ($this->moduleCount - $length) / 2;
477
		// skip quiet zone and the first 9 rows/columns (finder-, mode-, version- and timing patterns)
478
		$start = $qz + 9;
479
		// skip quiet zone
480
		$end   = $this->moduleCount - $qz;
481
482
		// determine start coordinates
483
		$startX = ($startX !== null ? $startX : ($length - $width) / 2) + $qz;
484
		$startY = ($startY !== null ? $startY : ($length - $height) / 2) + $qz;
485
486
		// clear the space
487
		foreach($this->matrix as $y => $row){
488
			foreach($row as $x => $val){
489
				// out of bounds, skip
490
				if($x < $start || $y < $start ||$x >= $end || $y >= $end){
491
					continue;
492
				}
493
				// a match
494
				if($x >= $startX && $x < ($startX + $width) && $y >= $startY && $y < ($startY + $height)){
495
					$this->set($x, $y, false, $this::M_LOGO);
496
				}
497
			}
498
		}
499
500
		return $this;
501
	}
502
503
	/**
504
	 * Maps the binary $data array from QRData::maskECC() on the matrix,
505
	 * masking the data using $maskPattern (ISO/IEC 18004:2000 Section 8.8)
506
	 *
507
	 * @see \chillerlan\QRCode\Data\QRData::maskECC()
508
	 *
509
	 * @param \SplFixedArray<int> $data
510
	 *
511
	 * @return \chillerlan\QRCode\Data\QRMatrix
512
	 */
513
	public function mapData(SplFixedArray $data):self{
514
		$byteCount         = $data->count();
515
		$y                 = $this->moduleCount - 1;
516
		$inc               = -1;
517
		$byteIndex         = 0;
518
		$bitIndex          = 7;
519
520
		for($i = $y; $i > 0; $i -= 2){
521
522
			if($i === 6){
523
				$i--;
524
			}
525
526
			while(true){
527
				for($c = 0; $c < 2; $c++){
528
					$x = $i - $c;
529
530
					if($this->matrix[$y][$x] === $this::M_NULL){
531
						$v = false;
532
533
						if($byteIndex < $byteCount){
534
							$v = (($data[$byteIndex] >> $bitIndex) & 1) === 1;
535
						}
536
537
						$this->matrix[$y][$x] = $this::M_DATA | ($v ? $this::IS_DARK : 0);
538
						$bitIndex--;
539
540
						if($bitIndex === -1){
541
							$byteIndex++;
542
							$bitIndex = 7;
543
						}
544
545
					}
546
				}
547
548
				$y += $inc;
549
550
				if($y < 0 || $this->moduleCount <= $y){
551
					$y   -=  $inc;
552
					$inc  = -$inc;
553
554
					break;
555
				}
556
557
			}
558
		}
559
560
		return $this;
561
	}
562
563
	/**
564
	 * Applies the mask pattern
565
	 *
566
	 * ISO/IEC 18004:2000 Section 8.8.1
567
	 */
568
	public function mask(MaskPattern $maskPattern):self{
569
		$this->maskPattern = $maskPattern;
570
		$mask              = $this->maskPattern->getMask();
571
572
		foreach($this->matrix as $y => &$row){
573
			foreach($row as $x => &$val){
574
				if($mask($x, $y) === 0 && ($val & $this::M_DATA) === $this::M_DATA){
575
					$val ^= $this::IS_DARK;
576
				}
577
			}
578
		}
579
580
		return $this;
581
	}
582
583
}
584