Total Complexity | 83 |
Total Lines | 521 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like BitMatrix often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use BitMatrix, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
22 | final class BitMatrix{ |
||
23 | |||
24 | private int $dimension; |
||
25 | private int $rowSize; |
||
26 | private array $bits; |
||
27 | private ?Version $version = null; |
||
28 | private ?FormatInformation $formatInfo = null; |
||
29 | private bool $mirror = false; |
||
30 | |||
31 | /** |
||
32 | * |
||
33 | */ |
||
34 | public function __construct(int $dimension){ |
||
35 | $this->dimension = $dimension; |
||
36 | $this->rowSize = ((int)(($this->dimension + 0x1f) / 0x20)); |
||
37 | $this->bits = array_fill(0, $this->rowSize * $this->dimension, 0); |
||
38 | } |
||
39 | |||
40 | /** |
||
41 | * <p>Sets the given bit to true.</p> |
||
42 | * |
||
43 | * @param int $x ; The horizontal component (i.e. which column) |
||
44 | * @param int $y ; The vertical component (i.e. which row) |
||
45 | */ |
||
46 | public function set(int $x, int $y):self{ |
||
47 | $offset = (int)($y * $this->rowSize + ($x / 0x20)); |
||
48 | |||
49 | $this->bits[$offset] ??= 0; |
||
50 | $this->bits[$offset] |= ($this->bits[$offset] |= 1 << ($x & 0x1f)); |
||
51 | |||
52 | return $this; |
||
53 | } |
||
54 | |||
55 | /** |
||
56 | * <p>Flips the given bit. 1 << (0xf9 & 0x1f)</p> |
||
57 | * |
||
58 | * @param int $x ; The horizontal component (i.e. which column) |
||
59 | * @param int $y ; The vertical component (i.e. which row) |
||
60 | */ |
||
61 | public function flip(int $x, int $y):self{ |
||
62 | $offset = $y * $this->rowSize + (int)($x / 0x20); |
||
63 | |||
64 | $this->bits[$offset] = ($this->bits[$offset] ^ (1 << ($x & 0x1f))); |
||
65 | |||
66 | return $this; |
||
67 | } |
||
68 | |||
69 | /** |
||
70 | * <p>Sets a square region of the bit matrix to true.</p> |
||
71 | * |
||
72 | * @param int $left ; The horizontal position to begin at (inclusive) |
||
73 | * @param int $top ; The vertical position to begin at (inclusive) |
||
74 | * @param int $width ; The width of the region |
||
75 | * @param int $height ; The height of the region |
||
76 | * |
||
77 | * @throws \InvalidArgumentException |
||
78 | */ |
||
79 | public function setRegion(int $left, int $top, int $width, int $height):self{ |
||
80 | |||
81 | if($top < 0 || $left < 0){ |
||
82 | throw new InvalidArgumentException('Left and top must be nonnegative'); |
||
83 | } |
||
84 | |||
85 | if($height < 1 || $width < 1){ |
||
86 | throw new InvalidArgumentException('Height and width must be at least 1'); |
||
87 | } |
||
88 | |||
89 | $right = $left + $width; |
||
90 | $bottom = $top + $height; |
||
91 | |||
92 | if($bottom > $this->dimension || $right > $this->dimension){ |
||
93 | throw new InvalidArgumentException('The region must fit inside the matrix'); |
||
94 | } |
||
95 | |||
96 | for($y = $top; $y < $bottom; $y++){ |
||
97 | $yOffset = $y * $this->rowSize; |
||
98 | |||
99 | for($x = $left; $x < $right; $x++){ |
||
100 | $xOffset = $yOffset + (int)($x / 0x20); |
||
101 | $this->bits[$xOffset] = ($this->bits[$xOffset] |= 1 << ($x & 0x1f)); |
||
102 | } |
||
103 | } |
||
104 | |||
105 | return $this; |
||
106 | } |
||
107 | |||
108 | /** |
||
109 | * @return int The dimension (width/height) of the matrix |
||
110 | */ |
||
111 | public function getDimension():int{ |
||
112 | return $this->dimension; |
||
113 | } |
||
114 | |||
115 | /** |
||
116 | * |
||
117 | */ |
||
118 | public function getFormatInfo():?FormatInformation{ |
||
119 | return $this->formatInfo; |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * |
||
124 | */ |
||
125 | public function getVersion():?Version{ |
||
126 | return $this->version; |
||
127 | } |
||
128 | |||
129 | /** |
||
130 | * <p>Gets the requested bit, where true means black.</p> |
||
131 | * |
||
132 | * @param int $x The horizontal component (i.e. which column) |
||
133 | * @param int $y The vertical component (i.e. which row) |
||
134 | * |
||
135 | * @return bool value of given bit in matrix |
||
136 | */ |
||
137 | public function get(int $x, int $y):bool{ |
||
138 | $offset = (int)($y * $this->rowSize + ($x / 0x20)); |
||
139 | |||
140 | $this->bits[$offset] ??= 0; |
||
141 | |||
142 | return ($this->uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0; |
||
143 | } |
||
144 | |||
145 | /** |
||
146 | * See ISO 18004:2006 Annex E |
||
147 | */ |
||
148 | private function buildFunctionPattern():self{ |
||
149 | $dimension = $this->version->getDimension(); |
||
|
|||
150 | $bitMatrix = new self($dimension); |
||
151 | |||
152 | // Top left finder pattern + separator + format |
||
153 | $bitMatrix->setRegion(0, 0, 9, 9); |
||
154 | // Top right finder pattern + separator + format |
||
155 | $bitMatrix->setRegion($dimension - 8, 0, 8, 9); |
||
156 | // Bottom left finder pattern + separator + format |
||
157 | $bitMatrix->setRegion(0, $dimension - 8, 9, 8); |
||
158 | |||
159 | // Alignment patterns |
||
160 | $apc = $this->version->getAlignmentPattern(); |
||
161 | $max = count($apc); |
||
162 | |||
163 | for($x = 0; $x < $max; $x++){ |
||
164 | $i = $apc[$x] - 2; |
||
165 | |||
166 | for($y = 0; $y < $max; $y++){ |
||
167 | if(($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)){ |
||
168 | // No alignment patterns near the three finder paterns |
||
169 | continue; |
||
170 | } |
||
171 | |||
172 | $bitMatrix->setRegion($apc[$y] - 2, $i, 5, 5); |
||
173 | } |
||
174 | } |
||
175 | |||
176 | // Vertical timing pattern |
||
177 | $bitMatrix->setRegion(6, 9, 1, $dimension - 17); |
||
178 | // Horizontal timing pattern |
||
179 | $bitMatrix->setRegion(9, 6, $dimension - 17, 1); |
||
180 | |||
181 | if($this->version->getVersionNumber() > 6){ |
||
182 | // Version info, top right |
||
183 | $bitMatrix->setRegion($dimension - 11, 0, 3, 6); |
||
184 | // Version info, bottom left |
||
185 | $bitMatrix->setRegion(0, $dimension - 11, 6, 3); |
||
186 | } |
||
187 | |||
188 | return $bitMatrix; |
||
189 | } |
||
190 | |||
191 | /** |
||
192 | * Mirror the bit matrix in order to attempt a second reading. |
||
193 | */ |
||
194 | public function mirror():self{ |
||
195 | |||
196 | for($x = 0; $x < $this->dimension; $x++){ |
||
197 | for($y = $x + 1; $y < $this->dimension; $y++){ |
||
198 | if($this->get($x, $y) !== $this->get($y, $x)){ |
||
199 | $this->flip($y, $x); |
||
200 | $this->flip($x, $y); |
||
201 | } |
||
202 | } |
||
203 | } |
||
204 | |||
205 | return $this; |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * Implementations of this method reverse the data masking process applied to a QR Code and |
||
210 | * make its bits ready to read. |
||
211 | */ |
||
212 | private function unmask():void{ |
||
213 | $mask = $this->formatInfo->getMaskPattern()->getMask(); |
||
214 | |||
215 | for($y = 0; $y < $this->dimension; $y++){ |
||
216 | for($x = 0; $x < $this->dimension; $x++){ |
||
217 | if($mask($x, $y)){ |
||
218 | $this->flip($x, $y); |
||
219 | } |
||
220 | } |
||
221 | } |
||
222 | |||
223 | } |
||
224 | |||
225 | /** |
||
226 | * Prepare the parser for a mirrored operation. |
||
227 | * This flag has effect only on the {@link #readFormatInformation()} and the |
||
228 | * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the |
||
229 | * {@link #mirror()} method should be called. |
||
230 | * |
||
231 | * @param bool $mirror Whether to read version and format information mirrored. |
||
232 | */ |
||
233 | public function setMirror(bool $mirror):self{ |
||
234 | $this->version = null; |
||
235 | $this->formatInfo = null; |
||
236 | $this->mirror = $mirror; |
||
237 | |||
238 | return $this; |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * |
||
243 | */ |
||
244 | private function copyBit(int $i, int $j, int $versionBits):int{ |
||
245 | |||
246 | $bit = $this->mirror |
||
247 | ? $this->get($j, $i) |
||
248 | : $this->get($i, $j); |
||
249 | |||
250 | return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1; |
||
251 | } |
||
252 | |||
253 | /** |
||
254 | * <p>Reads the bits in the {@link BitMatrix} representing the finder pattern in the |
||
255 | * correct order in order to reconstruct the codewords bytes contained within the |
||
256 | * QR Code.</p> |
||
257 | * |
||
258 | * @return array bytes encoded within the QR Code |
||
259 | * @throws \RuntimeException if the exact number of bytes expected is not read |
||
260 | */ |
||
261 | public function readCodewords():array{ |
||
262 | $this->formatInfo = $this->readFormatInformation(); |
||
263 | $this->version = $this->readVersion(); |
||
264 | |||
265 | // Get the data mask for the format used in this QR Code. This will exclude |
||
266 | // some bits from reading as we wind through the bit matrix. |
||
267 | $this->unmask(); |
||
268 | $functionPattern = $this->buildFunctionPattern(); |
||
269 | |||
270 | $readingUp = true; |
||
271 | $result = []; |
||
272 | $resultOffset = 0; |
||
273 | $currentByte = 0; |
||
274 | $bitsRead = 0; |
||
275 | // Read columns in pairs, from right to left |
||
276 | for($j = $this->dimension - 1; $j > 0; $j -= 2){ |
||
277 | |||
278 | if($j === 6){ |
||
279 | // Skip whole column with vertical alignment pattern; |
||
280 | // saves time and makes the other code proceed more cleanly |
||
281 | $j--; |
||
282 | } |
||
283 | // Read alternatingly from bottom to top then top to bottom |
||
284 | for($count = 0; $count < $this->dimension; $count++){ |
||
285 | $i = $readingUp ? $this->dimension - 1 - $count : $count; |
||
286 | |||
287 | for($col = 0; $col < 2; $col++){ |
||
288 | // Ignore bits covered by the function pattern |
||
289 | if(!$functionPattern->get($j - $col, $i)){ |
||
290 | // Read a bit |
||
291 | $bitsRead++; |
||
292 | $currentByte <<= 1; |
||
293 | |||
294 | if($this->get($j - $col, $i)){ |
||
295 | $currentByte |= 1; |
||
296 | } |
||
297 | // If we've made a whole byte, save it off |
||
298 | if($bitsRead === 8){ |
||
299 | $result[$resultOffset++] = $currentByte; //(byte) |
||
300 | $bitsRead = 0; |
||
301 | $currentByte = 0; |
||
302 | } |
||
303 | } |
||
304 | } |
||
305 | } |
||
306 | |||
307 | $readingUp = !$readingUp; // switch directions |
||
308 | } |
||
309 | |||
310 | if($resultOffset !== $this->version->getTotalCodewords()){ |
||
311 | throw new RuntimeException('offset differs from total codewords for version'); |
||
312 | } |
||
313 | |||
314 | return $result; |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * <p>Reads format information from one of its two locations within the QR Code.</p> |
||
319 | * |
||
320 | * @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info |
||
321 | * @throws \RuntimeException if both format information locations cannot be parsed as |
||
322 | * the valid encoding of format information |
||
323 | */ |
||
324 | private function readFormatInformation():FormatInformation{ |
||
376 | } |
||
377 | |||
378 | /** |
||
379 | * @param int $maskedFormatInfo1 format info indicator, with mask still applied |
||
380 | * @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time |
||
381 | * to establish best match |
||
382 | * |
||
383 | * @return \chillerlan\QRCode\Common\FormatInformation|null information about the format it specifies, or null |
||
384 | * if doesn't seem to match any known pattern |
||
385 | */ |
||
386 | private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?FormatInformation{ |
||
387 | // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing |
||
388 | $bestDifference = PHP_INT_MAX; |
||
389 | $bestFormatInfo = 0; |
||
390 | |||
391 | foreach(FormatInformation::DECODE_LOOKUP as $decodeInfo){ |
||
392 | [$maskedBits, $dataBits] = $decodeInfo; |
||
393 | |||
394 | if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){ |
||
395 | // Found an exact match |
||
396 | return new FormatInformation($maskedBits); |
||
397 | } |
||
398 | |||
399 | $bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits); |
||
400 | |||
401 | if($bitsDifference < $bestDifference){ |
||
402 | $bestFormatInfo = $maskedBits; |
||
403 | $bestDifference = $bitsDifference; |
||
404 | } |
||
405 | |||
406 | if($maskedFormatInfo1 !== $maskedFormatInfo2){ |
||
407 | // also try the other option |
||
408 | $bitsDifference = $this->numBitsDiffering($maskedFormatInfo2, $dataBits); |
||
409 | |||
410 | if($bitsDifference < $bestDifference){ |
||
411 | $bestFormatInfo = $maskedBits; |
||
412 | $bestDifference = $bitsDifference; |
||
413 | } |
||
414 | } |
||
415 | } |
||
416 | // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match |
||
417 | if($bestDifference <= 3){ |
||
418 | return new FormatInformation($bestFormatInfo); |
||
419 | } |
||
420 | |||
421 | return null; |
||
422 | } |
||
423 | |||
424 | /** |
||
425 | * <p>Reads version information from one of its two locations within the QR Code.</p> |
||
426 | * |
||
427 | * @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version |
||
428 | * @throws \RuntimeException if both version information locations cannot be parsed as |
||
429 | * the valid encoding of version information |
||
430 | */ |
||
431 | private function readVersion():Version{ |
||
432 | |||
433 | if($this->version !== null){ |
||
434 | return $this->version; |
||
435 | } |
||
436 | |||
437 | $provisionalVersion = ($this->dimension - 17) / 4; |
||
438 | |||
439 | if($provisionalVersion <= 6){ |
||
440 | return new Version($provisionalVersion); |
||
441 | } |
||
442 | |||
443 | // Read top-right version info: 3 wide by 6 tall |
||
444 | $versionBits = 0; |
||
445 | $ijMin = $this->dimension - 11; |
||
446 | |||
447 | for($j = 5; $j >= 0; $j--){ |
||
448 | for($i = $this->dimension - 9; $i >= $ijMin; $i--){ |
||
449 | $versionBits = $this->copyBit($i, $j, $versionBits); |
||
450 | } |
||
451 | } |
||
452 | |||
453 | $this->version = $this->decodeVersionInformation($versionBits); |
||
454 | |||
455 | if($this->version !== null && $this->version->getDimension() === $this->dimension){ |
||
456 | return $this->version; |
||
457 | } |
||
458 | |||
459 | // Hmm, failed. Try bottom left: 6 wide by 3 tall |
||
460 | $versionBits = 0; |
||
461 | |||
462 | for($i = 5; $i >= 0; $i--){ |
||
463 | for($j = $this->dimension - 9; $j >= $ijMin; $j--){ |
||
464 | $versionBits = $this->copyBit($i, $j, $versionBits); |
||
465 | } |
||
466 | } |
||
467 | |||
468 | $this->version = $this->decodeVersionInformation($versionBits); |
||
469 | |||
470 | if($this->version !== null && $this->version->getDimension() === $this->dimension){ |
||
471 | return $this->version; |
||
472 | } |
||
473 | |||
474 | throw new RuntimeException('failed to read version'); |
||
475 | } |
||
476 | |||
477 | /** |
||
478 | * @param int $versionBits |
||
479 | * |
||
480 | * @return \chillerlan\QRCode\Common\Version|null |
||
481 | */ |
||
482 | private function decodeVersionInformation(int $versionBits):?Version{ |
||
513 | } |
||
514 | |||
515 | /** |
||
516 | * |
||
517 | */ |
||
518 | private function uRShift(int $a, int $b):int{ |
||
519 | |||
520 | if($b === 0){ |
||
521 | return $a; |
||
522 | } |
||
523 | |||
524 | return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)); |
||
525 | } |
||
526 | |||
527 | /** |
||
528 | * |
||
529 | */ |
||
530 | private function numBitsDiffering(int $a, int $b):int{ |
||
543 | } |
||
544 | |||
545 | } |
||
546 |
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.