From e57f24c5d0b75d13fb92ce1c215012294b0371bb Mon Sep 17 00:00:00 2001 From: codemasher Date: Mon, 29 Nov 2021 12:59:32 +0100 Subject: [PATCH] :shower: include BitMatrixParser in BitMatrix, cleanup --- src/Decoder/BitMatrix.php | 382 ++++++++++++++++++++++++++++++-- src/Decoder/BitMatrixParser.php | 371 ------------------------------- src/Decoder/Decoder.php | 74 +++---- 3 files changed, 394 insertions(+), 433 deletions(-) delete mode 100644 src/Decoder/BitMatrixParser.php diff --git a/src/Decoder/BitMatrix.php b/src/Decoder/BitMatrix.php index 11201d3ef..52065842e 100644 --- a/src/Decoder/BitMatrix.php +++ b/src/Decoder/BitMatrix.php @@ -11,18 +11,22 @@ namespace chillerlan\QRCode\Decoder; -use chillerlan\QRCode\Common\{MaskPattern, Version}; -use InvalidArgumentException; +use chillerlan\QRCode\Common\{FormatInformation, Version}; +use InvalidArgumentException, RuntimeException; use function array_fill, count; +use const PHP_INT_MAX, PHP_INT_SIZE; /** * */ final class BitMatrix{ - private int $dimension; - private int $rowSize; - private array $bits; + private int $dimension; + private int $rowSize; + private array $bits; + private ?Version $version = null; + private ?FormatInformation $formatInfo = null; + private bool $mirror = false; /** * @@ -39,11 +43,13 @@ final class BitMatrix{ * @param int $x ; The horizontal component (i.e. which column) * @param int $y ; The vertical component (i.e. which row) */ - public function set(int $x, int $y):void{ + public function set(int $x, int $y):self{ $offset = (int)($y * $this->rowSize + ($x / 0x20)); $this->bits[$offset] ??= 0; $this->bits[$offset] |= ($this->bits[$offset] |= 1 << ($x & 0x1f)); + + return $this; } /** @@ -52,10 +58,12 @@ final class BitMatrix{ * @param int $x ; The horizontal component (i.e. which column) * @param int $y ; The vertical component (i.e. which row) */ - public function flip(int $x, int $y):void{ + public function flip(int $x, int $y):self{ $offset = $y * $this->rowSize + (int)($x / 0x20); $this->bits[$offset] = ($this->bits[$offset] ^ (1 << ($x & 0x1f))); + + return $this; } /** @@ -68,7 +76,7 @@ final class BitMatrix{ * * @throws \InvalidArgumentException */ - public function setRegion(int $left, int $top, int $width, int $height):void{ + public function setRegion(int $left, int $top, int $width, int $height):self{ if($top < 0 || $left < 0){ throw new InvalidArgumentException('Left and top must be nonnegative'); @@ -93,6 +101,8 @@ final class BitMatrix{ $this->bits[$xOffset] = ($this->bits[$xOffset] |= 1 << ($x & 0x1f)); } } + + return $this; } /** @@ -102,6 +112,20 @@ final class BitMatrix{ return $this->dimension; } + /** + * + */ + public function getFormatInfo():?FormatInformation{ + return $this->formatInfo; + } + + /** + * + */ + public function getVersion():?Version{ + return $this->version; + } + /** *

Gets the requested bit, where true means black.

* @@ -115,15 +139,14 @@ final class BitMatrix{ $this->bits[$offset] ??= 0; - return (BitMatrixParser::uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0; + return ($this->uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0; } /** * See ISO 18004:2006 Annex E */ - public function buildFunctionPattern(Version $version):BitMatrix{ - $dimension = $version->getDimension(); - // @todo + private function buildFunctionPattern():self{ + $dimension = $this->version->getDimension(); $bitMatrix = new self($dimension); // Top left finder pattern + separator + format @@ -134,7 +157,7 @@ final class BitMatrix{ $bitMatrix->setRegion(0, $dimension - 8, 9, 8); // Alignment patterns - $apc = $version->getAlignmentPattern(); + $apc = $this->version->getAlignmentPattern(); $max = count($apc); for($x = 0; $x < $max; $x++){ @@ -155,7 +178,7 @@ final class BitMatrix{ // Horizontal timing pattern $bitMatrix->setRegion(9, 6, $dimension - 17, 1); - if($version->getVersionNumber() > 6){ + if($this->version->getVersionNumber() > 6){ // Version info, top right $bitMatrix->setRegion($dimension - 11, 0, 3, 6); // Version info, bottom left @@ -168,7 +191,7 @@ final class BitMatrix{ /** * Mirror the bit matrix in order to attempt a second reading. */ - public function mirror():void{ + public function mirror():self{ for($x = 0; $x < $this->dimension; $x++){ for($y = $x + 1; $y < $this->dimension; $y++){ @@ -179,17 +202,18 @@ final class BitMatrix{ } } + return $this; } /** * Implementations of this method reverse the data masking process applied to a QR Code and * make its bits ready to read. */ - public function unmask(int $dimension, MaskPattern $maskPattern):void{ - $mask = $maskPattern->getMask(); + private function unmask():void{ + $mask = $this->formatInfo->getMaskPattern()->getMask(); - for($y = 0; $y < $dimension; $y++){ - for($x = 0; $x < $dimension; $x++){ + for($y = 0; $y < $this->dimension; $y++){ + for($x = 0; $x < $this->dimension; $x++){ if($mask($x, $y)){ $this->flip($x, $y); } @@ -198,4 +222,324 @@ final class BitMatrix{ } + /** + * Prepare the parser for a mirrored operation. + * This flag has effect only on the {@link #readFormatInformation()} and the + * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the + * {@link #mirror()} method should be called. + * + * @param bool $mirror Whether to read version and format information mirrored. + */ + public function setMirror(bool $mirror):self{ + $this->version = null; + $this->formatInfo = null; + $this->mirror = $mirror; + + return $this; + } + + /** + * + */ + private function copyBit(int $i, int $j, int $versionBits):int{ + + $bit = $this->mirror + ? $this->get($j, $i) + : $this->get($i, $j); + + return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1; + } + + /** + *

Reads the bits in the {@link BitMatrix} representing the finder pattern in the + * correct order in order to reconstruct the codewords bytes contained within the + * QR Code.

+ * + * @return array bytes encoded within the QR Code + * @throws \RuntimeException if the exact number of bytes expected is not read + */ + public function readCodewords():array{ + $this->formatInfo = $this->readFormatInformation(); + $this->version = $this->readVersion(); + + // Get the data mask for the format used in this QR Code. This will exclude + // some bits from reading as we wind through the bit matrix. + $this->unmask(); + $functionPattern = $this->buildFunctionPattern(); + + $readingUp = true; + $result = []; + $resultOffset = 0; + $currentByte = 0; + $bitsRead = 0; + // Read columns in pairs, from right to left + for($j = $this->dimension - 1; $j > 0; $j -= 2){ + + if($j === 6){ + // Skip whole column with vertical alignment pattern; + // saves time and makes the other code proceed more cleanly + $j--; + } + // Read alternatingly from bottom to top then top to bottom + for($count = 0; $count < $this->dimension; $count++){ + $i = $readingUp ? $this->dimension - 1 - $count : $count; + + for($col = 0; $col < 2; $col++){ + // Ignore bits covered by the function pattern + if(!$functionPattern->get($j - $col, $i)){ + // Read a bit + $bitsRead++; + $currentByte <<= 1; + + if($this->get($j - $col, $i)){ + $currentByte |= 1; + } + // If we've made a whole byte, save it off + if($bitsRead === 8){ + $result[$resultOffset++] = $currentByte; //(byte) + $bitsRead = 0; + $currentByte = 0; + } + } + } + } + + $readingUp = !$readingUp; // switch directions + } + + if($resultOffset !== $this->version->getTotalCodewords()){ + throw new RuntimeException('offset differs from total codewords for version'); + } + + return $result; + } + + /** + *

Reads format information from one of its two locations within the QR Code.

+ * + * @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info + * @throws \RuntimeException if both format information locations cannot be parsed as + * the valid encoding of format information + */ + private function readFormatInformation():FormatInformation{ + + if($this->formatInfo !== null){ + return $this->formatInfo; + } + + // Read top-left format info bits + $formatInfoBits1 = 0; + + for($i = 0; $i < 6; $i++){ + $formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1); + } + + // .. and skip a bit in the timing pattern ... + $formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1); + // .. and skip a bit in the timing pattern ... + for($j = 5; $j >= 0; $j--){ + $formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1); + } + + // Read the top-right/bottom-left pattern too + $formatInfoBits2 = 0; + $jMin = $this->dimension - 7; + + for($j = $this->dimension - 1; $j >= $jMin; $j--){ + $formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2); + } + + for($i = $this->dimension - 8; $i < $this->dimension; $i++){ + $formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2); + } + + $this->formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2); + + if($this->formatInfo !== null){ + return $this->formatInfo; + } + + // Should return null, but, some QR codes apparently do not mask this info. + // Try again by actually masking the pattern first. + $this->formatInfo = $this->doDecodeFormatInformation( + $formatInfoBits1 ^ FormatInformation::FORMAT_INFO_MASK_QR, + $formatInfoBits2 ^ FormatInformation::FORMAT_INFO_MASK_QR + ); + + if($this->formatInfo !== null){ + return $this->formatInfo; + } + + throw new RuntimeException('failed to read format info'); + } + + /** + * @param int $maskedFormatInfo1 format info indicator, with mask still applied + * @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time + * to establish best match + * + * @return \chillerlan\QRCode\Common\FormatInformation|null information about the format it specifies, or null + * if doesn't seem to match any known pattern + */ + private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?FormatInformation{ + // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing + $bestDifference = PHP_INT_MAX; + $bestFormatInfo = 0; + + foreach(FormatInformation::DECODE_LOOKUP as $decodeInfo){ + [$maskedBits, $dataBits] = $decodeInfo; + + if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){ + // Found an exact match + return new FormatInformation($maskedBits); + } + + $bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits); + + if($bitsDifference < $bestDifference){ + $bestFormatInfo = $maskedBits; + $bestDifference = $bitsDifference; + } + + if($maskedFormatInfo1 !== $maskedFormatInfo2){ + // also try the other option + $bitsDifference = $this->numBitsDiffering($maskedFormatInfo2, $dataBits); + + if($bitsDifference < $bestDifference){ + $bestFormatInfo = $maskedBits; + $bestDifference = $bitsDifference; + } + } + } + // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match + if($bestDifference <= 3){ + return new FormatInformation($bestFormatInfo); + } + + return null; + } + + /** + *

Reads version information from one of its two locations within the QR Code.

+ * + * @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version + * @throws \RuntimeException if both version information locations cannot be parsed as + * the valid encoding of version information + */ + private function readVersion():Version{ + + if($this->version !== null){ + return $this->version; + } + + $provisionalVersion = ($this->dimension - 17) / 4; + + if($provisionalVersion <= 6){ + return new Version($provisionalVersion); + } + + // Read top-right version info: 3 wide by 6 tall + $versionBits = 0; + $ijMin = $this->dimension - 11; + + for($j = 5; $j >= 0; $j--){ + for($i = $this->dimension - 9; $i >= $ijMin; $i--){ + $versionBits = $this->copyBit($i, $j, $versionBits); + } + } + + $this->version = $this->decodeVersionInformation($versionBits); + + if($this->version !== null && $this->version->getDimension() === $this->dimension){ + return $this->version; + } + + // Hmm, failed. Try bottom left: 6 wide by 3 tall + $versionBits = 0; + + for($i = 5; $i >= 0; $i--){ + for($j = $this->dimension - 9; $j >= $ijMin; $j--){ + $versionBits = $this->copyBit($i, $j, $versionBits); + } + } + + $this->version = $this->decodeVersionInformation($versionBits); + + if($this->version !== null && $this->version->getDimension() === $this->dimension){ + return $this->version; + } + + throw new RuntimeException('failed to read version'); + } + + /** + * @param int $versionBits + * + * @return \chillerlan\QRCode\Common\Version|null + */ + private function decodeVersionInformation(int $versionBits):?Version{ + $bestDifference = PHP_INT_MAX; + $bestVersion = 0; + + for($i = 7; $i <= 40; $i++){ + $targetVersion = new Version($i); + $targetVersionPattern = $targetVersion->getVersionPattern(); + + // Do the version info bits match exactly? done. + if($targetVersionPattern === $versionBits){ + return $targetVersion; + } + + // Otherwise see if this is the closest to a real version info bit string + // we have seen so far + /** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */ + $bitsDifference = $this->numBitsDiffering($versionBits, $targetVersionPattern); + + if($bitsDifference < $bestDifference){ + $bestVersion = $i; + $bestDifference = $bitsDifference; + } + } + // We can tolerate up to 3 bits of error since no two version info codewords will + // differ in less than 8 bits. + if($bestDifference <= 3){ + return new Version($bestVersion); + } + + // If we didn't find a close enough match, fail + return null; + } + + /** + * + */ + private function uRShift(int $a, int $b):int{ + + if($b === 0){ + return $a; + } + + return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)); + } + + /** + * + */ + private function numBitsDiffering(int $a, int $b):int{ + // a now has a 1 bit exactly where its bit differs with b's + $a ^= $b; + // Offset i holds the number of 1 bits in the binary representation of i + $BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4]; + // Count bits set quickly with a series of lookups: + $count = 0; + + for($i = 0; $i < 32; $i += 4){ + $count += $BITS_SET_IN_HALF_BYTE[$this->uRShift($a, $i) & 0x0F]; + } + + return $count; + } + } diff --git a/src/Decoder/BitMatrixParser.php b/src/Decoder/BitMatrixParser.php deleted file mode 100644 index 95ceb2ecc..000000000 --- a/src/Decoder/BitMatrixParser.php +++ /dev/null @@ -1,371 +0,0 @@ - - * @copyright 2021 Smiley - * @license Apache-2.0 - */ - -namespace chillerlan\QRCode\Decoder; - -use RuntimeException; -use chillerlan\QRCode\Common\{Version, FormatInformation}; -use const PHP_INT_MAX, PHP_INT_SIZE; - -/** - * @author Sean Owen - */ -final class BitMatrixParser{ - - private BitMatrix $bitMatrix; - private ?Version $parsedVersion = null; - private ?FormatInformation $parsedFormatInfo = null; - private bool $mirror = false; - - /** - * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix - * - * @throws \RuntimeException if dimension is not >= 21 and 1 mod 4 - */ - public function __construct(BitMatrix $bitMatrix){ - $dimension = $bitMatrix->getDimension(); - - if($dimension < 21 || ($dimension % 4) !== 1){ - throw new RuntimeException('dimension is not >= 21, dimension mod 4 not 1'); - } - - $this->bitMatrix = $bitMatrix; - } - - /** - * Prepare the parser for a mirrored operation. - * This flag has effect only on the {@link #readFormatInformation()} and the - * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the - * {@link #mirror()} method should be called. - * - * @param bool $mirror Whether to read version and format information mirrored. - */ - public function setMirror(bool $mirror):void{ - $this->parsedVersion = null; - $this->parsedFormatInfo = null; - $this->mirror = $mirror; - } - - /** - * Mirror the bit matrix in order to attempt a second reading. - */ - public function mirror():void{ - $this->bitMatrix->mirror(); - } - - /** - * - */ - private function copyBit(int $i, int $j, int $versionBits):int{ - - $bit = $this->mirror - ? $this->bitMatrix->get($j, $i) - : $this->bitMatrix->get($i, $j); - - return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1; - } - - /** - *

Reads the bits in the {@link BitMatrix} representing the finder pattern in the - * correct order in order to reconstruct the codewords bytes contained within the - * QR Code.

- * - * @return array bytes encoded within the QR Code - * @throws \RuntimeException if the exact number of bytes expected is not read - */ - public function readCodewords():array{ - $formatInfo = $this->readFormatInformation(); - $version = $this->readVersion(); - - // Get the data mask for the format used in this QR Code. This will exclude - // some bits from reading as we wind through the bit matrix. - $dimension = $this->bitMatrix->getDimension(); - $this->bitMatrix->unmask($dimension, $formatInfo->getMaskPattern()); - $functionPattern = $this->bitMatrix->buildFunctionPattern($version); - - $readingUp = true; - $result = []; - $resultOffset = 0; - $currentByte = 0; - $bitsRead = 0; - // Read columns in pairs, from right to left - for($j = $dimension - 1; $j > 0; $j -= 2){ - - if($j === 6){ - // Skip whole column with vertical alignment pattern; - // saves time and makes the other code proceed more cleanly - $j--; - } - // Read alternatingly from bottom to top then top to bottom - for($count = 0; $count < $dimension; $count++){ - $i = $readingUp ? $dimension - 1 - $count : $count; - - for($col = 0; $col < 2; $col++){ - // Ignore bits covered by the function pattern - if(!$functionPattern->get($j - $col, $i)){ - // Read a bit - $bitsRead++; - $currentByte <<= 1; - - if($this->bitMatrix->get($j - $col, $i)){ - $currentByte |= 1; - } - // If we've made a whole byte, save it off - if($bitsRead === 8){ - $result[$resultOffset++] = $currentByte; //(byte) - $bitsRead = 0; - $currentByte = 0; - } - } - } - } - - $readingUp = !$readingUp; // switch directions - } - - if($resultOffset !== $version->getTotalCodewords()){ - throw new RuntimeException('offset differs from total codewords for version'); - } - - return $result; - } - - /** - *

Reads format information from one of its two locations within the QR Code.

- * - * @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info - * @throws \RuntimeException if both format information locations cannot be parsed as - * the valid encoding of format information - */ - public function readFormatInformation():FormatInformation{ - - if($this->parsedFormatInfo !== null){ - return $this->parsedFormatInfo; - } - - // Read top-left format info bits - $formatInfoBits1 = 0; - - for($i = 0; $i < 6; $i++){ - $formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1); - } - - // .. and skip a bit in the timing pattern ... - $formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1); - $formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1); - $formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1); - // .. and skip a bit in the timing pattern ... - for($j = 5; $j >= 0; $j--){ - $formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1); - } - - // Read the top-right/bottom-left pattern too - $dimension = $this->bitMatrix->getDimension(); - $formatInfoBits2 = 0; - $jMin = $dimension - 7; - - for($j = $dimension - 1; $j >= $jMin; $j--){ - $formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2); - } - - for($i = $dimension - 8; $i < $dimension; $i++){ - $formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2); - } - - $this->parsedFormatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2); - - if($this->parsedFormatInfo !== null){ - return $this->parsedFormatInfo; - } - - // Should return null, but, some QR codes apparently do not mask this info. - // Try again by actually masking the pattern first. - $this->parsedFormatInfo = $this->doDecodeFormatInformation( - $formatInfoBits1 ^ FormatInformation::FORMAT_INFO_MASK_QR, - $formatInfoBits2 ^ FormatInformation::FORMAT_INFO_MASK_QR - ); - - if($this->parsedFormatInfo !== null){ - return $this->parsedFormatInfo; - } - - throw new RuntimeException('failed to read format info'); - } - - /** - * @param int $maskedFormatInfo1 format info indicator, with mask still applied - * @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time - * to establish best match - * - * @return \chillerlan\QRCode\Common\FormatInformation|null information about the format it specifies, or null - * if doesn't seem to match any known pattern - */ - private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?FormatInformation{ - // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing - $bestDifference = PHP_INT_MAX; - $bestFormatInfo = 0; - - foreach(FormatInformation::DECODE_LOOKUP as $decodeInfo){ - [$maskedBits, $dataBits] = $decodeInfo; - - if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){ - // Found an exact match - return new FormatInformation($maskedBits); - } - - $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $dataBits); - - if($bitsDifference < $bestDifference){ - $bestFormatInfo = $maskedBits; - $bestDifference = $bitsDifference; - } - - if($maskedFormatInfo1 !== $maskedFormatInfo2){ - // also try the other option - $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $dataBits); - - if($bitsDifference < $bestDifference){ - $bestFormatInfo = $maskedBits; - $bestDifference = $bitsDifference; - } - } - } - // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match - if($bestDifference <= 3){ - return new FormatInformation($bestFormatInfo); - } - - return null; - } - - /** - *

Reads version information from one of its two locations within the QR Code.

- * - * @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version - * @throws \RuntimeException if both version information locations cannot be parsed as - * the valid encoding of version information - */ - public function readVersion():Version{ - - if($this->parsedVersion !== null){ - return $this->parsedVersion; - } - - $dimension = $this->bitMatrix->getDimension(); - $provisionalVersion = ($dimension - 17) / 4; - - if($provisionalVersion <= 6){ - return new Version($provisionalVersion); - } - - // Read top-right version info: 3 wide by 6 tall - $versionBits = 0; - $ijMin = $dimension - 11; - - for($j = 5; $j >= 0; $j--){ - for($i = $dimension - 9; $i >= $ijMin; $i--){ - $versionBits = $this->copyBit($i, $j, $versionBits); - } - } - - $this->parsedVersion = $this->decodeVersionInformation($versionBits); - - if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){ - return $this->parsedVersion; - } - - // Hmm, failed. Try bottom left: 6 wide by 3 tall - $versionBits = 0; - - for($i = 5; $i >= 0; $i--){ - for($j = $dimension - 9; $j >= $ijMin; $j--){ - $versionBits = $this->copyBit($i, $j, $versionBits); - } - } - - $this->parsedVersion = $this->decodeVersionInformation($versionBits); - - if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){ - return $this->parsedVersion; - } - - throw new RuntimeException('failed to read version'); - } - - /** - * @param int $versionBits - * - * @return \chillerlan\QRCode\Common\Version|null - */ - private function decodeVersionInformation(int $versionBits):?Version{ - $bestDifference = PHP_INT_MAX; - $bestVersion = 0; - - for($i = 7; $i <= 40; $i++){ - $targetVersion = new Version($i); - $targetVersionPattern = $targetVersion->getVersionPattern(); - - // Do the version info bits match exactly? done. - if($targetVersionPattern === $versionBits){ - return $targetVersion; - } - - // Otherwise see if this is the closest to a real version info bit string - // we have seen so far - /** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */ - $bitsDifference = self::numBitsDiffering($versionBits, $targetVersionPattern); - - if($bitsDifference < $bestDifference){ - $bestVersion = $i; - $bestDifference = $bitsDifference; - } - } - // We can tolerate up to 3 bits of error since no two version info codewords will - // differ in less than 8 bits. - if($bestDifference <= 3){ - return new Version($bestVersion); - } - - // If we didn't find a close enough match, fail - return null; - } - - /** - * - */ - public static function uRShift(int $a, int $b):int{ - - if($b === 0){ - return $a; - } - - return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)); - } - - /** - * - */ - private static function numBitsDiffering(int $a, int $b):int{ - // a now has a 1 bit exactly where its bit differs with b's - $a ^= $b; - // Offset i holds the number of 1 bits in the binary representation of i - $BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4]; - // Count bits set quickly with a series of lookups: - $count = 0; - - for($i = 0; $i < 32; $i += 4){ - $count += $BITS_SET_IN_HALF_BYTE[self::uRShift($a, $i) & 0x0F]; - } - - return $count; - } - -} diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php index 7a48d893d..b7b55ed5c 100644 --- a/src/Decoder/Decoder.php +++ b/src/Decoder/Decoder.php @@ -11,7 +11,7 @@ namespace chillerlan\QRCode\Decoder; -use Exception, InvalidArgumentException, RuntimeException; +use InvalidArgumentException, RuntimeException, Throwable; use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, ReedSolomonDecoder, Version}; use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number}; use chillerlan\QRCode\Detector\Detector; @@ -34,68 +34,56 @@ final class Decoder{ * @param \chillerlan\QRCode\Decoder\LuminanceSourceInterface $source * * @return \chillerlan\QRCode\Decoder\DecoderResult text and bytes encoded within the QR Code - * @throws \Exception if the QR Code cannot be decoded + * @throws \Throwable if the QR Code cannot be decoded */ public function decode(LuminanceSourceInterface $source):DecoderResult{ $matrix = (new Binarizer($source))->getBlackMatrix(); $bitMatrix = (new Detector($matrix))->detect(); - $fe = null; - try{ - // Construct a parser and read version, error-correction level // clone the BitMatrix to avoid errors in case we run into mirroring - return $this->decodeParser(new BitMatrixParser(clone $bitMatrix)); - } - catch(Exception $e){ - $fe = $e; + return $this->decodeMatrix(clone $bitMatrix); } + catch(Throwable $e){ - try{ - $parser = new BitMatrixParser(clone $bitMatrix); - - // Will be attempting a mirrored reading of the version and format info. - $parser->setMirror(true); - - // Preemptively read the version. -# $parser->readVersion(); - - // Preemptively read the format information. -# $parser->readFormatInformation(); - - /* - * Since we're here, this means we have successfully detected some kind - * of version and format information when mirrored. This is a good sign, - * that the QR code may be mirrored, and we should try once more with a - * mirrored content. - */ - // Prepare for a mirrored reading. - $parser->mirror(); - - return $this->decodeParser($parser); - } - catch(Exception $e){ - // Throw the exception from the original reading - if($fe instanceof Exception){ - throw $fe; + try{ + /* + * Prepare for a mirrored reading. + * + * Since we're here, this means we have successfully detected some kind + * of version and format information when mirrored. This is a good sign, + * that the QR code may be mirrored, and we should try once more with a + * mirrored content. + */ + return $this->decodeMatrix($bitMatrix->setMirror(true)->mirror()); + } + catch(Throwable $f){ + // Throw the exception from the original reading + throw $e; } - throw $e; } } /** - * @param \chillerlan\QRCode\Decoder\BitMatrixParser $parser + * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix * * @return \chillerlan\QRCode\Decoder\DecoderResult */ - private function decodeParser(BitMatrixParser $parser):DecoderResult{ - $version = $parser->readVersion(); - $eccLevel = $parser->readFormatInformation()->getErrorCorrectionLevel(); - + private function decodeMatrix(BitMatrix $bitMatrix):DecoderResult{ // Read raw codewords - $rawCodewords = $parser->readCodewords(); + $rawCodewords = $bitMatrix->readCodewords(); + $version = $bitMatrix->getVersion(); + $formatInfo = $bitMatrix->getFormatInfo(); + + // technically this shouldn't happen as the respective read meathods would throw first + if($version === null || $formatInfo === null){ + throw new RuntimeException('unable to read version or ecc level'); + } + + $eccLevel = $formatInfo->getErrorCorrectionLevel(); + // Separate into data blocks $dataBlocks = $this->getDataBlocks($rawCodewords, $version, $eccLevel);