Passed
Push — main ( 3d99ee...53f6dc )
by smiley
10:29
created

BitMatrix::setQuietZone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
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, count;
17
use const PHP_INT_MAX, PHP_INT_SIZE;
18
19
/**
20
 *
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
		0x00 => 0x5412,
31
		0x01 => 0x5125,
32
		0x02 => 0x5E7C,
33
		0x03 => 0x5B4B,
34
		0x04 => 0x45F9,
35
		0x05 => 0x40CE,
36
		0x06 => 0x4F97,
37
		0x07 => 0x4AA0,
38
		0x08 => 0x77C4,
39
		0x09 => 0x72F3,
40
		0x0A => 0x7DAA,
41
		0x0B => 0x789D,
42
		0x0C => 0x662F,
43
		0x0D => 0x6318,
44
		0x0E => 0x6C41,
45
		0x0F => 0x6976,
46
		0x10 => 0x1689,
47
		0x11 => 0x13BE,
48
		0x12 => 0x1CE7,
49
		0x13 => 0x19D0,
50
		0x14 => 0x0762,
51
		0x15 => 0x0255,
52
		0x16 => 0x0D0C,
53
		0x17 => 0x083B,
54
		0x18 => 0x355F,
55
		0x19 => 0x3068,
56
		0x1A => 0x3F31,
57
		0x1B => 0x3A06,
58
		0x1C => 0x24B4,
59
		0x1D => 0x2183,
60
		0x1E => 0x2EDA,
61
		0x1F => 0x2BED,
62
	];
63
64
	private const FORMAT_INFO_MASK_QR = 0x5412;
65
66
	private bool $mirror = false;
67
68
	/**
69
	 * @noinspection PhpMissingParentConstructorInspection
70
	 */
71
	public function __construct(int $dimension){
72
		$this->moduleCount = $dimension;
73
		$this->matrix      = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
74
	}
75
76
	/**
77
	 * Prepare the parser for a mirrored operation.
78
	 * This flag has effect only on the readFormatInformation() and the
79
	 * readVersion() methods. Before proceeding with readCodewords() the
80
	 * mirror() method should be called.
81
	 *
82
	 * @param bool $mirror Whether to read version and format information mirrored.
83
	 */
84
	public function setMirror(bool $mirror):self{
85
		$this->version     = null;
86
		$this->eccLevel    = null;
87
		$this->maskPattern = null;
88
		$this->mirror      = $mirror;
89
90
		return $this;
91
	}
92
93
	/**
94
	 * Mirror the bit matrix in order to attempt a second reading.
95
	 */
96
	public function mirror():self{
97
98
		for($x = 0; $x < $this->moduleCount; $x++){
99
			for($y = $x + 1; $y < $this->moduleCount; $y++){
100
				if($this->get($x, $y) !== $this->get($y, $x)){
101
					$this->flip($y, $x);
102
					$this->flip($x, $y);
103
				}
104
			}
105
		}
106
107
		return $this;
108
	}
109
110
	/**
111
	 * Reads the bits in the BitMatrix representing the finder pattern in the
112
	 * correct order in order to reconstruct the codewords bytes contained within the
113
	 * QR Code.
114
	 *
115
	 * @return array bytes encoded within the QR Code
116
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException if the exact number of bytes expected is not read
117
	 */
118
	public function readCodewords():array{
119
120
		$this
121
			->readFormatInformation()
122
			->readVersion()
123
			->mask() // reverse the mask pattern
124
		;
125
126
		// invoke a fresh matrix with only the function & format patterns to compare against
127
		$fp        = (new QRMatrix($this->version, $this->eccLevel, $this->maskPattern))->initFunctionalPatterns();
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

127
		$fp        = (new QRMatrix($this->version, /** @scrutinizer ignore-type */ $this->eccLevel, $this->maskPattern))->initFunctionalPatterns();
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

127
		$fp        = (new QRMatrix(/** @scrutinizer ignore-type */ $this->version, $this->eccLevel, $this->maskPattern))->initFunctionalPatterns();
Loading history...
Bug introduced by
It seems like $this->maskPattern can also be of type null; however, parameter $maskPattern of chillerlan\QRCode\Data\QRMatrix::__construct() 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

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

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

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