Explorar el Código

:shower: include BitMatrixParser in BitMatrix, cleanup

codemasher hace 4 años
padre
commit
e57f24c5d0
Se han modificado 3 ficheros con 395 adiciones y 434 borrados
  1. 363 19
      src/Decoder/BitMatrix.php
  2. 0 371
      src/Decoder/BitMatrixParser.php
  3. 32 44
      src/Decoder/Decoder.php

+ 363 - 19
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;
+	}
+
 	/**
 	 * <p>Gets the requested bit, where true means black.</p>
 	 *
@@ -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;
+	}
+
+	/**
+	 * <p>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.</p>
+	 *
+	 * @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;
+	}
+
+	/**
+	 * <p>Reads format information from one of its two locations within the QR Code.</p>
+	 *
+	 * @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;
+	}
+
+	/**
+	 * <p>Reads version information from one of its two locations within the QR Code.</p>
+	 *
+	 * @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;
+	}
+
 }

+ 0 - 371
src/Decoder/BitMatrixParser.php

@@ -1,371 +0,0 @@
-<?php
-/**
- * Class BitMatrixParser
- *
- * @created      17.01.2021
- * @author       ZXing Authors
- * @author       Smiley <smiley@chillerlan.net>
- * @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;
-	}
-
-	/**
-	 * <p>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.</p>
-	 *
-	 * @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;
-	}
-
-	/**
-	 * <p>Reads format information from one of its two locations within the QR Code.</p>
-	 *
-	 * @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;
-	}
-
-	/**
-	 * <p>Reads version information from one of its two locations within the QR Code.</p>
-	 *
-	 * @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;
-	}
-
-}

+ 32 - 44
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;
-		}
-
-		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);
+			return $this->decodeMatrix(clone $bitMatrix);
 		}
-		catch(Exception $e){
-			// Throw the exception from the original reading
-			if($fe instanceof Exception){
-				throw $fe;
+		catch(Throwable $e){
+
+			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);