Passed
Push — main ( 48b6c2...5c2c92 )
by smiley
02:24
created

BitMatrix::set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 7
rs 10
c 1
b 0
f 0
1
<?php
2
/**
3
 * Class BitMatrix
4
 *
5
 * @created      17.01.2021
6
 * @author       ZXing Authors
7
 * @author       Smiley <[email protected]>
8
 * @copyright    2021 Smiley
9
 * @license      Apache-2.0
10
 */
11
12
namespace chillerlan\QRCode\Decoder;
13
14
use chillerlan\QRCode\Common\{FormatInformation, Version};
15
use InvalidArgumentException, RuntimeException;
16
use function array_fill, count;
17
use const PHP_INT_MAX, PHP_INT_SIZE;
18
19
/**
20
 *
21
 */
22
final class BitMatrix{
23
24
	private int                $dimension;
25
	private int                $rowSize;
26
	private array              $bits;
27
	private ?Version           $version    = null;
28
	private ?FormatInformation $formatInfo = null;
29
	private bool               $mirror     = false;
30
31
	/**
32
	 *
33
	 */
34
	public function __construct(int $dimension){
35
		$this->dimension = $dimension;
36
		$this->rowSize   = ((int)(($this->dimension + 0x1f) / 0x20));
37
		$this->bits      = array_fill(0, $this->rowSize * $this->dimension, 0);
38
	}
39
40
	/**
41
	 * <p>Sets the given bit to true.</p>
42
	 *
43
	 * @param int $x ;  The horizontal component (i.e. which column)
44
	 * @param int $y ;  The vertical component (i.e. which row)
45
	 */
46
	public function set(int $x, int $y):self{
47
		$offset = (int)($y * $this->rowSize + ($x / 0x20));
48
49
		$this->bits[$offset] ??= 0;
50
		$this->bits[$offset] |= ($this->bits[$offset] |= 1 << ($x & 0x1f));
51
52
		return $this;
53
	}
54
55
	/**
56
	 * <p>Flips the given bit. 1 << (0xf9 & 0x1f)</p>
57
	 *
58
	 * @param int $x ;  The horizontal component (i.e. which column)
59
	 * @param int $y ;  The vertical component (i.e. which row)
60
	 */
61
	public function flip(int $x, int $y):self{
62
		$offset = $y * $this->rowSize + (int)($x / 0x20);
63
64
		$this->bits[$offset] = ($this->bits[$offset] ^ (1 << ($x & 0x1f)));
65
66
		return $this;
67
	}
68
69
	/**
70
	 * <p>Sets a square region of the bit matrix to true.</p>
71
	 *
72
	 * @param int $left   ;  The horizontal position to begin at (inclusive)
73
	 * @param int $top    ;  The vertical position to begin at (inclusive)
74
	 * @param int $width  ;  The width of the region
75
	 * @param int $height ;  The height of the region
76
	 *
77
	 * @throws \InvalidArgumentException
78
	 */
79
	public function setRegion(int $left, int $top, int $width, int $height):self{
80
81
		if($top < 0 || $left < 0){
82
			throw new InvalidArgumentException('Left and top must be nonnegative');
83
		}
84
85
		if($height < 1 || $width < 1){
86
			throw new InvalidArgumentException('Height and width must be at least 1');
87
		}
88
89
		$right  = $left + $width;
90
		$bottom = $top + $height;
91
92
		if($bottom > $this->dimension || $right > $this->dimension){
93
			throw new InvalidArgumentException('The region must fit inside the matrix');
94
		}
95
96
		for($y = $top; $y < $bottom; $y++){
97
			$yOffset = $y * $this->rowSize;
98
99
			for($x = $left; $x < $right; $x++){
100
				$xOffset              = $yOffset + (int)($x / 0x20);
101
				$this->bits[$xOffset] = ($this->bits[$xOffset] |= 1 << ($x & 0x1f));
102
			}
103
		}
104
105
		return $this;
106
	}
107
108
	/**
109
	 * @return int The dimension (width/height) of the matrix
110
	 */
111
	public function getDimension():int{
112
		return $this->dimension;
113
	}
114
115
	/**
116
	 *
117
	 */
118
	public function getFormatInfo():?FormatInformation{
119
		return $this->formatInfo;
120
	}
121
122
	/**
123
	 *
124
	 */
125
	public function getVersion():?Version{
126
		return $this->version;
127
	}
128
129
	/**
130
	 * <p>Gets the requested bit, where true means black.</p>
131
	 *
132
	 * @param int $x The horizontal component (i.e. which column)
133
	 * @param int $y The vertical component (i.e. which row)
134
	 *
135
	 * @return bool value of given bit in matrix
136
	 */
137
	public function get(int $x, int $y):bool{
138
		$offset = (int)($y * $this->rowSize + ($x / 0x20));
139
140
		$this->bits[$offset] ??= 0;
141
142
		return ($this->uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0;
143
	}
144
145
	/**
146
	 * See ISO 18004:2006 Annex E
147
	 */
148
	private function buildFunctionPattern():self{
149
		$dimension = $this->version->getDimension();
0 ignored issues
show
Bug introduced by
The method getDimension() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

149
		/** @scrutinizer ignore-call */ 
150
  $dimension = $this->version->getDimension();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
150
		$bitMatrix = new self($dimension);
151
152
		// Top left finder pattern + separator + format
153
		$bitMatrix->setRegion(0, 0, 9, 9);
154
		// Top right finder pattern + separator + format
155
		$bitMatrix->setRegion($dimension - 8, 0, 8, 9);
156
		// Bottom left finder pattern + separator + format
157
		$bitMatrix->setRegion(0, $dimension - 8, 9, 8);
158
159
		// Alignment patterns
160
		$apc = $this->version->getAlignmentPattern();
161
		$max = count($apc);
162
163
		for($x = 0; $x < $max; $x++){
164
			$i = $apc[$x] - 2;
165
166
			for($y = 0; $y < $max; $y++){
167
				if(($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)){
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($x === 0 && $y === 0 ||...== $max - 1 && $y === 0, Probably Intended Meaning: $x === 0 && ($y === 0 ||...= $max - 1 && $y === 0)
Loading history...
168
					// No alignment patterns near the three finder paterns
169
					continue;
170
				}
171
172
				$bitMatrix->setRegion($apc[$y] - 2, $i, 5, 5);
173
			}
174
		}
175
176
		// Vertical timing pattern
177
		$bitMatrix->setRegion(6, 9, 1, $dimension - 17);
178
		// Horizontal timing pattern
179
		$bitMatrix->setRegion(9, 6, $dimension - 17, 1);
180
181
		if($this->version->getVersionNumber() > 6){
182
			// Version info, top right
183
			$bitMatrix->setRegion($dimension - 11, 0, 3, 6);
184
			// Version info, bottom left
185
			$bitMatrix->setRegion(0, $dimension - 11, 6, 3);
186
		}
187
188
		return $bitMatrix;
189
	}
190
191
	/**
192
	 * Mirror the bit matrix in order to attempt a second reading.
193
	 */
194
	public function mirror():self{
195
196
		for($x = 0; $x < $this->dimension; $x++){
197
			for($y = $x + 1; $y < $this->dimension; $y++){
198
				if($this->get($x, $y) !== $this->get($y, $x)){
199
					$this->flip($y, $x);
200
					$this->flip($x, $y);
201
				}
202
			}
203
		}
204
205
		return $this;
206
	}
207
208
	/**
209
	 * Implementations of this method reverse the data masking process applied to a QR Code and
210
	 * make its bits ready to read.
211
	 */
212
	private function unmask():void{
213
		$mask = $this->formatInfo->getMaskPattern()->getMask();
0 ignored issues
show
Bug introduced by
The method getMaskPattern() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

213
		$mask = $this->formatInfo->/** @scrutinizer ignore-call */ getMaskPattern()->getMask();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
214
215
		for($y = 0; $y < $this->dimension; $y++){
216
			for($x = 0; $x < $this->dimension; $x++){
217
				if($mask($x, $y)){
218
					$this->flip($x, $y);
219
				}
220
			}
221
		}
222
223
	}
224
225
	/**
226
	 * Prepare the parser for a mirrored operation.
227
	 * This flag has effect only on the {@link #readFormatInformation()} and the
228
	 * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the
229
	 * {@link #mirror()} method should be called.
230
	 *
231
	 * @param bool $mirror Whether to read version and format information mirrored.
232
	 */
233
	public function setMirror(bool $mirror):self{
234
		$this->version    = null;
235
		$this->formatInfo = null;
236
		$this->mirror     = $mirror;
237
238
		return $this;
239
	}
240
241
	/**
242
	 *
243
	 */
244
	private function copyBit(int $i, int $j, int $versionBits):int{
245
246
		$bit = $this->mirror
247
			? $this->get($j, $i)
248
			: $this->get($i, $j);
249
250
		return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1;
251
	}
252
253
	/**
254
	 * <p>Reads the bits in the {@link BitMatrix} representing the finder pattern in the
255
	 * correct order in order to reconstruct the codewords bytes contained within the
256
	 * QR Code.</p>
257
	 *
258
	 * @return array bytes encoded within the QR Code
259
	 * @throws \RuntimeException if the exact number of bytes expected is not read
260
	 */
261
	public function readCodewords():array{
262
		$this->formatInfo = $this->readFormatInformation();
263
		$this->version    = $this->readVersion();
264
265
		// Get the data mask for the format used in this QR Code. This will exclude
266
		// some bits from reading as we wind through the bit matrix.
267
		$this->unmask();
268
		$functionPattern = $this->buildFunctionPattern();
269
270
		$readingUp    = true;
271
		$result       = [];
272
		$resultOffset = 0;
273
		$currentByte  = 0;
274
		$bitsRead     = 0;
275
		// Read columns in pairs, from right to left
276
		for($j = $this->dimension - 1; $j > 0; $j -= 2){
277
278
			if($j === 6){
279
				// Skip whole column with vertical alignment pattern;
280
				// saves time and makes the other code proceed more cleanly
281
				$j--;
282
			}
283
			// Read alternatingly from bottom to top then top to bottom
284
			for($count = 0; $count < $this->dimension; $count++){
285
				$i = $readingUp ? $this->dimension - 1 - $count : $count;
286
287
				for($col = 0; $col < 2; $col++){
288
					// Ignore bits covered by the function pattern
289
					if(!$functionPattern->get($j - $col, $i)){
290
						// Read a bit
291
						$bitsRead++;
292
						$currentByte <<= 1;
293
294
						if($this->get($j - $col, $i)){
295
							$currentByte |= 1;
296
						}
297
						// If we've made a whole byte, save it off
298
						if($bitsRead === 8){
299
							$result[$resultOffset++] = $currentByte; //(byte)
300
							$bitsRead                = 0;
301
							$currentByte             = 0;
302
						}
303
					}
304
				}
305
			}
306
307
			$readingUp = !$readingUp; // switch directions
0 ignored issues
show
introduced by
The condition $readingUp is always true.
Loading history...
308
		}
309
310
		if($resultOffset !== $this->version->getTotalCodewords()){
311
			throw new RuntimeException('offset differs from total codewords for version');
312
		}
313
314
		return $result;
315
	}
316
317
	/**
318
	 * <p>Reads format information from one of its two locations within the QR Code.</p>
319
	 *
320
	 * @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info
321
	 * @throws \RuntimeException                           if both format information locations cannot be parsed as
322
	 *                                                     the valid encoding of format information
323
	 */
324
	private function readFormatInformation():FormatInformation{
325
326
		if($this->formatInfo !== null){
327
			return $this->formatInfo;
328
		}
329
330
		// Read top-left format info bits
331
		$formatInfoBits1 = 0;
332
333
		for($i = 0; $i < 6; $i++){
334
			$formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1);
335
		}
336
337
		// .. and skip a bit in the timing pattern ...
338
		$formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1);
339
		$formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1);
340
		$formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1);
341
		// .. and skip a bit in the timing pattern ...
342
		for($j = 5; $j >= 0; $j--){
343
			$formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1);
344
		}
345
346
		// Read the top-right/bottom-left pattern too
347
		$formatInfoBits2 = 0;
348
		$jMin            = $this->dimension - 7;
349
350
		for($j = $this->dimension - 1; $j >= $jMin; $j--){
351
			$formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2);
352
		}
353
354
		for($i = $this->dimension - 8; $i < $this->dimension; $i++){
355
			$formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2);
356
		}
357
358
		$this->formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
359
360
		if($this->formatInfo !== null){
361
			return $this->formatInfo;
362
		}
363
364
		// Should return null, but, some QR codes apparently do not mask this info.
365
		// Try again by actually masking the pattern first.
366
		$this->formatInfo = $this->doDecodeFormatInformation(
367
			$formatInfoBits1 ^ FormatInformation::FORMAT_INFO_MASK_QR,
368
			$formatInfoBits2 ^ FormatInformation::FORMAT_INFO_MASK_QR
369
		);
370
371
		if($this->formatInfo !== null){
372
			return $this->formatInfo;
373
		}
374
375
		throw new RuntimeException('failed to read format info');
376
	}
377
378
	/**
379
	 * @param int $maskedFormatInfo1 format info indicator, with mask still applied
380
	 * @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time
381
	 *                               to establish best match
382
	 *
383
	 * @return \chillerlan\QRCode\Common\FormatInformation|null information about the format it specifies, or null
384
	 *                                                          if doesn't seem to match any known pattern
385
	 */
386
	private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?FormatInformation{
387
		// Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing
388
		$bestDifference = PHP_INT_MAX;
389
		$bestFormatInfo = 0;
390
391
		foreach(FormatInformation::DECODE_LOOKUP as $decodeInfo){
392
			[$maskedBits, $dataBits] = $decodeInfo;
393
394
			if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
395
				// Found an exact match
396
				return new FormatInformation($maskedBits);
397
			}
398
399
			$bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits);
400
401
			if($bitsDifference < $bestDifference){
402
				$bestFormatInfo = $maskedBits;
403
				$bestDifference = $bitsDifference;
404
			}
405
406
			if($maskedFormatInfo1 !== $maskedFormatInfo2){
407
				// also try the other option
408
				$bitsDifference = $this->numBitsDiffering($maskedFormatInfo2, $dataBits);
409
410
				if($bitsDifference < $bestDifference){
411
					$bestFormatInfo = $maskedBits;
412
					$bestDifference = $bitsDifference;
413
				}
414
			}
415
		}
416
		// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match
417
		if($bestDifference <= 3){
418
			return new FormatInformation($bestFormatInfo);
419
		}
420
421
		return null;
422
	}
423
424
	/**
425
	 * <p>Reads version information from one of its two locations within the QR Code.</p>
426
	 *
427
	 * @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version
428
	 * @throws \RuntimeException                 if both version information locations cannot be parsed as
429
	 *                                           the valid encoding of version information
430
	 */
431
	private function readVersion():Version{
432
433
		if($this->version !== null){
434
			return $this->version;
435
		}
436
437
		$provisionalVersion = ($this->dimension - 17) / 4;
438
439
		if($provisionalVersion <= 6){
440
			return new Version($provisionalVersion);
441
		}
442
443
		// Read top-right version info: 3 wide by 6 tall
444
		$versionBits = 0;
445
		$ijMin       = $this->dimension - 11;
446
447
		for($j = 5; $j >= 0; $j--){
448
			for($i = $this->dimension - 9; $i >= $ijMin; $i--){
449
				$versionBits = $this->copyBit($i, $j, $versionBits);
450
			}
451
		}
452
453
		$this->version = $this->decodeVersionInformation($versionBits);
454
455
		if($this->version !== null && $this->version->getDimension() === $this->dimension){
456
			return $this->version;
457
		}
458
459
		// Hmm, failed. Try bottom left: 6 wide by 3 tall
460
		$versionBits = 0;
461
462
		for($i = 5; $i >= 0; $i--){
463
			for($j = $this->dimension - 9; $j >= $ijMin; $j--){
464
				$versionBits = $this->copyBit($i, $j, $versionBits);
465
			}
466
		}
467
468
		$this->version = $this->decodeVersionInformation($versionBits);
469
470
		if($this->version !== null && $this->version->getDimension() === $this->dimension){
471
			return $this->version;
472
		}
473
474
		throw new RuntimeException('failed to read version');
475
	}
476
477
	/**
478
	 * @param int $versionBits
479
	 *
480
	 * @return \chillerlan\QRCode\Common\Version|null
481
	 */
482
	private function decodeVersionInformation(int $versionBits):?Version{
483
		$bestDifference = PHP_INT_MAX;
484
		$bestVersion    = 0;
485
486
		for($i = 7; $i <= 40; $i++){
487
			$targetVersion        = new Version($i);
488
			$targetVersionPattern = $targetVersion->getVersionPattern();
489
490
			// Do the version info bits match exactly? done.
491
			if($targetVersionPattern === $versionBits){
492
				return $targetVersion;
493
			}
494
495
			// Otherwise see if this is the closest to a real version info bit string
496
			// we have seen so far
497
			/** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */
498
			$bitsDifference = $this->numBitsDiffering($versionBits, $targetVersionPattern);
0 ignored issues
show
Bug introduced by
It seems like $targetVersionPattern can also be of type null; however, parameter $b of chillerlan\QRCode\Decode...rix::numBitsDiffering() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

498
			$bitsDifference = $this->numBitsDiffering($versionBits, /** @scrutinizer ignore-type */ $targetVersionPattern);
Loading history...
499
500
			if($bitsDifference < $bestDifference){
501
				$bestVersion    = $i;
502
				$bestDifference = $bitsDifference;
503
			}
504
		}
505
		// We can tolerate up to 3 bits of error since no two version info codewords will
506
		// differ in less than 8 bits.
507
		if($bestDifference <= 3){
508
			return new Version($bestVersion);
509
		}
510
511
		// If we didn't find a close enough match, fail
512
		return null;
513
	}
514
515
	/**
516
	 *
517
	 */
518
	private function uRShift(int $a, int $b):int{
519
520
		if($b === 0){
521
			return $a;
522
		}
523
524
		return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1));
525
	}
526
527
	/**
528
	 *
529
	 */
530
	private function numBitsDiffering(int $a, int $b):int{
531
		// a now has a 1 bit exactly where its bit differs with b's
532
		$a ^= $b;
533
		// Offset i holds the number of 1 bits in the binary representation of i
534
		$BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
535
		// Count bits set quickly with a series of lookups:
536
		$count = 0;
537
538
		for($i = 0; $i < 32; $i += 4){
539
			$count += $BITS_SET_IN_HALF_BYTE[$this->uRShift($a, $i) & 0x0F];
540
		}
541
542
		return $count;
543
	}
544
545
}
546