BitMatrix::readFormatInformation()   B
last analyzed

Complexity

Conditions 9
Paths 49

Size

Total Lines 56
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 26
nc 49
nop 0
dl 0
loc 56
rs 8.0555
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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_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
		$this->matrix = array_reverse($this->matrix);
99
		// rotate by 90 degrees clockwise
100
		/** @phan-suppress-next-line PhanTypeMismatchReturnSuperType */
101
		return $this->rotate90();
102
	}
103
104
	/**
105
	 * Reads the bits in the BitMatrix representing the finder pattern in the
106
	 * correct order in order to reconstruct the codewords bytes contained within the
107
	 * QR Code. Throws if the exact number of bytes expected is not read.
108
	 *
109
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
110
	 */
111
	public function readCodewords():array{
112
113
		$this
114
			->readFormatInformation()
115
			->readVersion()
116
			->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

116
			->mask(/** @scrutinizer ignore-type */ $this->maskPattern) // reverse the mask pattern
Loading history...
117
		;
118
119
		// invoke a fresh matrix with only the function & format patterns to compare against
120
		$matrix = (new QRMatrix($this->version, $this->eccLevel))
0 ignored issues
show
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

120
		$matrix = (new QRMatrix(/** @scrutinizer ignore-type */ $this->version, $this->eccLevel))
Loading history...
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

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

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

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