Passed
Push — gh-pages ( 20c441...dd59e5 )
by
unknown
02:54 queued 01:00
created

BitMatrixParser::uRShift()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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