diff --git a/src/Common/Version.php b/src/Common/Version.php new file mode 100644 index 000000000..4fd7bf6b2 --- /dev/null +++ b/src/Common/Version.php @@ -0,0 +1,209 @@ + + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\QRCodeException; + +use function array_column, array_combine, array_keys; + +/** + * Class Version + */ +class Version{ + + /** + * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40 + * + * @var int [][] + */ + const MAX_BITS = [ + // v => [ L, M, Q, H] // modules + 1 => [ 152, 128, 104, 72], // 21 + 2 => [ 272, 224, 176, 128], // 25 + 3 => [ 440, 352, 272, 208], // 29 + 4 => [ 640, 512, 384, 288], // 33 + 5 => [ 864, 688, 496, 368], // 37 + 6 => [ 1088, 864, 608, 480], // 41 + 7 => [ 1248, 992, 704, 528], // 45 + 8 => [ 1552, 1232, 880, 688], // 49 + 9 => [ 1856, 1456, 1056, 800], // 53 + 10 => [ 2192, 1728, 1232, 976], // 57 + 11 => [ 2592, 2032, 1440, 1120], // 61 + 12 => [ 2960, 2320, 1648, 1264], // 65 + 13 => [ 3424, 2672, 1952, 1440], // 69 NICE! + 14 => [ 3688, 2920, 2088, 1576], // 73 + 15 => [ 4184, 3320, 2360, 1784], // 77 + 16 => [ 4712, 3624, 2600, 2024], // 81 + 17 => [ 5176, 4056, 2936, 2264], // 85 + 18 => [ 5768, 4504, 3176, 2504], // 89 + 19 => [ 6360, 5016, 3560, 2728], // 93 + 20 => [ 6888, 5352, 3880, 3080], // 97 + 21 => [ 7456, 5712, 4096, 3248], // 101 + 22 => [ 8048, 6256, 4544, 3536], // 105 + 23 => [ 8752, 6880, 4912, 3712], // 109 + 24 => [ 9392, 7312, 5312, 4112], // 113 + 25 => [10208, 8000, 5744, 4304], // 117 + 26 => [10960, 8496, 6032, 4768], // 121 + 27 => [11744, 9024, 6464, 5024], // 125 + 28 => [12248, 9544, 6968, 5288], // 129 + 29 => [13048, 10136, 7288, 5608], // 133 + 30 => [13880, 10984, 7880, 5960], // 137 + 31 => [14744, 11640, 8264, 6344], // 141 + 32 => [15640, 12328, 8920, 6760], // 145 + 33 => [16568, 13048, 9368, 7208], // 149 + 34 => [17528, 13800, 9848, 7688], // 153 + 35 => [18448, 14496, 10288, 7888], // 157 + 36 => [19472, 15312, 10832, 8432], // 161 + 37 => [20528, 15936, 11408, 8768], // 165 + 38 => [21616, 16816, 12016, 9136], // 169 + 39 => [22496, 17728, 12656, 9776], // 173 + 40 => [23648, 18672, 13328, 10208], // 177 + ]; + + /** + * ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns + * + * version -> pattern + * + * @var int[][] + */ + public const ALIGNMENT_PATTERN = [ + 1 => [], + 2 => [6, 18], + 3 => [6, 22], + 4 => [6, 26], + 5 => [6, 30], + 6 => [6, 34], + 7 => [6, 22, 38], + 8 => [6, 24, 42], + 9 => [6, 26, 46], + 10 => [6, 28, 50], + 11 => [6, 30, 54], + 12 => [6, 32, 58], + 13 => [6, 34, 62], + 14 => [6, 26, 46, 66], + 15 => [6, 26, 48, 70], + 16 => [6, 26, 50, 74], + 17 => [6, 30, 54, 78], + 18 => [6, 30, 56, 82], + 19 => [6, 30, 58, 86], + 20 => [6, 34, 62, 90], + 21 => [6, 28, 50, 72, 94], + 22 => [6, 26, 50, 74, 98], + 23 => [6, 30, 54, 78, 102], + 24 => [6, 28, 54, 80, 106], + 25 => [6, 32, 58, 84, 110], + 26 => [6, 30, 58, 86, 114], + 27 => [6, 34, 62, 90, 118], + 28 => [6, 26, 50, 74, 98, 122], + 29 => [6, 30, 54, 78, 102, 126], + 30 => [6, 26, 52, 78, 104, 130], + 31 => [6, 30, 56, 82, 108, 134], + 32 => [6, 34, 60, 86, 112, 138], + 33 => [6, 30, 58, 86, 114, 142], + 34 => [6, 34, 62, 90, 118, 146], + 35 => [6, 30, 54, 78, 102, 126, 150], + 36 => [6, 24, 50, 76, 102, 128, 154], + 37 => [6, 28, 54, 80, 106, 132, 158], + 38 => [6, 32, 58, 84, 110, 136, 162], + 39 => [6, 26, 54, 82, 110, 138, 166], + 40 => [6, 30, 58, 86, 114, 142, 170], + ]; + + /** + * ISO/IEC 18004:2000 Annex D, Table D.1 - Version information bit stream for each version + * + * no version pattern for QR Codes < 7 + * + * @var int[] + */ + protected const VERSION_PATTERN = [ + 7 => 0b000111110010010100, + 8 => 0b001000010110111100, + 9 => 0b001001101010011001, + 10 => 0b001010010011010011, + 11 => 0b001011101111110110, + 12 => 0b001100011101100010, + 13 => 0b001101100001000111, + 14 => 0b001110011000001101, + 15 => 0b001111100100101000, + 16 => 0b010000101101111000, + 17 => 0b010001010001011101, + 18 => 0b010010101000010111, + 19 => 0b010011010100110010, + 20 => 0b010100100110100110, + 21 => 0b010101011010000011, + 22 => 0b010110100011001001, + 23 => 0b010111011111101100, + 24 => 0b011000111011000100, + 25 => 0b011001000111100001, + 26 => 0b011010111110101011, + 27 => 0b011011000010001110, + 28 => 0b011100110000011010, + 29 => 0b011101001100111111, + 30 => 0b011110110101110101, + 31 => 0b011111001001010000, + 32 => 0b100000100111010101, + 33 => 0b100001011011110000, + 34 => 0b100010100010111010, + 35 => 0b100011011110011111, + 36 => 0b100100101100001011, + 37 => 0b100101010000101110, + 38 => 0b100110101001100100, + 39 => 0b100111010101000001, + 40 => 0b101000110001101001, + ]; + + /** + * QR Code version number + */ + protected int $version; + + /** + * Version constructor. + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function __construct(int $version){ + + if($version < 1 || $version > 40){ + throw new QRCodeException('invalid version number'); + } + + $this->version = $version; + } + + public function getVersionNumber():int{ + return $this->version; + } + + public function getDimension():int{ + return $this->version * 4 + 17; + } + + public function getVersionPattern():?int{ + return self::VERSION_PATTERN[$this->version] ?? null; + } + + public function getAlignmentPattern():array{ + return self::ALIGNMENT_PATTERN[$this->version]; + } + + public static function getMaxBitsForEcc(int $eccLevel):array{ + return array_combine( + array_keys(self::MAX_BITS), + array_column(self::MAX_BITS, QRCode::ECC_MODES[$eccLevel]) + ); + } + +} diff --git a/src/Data/QRData.php b/src/Data/QRData.php index a3972b1ec..16b6dcf40 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -12,67 +12,18 @@ namespace chillerlan\QRCode\Data; +use chillerlan\QRCode\Common\{Mode, Version}; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial}; -use chillerlan\QRCode\Common\Mode; use chillerlan\Settings\SettingsContainerInterface; -use function array_column, array_combine, array_fill, array_keys, array_merge, count, max, range, sprintf; +use function array_fill, array_merge, count, max, range, sprintf; /** * Processes the binary data and maps it on a matrix which is then being returned */ class QRData{ - /** - * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40 - * - * @var int [][] - */ - const MAX_BITS = [ - // version => [L, M, Q, H ] - 1 => [ 152, 128, 104, 72], - 2 => [ 272, 224, 176, 128], - 3 => [ 440, 352, 272, 208], - 4 => [ 640, 512, 384, 288], - 5 => [ 864, 688, 496, 368], - 6 => [ 1088, 864, 608, 480], - 7 => [ 1248, 992, 704, 528], - 8 => [ 1552, 1232, 880, 688], - 9 => [ 1856, 1456, 1056, 800], - 10 => [ 2192, 1728, 1232, 976], - 11 => [ 2592, 2032, 1440, 1120], - 12 => [ 2960, 2320, 1648, 1264], - 13 => [ 3424, 2672, 1952, 1440], - 14 => [ 3688, 2920, 2088, 1576], - 15 => [ 4184, 3320, 2360, 1784], - 16 => [ 4712, 3624, 2600, 2024], - 17 => [ 5176, 4056, 2936, 2264], - 18 => [ 5768, 4504, 3176, 2504], - 19 => [ 6360, 5016, 3560, 2728], - 20 => [ 6888, 5352, 3880, 3080], - 21 => [ 7456, 5712, 4096, 3248], - 22 => [ 8048, 6256, 4544, 3536], - 23 => [ 8752, 6880, 4912, 3712], - 24 => [ 9392, 7312, 5312, 4112], - 25 => [10208, 8000, 5744, 4304], - 26 => [10960, 8496, 6032, 4768], - 27 => [11744, 9024, 6464, 5024], - 28 => [12248, 9544, 6968, 5288], - 29 => [13048, 10136, 7288, 5608], - 30 => [13880, 10984, 7880, 5960], - 31 => [14744, 11640, 8264, 6344], - 32 => [15640, 12328, 8920, 6760], - 33 => [16568, 13048, 9368, 7208], - 34 => [17528, 13800, 9848, 7688], - 35 => [18448, 14496, 10288, 7888], - 36 => [19472, 15312, 10832, 8432], - 37 => [20528, 15936, 11408, 8768], - 38 => [21616, 16816, 12016, 9136], - 39 => [22496, 17728, 12656, 9776], - 40 => [23648, 18672, 13328, 10208], - ]; - /** * @see http://www.thonky.com/qr-code-tutorial/error-correction-table * @@ -124,7 +75,7 @@ class QRData{ /** * current QR Code version */ - protected int $version; + protected Version $version; /** * ECC temp data @@ -167,13 +118,9 @@ class QRData{ * @param array|null $dataSegments */ public function __construct(SettingsContainerInterface $options, array $dataSegments = null){ - $this->options = $options; - $this->bitBuffer = new BitBuffer; - - $this->maxBitsForEcc = array_combine( - array_keys($this::MAX_BITS), - array_column($this::MAX_BITS, QRCode::ECC_MODES[$this->options->eccLevel]) - ); + $this->options = $options; + $this->bitBuffer = new BitBuffer; + $this->maxBitsForEcc = Version::getMaxBitsForEcc($this->options->eccLevel); if(!empty($dataSegments)){ $this->setData($dataSegments); @@ -192,10 +139,12 @@ class QRData{ $this->dataSegments[] = new $class($data); } - $this->version = $this->options->version === QRCode::VERSION_AUTO + $version = $this->options->version === QRCode::VERSION_AUTO ? $this->getMinimumVersion() : $this->options->version; + $this->version = new Version($version); + $this->writeBitBuffer(); return $this; @@ -272,10 +221,11 @@ class QRData{ * @throws \chillerlan\QRCode\QRCodeException on data overflow */ protected function writeBitBuffer():void{ - $MAX_BITS = $this->maxBitsForEcc[$this->version]; + $version = $this->version->getVersionNumber(); + $MAX_BITS = $this->maxBitsForEcc[$version]; foreach($this->dataSegments as $segment){ - $segment->write($this->bitBuffer, $this->version); + $segment->write($this->bitBuffer, $version); } // overflow, likely caused due to invalid version setting @@ -320,7 +270,7 @@ class QRData{ * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding */ protected function maskECC():array{ - [$l1, $l2, $b1, $b2] = $this::RSBLOCKS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]]; + [$l1, $l2, $b1, $b2] = $this::RSBLOCKS[$this->version->getVersionNumber()][QRCode::ECC_MODES[$this->options->eccLevel]]; $rsBlocks = array_fill(0, $l1, [$b1, $b2]); $rsCount = $l1 + $l2; diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index 2ba1e50a6..742bc8f71 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -12,10 +12,11 @@ namespace chillerlan\QRCode\Data; +use chillerlan\QRCode\Common\Version; use chillerlan\QRCode\QRCode; use Closure; -use function array_fill, array_key_exists, array_push, array_unshift, count, floor, in_array, max, min, range; +use function array_fill, array_key_exists, array_push, array_unshift, count, floor, max, min, range; /** * Holds a numerical representation of the final QR Code; @@ -52,100 +53,6 @@ final class QRMatrix{ /** @var int */ public const M_TEST = 0xff; - /** - * ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns - * - * version -> pattern - * - * @var int[][] - */ - protected const alignmentPattern = [ - 1 => [], - 2 => [6, 18], - 3 => [6, 22], - 4 => [6, 26], - 5 => [6, 30], - 6 => [6, 34], - 7 => [6, 22, 38], - 8 => [6, 24, 42], - 9 => [6, 26, 46], - 10 => [6, 28, 50], - 11 => [6, 30, 54], - 12 => [6, 32, 58], - 13 => [6, 34, 62], - 14 => [6, 26, 46, 66], - 15 => [6, 26, 48, 70], - 16 => [6, 26, 50, 74], - 17 => [6, 30, 54, 78], - 18 => [6, 30, 56, 82], - 19 => [6, 30, 58, 86], - 20 => [6, 34, 62, 90], - 21 => [6, 28, 50, 72, 94], - 22 => [6, 26, 50, 74, 98], - 23 => [6, 30, 54, 78, 102], - 24 => [6, 28, 54, 80, 106], - 25 => [6, 32, 58, 84, 110], - 26 => [6, 30, 58, 86, 114], - 27 => [6, 34, 62, 90, 118], - 28 => [6, 26, 50, 74, 98, 122], - 29 => [6, 30, 54, 78, 102, 126], - 30 => [6, 26, 52, 78, 104, 130], - 31 => [6, 30, 56, 82, 108, 134], - 32 => [6, 34, 60, 86, 112, 138], - 33 => [6, 30, 58, 86, 114, 142], - 34 => [6, 34, 62, 90, 118, 146], - 35 => [6, 30, 54, 78, 102, 126, 150], - 36 => [6, 24, 50, 76, 102, 128, 154], - 37 => [6, 28, 54, 80, 106, 132, 158], - 38 => [6, 32, 58, 84, 110, 136, 162], - 39 => [6, 26, 54, 82, 110, 138, 166], - 40 => [6, 30, 58, 86, 114, 142, 170], - ]; - - /** - * ISO/IEC 18004:2000 Annex D, Table D.1 - Version information bit stream for each version - * - * no version pattern for QR Codes < 7 - * - * @var int[] - */ - protected const versionPattern = [ - 7 => 0b000111110010010100, - 8 => 0b001000010110111100, - 9 => 0b001001101010011001, - 10 => 0b001010010011010011, - 11 => 0b001011101111110110, - 12 => 0b001100011101100010, - 13 => 0b001101100001000111, - 14 => 0b001110011000001101, - 15 => 0b001111100100101000, - 16 => 0b010000101101111000, - 17 => 0b010001010001011101, - 18 => 0b010010101000010111, - 19 => 0b010011010100110010, - 20 => 0b010100100110100110, - 21 => 0b010101011010000011, - 22 => 0b010110100011001001, - 23 => 0b010111011111101100, - 24 => 0b011000111011000100, - 25 => 0b011001000111100001, - 26 => 0b011010111110101011, - 27 => 0b011011000010001110, - 28 => 0b011100110000011010, - 29 => 0b011101001100111111, - 30 => 0b011110110101110101, - 31 => 0b011111001001010000, - 32 => 0b100000100111010101, - 33 => 0b100001011011110000, - 34 => 0b100010100010111010, - 35 => 0b100011011110011111, - 36 => 0b100100101100001011, - 37 => 0b100101010000101110, - 38 => 0b100110101001100100, - 39 => 0b100111010101000001, - 40 => 0b101000110001101001, - ]; - /** * ISO/IEC 18004:2000 Section 8.9 - Format Information * @@ -196,11 +103,6 @@ final class QRMatrix{ ], ]; - /** - * the current QR Code version number - */ - protected int $version; - /** * the current ECC level */ @@ -212,7 +114,7 @@ final class QRMatrix{ protected int $maskPattern = QRCode::MASK_PATTERN_AUTO; /** - * the size (side length) of the matrix + * the size (side length) of the matrix, including quiet zone (if created) */ protected int $moduleCount; @@ -223,16 +125,17 @@ final class QRMatrix{ */ protected array $matrix; + /** + * a Version instance + */ + protected Version $version; + /** * QRMatrix constructor. * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - public function __construct(int $version, int $eclevel){ - - if(!in_array($version, range(1, 40), true)){ - throw new QRCodeDataException('invalid QR Code version'); - } + public function __construct(Version $version, int $eclevel){ if(!array_key_exists($eclevel, QRCode::ECC_MODES)){ throw new QRCodeDataException('invalid ecc level'); @@ -240,7 +143,7 @@ final class QRMatrix{ $this->version = $version; $this->eclevel = $eclevel; - $this->moduleCount = $this->version * 4 + 17; + $this->moduleCount = $this->version->getDimension(); $this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL)); } @@ -287,7 +190,7 @@ final class QRMatrix{ * Returns the current version number */ public function version():int{ - return $this->version; + return $this->version->getVersionNumber(); } /** @@ -350,7 +253,7 @@ final class QRMatrix{ * Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder */ public function setDarkModule():QRMatrix{ - $this->set(8, 4 * $this->version + 9, true, $this::M_DARKMODULE); + $this->set(8, 4 * $this->version->getVersionNumber() + 9, true, $this::M_DARKMODULE); return $this; } @@ -426,9 +329,10 @@ final class QRMatrix{ * ISO/IEC 18004:2000 Section 7.3.5 */ public function setAlignmentPattern():QRMatrix{ + $alignmentPattern = $this->version->getAlignmentPattern(); - foreach($this::alignmentPattern[$this->version] as $y){ - foreach($this::alignmentPattern[$this->version] as $x){ + foreach($alignmentPattern as $y){ + foreach($alignmentPattern as $x){ // skip existing patterns if($this->matrix[$y][$x] !== $this::M_NULL){ @@ -478,9 +382,9 @@ final class QRMatrix{ * ISO/IEC 18004:2000 Section 8.10 */ public function setVersionNumber(bool $test = null):QRMatrix{ - $bits = $this::versionPattern[$this->version] ?? false; + $bits = $this->version->getVersionPattern(); - if($bits !== false){ + if($bits !== null){ for($i = 0; $i < 18; $i++){ $a = (int)floor($i / 3); @@ -605,7 +509,7 @@ final class QRMatrix{ } // $this->moduleCount includes the quiet zone (if created), we need the QR size here - $length = $this->version * 4 + 17; + $length = $this->version->getDimension(); // throw if the logo space exceeds the maximum error correction capacity if($width * $height > floor($length * $length * 0.2)){ diff --git a/src/QRCode.php b/src/QRCode.php index 5a29d5f22..5de32ce4a 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -51,7 +51,7 @@ class QRCode{ /** * References to the keys of the following tables: * - * @see \chillerlan\QRCode\Data\QRData::MAX_BITS + * @see \chillerlan\QRCode\Common\Version::MAX_BITS * @see \chillerlan\QRCode\Data\QRData::RSBLOCKS * @see \chillerlan\QRCode\Data\QRMatrix::formatPattern * diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index 046cc86bb..77d9813d4 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -12,6 +12,7 @@ namespace chillerlan\QRCodeTest\Data; +use chillerlan\QRCode\Common\Version; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix}; @@ -43,7 +44,7 @@ final class QRMatrixTest extends TestCase{ * @internal */ protected function getMatrix(int $version):QRMatrix{ - return new QRMatrix($version, QRCode::ECC_L); + return new QRMatrix(new Version($version), QRCode::ECC_L); } /** @@ -53,16 +54,6 @@ final class QRMatrixTest extends TestCase{ $this::assertInstanceOf(QRMatrix::class, $this->matrix); } - /** - * Tests if an exception is thrown when an invalid QR version was given - */ - public function testInvalidVersionException():void{ - $this->expectException(QRCodeDataException::class); - $this->expectExceptionMessage('invalid QR Code version'); - - $this->matrix = new QRMatrix(42, 0); - } - /** * Tests if an exception is thrown when an invalid ECC level was given */ @@ -70,7 +61,7 @@ final class QRMatrixTest extends TestCase{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('invalid ecc level'); - $this->matrix = new QRMatrix(1, 42); + $this->matrix = new QRMatrix(new Version(1), 42); } /** @@ -190,7 +181,7 @@ final class QRMatrixTest extends TestCase{ ->setAlignmentPattern() ; - $alignmentPattern = (new ReflectionClass(QRMatrix::class))->getConstant('alignmentPattern')[$version]; + $alignmentPattern = Version::ALIGNMENT_PATTERN[$version]; foreach($alignmentPattern as $py){ foreach($alignmentPattern as $px){