From 182ebf4e70a78c893deaeada5c209ce8e4c24fce Mon Sep 17 00:00:00 2001 From: codemasher Date: Tue, 19 Jan 2021 01:15:34 +0100 Subject: [PATCH] :octocat: extract MaskPattern --- src/Common/EccLevel.php | 11 +--- src/Common/MaskPattern.php | 84 ++++++++++++++++++++++++ src/Data/MaskPatternTester.php | 11 ++-- src/Data/QRData.php | 4 +- src/Data/QRMatrix.php | 48 +++----------- src/QRCode.php | 3 +- tests/Data/DatainterfaceTestAbstract.php | 5 +- tests/Data/MaskPatternTesterTest.php | 5 +- tests/Data/QRMatrixTest.php | 11 ++-- tests/Output/QROutputTestAbstract.php | 4 +- 10 files changed, 120 insertions(+), 66 deletions(-) create mode 100644 src/Common/MaskPattern.php diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php index a5c468ecb..917963bd4 100644 --- a/src/Common/EccLevel.php +++ b/src/Common/EccLevel.php @@ -193,16 +193,9 @@ final class EccLevel{ /** * returns the format pattern for the given $eccLevel and $maskPattern - * - * @throws \chillerlan\QRCode\QRCodeException */ - public function getformatPattern(int $maskPattern):int{ - - if((0b111 & $maskPattern) !== $maskPattern){ - throw new QRCodeException('invalid mask pattern'); - } - - return self::FORMAT_PATTERN[self::MODES[$this->eccLevel]][$maskPattern]; + public function getformatPattern(MaskPattern $maskPattern):int{ + return self::FORMAT_PATTERN[self::MODES[$this->eccLevel]][$maskPattern->getPattern()]; } /** diff --git a/src/Common/MaskPattern.php b/src/Common/MaskPattern.php new file mode 100644 index 000000000..a036f93b5 --- /dev/null +++ b/src/Common/MaskPattern.php @@ -0,0 +1,84 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; +use Closure; + +/** + * + */ +class MaskPattern{ + + public const PATTERN_000 = 0b000; + public const PATTERN_001 = 0b001; + public const PATTERN_010 = 0b010; + public const PATTERN_011 = 0b011; + public const PATTERN_100 = 0b100; + public const PATTERN_101 = 0b101; + public const PATTERN_110 = 0b110; + public const PATTERN_111 = 0b111; + + public const PATTERNS = [ + self::PATTERN_000, + self::PATTERN_001, + self::PATTERN_010, + self::PATTERN_011, + self::PATTERN_100, + self::PATTERN_101, + self::PATTERN_110, + self::PATTERN_111, + ]; + + private int $maskPattern; + + /** + * MaskPattern constructor. + * + * ISO/IEC 18004:2000 Section 8.8.1 + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function __construct(int $maskPattern){ + + if((0b111 & $maskPattern) !== $maskPattern){ + throw new QRCodeException('invalid mask pattern'); // @codeCoverageIgnore + } + + $this->maskPattern = $maskPattern; + } + + public function getPattern():int{ + return $this->maskPattern; + } + + /** + * ISO/IEC 18004:2000 Section 8.8.1 + * + * Note that some versions of the QR code standard have had errors in the section about mask patterns. + * The information below has been corrected. (https://www.thonky.com/qr-code-tutorial/mask-patterns) + */ + public function getMask():Closure{ + return [ + self::PATTERN_000 => fn($x, $y):int => ($x + $y) % 2, + self::PATTERN_001 => fn($x, $y):int => $y % 2, + self::PATTERN_010 => fn($x, $y):int => $x % 3, + self::PATTERN_011 => fn($x, $y):int => ($x + $y) % 3, + self::PATTERN_100 => fn($x, $y):int => ((int)($y / 2) + (int)($x / 3)) % 2, + self::PATTERN_101 => fn($x, $y):int => (($x * $y) % 2) + (($x * $y) % 3), + self::PATTERN_110 => fn($x, $y):int => ((($x * $y) % 2) + (($x * $y) % 3)) % 2, + self::PATTERN_111 => fn($x, $y):int => ((($x * $y) % 3) + (($x + $y) % 2)) % 2, + ][$this->maskPattern]; + } + +} diff --git a/src/Data/MaskPatternTester.php b/src/Data/MaskPatternTester.php index 8e470f57b..d303bc2ef 100644 --- a/src/Data/MaskPatternTester.php +++ b/src/Data/MaskPatternTester.php @@ -14,6 +14,7 @@ namespace chillerlan\QRCode\Data; +use chillerlan\QRCode\Common\MaskPattern; use function abs, array_search, call_user_func_array, min; /** @@ -45,14 +46,14 @@ final class MaskPatternTester{ * * @see \chillerlan\QRCode\Data\MaskPatternTester */ - public function getBestMaskPattern():int{ + public function getBestMaskPattern():MaskPattern{ $penalties = []; - for($pattern = 0; $pattern < 8; $pattern++){ - $penalties[$pattern] = $this->testPattern($pattern); + foreach(MaskPattern::PATTERNS as $pattern){ + $penalties[$pattern] = $this->testPattern(new MaskPattern($pattern)); } - return array_search(min($penalties), $penalties, true); + return new MaskPattern(array_search(min($penalties), $penalties, true)); } /** @@ -61,7 +62,7 @@ final class MaskPatternTester{ * @see \chillerlan\QRCode\QROptions::$maskPattern * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern */ - public function testPattern(int $pattern):int{ + public function testPattern(MaskPattern $pattern):int{ $matrix = $this->qrData->writeMatrix($pattern, true); $penalty = 0; diff --git a/src/Data/QRData.php b/src/Data/QRData.php index 6f62cdb4e..f717e29ca 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, ReedSolomonEncoder, Version}; +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, ReedSolomonEncoder, Version}; use chillerlan\QRCode\QRCode; use chillerlan\Settings\SettingsContainerInterface; @@ -100,7 +100,7 @@ final class QRData{ /** * returns a fresh matrix object with the data written for the given $maskPattern */ - public function writeMatrix(int $maskPattern, bool $test = null):QRMatrix{ + public function writeMatrix(MaskPattern $maskPattern, bool $test = null):QRMatrix{ $data = (new ReedSolomonEncoder)->interleaveEcBytes($this->bitBuffer, $this->version, $this->eccLevel); return (new QRMatrix($this->version, $this->eccLevel)) diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index d06aa36bc..d44c732ad 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -12,9 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{EccLevel, Version}; -use chillerlan\QRCode\QRCode; -use Closure; +use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version}; use SplFixedArray; use function array_fill, array_push, array_unshift, floor, max, min, range; @@ -57,9 +55,9 @@ final class QRMatrix{ public const IS_DARK = 0b100000000000; /** - * the used mask pattern, set via QRMatrix::mapData() + * the used mask pattern, set via QRMatrix::mask() */ - protected int $maskPattern = QRCode::MASK_PATTERN_AUTO; + protected ?MaskPattern $maskPattern = null; /** * the size (side length) of the matrix, including quiet zone (if created) @@ -96,7 +94,7 @@ final class QRMatrix{ /** * shortcut to initialize the matrix */ - public function init(int $maskPattern, bool $test = null):QRMatrix{ + public function init(MaskPattern $maskPattern, bool $test = null):QRMatrix{ return $this ->setFinderPattern() ->setSeparators() @@ -149,7 +147,7 @@ final class QRMatrix{ /** * Returns the current mask pattern */ - public function maskPattern():int{ + public function maskPattern():?MaskPattern{ return $this->maskPattern; } @@ -365,7 +363,7 @@ final class QRMatrix{ * * ISO/IEC 18004:2000 Section 8.9 */ - public function setFormatInfo(int $maskPattern, bool $test = null):QRMatrix{ + public function setFormatInfo(MaskPattern $maskPattern, bool $test = null):QRMatrix{ $bits = $this->eccLevel->getformatPattern($maskPattern); for($i = 0; $i < 15; $i++){ @@ -569,9 +567,9 @@ final class QRMatrix{ * * ISO/IEC 18004:2000 Section 8.8.1 */ - public function mask(int $maskPattern):QRMatrix{ + public function mask(MaskPattern $maskPattern):QRMatrix{ $this->maskPattern = $maskPattern; - $mask = $this->getMask($this->maskPattern); + $mask = $this->maskPattern->getMask(); foreach($this->matrix as $y => &$row){ foreach($row as $x => &$val){ @@ -584,34 +582,4 @@ final class QRMatrix{ return $this; } - /** - * ISO/IEC 18004:2000 Section 8.8.1 - * - * Note that some versions of the QR code standard have had errors in the section about mask patterns. - * The information below has been corrected. (https://www.thonky.com/qr-code-tutorial/mask-patterns) - * - * @see \chillerlan\QRCode\QRMatrix::mapData() - * - * @internal - * - * @throws \chillerlan\QRCode\Data\QRCodeDataException - */ - protected function getMask(int $maskPattern):Closure{ - - if((0b111 & $maskPattern) !== $maskPattern){ - throw new QRCodeDataException('invalid mask pattern'); // @codeCoverageIgnore - } - - return [ - 0b000 => fn($x, $y):int => ($x + $y) % 2, - 0b001 => fn($x, $y):int => $y % 2, - 0b010 => fn($x, $y):int => $x % 3, - 0b011 => fn($x, $y):int => ($x + $y) % 3, - 0b100 => fn($x, $y):int => ((int)($y / 2) + (int)($x / 3)) % 2, - 0b101 => fn($x, $y):int => (($x * $y) % 2) + (($x * $y) % 3), - 0b110 => fn($x, $y):int => ((($x * $y) % 2) + (($x * $y) % 3)) % 2, - 0b111 => fn($x, $y):int => ((($x * $y) % 3) + (($x + $y) % 2)) % 2, - ][$maskPattern]; - } - } diff --git a/src/QRCode.php b/src/QRCode.php index d654d2a3b..7dd57d949 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -13,6 +13,7 @@ namespace chillerlan\QRCode; use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRMatrix}; +use chillerlan\QRCode\Common\MaskPattern; use chillerlan\QRCode\Common\Mode; use chillerlan\QRCode\Output\{ QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString @@ -156,7 +157,7 @@ class QRCode{ $maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO ? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern() - : $this->options->maskPattern; + : new MaskPattern($this->options->maskPattern); $matrix = $this->dataInterface->writeMatrix($maskPattern); diff --git a/tests/Data/DatainterfaceTestAbstract.php b/tests/Data/DatainterfaceTestAbstract.php index e0faee38a..60fb3f548 100644 --- a/tests/Data/DatainterfaceTestAbstract.php +++ b/tests/Data/DatainterfaceTestAbstract.php @@ -12,6 +12,7 @@ namespace chillerlan\QRCodeTest\Data; +use chillerlan\QRCode\Common\MaskPattern; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; use PHPUnit\Framework\TestCase; @@ -82,10 +83,10 @@ abstract class DatainterfaceTestAbstract extends TestCase{ public function testInitMatrix(int $maskPattern):void{ $this->dataInterface->setData([$this->testdata]); - $matrix = $this->dataInterface->writeMatrix($maskPattern); + $matrix = $this->dataInterface->writeMatrix(new MaskPattern($maskPattern)); $this::assertInstanceOf(QRMatrix::class, $matrix); - $this::assertSame($maskPattern, $matrix->maskPattern()); + $this::assertSame($maskPattern, $matrix->maskPattern()->getPattern()); } /** diff --git a/tests/Data/MaskPatternTesterTest.php b/tests/Data/MaskPatternTesterTest.php index 46c98c99c..e7571c24f 100644 --- a/tests/Data/MaskPatternTesterTest.php +++ b/tests/Data/MaskPatternTesterTest.php @@ -12,6 +12,7 @@ namespace chillerlan\QRCodeTest\Data; +use chillerlan\QRCode\Common\MaskPattern; use chillerlan\QRCode\QROptions; use chillerlan\QRCode\Data\{Byte, MaskPatternTester, QRData}; use PHPUnit\Framework\TestCase; @@ -27,7 +28,7 @@ final class MaskPatternTesterTest extends TestCase{ public function testMaskpattern():void{ $dataInterface = new QRData(new QROptions(['version' => 10]), [[Byte::class, 'test']]); - $this::assertSame(3, (new MaskPatternTester($dataInterface))->getBestMaskPattern()); + $this::assertSame(3, (new MaskPatternTester($dataInterface))->getBestMaskPattern()->getPattern()); } /** @@ -36,7 +37,7 @@ final class MaskPatternTesterTest extends TestCase{ public function testMaskpatternID():void{ $dataInterface = new QRData(new QROptions(['version' => 10]), [[Byte::class, 'test']]); - $this::assertSame(4243, (new MaskPatternTester($dataInterface))->testPattern(3)); + $this::assertSame(4243, (new MaskPatternTester($dataInterface))->testPattern(new MaskPattern(MaskPattern::PATTERN_011))); } } diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index 53ec8ea3c..39dd6cedd 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCodeTest\Data; -use chillerlan\QRCode\Common\{EccLevel, Version}; +use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version}; use chillerlan\QRCode\{QRCode, QROptions}; use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix}; use PHPUnit\Framework\TestCase; @@ -77,9 +77,12 @@ final class QRMatrixTest extends TestCase{ * Tests if maskPattern() returns the current (or default) mask pattern */ public function testMaskPattern():void{ - $this::assertSame(-1, $this->matrix->maskPattern()); // default + $this::assertSame(null, $this->matrix->maskPattern()); - // @todo: actual mask pattern after mapData() + $matrix = (new QRCode)->addByteSegment('testdata')->getMatrix(); + + $this::assertInstanceOf(MaskPattern::class, $matrix->maskPattern()); + $this::assertSame(MaskPattern::PATTERN_010, $matrix->maskPattern()->getPattern()); } /** @@ -243,7 +246,7 @@ final class QRMatrixTest extends TestCase{ * @dataProvider versionProvider */ public function testSetFormatInfo(int $version):void{ - $matrix = $this->getMatrix($version)->setFormatInfo(0, true); + $matrix = $this->getMatrix($version)->setFormatInfo(new MaskPattern(MaskPattern::PATTERN_000), true); $this::assertSame(QRMatrix::M_FORMAT, $matrix->get(8, 0)); $this::assertSame(QRMatrix::M_FORMAT, $matrix->get(0, 8)); diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index b6eec6b99..6e6d9f469 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -13,6 +13,7 @@ namespace chillerlan\QRCodeTest\Output; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\MaskPattern; use chillerlan\QRCode\Data\{Byte, QRData, QRMatrix}; use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface}; use PHPUnit\Framework\TestCase; @@ -48,7 +49,8 @@ abstract class QROutputTestAbstract extends TestCase{ } $this->options = new QROptions; - $this->matrix = (new QRData($this->options, [[Byte::class, 'testdata']]))->writeMatrix(0); + $this->matrix = (new QRData($this->options, [[Byte::class, 'testdata']])) + ->writeMatrix(new MaskPattern(MaskPattern::PATTERN_010)); $this->outputInterface = $this->getOutputInterface($this->options); }