Passed
Push — main ( b62539...4e03cd )
by smiley
01:53
created

BitMatrixParser::decodeVersionInformation()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
c 1
b 0
f 0
nc 7
nop 1
dl 0
loc 31
rs 9.4888
1
<?php
2
/**
3
 * Class BitMatrixParser
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 RuntimeException;
15
use chillerlan\QRCode\Common\{Version, FormatInformation};
16
use const PHP_INT_MAX, PHP_INT_SIZE;
17
18
/**
19
 * @author Sean Owen
20
 */
21
final class BitMatrixParser{
22
23
	private BitMatrix          $bitMatrix;
24
	private ?Version           $parsedVersion    = null;
25
	private ?FormatInformation $parsedFormatInfo = null;
26
	private bool               $mirror           = false;
27
28
	/**
29
	 * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix
30
	 *
31
	 * @throws \RuntimeException if dimension is not >= 21 and 1 mod 4
32
	 */
33
	public function __construct(BitMatrix $bitMatrix){
34
		$dimension = $bitMatrix->getDimension();
35
36
		if($dimension < 21 || ($dimension % 4) !== 1){
37
			throw new RuntimeException('dimension is not >= 21, dimension mod 4 not 1');
38
		}
39
40
		$this->bitMatrix = $bitMatrix;
41
	}
42
43
	/**
44
	 * Prepare the parser for a mirrored operation.
45
	 * This flag has effect only on the {@link #readFormatInformation()} and the
46
	 * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the
47
	 * {@link #mirror()} method should be called.
48
	 *
49
	 * @param bool $mirror Whether to read version and format information mirrored.
50
	 */
51
	public function setMirror(bool $mirror):void{
52
		$this->parsedVersion    = null;
53
		$this->parsedFormatInfo = null;
54
		$this->mirror           = $mirror;
55
	}
56
57
	/**
58
	 * Mirror the bit matrix in order to attempt a second reading.
59
	 */
60
	public function mirror():void{
61
		$this->bitMatrix->mirror();
62
	}
63
64
	private function copyBit(int $i, int $j, int $versionBits):int{
65
66
		$bit = $this->mirror
67
			? $this->bitMatrix->get($j, $i)
68
			: $this->bitMatrix->get($i, $j);
69
70
		return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1;
71
	}
72
73
	/**
74
	 * <p>Reads the bits in the {@link BitMatrix} representing the finder pattern in the
75
	 * correct order in order to reconstruct the codewords bytes contained within the
76
	 * QR Code.</p>
77
	 *
78
	 * @return array bytes encoded within the QR Code
79
	 * @throws \RuntimeException if the exact number of bytes expected is not read
80
	 */
81
	public function readCodewords():array{
82
		$formatInfo = $this->readFormatInformation();
83
		$version    = $this->readVersion();
84
85
		// Get the data mask for the format used in this QR Code. This will exclude
86
		// some bits from reading as we wind through the bit matrix.
87
		$dimension = $this->bitMatrix->getDimension();
88
		$this->bitMatrix->unmask($dimension, $formatInfo->getDataMask());
89
		$functionPattern = $this->bitMatrix->buildFunctionPattern($version);
90
91
		$readingUp    = true;
92
		$result       = [];
93
		$resultOffset = 0;
94
		$currentByte  = 0;
95
		$bitsRead     = 0;
96
		// Read columns in pairs, from right to left
97
		for($j = $dimension - 1; $j > 0; $j -= 2){
98
99
			if($j === 6){
100
				// Skip whole column with vertical alignment pattern;
101
				// saves time and makes the other code proceed more cleanly
102
				$j--;
103
			}
104
			// Read alternatingly from bottom to top then top to bottom
105
			for($count = 0; $count < $dimension; $count++){
106
				$i = $readingUp ? $dimension - 1 - $count : $count;
107
108
				for($col = 0; $col < 2; $col++){
109
					// Ignore bits covered by the function pattern
110
					if(!$functionPattern->get($j - $col, $i)){
111
						// Read a bit
112
						$bitsRead++;
113
						$currentByte <<= 1;
114
115
						if($this->bitMatrix->get($j - $col, $i)){
116
							$currentByte |= 1;
117
						}
118
						// If we've made a whole byte, save it off
119
						if($bitsRead === 8){
120
							$result[$resultOffset++] = $currentByte; //(byte)
121
							$bitsRead                = 0;
122
							$currentByte             = 0;
123
						}
124
					}
125
				}
126
			}
127
128
			$readingUp = !$readingUp; // switch directions
0 ignored issues
show
introduced by
The condition $readingUp is always true.
Loading history...
129
		}
130
131
		if($resultOffset !== $version->getTotalCodewords()){
132
			throw new RuntimeException('offset differs from total codewords for version');
133
		}
134
135
		return $result;
136
	}
137
138
	/**
139
	 * <p>Reads format information from one of its two locations within the QR Code.</p>
140
	 *
141
	 * @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info
142
	 * @throws \RuntimeException                           if both format information locations cannot be parsed as
143
	 *                                                     the valid encoding of format information
144
	 */
145
	public function readFormatInformation():FormatInformation{
146
147
		if($this->parsedFormatInfo !== null){
148
			return $this->parsedFormatInfo;
149
		}
150
151
		// Read top-left format info bits
152
		$formatInfoBits1 = 0;
153
154
		for($i = 0; $i < 6; $i++){
155
			$formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1);
156
		}
157
158
		// .. and skip a bit in the timing pattern ...
159
		$formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1);
160
		$formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1);
161
		$formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1);
162
		// .. and skip a bit in the timing pattern ...
163
		for($j = 5; $j >= 0; $j--){
164
			$formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1);
165
		}
166
167
		// Read the top-right/bottom-left pattern too
168
		$dimension       = $this->bitMatrix->getDimension();
169
		$formatInfoBits2 = 0;
170
		$jMin            = $dimension - 7;
171
172
		for($j = $dimension - 1; $j >= $jMin; $j--){
173
			$formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2);
174
		}
175
176
		for($i = $dimension - 8; $i < $dimension; $i++){
177
			$formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2);
178
		}
179
180
		$this->parsedFormatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
181
182
		if($this->parsedFormatInfo !== null){
183
			return $this->parsedFormatInfo;
184
		}
185
186
		// Should return null, but, some QR codes apparently do not mask this info.
187
		// Try again by actually masking the pattern first.
188
		$this->parsedFormatInfo = $this->doDecodeFormatInformation(
189
			$formatInfoBits1 ^ FormatInformation::MASK_QR,
190
			$formatInfoBits2 ^ FormatInformation::MASK_QR
191
		);
192
193
		if($this->parsedFormatInfo !== null){
194
			return $this->parsedFormatInfo;
195
		}
196
197
		throw new RuntimeException('failed to read format info');
198
	}
199
200
	/**
201
	 * @param int $maskedFormatInfo1 format info indicator, with mask still applied
202
	 * @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time
203
	 *                               to establish best match
204
	 *
205
	 * @return \chillerlan\QRCode\Common\FormatInformation|null information about the format it specifies, or null
206
	 *                                                          if doesn't seem to match any known pattern
207
	 */
208
	private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?FormatInformation{
209
		// Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing
210
		$bestDifference = PHP_INT_MAX;
211
		$bestFormatInfo = 0;
212
213
		foreach(FormatInformation::DECODE_LOOKUP as $decodeInfo){
214
			[$maskedBits, $dataBits] = $decodeInfo;
215
216
			if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
217
				// Found an exact match
218
				return new FormatInformation($maskedBits);
219
			}
220
221
			$bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $dataBits);
222
223
			if($bitsDifference < $bestDifference){
224
				$bestFormatInfo = $maskedBits;
225
				$bestDifference = $bitsDifference;
226
			}
227
228
			if($maskedFormatInfo1 !== $maskedFormatInfo2){
229
				// also try the other option
230
				$bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $dataBits);
231
232
				if($bitsDifference < $bestDifference){
233
					$bestFormatInfo = $maskedBits;
234
					$bestDifference = $bitsDifference;
235
				}
236
			}
237
		}
238
		// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match
239
		if($bestDifference <= 3){
240
			return new FormatInformation($bestFormatInfo);
241
		}
242
243
		return null;
244
	}
245
246
	/**
247
	 * <p>Reads version information from one of its two locations within the QR Code.</p>
248
	 *
249
	 * @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version
250
	 * @throws \RuntimeException                 if both version information locations cannot be parsed as
251
	 *                                           the valid encoding of version information
252
	 */
253
	public function readVersion():Version{
254
255
		if($this->parsedVersion !== null){
256
			return $this->parsedVersion;
257
		}
258
259
		$dimension          = $this->bitMatrix->getDimension();
260
		$provisionalVersion = ($dimension - 17) / 4;
261
262
		if($provisionalVersion <= 6){
263
			return new Version($provisionalVersion);
264
		}
265
266
		// Read top-right version info: 3 wide by 6 tall
267
		$versionBits = 0;
268
		$ijMin       = $dimension - 11;
269
270
		for($j = 5; $j >= 0; $j--){
271
			for($i = $dimension - 9; $i >= $ijMin; $i--){
272
				$versionBits = $this->copyBit($i, $j, $versionBits);
273
			}
274
		}
275
276
		$this->parsedVersion = $this->decodeVersionInformation($versionBits);
277
278
		if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){
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

278
		if($this->parsedVersion !== null && $this->parsedVersion->/** @scrutinizer ignore-call */ getDimension() === $dimension){

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...
279
			return $this->parsedVersion;
280
		}
281
282
		// Hmm, failed. Try bottom left: 6 wide by 3 tall
283
		$versionBits = 0;
284
285
		for($i = 5; $i >= 0; $i--){
286
			for($j = $dimension - 9; $j >= $ijMin; $j--){
287
				$versionBits = $this->copyBit($i, $j, $versionBits);
288
			}
289
		}
290
291
		$this->parsedVersion = $this->decodeVersionInformation($versionBits);
292
293
		if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){
294
			return $this->parsedVersion;
295
		}
296
297
		throw new RuntimeException('failed to read version');
298
	}
299
300
	/**
301
	 * @param int $versionBits
302
	 *
303
	 * @return \chillerlan\QRCode\Common\Version|null
304
	 */
305
	private function decodeVersionInformation(int $versionBits):?Version{
306
		$bestDifference = PHP_INT_MAX;
307
		$bestVersion    = 0;
308
309
		for($i = 7; $i <= 40; $i++){
310
			$targetVersion        = new Version($i);
311
			$targetVersionPattern = $targetVersion->getVersionPattern();
312
313
			// Do the version info bits match exactly? done.
314
			if($targetVersionPattern === $versionBits){
315
				return $targetVersion;
316
			}
317
318
			// Otherwise see if this is the closest to a real version info bit string
319
			// we have seen so far
320
			/** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */
321
			$bitsDifference = self::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...ser::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

321
			$bitsDifference = self::numBitsDiffering($versionBits, /** @scrutinizer ignore-type */ $targetVersionPattern);
Loading history...
322
323
			if($bitsDifference < $bestDifference){
324
				$bestVersion    = $i;
325
				$bestDifference = $bitsDifference;
326
			}
327
		}
328
		// We can tolerate up to 3 bits of error since no two version info codewords will
329
		// differ in less than 8 bits.
330
		if($bestDifference <= 3){
331
			return new Version($bestVersion);
332
		}
333
334
		// If we didn't find a close enough match, fail
335
		return null;
336
	}
337
338
	public static function uRShift(int $a, int $b):int{
339
340
		if($b === 0){
341
			return $a;
342
		}
343
344
		return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1));
345
	}
346
347
	private static function numBitsDiffering(int $a, int $b):int{
348
		// a now has a 1 bit exactly where its bit differs with b's
349
		$a ^= $b;
350
		// Offset i holds the number of 1 bits in the binary representation of i
351
		$BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
352
		// Count bits set quickly with a series of lookups:
353
		$count = 0;
354
355
		for($i = 0; $i < 32; $i += 4){
356
			$count += $BITS_SET_IN_HALF_BYTE[self::uRShift($a, $i) & 0x0F];
357
		}
358
359
		return $count;
360
	}
361
362
}
363