From 7c183e7e88d386993a51494400ff3d0d4df24c96 Mon Sep 17 00:00:00 2001 From: smiley Date: Thu, 26 May 2022 22:16:19 +0200 Subject: [PATCH] :bath: merge QRMatrix and BitMatrix --- src/Common/FormatInformation.php | 102 ----- src/Common/ReedSolomonEncoder.php | 66 +--- src/Data/QRData.php | 12 +- src/Data/QRMatrix.php | 113 ++++-- src/Decoder/Binarizer.php | 13 +- src/Decoder/BitMatrix.php | 479 +++++++++--------------- src/Decoder/Decoder.php | 29 +- src/Detector/AlignmentPatternFinder.php | 24 +- src/Detector/Detector.php | 16 +- src/Detector/FinderPatternFinder.php | 56 +-- src/Detector/GridSampler.php | 19 +- tests/Data/QRMatrixTest.php | 7 +- 12 files changed, 348 insertions(+), 588 deletions(-) delete mode 100644 src/Common/FormatInformation.php diff --git a/src/Common/FormatInformation.php b/src/Common/FormatInformation.php deleted file mode 100644 index ac5500f21..000000000 --- a/src/Common/FormatInformation.php +++ /dev/null @@ -1,102 +0,0 @@ - - * @copyright 2021 Smiley - * @license Apache-2.0 - */ - -namespace chillerlan\QRCode\Common; - -/** - * Encapsulates a QR Code's format information, including the data mask used and error correction level. - * - * @author Sean Owen - * @see \chillerlan\QRCode\Common\EccLevel - */ -final class FormatInformation{ - - public const FORMAT_INFO_MASK_QR = 0x5412; - - /** - * See ISO 18004:2006, Annex C, Table C.1 - * - * [data bits, sequence after masking] - */ - public const DECODE_LOOKUP = [ - [0x00, 0x5412], - [0x01, 0x5125], - [0x02, 0x5E7C], - [0x03, 0x5B4B], - [0x04, 0x45F9], - [0x05, 0x40CE], - [0x06, 0x4F97], - [0x07, 0x4AA0], - [0x08, 0x77C4], - [0x09, 0x72F3], - [0x0A, 0x7DAA], - [0x0B, 0x789D], - [0x0C, 0x662F], - [0x0D, 0x6318], - [0x0E, 0x6C41], - [0x0F, 0x6976], - [0x10, 0x1689], - [0x11, 0x13BE], - [0x12, 0x1CE7], - [0x13, 0x19D0], - [0x14, 0x0762], - [0x15, 0x0255], - [0x16, 0x0D0C], - [0x17, 0x083B], - [0x18, 0x355F], - [0x19, 0x3068], - [0x1A, 0x3F31], - [0x1B, 0x3A06], - [0x1C, 0x24B4], - [0x1D, 0x2183], - [0x1E, 0x2EDA], - [0x1F, 0x2BED], - ]; - - /** - * The current ECC level value - * - * L: 0b01 - * M: 0b00 - * Q: 0b11 - * H: 0b10 - */ - private int $errorCorrectionLevel; - - /** - * The current mask pattern (0-7) - */ - private int $maskPattern; - - /** - * Receives the format information from a parsed QR Code, detects ECC level and mask pattern - */ - public function __construct(int $formatInfo){ - $this->errorCorrectionLevel = ($formatInfo >> 3) & 0x03; // Bits 3,4 - $this->maskPattern = ($formatInfo & 0x07); // Bottom 3 bits - } - - /** - * Returns and EccLevel instance ith the detected ECC level set - */ - public function getErrorCorrectionLevel():EccLevel{ - return new EccLevel($this->errorCorrectionLevel); - } - - /** - * Returns a MaskPattern instance with the detected mask pattern set - */ - public function getMaskPattern():MaskPattern{ - return new MaskPattern($this->maskPattern); - } - -} - diff --git a/src/Common/ReedSolomonEncoder.php b/src/Common/ReedSolomonEncoder.php index 5f3c6549b..375ac67d8 100644 --- a/src/Common/ReedSolomonEncoder.php +++ b/src/Common/ReedSolomonEncoder.php @@ -10,7 +10,6 @@ namespace chillerlan\QRCode\Common; -use chillerlan\QRCode\Data\QRMatrix; use function array_fill, array_merge, count, max; /** @@ -25,10 +24,11 @@ final class ReedSolomonEncoder{ /** * ECC interleaving + * + * @throws \chillerlan\QRCode\QRCodeException */ - public function interleaveEcBytes(BitBuffer $bitBuffer, QRMatrix $matrix):QRMatrix{ - $version = $matrix->version(); - [$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $version->getRSBlocks($matrix->eccLevel()); + public function interleaveEcBytes(BitBuffer $bitBuffer, Version $version, EccLevel $eccLevel):array{ + [$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $version->getRSBlocks($eccLevel); $rsBlocks = array_fill(0, $l1, [$numEccCodewords + $b1, $b1]); @@ -66,7 +66,7 @@ final class ReedSolomonEncoder{ $this->interleave($dataBytes, $maxDataBytes, $numRsBlocks); $this->interleave($ecBytes, $maxEcBytes, $numRsBlocks); - return $this->mapData($matrix); + return $this->interleavedData; } /** @@ -110,60 +110,4 @@ final class ReedSolomonEncoder{ } } - /** - * Maps the interleaved binary $data on the matrix - */ - private function mapData(QRMatrix $matrix):QRMatrix{ - $byteCount = count($this->interleavedData); - $size = $matrix->size(); - $y = $size - 1; - $inc = -1; - $byteIndex = 0; - $bitIndex = 7; - - for($i = $y; $i > 0; $i -= 2){ - - if($i === 6){ - $i--; - } - - while(true){ - for($c = 0; $c < 2; $c++){ - $x = $i - $c; - - if($matrix->get($x, $y) !== QRMatrix::M_NULL){ - continue; - } - - $v = false; - - if($byteIndex < $byteCount){ - $v = (($this->interleavedData[$byteIndex] >> $bitIndex) & 1) === 1; - } - - $matrix->set($x, $y, $v, QRMatrix::M_DATA); - $bitIndex--; - - if($bitIndex === -1){ - $byteIndex++; - $bitIndex = 7; - } - - } - - $y += $inc; - - if($y < 0 || $size <= $y){ - $y -= $inc; - $inc = -$inc; - - break; - } - - } - } - - return $matrix; - } - } diff --git a/src/Data/QRData.php b/src/Data/QRData.php index 351be90f9..dc9e88a47 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -10,7 +10,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, ReedSolomonEncoder, Version}; +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, Version}; use chillerlan\QRCode\QRCode; use chillerlan\Settings\SettingsContainerInterface; @@ -94,14 +94,10 @@ final class QRData{ * returns a fresh matrix object with the data written and masked with the given $maskPattern */ public function writeMatrix(MaskPattern $maskPattern):QRMatrix{ - $matrix = (new QRMatrix($this->version, $this->eccLevel)) + return (new QRMatrix($this->version, $this->eccLevel, $maskPattern)) ->initFunctionalPatterns() - ->initFormatInfo($maskPattern) - ; - - return (new ReedSolomonEncoder) - ->interleaveEcBytes($this->bitBuffer, $matrix) - ->mask($maskPattern) + ->writeCodewords($this->bitBuffer) + ->mask() ; } diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index 5bf0ed9c4..f9509c46d 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -10,10 +10,8 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version}; - -use SplFixedArray; -use function array_fill, array_unshift, floor, max, min, range; +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, ReedSolomonEncoder, Version}; +use function array_fill, array_unshift, count, floor, max, min, range; /** * Holds a numerical representation of the final QR Code; @@ -21,7 +19,7 @@ use function array_fill, array_unshift, floor, max, min, range; * * @see http://www.thonky.com/qr-code-tutorial/format-version-information */ -final class QRMatrix{ +class QRMatrix{ /** @var int */ public const M_NULL = 0b000000000000; @@ -55,36 +53,37 @@ final class QRMatrix{ /** * the used mask pattern, set via QRMatrix::mask() */ - private ?MaskPattern $maskPattern = null; + protected ?MaskPattern $maskPattern = null; + + /** + * the current ECC level + */ + protected ?EccLevel $eccLevel = null; + + /** + * a Version instance + */ + protected ?Version $version = null; /** * the size (side length) of the matrix, including quiet zone (if created) */ - private int $moduleCount; + protected int $moduleCount; /** * the actual matrix data array * * @var int[][] */ - private array $matrix; - - /** - * the current ECC level - */ - private EccLevel $eccLevel; - - /** - * a Version instance - */ - private Version $version; + protected array $matrix; /** * QRMatrix constructor. */ - public function __construct(Version $version, EccLevel $eccLevel){ + public function __construct(Version $version, EccLevel $eccLevel, MaskPattern $maskPattern){ $this->version = $version; $this->eccLevel = $eccLevel; + $this->maskPattern = $maskPattern; $this->moduleCount = $this->version->getDimension(); $this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL)); } @@ -99,16 +98,8 @@ final class QRMatrix{ ->setAlignmentPattern() ->setTimingPattern() ->setDarkModule() - ; - } - - /** - * shortcut to set format and version info - */ - public function initFormatInfo(MaskPattern $maskPattern):self{ - return $this ->setVersionNumber() - ->setFormatInfo($maskPattern) + ->setFormatInfo() ; } @@ -139,14 +130,14 @@ final class QRMatrix{ /** * Returns the current version number */ - public function version():Version{ + public function version():?Version{ return $this->version; } /** * Returns the current ECC level */ - public function eccLevel():EccLevel{ + public function eccLevel():?EccLevel{ return $this->eccLevel; } @@ -385,8 +376,8 @@ final class QRMatrix{ * * ISO/IEC 18004:2000 Section 8.9 */ - public function setFormatInfo(MaskPattern $maskPattern):self{ - $bits = $this->eccLevel->getformatPattern($maskPattern); + public function setFormatInfo():self{ + $bits = $this->eccLevel->getformatPattern($this->maskPattern); for($i = 0; $i < 15; $i++){ $v = (($bits >> $i) & 1) === 1; @@ -533,18 +524,62 @@ final class QRMatrix{ } /** - * Applies the mask pattern + * Maps the interleaved binary $data on the matrix + */ + public function writeCodewords(BitBuffer $bitBuffer):self{ + $data = (new ReedSolomonEncoder)->interleaveEcBytes($bitBuffer, $this->version, $this->eccLevel); + $byteCount = count($data); + $iByte = 0; + $iBit = 7; + $direction = true; + + for($i = $this->moduleCount - 1; $i > 0; $i -= 2){ + + // skip vertical alignment pattern + if($i === 6){ + $i--; + } + + for($count = 0; $count < $this->moduleCount; $count++){ + $y = $direction ? $this->moduleCount - 1 - $count : $count; + + for($col = 0; $col < 2; $col++){ + $x = $i - $col; + + // skip functional patterns + if($this->get($x, $y) !== $this::M_NULL){ + continue; + } + + $v = $iByte < $byteCount && (($data[$iByte] >> $iBit--) & 1) === 1; + + $this->set($x, $y, $v, $this::M_DATA); + + if($iBit === -1){ + $iByte++; + $iBit = 7; + } + } + } + + $direction = !$direction; // switch directions + } + + return $this; + } + + /** + * Applies/reverses the mask pattern * * ISO/IEC 18004:2000 Section 8.8.1 */ - public function mask(MaskPattern $maskPattern):self{ - $this->maskPattern = $maskPattern; - $mask = $this->maskPattern->getMask(); + public function mask():self{ + $mask = $this->maskPattern->getMask(); - foreach($this->matrix as $y => &$row){ - foreach($row as $x => &$val){ + foreach($this->matrix as $y => $row){ + foreach($row as $x => $val){ if($mask($x, $y) && ($val & $this::M_DATA) === $this::M_DATA){ - $val ^= $this::IS_DARK; + $this->flip($x, $y); } } } diff --git a/src/Decoder/Binarizer.php b/src/Decoder/Binarizer.php index f6bf11f7b..9a5e8af63 100644 --- a/src/Decoder/Binarizer.php +++ b/src/Decoder/Binarizer.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCode\Decoder; +use chillerlan\QRCode\Data\QRMatrix; use function array_fill, count, max; /** @@ -160,7 +161,6 @@ final class Binarizer{ * */ private function getHistogramBlackMatrix(int $width, int $height):BitMatrix{ - $matrix = new BitMatrix(max($width, $height)); // Quickly calculates the histogram by sampling four rows from the image. This proved to be // more robust on the blackbox tests than sampling a diagonal as we used to do. @@ -183,16 +183,13 @@ final class Binarizer{ // Although we end up reading four rows twice, it is consistent with our motto of // "fail quickly" which is necessary for continuous scanning. $localLuminances = $this->source->getMatrix(); + $matrix = new BitMatrix(max($width, $height)); for($y = 0; $y < $height; $y++){ $offset = $y * $width; for($x = 0; $x < $width; $x++){ - $pixel = $localLuminances[$offset + $x] & 0xff; - - if($pixel < $blackPoint){ - $matrix->set($x, $y); - } + $matrix->set($x, $y, (($localLuminances[$offset + $x] & 0xff) < $blackPoint), QRMatrix::M_DATA); } } @@ -339,9 +336,7 @@ final class Binarizer{ for($j = 0, $o = $yoffset * $width + $xoffset; $j < self::BLOCK_SIZE; $j++, $o += $width){ for($i = 0; $i < self::BLOCK_SIZE; $i++){ // Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0. - if(((int)($luminances[$o + $i]) & 0xff) <= $average){ - $matrix->set($xoffset + $i, $yoffset + $j); - } + $matrix->set($xoffset + $i, $yoffset + $j, (((int)($luminances[$o + $i]) & 0xff) <= $average), QRMatrix::M_DATA); } } } diff --git a/src/Decoder/BitMatrix.php b/src/Decoder/BitMatrix.php index fa23633fe..752da2e28 100644 --- a/src/Decoder/BitMatrix.php +++ b/src/Decoder/BitMatrix.php @@ -11,214 +11,66 @@ namespace chillerlan\QRCode\Decoder; -use chillerlan\QRCode\Common\{FormatInformation, Version}; +use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version}; +use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix}; 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 ?Version $version = null; - private ?FormatInformation $formatInfo = null; - private bool $mirror = false; +final class BitMatrix extends QRMatrix{ /** + * See ISO 18004:2006, Annex C, Table C.1 * + * [data bits, sequence after masking] + */ + private const DECODE_LOOKUP = [ + 0x00 => 0x5412, + 0x01 => 0x5125, + 0x02 => 0x5E7C, + 0x03 => 0x5B4B, + 0x04 => 0x45F9, + 0x05 => 0x40CE, + 0x06 => 0x4F97, + 0x07 => 0x4AA0, + 0x08 => 0x77C4, + 0x09 => 0x72F3, + 0x0A => 0x7DAA, + 0x0B => 0x789D, + 0x0C => 0x662F, + 0x0D => 0x6318, + 0x0E => 0x6C41, + 0x0F => 0x6976, + 0x10 => 0x1689, + 0x11 => 0x13BE, + 0x12 => 0x1CE7, + 0x13 => 0x19D0, + 0x14 => 0x0762, + 0x15 => 0x0255, + 0x16 => 0x0D0C, + 0x17 => 0x083B, + 0x18 => 0x355F, + 0x19 => 0x3068, + 0x1A => 0x3F31, + 0x1B => 0x3A06, + 0x1C => 0x24B4, + 0x1D => 0x2183, + 0x1E => 0x2EDA, + 0x1F => 0x2BED, + ]; + + private const FORMAT_INFO_MASK_QR = 0x5412; + + private bool $mirror = false; + + /** + * @noinspection PhpMissingParentConstructorInspection */ public function __construct(int $dimension){ - $this->dimension = $dimension; - $this->rowSize = ((int)(($this->dimension + 0x1f) / 0x20)); - $this->bits = array_fill(0, $this->rowSize * $this->dimension, 0); - } - - /** - * Sets the given bit to true. - * - * @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):self{ - $offset = (int)($y * $this->rowSize + ($x / 0x20)); - - $this->bits[$offset] ??= 0; - $this->bits[$offset] |= ($this->bits[$offset] |= 1 << ($x & 0x1f)); - - return $this; - } - - /** - * Flips the given bit. 1 << (0xf9 & 0x1f) - * - * @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):self{ - $offset = $y * $this->rowSize + (int)($x / 0x20); - - $this->bits[$offset] = ($this->bits[$offset] ^ (1 << ($x & 0x1f))); - - return $this; - } - - /** - * Sets a square region of the bit matrix to true. - * - * @param int $left ; The horizontal position to begin at (inclusive) - * @param int $top ; The vertical position to begin at (inclusive) - * @param int $width ; The width of the region - * @param int $height ; The height of the region - * - * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException - */ - public function setRegion(int $left, int $top, int $width, int $height):self{ - - if($top < 0 || $left < 0){ - throw new QRCodeDecoderException('Left and top must be non-negative'); - } - - if($height < 1 || $width < 1){ - throw new QRCodeDecoderException('Height and width must be at least 1'); - } - - $right = $left + $width; - $bottom = $top + $height; - - if($bottom > $this->dimension || $right > $this->dimension){ - throw new QRCodeDecoderException('The region must fit inside the matrix'); - } - - for($y = $top; $y < $bottom; $y++){ - $yOffset = $y * $this->rowSize; - - for($x = $left; $x < $right; $x++){ - $xOffset = $yOffset + (int)($x / 0x20); - $this->bits[$xOffset] = ($this->bits[$xOffset] |= 1 << ($x & 0x1f)); - } - } - - return $this; - } - - /** - * @return int The dimension (width/height) of the matrix - */ - public function getDimension():int{ - 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. - * - * @param int $x The horizontal component (i.e. which column) - * @param int $y The vertical component (i.e. which row) - * - * @return bool value of given bit in matrix - */ - public function get(int $x, int $y):bool{ - $offset = (int)($y * $this->rowSize + ($x / 0x20)); - - $this->bits[$offset] ??= 0; - - return ($this->uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0; - } - - /** - * See ISO 18004:2006 Annex E - */ - private function buildFunctionPattern():self{ - $dimension = $this->version->getDimension(); - $bitMatrix = new self($dimension); - - // Top left finder pattern + separator + format - $bitMatrix->setRegion(0, 0, 9, 9); - // Top right finder pattern + separator + format - $bitMatrix->setRegion($dimension - 8, 0, 8, 9); - // Bottom left finder pattern + separator + format - $bitMatrix->setRegion(0, $dimension - 8, 9, 8); - - // Alignment patterns - $apc = $this->version->getAlignmentPattern(); - $max = count($apc); - - for($x = 0; $x < $max; $x++){ - $i = $apc[$x] - 2; - - for($y = 0; $y < $max; $y++){ - if(($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)){ - // No alignment patterns near the three finder paterns - continue; - } - - $bitMatrix->setRegion($apc[$y] - 2, $i, 5, 5); - } - } - - // Vertical timing pattern - $bitMatrix->setRegion(6, 9, 1, $dimension - 17); - // Horizontal timing pattern - $bitMatrix->setRegion(9, 6, $dimension - 17, 1); - - if($this->version->getVersionNumber() > 6){ - // Version info, top right - $bitMatrix->setRegion($dimension - 11, 0, 3, 6); - // Version info, bottom left - $bitMatrix->setRegion(0, $dimension - 11, 6, 3); - } - - return $bitMatrix; - } - - /** - * Mirror the bit matrix in order to attempt a second reading. - */ - public function mirror():self{ - - for($x = 0; $x < $this->dimension; $x++){ - for($y = $x + 1; $y < $this->dimension; $y++){ - if($this->get($x, $y) !== $this->get($y, $x)){ - $this->flip($y, $x); - $this->flip($x, $y); - } - } - } - - return $this; - } - - /** - * Implementations of this method reverse the data masking process applied to a QR Code and - * make its bits ready to read. - */ - private function unmask():void{ - $mask = $this->formatInfo->getMaskPattern()->getMask(); - - for($y = 0; $y < $this->dimension; $y++){ - for($x = 0; $x < $this->dimension; $x++){ - if($mask($x, $y)){ - $this->flip($x, $y); - } - } - } - + $this->moduleCount = $dimension; + $this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL)); } /** @@ -230,23 +82,29 @@ final class BitMatrix{ * @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; + $this->version = null; + $this->eccLevel = null; + $this->maskPattern = null; + $this->mirror = $mirror; return $this; } /** - * + * Mirror the bit matrix in order to attempt a second reading. */ - private function copyBit(int $i, int $j, int $versionBits):int{ + public function mirror():self{ - $bit = $this->mirror - ? $this->get($j, $i) - : $this->get($i, $j); + for($x = 0; $x < $this->moduleCount; $x++){ + for($y = $x + 1; $y < $this->moduleCount; $y++){ + if($this->get($x, $y) !== $this->get($y, $x)){ + $this->flip($y, $x); + $this->flip($x, $y); + } + } + } - return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1; + return $this; } /** @@ -258,56 +116,60 @@ final class BitMatrix{ * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException 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(); + $this + ->readFormatInformation() + ->readVersion() + ->mask() // reverse the mask pattern + ; + + // invoke a fresh matrix with only the function & format patterns to compare against + $fp = (new QRMatrix($this->version, $this->eccLevel, $this->maskPattern))->initFunctionalPatterns(); + $result = []; + $byte = 0; + $bitsRead = 0; + $direction = true; - $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){ + for($i = $this->moduleCount - 1; $i > 0; $i -= 2){ - if($j === 6){ - // Skip whole column with vertical alignment pattern; - // saves time and makes the other code proceed more cleanly - $j--; + // Skip whole column with vertical alignment pattern; + // saves time and makes the other code proceed more cleanly + if($i === 6){ + $i--; } // 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($count = 0; $count < $this->moduleCount; $count++){ + $y = $direction ? $this->moduleCount - 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; + $x = $i - $col; - 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; - } + // Ignore bits covered by the function pattern + if($fp->get($x, $y) !== $this::M_NULL){ + continue; + } + + $bitsRead++; + $byte <<= 1; + + if($this->check($x, $y)){ + $byte |= 1; + } + // If we've made a whole byte, save it off + if($bitsRead === 8){ + $result[] = $byte; + $bitsRead = 0; + $byte = 0; } } } - $readingUp = !$readingUp; // switch directions + $direction = !$direction; // switch directions } - if($resultOffset !== $this->version->getTotalCodewords()){ - throw new QRCodeDecoderException('offset differs from total codewords for version'); + if(count($result) !== $this->version->getTotalCodewords()){ + throw new QRCodeDecoderException('result count differs from total codewords for version'); } return $result; @@ -316,62 +178,77 @@ final class BitMatrix{ /** * 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 \chillerlan\QRCode\Decoder\QRCodeDecoderException if both format information locations cannot be parsed as * the valid encoding of format information */ - private function readFormatInformation():FormatInformation{ + private function readFormatInformation():self{ - if($this->formatInfo !== null){ - return $this->formatInfo; + if($this->eccLevel !== null && $this->maskPattern !== null){ + return $this; } // Read top-left format info bits $formatInfoBits1 = 0; for($i = 0; $i < 6; $i++){ - $formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyVersionBit($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); + $formatInfoBits1 = $this->copyVersionBit(7, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyVersionBit(8, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyVersionBit(8, 7, $formatInfoBits1); // .. and skip a bit in the timing pattern ... for($j = 5; $j >= 0; $j--){ - $formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1); + $formatInfoBits1 = $this->copyVersionBit(8, $j, $formatInfoBits1); } // Read the top-right/bottom-left pattern too $formatInfoBits2 = 0; - $jMin = $this->dimension - 7; + $jMin = $this->moduleCount - 7; - for($j = $this->dimension - 1; $j >= $jMin; $j--){ - $formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2); + for($j = $this->moduleCount - 1; $j >= $jMin; $j--){ + $formatInfoBits2 = $this->copyVersionBit(8, $j, $formatInfoBits2); } - for($i = $this->dimension - 8; $i < $this->dimension; $i++){ - $formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2); + for($i = $this->moduleCount - 8; $i < $this->moduleCount; $i++){ + $formatInfoBits2 = $this->copyVersionBit($i, 8, $formatInfoBits2); } - $this->formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2); + $formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2); + + if($formatInfo === null){ + + // Should return null, but, some QR codes apparently do not mask this info. + // Try again by actually masking the pattern first. + $formatInfo = $this->doDecodeFormatInformation( + $formatInfoBits1 ^ $this::FORMAT_INFO_MASK_QR, + $formatInfoBits2 ^ $this::FORMAT_INFO_MASK_QR + ); + + // still nothing??? + if($formatInfo === null){ + throw new QRCodeDecoderException('failed to read format info'); // @codeCoverageIgnore + } - 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 - ); + $this->eccLevel = new EccLevel(($formatInfo >> 3) & 0x03); // Bits 3,4 + $this->maskPattern = new MaskPattern($formatInfo & 0x07); // Bottom 3 bits - if($this->formatInfo !== null){ - return $this->formatInfo; - } + return $this; + } - throw new QRCodeDecoderException('failed to read format info'); + /** + * + */ + private function copyVersionBit(int $i, int $j, int $versionBits):int{ + + $bit = $this->mirror + ? $this->check($j, $i) + : $this->check($i, $j); + + return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1; } /** @@ -379,20 +256,18 @@ final class BitMatrix{ * @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 + * @return int|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{ + private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?int{ // 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; + foreach($this::DECODE_LOOKUP as $maskedBits => $dataBits){ if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){ // Found an exact match - return new FormatInformation($maskedBits); + return $maskedBits; } $bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits); @@ -414,7 +289,7 @@ final class BitMatrix{ } // 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 $bestFormatInfo; } return null; @@ -423,61 +298,61 @@ final class BitMatrix{ /** * 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 \chillerlan\QRCode\Decoder\QRCodeDecoderException if both version information locations cannot be parsed as * the valid encoding of version information * @noinspection DuplicatedCode */ - private function readVersion():Version{ + private function readVersion():self{ if($this->version !== null){ - return $this->version; + return $this; } - $provisionalVersion = ($this->dimension - 17) / 4; + $provisionalVersion = ($this->moduleCount - 17) / 4; - if($provisionalVersion <= 6){ - return new Version($provisionalVersion); + // no version info if v < 7 + if($provisionalVersion < 7){ + $this->version = new Version($provisionalVersion); + + return $this; } // Read top-right version info: 3 wide by 6 tall $versionBits = 0; - $ijMin = $this->dimension - 11; + $ijMin = $this->moduleCount - 11; - for($j = 5; $j >= 0; $j--){ - for($i = $this->dimension - 9; $i >= $ijMin; $i--){ - $versionBits = $this->copyBit($i, $j, $versionBits); + for($y = 5; $y >= 0; $y--){ + for($x = $this->moduleCount - 9; $x >= $ijMin; $x--){ + $versionBits = $this->copyVersionBit($x, $y, $versionBits); } } $this->version = $this->decodeVersionInformation($versionBits); - if($this->version !== null && $this->version->getDimension() === $this->dimension){ - return $this->version; + if($this->version !== null && $this->version->getDimension() === $this->moduleCount){ + return $this; } // 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); + for($x = 5; $x >= 0; $x--){ + for($y = $this->moduleCount - 9; $y >= $ijMin; $y--){ + $versionBits = $this->copyVersionBit($x, $y, $versionBits); } } $this->version = $this->decodeVersionInformation($versionBits); - if($this->version !== null && $this->version->getDimension() === $this->dimension){ - return $this->version; + if($this->version !== null && $this->version->getDimension() === $this->moduleCount){ + return $this; } throw new QRCodeDecoderException('failed to read version'); } /** - * @param int $versionBits * - * @return \chillerlan\QRCode\Common\Version|null */ private function decodeVersionInformation(int $versionBits):?Version{ $bestDifference = PHP_INT_MAX; @@ -542,4 +417,20 @@ final class BitMatrix{ return $count; } + /** + * @codeCoverageIgnore + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function setQuietZone(int $size = null):self{ + throw new QRCodeDataException('not supported'); + } + + /** + * @codeCoverageIgnore + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):self{ + throw new QRCodeDataException('not supported'); + } + } diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php index 8ddcb1feb..d6251aa2b 100644 --- a/src/Decoder/Decoder.php +++ b/src/Decoder/Decoder.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCode\Decoder; use Throwable; -use chillerlan\QRCode\Common\{BitBuffer, EccLevel, FormatInformation, Mode, ReedSolomonDecoder, Version}; +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, ReedSolomonDecoder, Version}; use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number}; use chillerlan\QRCode\Detector\Detector; use function count, array_fill, mb_convert_encoding, mb_detect_encoding; @@ -28,8 +28,8 @@ final class Decoder{ # private const GB2312_SUBSET = 1; private ?Version $version = null; - private ?FormatInformation $formatInfo = null; - private EccLevel $eccLevel; + private ?EccLevel $eccLevel = null; + private ?MaskPattern $maskPattern = null; /** * Decodes a QR Code represented as a BitMatrix. @@ -41,11 +41,11 @@ final class Decoder{ * @throws \Throwable|\chillerlan\QRCode\Decoder\QRCodeDecoderException if the QR Code cannot be decoded */ public function decode(LuminanceSourceInterface $source):DecoderResult{ - $bitMatrix = (new Detector($source))->detect(); + $matrix = (new Detector($source))->detect(); try{ // clone the BitMatrix to avoid errors in case we run into mirroring - return $this->decodeMatrix(clone $bitMatrix); + return $this->decodeMatrix(clone $matrix); } catch(Throwable $e){ @@ -58,7 +58,7 @@ final class Decoder{ * that the QR code may be mirrored, and we should try once more with a * mirrored content. */ - return $this->decodeMatrix($bitMatrix->setMirror(true)->mirror()); + return $this->decodeMatrix($matrix->setMirror(true)->mirror()); } catch(Throwable $f){ // Throw the exception from the original reading @@ -72,18 +72,18 @@ final class Decoder{ /** * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException */ - private function decodeMatrix(BitMatrix $bitMatrix):DecoderResult{ + private function decodeMatrix(BitMatrix $matrix):DecoderResult{ // Read raw codewords - $rawCodewords = $bitMatrix->readCodewords(); - $this->version = $bitMatrix->getVersion(); - $this->formatInfo = $bitMatrix->getFormatInfo(); + $rawCodewords = $matrix->readCodewords(); + $this->version = $matrix->version(); + $this->eccLevel = $matrix->eccLevel(); + $this->maskPattern = $matrix->maskPattern(); - if($this->version === null || $this->formatInfo === null){ + if($this->version === null || $this->eccLevel === null || $this->maskPattern === null){ throw new QRCodeDecoderException('unable to read version or format info'); // @codeCoverageIgnore } - $this->eccLevel = $this->formatInfo->getErrorCorrectionLevel(); - $resultBytes = (new ReedSolomonDecoder)->decode($this->getDataBlocks($rawCodewords)); + $resultBytes = (new ReedSolomonDecoder)->decode($this->getDataBlocks($rawCodewords)); // Decode the contents of that stream of bytes return $this->decodeBitStream($resultBytes); } @@ -102,6 +102,7 @@ final class Decoder{ // Figure out the number and size of data blocks used by this version and // error correction level + /** @phan-suppress-next-line PhanTypeMismatchArgumentNullable */ [$numEccCodewords, $eccBlocks] = $this->version->getRSBlocks($this->eccLevel); // Now establish DataBlocks of the appropriate size and number of data codewords @@ -278,7 +279,7 @@ final class Decoder{ 'data' => $result, 'version' => $this->version, 'eccLevel' => $this->eccLevel, - 'maskPattern' => $this->formatInfo->getMaskPattern(), + 'maskPattern' => $this->maskPattern, 'structuredAppendParity' => $parityData, 'structuredAppendSequence' => $symbolSequence ]); diff --git a/src/Detector/AlignmentPatternFinder.php b/src/Detector/AlignmentPatternFinder.php index f2928203e..f17cf0dc0 100644 --- a/src/Detector/AlignmentPatternFinder.php +++ b/src/Detector/AlignmentPatternFinder.php @@ -30,7 +30,7 @@ use function abs, count; */ final class AlignmentPatternFinder{ - private BitMatrix $bitMatrix; + private BitMatrix $matrix; private float $moduleSize; /** @var \chillerlan\QRCode\Detector\AlignmentPattern[] */ private array $possibleCenters; @@ -38,12 +38,12 @@ final class AlignmentPatternFinder{ /** * Creates a finder that will look in a portion of the whole image. * - * @param \chillerlan\QRCode\Decoder\BitMatrix $image image to search + * @param \chillerlan\QRCode\Decoder\BitMatrix $matrix image to search * @param float $moduleSize estimated module size so far */ - public function __construct(BitMatrix $image, float $moduleSize){ - $this->bitMatrix = $image; - $this->moduleSize = $moduleSize; + public function __construct(BitMatrix $matrix, float $moduleSize){ + $this->matrix = $matrix; + $this->moduleSize = $moduleSize; $this->possibleCenters = []; } @@ -75,7 +75,7 @@ final class AlignmentPatternFinder{ // Burn off leading white pixels before anything else; if we start in the middle of // a white run, it doesn't make sense to count its length, since we don't know if the // white run continued to the left of the start point - while($j < $maxJ && !$this->bitMatrix->get($j, $i)){ + while($j < $maxJ && !$this->matrix->check($j, $i)){ $j++; } @@ -83,7 +83,7 @@ final class AlignmentPatternFinder{ while($j < $maxJ){ - if($this->bitMatrix->get($j, $i)){ + if($this->matrix->check($j, $i)){ // Black pixel if($currentState === 1){ // Counting black pixels $stateCount[$currentState]++; @@ -224,7 +224,7 @@ final class AlignmentPatternFinder{ * @return float|null vertical center of alignment pattern, or null if not found */ private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{ - $maxI = $this->bitMatrix->getDimension(); + $maxI = $this->matrix->size(); $stateCount = []; $stateCount[0] = 0; $stateCount[1] = 0; @@ -232,7 +232,7 @@ final class AlignmentPatternFinder{ // Start counting up from center $i = $startI; - while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){ + while($i >= 0 && $this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){ $stateCount[1]++; $i--; } @@ -241,7 +241,7 @@ final class AlignmentPatternFinder{ return null; } - while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){ + while($i >= 0 && !$this->matrix->check($centerJ, $i) && $stateCount[0] <= $maxCount){ $stateCount[0]++; $i--; } @@ -252,7 +252,7 @@ final class AlignmentPatternFinder{ // Now also count down from center $i = $startI + 1; - while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){ + while($i < $maxI && $this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){ $stateCount[1]++; $i++; } @@ -261,7 +261,7 @@ final class AlignmentPatternFinder{ return null; } - while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[2] <= $maxCount){ + while($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[2] <= $maxCount){ $stateCount[2]++; $i++; } diff --git a/src/Detector/Detector.php b/src/Detector/Detector.php index 5f8f0f45f..4f04995e1 100644 --- a/src/Detector/Detector.php +++ b/src/Detector/Detector.php @@ -25,20 +25,20 @@ use const NAN; */ final class Detector{ - private BitMatrix $bitMatrix; + private BitMatrix $matrix; /** * Detector constructor. */ public function __construct(LuminanceSourceInterface $source){ - $this->bitMatrix = (new Binarizer($source))->getBlackMatrix(); + $this->matrix = (new Binarizer($source))->getBlackMatrix(); } /** * Detects a QR Code in an image. */ public function detect():BitMatrix{ - [$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->bitMatrix))->find(); + [$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->matrix))->find(); $moduleSize = $this->calculateModuleSize($topLeft, $topRight, $bottomLeft); $dimension = $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize); @@ -70,7 +70,7 @@ final class Detector{ $transform = $this->createTransform($topLeft, $topRight, $bottomLeft, $dimension, $alignmentPattern); - return (new GridSampler)->sampleGrid($this->bitMatrix, $dimension, $transform); + return (new GridSampler)->sampleGrid($this->matrix, $dimension, $transform); } /** @@ -135,7 +135,7 @@ final class Detector{ */ private function sizeOfBlackWhiteBlackRunBothWays(float $fromX, float $fromY, float $toX, float $toY):float{ $result = $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$toX, (int)$toY); - $dimension = $this->bitMatrix->getDimension(); + $dimension = $this->matrix->size(); // Now count other way -- don't run off image though of course $scale = 1.0; $otherToX = $fromX - ($toX - $fromX); @@ -208,7 +208,7 @@ final class Detector{ // Does current pixel mean we have moved white to black or vice versa? // Scanning black in state 0,2 and white in state 1, so if we find the wrong // color, advance to next state or end if we are in state 2 already - if(($state === 1) === $this->bitMatrix->get($realX, $realY)){ + if(($state === 1) === $this->matrix->check($realX, $realY)){ if($state === 2){ return FinderPattern::distance($x, $y, $fromX, $fromY); @@ -294,7 +294,7 @@ final class Detector{ float $allowanceFactor ):?AlignmentPattern{ // Look for an alignment pattern (3 modules in size) around where it should be - $dimension = $this->bitMatrix->getDimension(); + $dimension = $this->matrix->size(); $allowance = (int)($allowanceFactor * $overallEstModuleSize); $alignmentAreaLeftX = max(0, $estAlignmentX - $allowance); $alignmentAreaRightX = min($dimension - 1, $estAlignmentX + $allowance); @@ -310,7 +310,7 @@ final class Detector{ return null; } - return (new AlignmentPatternFinder($this->bitMatrix, $overallEstModuleSize))->find( + return (new AlignmentPatternFinder($this->matrix, $overallEstModuleSize))->find( $alignmentAreaLeftX, $alignmentAreaTopY, $alignmentAreaRightX - $alignmentAreaLeftX, diff --git a/src/Detector/FinderPatternFinder.php b/src/Detector/FinderPatternFinder.php index 9ba91d552..345d4dd04 100644 --- a/src/Detector/FinderPatternFinder.php +++ b/src/Detector/FinderPatternFinder.php @@ -30,7 +30,7 @@ final class FinderPatternFinder{ private const MIN_SKIP = 2; private const MAX_MODULES = 177; // 1 pixel/module times 3 modules/center private const CENTER_QUORUM = 2; // support up to version 10 for mobile clients - private BitMatrix $bitMatrix; + private BitMatrix $matrix; /** @var \chillerlan\QRCode\Detector\FinderPattern[] */ private array $possibleCenters; private bool $hasSkipped = false; @@ -38,10 +38,10 @@ final class FinderPatternFinder{ /** * Creates a finder that will search the image for three finder patterns. * - * @param BitMatrix $bitMatrix image to search + * @param BitMatrix $matrix image to search */ - public function __construct(BitMatrix $bitMatrix){ - $this->bitMatrix = $bitMatrix; + public function __construct(BitMatrix $matrix){ + $this->matrix = $matrix; $this->possibleCenters = []; } @@ -49,7 +49,7 @@ final class FinderPatternFinder{ * @return \chillerlan\QRCode\Detector\FinderPattern[] */ public function find():array{ - $dimension = $this->bitMatrix->getDimension(); + $dimension = $this->matrix->size(); // We are looking for black/white/black/white/black modules in // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far @@ -72,7 +72,7 @@ final class FinderPatternFinder{ for($j = 0; $j < $dimension; $j++){ // Black pixel - if($this->bitMatrix->get($j, $i)){ + if($this->matrix->check($j, $i)){ // Counting white pixels if(($currentState & 1) === 1){ $currentState++; @@ -257,7 +257,7 @@ final class FinderPatternFinder{ // Start counting up, left from center finding black center mass $i = 0; - while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){ + while($centerI >= $i && $centerJ >= $i && $this->matrix->check($centerJ - $i, $centerI - $i)){ $stateCount[2]++; $i++; } @@ -267,7 +267,7 @@ final class FinderPatternFinder{ } // Continue up, left finding white space - while($centerI >= $i && $centerJ >= $i && !$this->bitMatrix->get($centerJ - $i, $centerI - $i)){ + while($centerI >= $i && $centerJ >= $i && !$this->matrix->check($centerJ - $i, $centerI - $i)){ $stateCount[1]++; $i++; } @@ -277,7 +277,7 @@ final class FinderPatternFinder{ } // Continue up, left finding black border - while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){ + while($centerI >= $i && $centerJ >= $i && $this->matrix->check($centerJ - $i, $centerI - $i)){ $stateCount[0]++; $i++; } @@ -286,16 +286,16 @@ final class FinderPatternFinder{ return false; } - $dimension = $this->bitMatrix->getDimension(); + $dimension = $this->matrix->size(); // Now also count down, right from center $i = 1; - while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){ + while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->matrix->check($centerJ + $i, $centerI + $i)){ $stateCount[2]++; $i++; } - while($centerI + $i < $dimension && $centerJ + $i < $dimension && !$this->bitMatrix->get($centerJ + $i, $centerI + $i)){ + while($centerI + $i < $dimension && $centerJ + $i < $dimension && !$this->matrix->check($centerJ + $i, $centerI + $i)){ $stateCount[3]++; $i++; } @@ -304,7 +304,7 @@ final class FinderPatternFinder{ return false; } - while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){ + while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->matrix->check($centerJ + $i, $centerI + $i)){ $stateCount[4]++; $i++; } @@ -328,14 +328,15 @@ final class FinderPatternFinder{ * @param int $originalStateCountTotal * * @return float|null vertical center of finder pattern, or null if not found + * @noinspection DuplicatedCode */ private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{ - $maxI = $this->bitMatrix->getDimension(); + $maxI = $this->matrix->size(); $stateCount = $this->getCrossCheckStateCount(); // Start counting up from center $i = $startI; - while($i >= 0 && $this->bitMatrix->get($centerJ, $i)){ + while($i >= 0 && $this->matrix->check($centerJ, $i)){ $stateCount[2]++; $i--; } @@ -344,7 +345,7 @@ final class FinderPatternFinder{ return null; } - while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){ + while($i >= 0 && !$this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){ $stateCount[1]++; $i--; } @@ -354,7 +355,7 @@ final class FinderPatternFinder{ return null; } - while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){ + while($i >= 0 && $this->matrix->check($centerJ, $i) && $stateCount[0] <= $maxCount){ $stateCount[0]++; $i--; } @@ -365,7 +366,7 @@ final class FinderPatternFinder{ // Now also count down from center $i = $startI + 1; - while($i < $maxI && $this->bitMatrix->get($centerJ, $i)){ + while($i < $maxI && $this->matrix->check($centerJ, $i)){ $stateCount[2]++; $i++; } @@ -374,7 +375,7 @@ final class FinderPatternFinder{ return null; } - while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[3] < $maxCount){ + while($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[3] < $maxCount){ $stateCount[3]++; $i++; } @@ -383,7 +384,7 @@ final class FinderPatternFinder{ return null; } - while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[4] < $maxCount){ + while($i < $maxI && $this->matrix->check($centerJ, $i) && $stateCount[4] < $maxCount){ $stateCount[4]++; $i++; } @@ -411,13 +412,14 @@ final class FinderPatternFinder{ * Like #crossCheckVertical(int, int, int, int), and in fact is basically identical, * except it reads horizontally instead of vertically. This is used to cross-cross * check a vertical cross check and locate the real center of the alignment pattern. + * @noinspection DuplicatedCode */ private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{ - $maxJ = $this->bitMatrix->getDimension(); + $maxJ = $this->matrix->size(); $stateCount = $this->getCrossCheckStateCount(); $j = $startJ; - while($j >= 0 && $this->bitMatrix->get($j, $centerI)){ + while($j >= 0 && $this->matrix->check($j, $centerI)){ $stateCount[2]++; $j--; } @@ -426,7 +428,7 @@ final class FinderPatternFinder{ return null; } - while($j >= 0 && !$this->bitMatrix->get($j, $centerI) && $stateCount[1] <= $maxCount){ + while($j >= 0 && !$this->matrix->check($j, $centerI) && $stateCount[1] <= $maxCount){ $stateCount[1]++; $j--; } @@ -435,7 +437,7 @@ final class FinderPatternFinder{ return null; } - while($j >= 0 && $this->bitMatrix->get($j, $centerI) && $stateCount[0] <= $maxCount){ + while($j >= 0 && $this->matrix->check($j, $centerI) && $stateCount[0] <= $maxCount){ $stateCount[0]++; $j--; } @@ -445,7 +447,7 @@ final class FinderPatternFinder{ } $j = $startJ + 1; - while($j < $maxJ && $this->bitMatrix->get($j, $centerI)){ + while($j < $maxJ && $this->matrix->check($j, $centerI)){ $stateCount[2]++; $j++; } @@ -454,7 +456,7 @@ final class FinderPatternFinder{ return null; } - while($j < $maxJ && !$this->bitMatrix->get($j, $centerI) && $stateCount[3] < $maxCount){ + while($j < $maxJ && !$this->matrix->check($j, $centerI) && $stateCount[3] < $maxCount){ $stateCount[3]++; $j++; } @@ -463,7 +465,7 @@ final class FinderPatternFinder{ return null; } - while($j < $maxJ && $this->bitMatrix->get($j, $centerI) && $stateCount[4] < $maxCount){ + while($j < $maxJ && $this->matrix->check($j, $centerI) && $stateCount[4] < $maxCount){ $stateCount[4]++; $j++; } diff --git a/src/Detector/GridSampler.php b/src/Detector/GridSampler.php index ebcf03be3..ef98d9f14 100644 --- a/src/Detector/GridSampler.php +++ b/src/Detector/GridSampler.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCode\Detector; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Decoder\BitMatrix; use Throwable; use function array_fill, count, sprintf; @@ -41,13 +42,13 @@ final class GridSampler{ * For efficiency, the method will check points from either end of the line until one is found * to be within the image. Because the set of points are assumed to be linear, this is valid. * - * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix image into which the points should map - * @param float[] $points actual points in x1,y1,...,xn,yn form + * @param \chillerlan\QRCode\Decoder\BitMatrix $matrix image into which the points should map + * @param float[] $points actual points in x1,y1,...,xn,yn form * * @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if an endpoint is lies outside the image boundaries */ - private function checkAndNudgePoints(BitMatrix $bitMatrix, array $points):void{ - $dimension = $bitMatrix->getDimension(); + private function checkAndNudgePoints(BitMatrix $matrix, array $points):void{ + $dimension = $matrix->size(); $nudged = true; $max = count($points); @@ -121,7 +122,7 @@ final class GridSampler{ * @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if image can't be sampled, for example, if the transformation defined * by the given points is invalid or results in sampling outside the image boundaries */ - public function sampleGrid(BitMatrix $image, int $dimension, PerspectiveTransform $transform):BitMatrix{ + public function sampleGrid(BitMatrix $matrix, int $dimension, PerspectiveTransform $transform):BitMatrix{ if($dimension <= 0){ throw new QRCodeDetectorException('invalid matrix size'); @@ -142,14 +143,12 @@ final class GridSampler{ $transform->transformPoints($points); // Quick check to see if points transformed to something inside the image; // sufficient to check the endpoints - $this->checkAndNudgePoints($image, $points); + $this->checkAndNudgePoints($matrix, $points); try{ for($x = 0; $x < $max; $x += 2){ - if($image->get((int)$points[$x], (int)$points[$x + 1])){ - // Black(-ish) pixel - $bits->set($x / 2, $y); - } + // Black(-ish) pixel + $bits->set($x / 2, $y, $matrix->check((int)$points[$x], (int)$points[$x + 1]), QRMatrix::M_DATA); } } // @codeCoverageIgnoreStart diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index 4396f212f..08d4216f9 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -35,7 +35,7 @@ final class QRMatrixTest extends TestCase{ * shortcut */ protected function getMatrix(int $version):QRMatrix{ - return new QRMatrix(new Version($version), new EccLevel(EccLevel::L)); + return new QRMatrix(new Version($version), new EccLevel(EccLevel::L), new MaskPattern(MaskPattern::PATTERN_000)); } /** @@ -70,8 +70,7 @@ final class QRMatrixTest extends TestCase{ * Tests if maskPattern() returns the current (or default) mask pattern */ public function testMaskPattern():void{ - $this::assertSame(null, $this->matrix->maskPattern()); - + // set via matrix evaluation $matrix = (new QRCode)->addByteSegment('testdata')->getMatrix(); $this::assertInstanceOf(MaskPattern::class, $matrix->maskPattern()); @@ -225,7 +224,7 @@ final class QRMatrixTest extends TestCase{ * @dataProvider versionProvider */ public function testSetFormatInfo(int $version):void{ - $matrix = $this->getMatrix($version)->setFormatInfo(new MaskPattern(MaskPattern::PATTERN_000)); + $matrix = $this->getMatrix($version)->setFormatInfo(); $this::assertTrue($matrix->checkType(8, 0, QRMatrix::M_FORMAT)); $this::assertTrue($matrix->checkType(0, 8, QRMatrix::M_FORMAT));