Passed
Push — v5 ( 93618e...84eb31 )
by smiley
01:52
created

BitMatrixParser::mirror()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 2
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 function chillerlan\QRCode\Common\numBitsDiffering;
17
use const PHP_INT_MAX;
18
19
/**
20
 * @author Sean Owen
21
 */
22
final class BitMatrixParser{
23
24
	private BitMatrix          $bitMatrix;
25
	private ?Version           $parsedVersion    = null;
26
	private ?FormatInformation $parsedFormatInfo = null;
27
	private bool               $mirror           = false;
28
29
	/**
30
	 * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix
31
	 *
32
	 * @throws \RuntimeException if dimension is not >= 21 and 1 mod 4
33
	 */
34
	public function __construct(BitMatrix $bitMatrix){
35
		$dimension = $bitMatrix->getDimension();
36
37
		if($dimension < 21 || ($dimension % 4) !== 1){
38
			throw new RuntimeException('dimension is not >= 21, dimension mod 4 not 1');
39
		}
40
41
		$this->bitMatrix = $bitMatrix;
42
	}
43
44
	/**
45
	 * Prepare the parser for a mirrored operation.
46
	 * This flag has effect only on the {@link #readFormatInformation()} and the
47
	 * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the
48
	 * {@link #mirror()} method should be called.
49
	 *
50
	 * @param bool mirror Whether to read version and format information mirrored.
0 ignored issues
show
Bug introduced by
The type chillerlan\QRCode\Decoder\mirror was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

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

316
			$bitsDifference = numBitsDiffering($versionBits, /** @scrutinizer ignore-type */ $targetVersionPattern);
Loading history...
317
318
			if($bitsDifference < $bestDifference){
319
				$bestVersion    = $i;
320
				$bestDifference = $bitsDifference;
321
			}
322
		}
323
		// We can tolerate up to 3 bits of error since no two version info codewords will
324
		// differ in less than 8 bits.
325
		if($bestDifference <= 3){
326
			return new Version($bestVersion);
327
		}
328
329
		// If we didn't find a close enough match, fail
330
		return null;
331
	}
332
333
}
334