Passed
Push — main ( a1f051...4bdfa7 )
by smiley
02:03
created

BitMatrix::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
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\{EccLevel, MaskPattern, Version};
15
use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix};
16
use function array_fill, array_map, array_reverse, count;
17
use const PHP_INT_MAX, PHP_INT_SIZE;
18
19
/**
20
 * Extended QRMatrix to map read data from the Binarizer
21
 */
22
final class BitMatrix extends QRMatrix{
23
24
	/**
25
	 * See ISO 18004:2006, Annex C, Table C.1
26
	 *
27
	 * [data bits, sequence after masking]
28
	 */
29
	private const DECODE_LOOKUP = [
30
		0x5412, // 0101010000010010
31
		0x5125, // 0101000100100101
32
		0x5E7C, // 0101111001111100
33
		0x5B4B, // 0101101101001011
34
		0x45F9, // 0100010111111001
35
		0x40CE, // 0100000011001110
36
		0x4F97, // 0100111110010111
37
		0x4AA0, // 0100101010100000
38
		0x77C4, // 0111011111000100
39
		0x72F3, // 0111001011110011
40
		0x7DAA, // 0111110110101010
41
		0x789D, // 0111100010011101
42
		0x662F, // 0110011000101111
43
		0x6318, // 0110001100011000
44
		0x6C41, // 0110110001000001
45
		0x6976, // 0110100101110110
46
		0x1689, // 0001011010001001
47
		0x13BE, // 0001001110111110
48
		0x1CE7, // 0001110011100111
49
		0x19D0, // 0001100111010000
50
		0x0762, // 0000011101100010
51
		0x0255, // 0000001001010101
52
		0x0D0C, // 0000110100001100
53
		0x083B, // 0000100000111011
54
		0x355F, // 0011010101011111
55
		0x3068, // 0011000001101000
56
		0x3F31, // 0011111100110001
57
		0x3A06, // 0011101000000110
58
		0x24B4, // 0010010010110100
59
		0x2183, // 0010000110000011
60
		0x2EDA, // 0010111011011010
61
		0x2BED, // 0010101111101101
62
	];
63
64
	private const FORMAT_INFO_MASK_QR = 0x5412; // 0101010000010010
65
66
	/**
67
	 * This flag has effect only on the copyVersionBit() method.
68
	 * Before proceeding with readCodewords() the resetInfo() method should be called.
69
	 */
70
	private bool $mirror = false;
71
72
	/**
73
	 * @noinspection PhpMissingParentConstructorInspection
74
	 */
75
	public function __construct(int $dimension){
76
		$this->moduleCount = $dimension;
77
		$this->matrix      = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
78
	}
79
80
	/**
81
	 * Resets the current version info in order to attempt another reading
82
	 */
83
	public function resetVersionInfo():self{
84
		$this->version     = null;
85
		$this->eccLevel    = null;
86
		$this->maskPattern = null;
87
88
		return $this;
89
	}
90
91
	/**
92
	 * Mirror the bit matrix diagonally in order to attempt a second reading.
93
	 */
94
	public function mirrorDiagonal():self{
95
		$this->mirror = !$this->mirror;
96
97
		// mirror vertically
98
		$matrix = array_reverse($this->matrix);
99
		// rotate by 90 degrees clockwise
100
		$this->matrix = array_map(fn(...$a) => array_reverse($a), ...$matrix);
101
102
		return $this;
103
	}
104
105
	/**
106
	 * Reads the bits in the BitMatrix representing the finder pattern in the
107
	 * correct order in order to reconstruct the codewords bytes contained within the
108
	 * QR Code. Throws if the exact number of bytes expected is not read.
109
	 *
110
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
111
	 */
112
	public function readCodewords():array{
113
114
		$this
115
			->readFormatInformation()
116
			->readVersion()
117
			->mask($this->maskPattern) // reverse the mask pattern
0 ignored issues
show
Bug introduced by
It seems like $this->maskPattern can also be of type null; however, parameter $maskPattern of chillerlan\QRCode\Data\QRMatrix::mask() does only seem to accept chillerlan\QRCode\Common\MaskPattern, 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

117
			->mask(/** @scrutinizer ignore-type */ $this->maskPattern) // reverse the mask pattern
Loading history...
118
		;
119
120
		// invoke a fresh matrix with only the function & format patterns to compare against
121
		$matrix = (new QRMatrix($this->version, $this->eccLevel))
0 ignored issues
show
Bug introduced by
It seems like $this->eccLevel can also be of type null; however, parameter $eccLevel of chillerlan\QRCode\Data\QRMatrix::__construct() does only seem to accept chillerlan\QRCode\Common\EccLevel, 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

121
		$matrix = (new QRMatrix($this->version, /** @scrutinizer ignore-type */ $this->eccLevel))
Loading history...
Bug introduced by
It seems like $this->version can also be of type null; however, parameter $version of chillerlan\QRCode\Data\QRMatrix::__construct() does only seem to accept chillerlan\QRCode\Common\Version, 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

121
		$matrix = (new QRMatrix(/** @scrutinizer ignore-type */ $this->version, $this->eccLevel))
Loading history...
122
			->initFunctionalPatterns()
123
			->setFormatInfo($this->maskPattern)
124
		;
125
126
		$result    = [];
127
		$byte      = 0;
128
		$bitsRead  = 0;
129
		$direction = true;
130
131
		// Read columns in pairs, from right to left
132
		for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){
133
134
			// Skip whole column with vertical alignment pattern;
135
			// saves time and makes the other code proceed more cleanly
136
			if($i === 6){
137
				$i--;
138
			}
139
			// Read alternatingly from bottom to top then top to bottom
140
			for($count = 0; $count < $this->moduleCount; $count++){
141
				$y = ($direction) ? ($this->moduleCount - 1 - $count) : $count;
142
143
				for($col = 0; $col < 2; $col++){
144
					$x = ($i - $col);
145
146
					// Ignore bits covered by the function pattern
147
					if($matrix->get($x, $y) !== $this::M_NULL){
148
						continue;
149
					}
150
151
					$bitsRead++;
152
					$byte <<= 1;
153
154
					if($this->check($x, $y)){
155
						$byte |= 1;
156
					}
157
					// If we've made a whole byte, save it off
158
					if($bitsRead === 8){
159
						$result[] = $byte;
160
						$bitsRead = 0;
161
						$byte     = 0;
162
					}
163
				}
164
			}
165
166
			$direction = !$direction; // switch directions
0 ignored issues
show
introduced by
The condition $direction is always true.
Loading history...
167
		}
168
169
		if(count($result) !== $this->version->getTotalCodewords()){
0 ignored issues
show
Bug introduced by
The method getTotalCodewords() 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

169
		if(count($result) !== $this->version->/** @scrutinizer ignore-call */ getTotalCodewords()){

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...
170
			throw new QRCodeDecoderException('result count differs from total codewords for version');
171
		}
172
173
		// bytes encoded within the QR Code
174
		return $result;
175
	}
176
177
	/**
178
	 * Reads format information from one of its two locations within the QR Code.
179
	 * Throws if both format information locations cannot be parsed as the valid encoding of format information.
180
	 *
181
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
182
	 */
183
	private function readFormatInformation():self{
184
185
		if($this->eccLevel !== null && $this->maskPattern !== null){
186
			return $this;
187
		}
188
189
		// Read top-left format info bits
190
		$formatInfoBits1 = 0;
191
192
		for($i = 0; $i < 6; $i++){
193
			$formatInfoBits1 = $this->copyVersionBit($i, 8, $formatInfoBits1);
194
		}
195
196
		// ... and skip a bit in the timing pattern ...
197
		$formatInfoBits1 = $this->copyVersionBit(7, 8, $formatInfoBits1);
198
		$formatInfoBits1 = $this->copyVersionBit(8, 8, $formatInfoBits1);
199
		$formatInfoBits1 = $this->copyVersionBit(8, 7, $formatInfoBits1);
200
		// ... and skip a bit in the timing pattern ...
201
		for($j = 5; $j >= 0; $j--){
202
			$formatInfoBits1 = $this->copyVersionBit(8, $j, $formatInfoBits1);
203
		}
204
205
		// Read the top-right/bottom-left pattern too
206
		$formatInfoBits2 = 0;
207
		$jMin            = ($this->moduleCount - 7);
208
209
		for($j = ($this->moduleCount - 1); $j >= $jMin; $j--){
210
			$formatInfoBits2 = $this->copyVersionBit(8, $j, $formatInfoBits2);
211
		}
212
213
		for($i = ($this->moduleCount - 8); $i < $this->moduleCount; $i++){
214
			$formatInfoBits2 = $this->copyVersionBit($i, 8, $formatInfoBits2);
215
		}
216
217
		$formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
218
219
		if($formatInfo === null){
220
221
			// Should return null, but, some QR codes apparently do not mask this info.
222
			// Try again by actually masking the pattern first.
223
			$formatInfo = $this->doDecodeFormatInformation(
224
				($formatInfoBits1 ^ $this::FORMAT_INFO_MASK_QR),
225
				($formatInfoBits2 ^ $this::FORMAT_INFO_MASK_QR)
226
			);
227
228
			// still nothing???
229
			if($formatInfo === null){
230
				throw new QRCodeDecoderException('failed to read format info'); // @codeCoverageIgnore
231
			}
232
233
		}
234
235
		$this->eccLevel    = new EccLevel(($formatInfo >> 3) & 0x03); // Bits 3,4
236
		$this->maskPattern = new MaskPattern($formatInfo & 0x07); // Bottom 3 bits
237
238
		return $this;
239
	}
240
241
	/**
242
	 *
243
	 */
244
	private function copyVersionBit(int $i, int $j, int $versionBits):int{
245
246
		$bit = $this->mirror
247
			? $this->check($j, $i)
248
			: $this->check($i, $j);
249
250
		return ($bit) ? (($versionBits << 1) | 0x1) : ($versionBits << 1);
251
	}
252
253
	/**
254
	 * Returns information about the format it specifies, or null if it doesn't seem to match any known pattern
255
	 */
256
	private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?int{
257
		$bestDifference = PHP_INT_MAX;
258
		$bestFormatInfo = 0;
259
260
		// Find the int in FORMAT_INFO_DECODE_LOOKUP with the fewest bits differing
261
		foreach($this::DECODE_LOOKUP as $maskedBits => $dataBits){
262
263
			if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
264
				// Found an exact match
265
				return $maskedBits;
266
			}
267
268
			$bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits);
269
270
			if($bitsDifference < $bestDifference){
271
				$bestFormatInfo = $maskedBits;
272
				$bestDifference = $bitsDifference;
273
			}
274
275
			if($maskedFormatInfo1 !== $maskedFormatInfo2){
276
				// also try the other option
277
				$bitsDifference = $this->numBitsDiffering($maskedFormatInfo2, $dataBits);
278
279
				if($bitsDifference < $bestDifference){
280
					$bestFormatInfo = $maskedBits;
281
					$bestDifference = $bitsDifference;
282
				}
283
			}
284
		}
285
		// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match
286
		if($bestDifference <= 3){
287
			return $bestFormatInfo;
288
		}
289
290
		return null;
291
	}
292
293
	/**
294
	 * Reads version information from one of its two locations within the QR Code.
295
	 * Throws if both version information locations cannot be parsed as the valid encoding of version information.
296
	 *
297
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
298
	 * @noinspection DuplicatedCode
299
	 */
300
	private function readVersion():self{
301
302
		if($this->version !== null){
303
			return $this;
304
		}
305
306
		$provisionalVersion = (($this->moduleCount - 17) / 4);
307
308
		// no version info if v < 7
309
		if($provisionalVersion < 7){
310
			$this->version = new Version($provisionalVersion);
311
312
			return $this;
313
		}
314
315
		// Read top-right version info: 3 wide by 6 tall
316
		$versionBits = 0;
317
		$ijMin       = ($this->moduleCount - 11);
318
319
		for($y = 5; $y >= 0; $y--){
320
			for($x = ($this->moduleCount - 9); $x >= $ijMin; $x--){
321
				$versionBits = $this->copyVersionBit($x, $y, $versionBits);
322
			}
323
		}
324
325
		$this->version = $this->decodeVersionInformation($versionBits);
326
327
		if($this->version !== null && $this->version->getDimension() === $this->moduleCount){
328
			return $this;
329
		}
330
331
		// Hmm, failed. Try bottom left: 6 wide by 3 tall
332
		$versionBits = 0;
333
334
		for($x = 5; $x >= 0; $x--){
335
			for($y = ($this->moduleCount - 9); $y >= $ijMin; $y--){
336
				$versionBits = $this->copyVersionBit($x, $y, $versionBits);
337
			}
338
		}
339
340
		$this->version = $this->decodeVersionInformation($versionBits);
341
342
		if($this->version !== null && $this->version->getDimension() === $this->moduleCount){
343
			return $this;
344
		}
345
346
		throw new QRCodeDecoderException('failed to read version');
347
	}
348
349
	/**
350
	 * Decodes the version information from the given bit sequence, returns null if no valid match is found.
351
	 */
352
	private function decodeVersionInformation(int $versionBits):?Version{
353
		$bestDifference = PHP_INT_MAX;
354
		$bestVersion    = 0;
355
356
		for($i = 7; $i <= 40; $i++){
357
			$targetVersion        = new Version($i);
358
			$targetVersionPattern = $targetVersion->getVersionPattern();
359
360
			// Do the version info bits match exactly? done.
361
			if($targetVersionPattern === $versionBits){
362
				return $targetVersion;
363
			}
364
365
			// Otherwise see if this is the closest to a real version info bit string
366
			// we have seen so far
367
			/** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */
368
			$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

368
			$bitsDifference = $this->numBitsDiffering($versionBits, /** @scrutinizer ignore-type */ $targetVersionPattern);
Loading history...
369
370
			if($bitsDifference < $bestDifference){
371
				$bestVersion    = $i;
372
				$bestDifference = $bitsDifference;
373
			}
374
		}
375
		// We can tolerate up to 3 bits of error since no two version info codewords will
376
		// differ in less than 8 bits.
377
		if($bestDifference <= 3){
378
			return new Version($bestVersion);
379
		}
380
381
		// If we didn't find a close enough match, fail
382
		return null;
383
	}
384
385
	/**
386
	 *
387
	 */
388
	private function uRShift(int $a, int $b):int{
389
390
		if($b === 0){
391
			return $a;
392
		}
393
394
		return (($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)));
395
	}
396
397
	/**
398
	 *
399
	 */
400
	private function numBitsDiffering(int $a, int $b):int{
401
		// a now has a 1 bit exactly where its bit differs with b's
402
		$a ^= $b;
403
		// Offset $i holds the number of 1-bits in the binary representation of $i
404
		$BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
405
		// Count bits set quickly with a series of lookups:
406
		$count = 0;
407
408
		for($i = 0; $i < 32; $i += 4){
409
			$count += $BITS_SET_IN_HALF_BYTE[($this->uRShift($a, $i) & 0x0F)];
410
		}
411
412
		return $count;
413
	}
414
415
	/**
416
	 * @codeCoverageIgnore
417
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
418
	 */
419
	public function setQuietZone(int $quietZoneSize = null):self{
420
		throw new QRCodeDataException('not supported');
421
	}
422
423
	/**
424
	 * @codeCoverageIgnore
425
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
426
	 */
427
	public function setLogoSpace(int $width, int $height = null, int $startX = null, int $startY = null):self{
428
		throw new QRCodeDataException('not supported');
429
	}
430
431
}
432