From 9f5d0bc06073b152a5ff93d8f11d9e8ff8e3acc5 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 21 Nov 2020 02:43:14 +0100 Subject: [PATCH 01/78] :bath: data mode rework --- examples/custom_output.php | 5 +- examples/imageWithLogo.php | 5 +- examples/imageWithText.php | 5 +- src/Data/AlphaNum.php | 55 ++- src/Data/Byte.php | 26 +- src/Data/Kanji.php | 50 ++- src/Data/MaskPatternTester.php | 12 +- src/Data/Number.php | 54 ++- src/Data/QRData.php | 408 +++++++++++++++++++++++ src/Data/QRDataAbstract.php | 311 ----------------- src/Data/QRDataInterface.php | 200 ----------- src/Data/QRDataModeAbstract.php | 101 ++++++ src/Data/QRDataModeInterface.php | 43 +++ src/Data/QRMatrix.php | 4 +- src/QRCode.php | 187 +++++------ src/QROptions.php | 1 - src/QROptionsTrait.php | 9 - tests/Data/AlphaNumTest.php | 15 +- tests/Data/ByteTest.php | 12 +- tests/Data/DatainterfaceTestAbstract.php | 35 +- tests/Data/KanjiTest.php | 17 +- tests/Data/MaskPatternTesterTest.php | 6 +- tests/Data/NumberTest.php | 15 +- tests/Data/QRMatrixTest.php | 8 +- tests/Output/QROutputTestAbstract.php | 4 +- tests/QRCodeTest.php | 54 +-- 26 files changed, 853 insertions(+), 789 deletions(-) create mode 100644 src/Data/QRData.php delete mode 100644 src/Data/QRDataAbstract.php delete mode 100644 src/Data/QRDataInterface.php create mode 100644 src/Data/QRDataModeAbstract.php create mode 100644 src/Data/QRDataModeInterface.php diff --git a/examples/custom_output.php b/examples/custom_output.php index 71ea62682..e21ce1073 100644 --- a/examples/custom_output.php +++ b/examples/custom_output.php @@ -22,7 +22,10 @@ $options = new QROptions([ 'eccLevel' => QRCode::ECC_L, ]); -$qrOutputInterface = new MyCustomOutput($options, (new QRCode($options))->getMatrix($data)); +$qrcode = new QRCode($options); +$qrcode->addByteSegment($data); + +$qrOutputInterface = new MyCustomOutput($options, $qrcode->getMatrix()); var_dump($qrOutputInterface->dump()); diff --git a/examples/imageWithLogo.php b/examples/imageWithLogo.php index 90e41e069..bfdf11f42 100644 --- a/examples/imageWithLogo.php +++ b/examples/imageWithLogo.php @@ -36,9 +36,12 @@ $options->logoHeight = 13; $options->scale = 5; $options->imageTransparent = false; +$qrcode = new QRCode($options); +$qrcode->addByteSegment($data); + header('Content-type: image/png'); -$qrOutputInterface = new QRImageWithLogo($options, (new QRCode($options))->getMatrix($data)); +$qrOutputInterface = new QRImageWithLogo($options, $qrcode->getMatrix()); // dump the output, with an additional logo echo $qrOutputInterface->dump(null, __DIR__.'/octocat.png'); diff --git a/examples/imageWithText.php b/examples/imageWithText.php index 050781cba..44175b5b0 100644 --- a/examples/imageWithText.php +++ b/examples/imageWithText.php @@ -25,9 +25,12 @@ $options = new QROptions([ 'imageBase64' => false, ]); +$qrcode = new QRCode($options); +$qrcode->addByteSegment($data); + header('Content-type: image/png'); -$qrOutputInterface = new QRImageWithText($options, (new QRCode($options))->getMatrix($data)); +$qrOutputInterface = new QRImageWithText($options, $qrcode->getMatrix()); // dump the output, with additional text echo $qrOutputInterface->dump(null, 'example text'); diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index 28d9d7563..fd16000c6 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -14,7 +14,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\QRCode; -use function ord, sprintf; +use function ceil, ord, sprintf, str_split; /** * Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / : @@ -22,7 +22,21 @@ use function ord, sprintf; * ISO/IEC 18004:2000 Section 8.3.3 * ISO/IEC 18004:2000 Section 8.4.3 */ -final class AlphaNum extends QRDataAbstract{ +final class AlphaNum extends QRDataModeAbstract{ + + /** + * ISO/IEC 18004:2000 Table 5 + * + * @var int[] + */ + protected const CHAR_MAP_ALPHANUM = [ + '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, + '8' => 8, '9' => 9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15, + 'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, + 'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31, + 'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39, + '+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44, + ]; protected int $datamode = QRCode::DATA_ALPHANUM; @@ -31,14 +45,37 @@ final class AlphaNum extends QRDataAbstract{ /** * @inheritdoc */ - protected function write(string $data):void{ + public function getLengthInBits():int{ + return (int)ceil($this->getLength() * (11 / 2)); + } - for($i = 0; $i + 1 < $this->strlen; $i += 2){ - $this->bitBuffer->put($this->getCharCode($data[$i]) * 45 + $this->getCharCode($data[$i + 1]), 11); + /** + * @inheritdoc + */ + public static function validateString(string $string):bool{ + + foreach(str_split($string) as $chr){ + if(!isset(self::CHAR_MAP_ALPHANUM[$chr])){ + return false; + } } - if($i < $this->strlen){ - $this->bitBuffer->put($this->getCharCode($data[$i]), 6); + return true; + } + + /** + * @inheritdoc + */ + public function write(int $version):void{ + $this->writeSegmentHeader($version); + $len = $this->getLength(); + + for($i = 0; $i + 1 < $len; $i += 2){ + $this->bitBuffer->put($this->getCharCode($this->data[$i]) * 45 + $this->getCharCode($this->data[$i + 1]), 11); + } + + if($i < $len){ + $this->bitBuffer->put($this->getCharCode($this->data[$i]), 6); } } @@ -50,11 +87,11 @@ final class AlphaNum extends QRDataAbstract{ */ protected function getCharCode(string $chr):int{ - if(!isset($this::CHAR_MAP_ALPHANUM[$chr])){ + if(!isset(self::CHAR_MAP_ALPHANUM[$chr])){ throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, ord($chr))); } - return $this::CHAR_MAP_ALPHANUM[$chr]; + return self::CHAR_MAP_ALPHANUM[$chr]; } } diff --git a/src/Data/Byte.php b/src/Data/Byte.php index 02e76a639..c93a7f40e 100644 --- a/src/Data/Byte.php +++ b/src/Data/Byte.php @@ -22,7 +22,7 @@ use function ord; * ISO/IEC 18004:2000 Section 8.3.4 * ISO/IEC 18004:2000 Section 8.4.4 */ -final class Byte extends QRDataAbstract{ +final class Byte extends QRDataModeAbstract{ protected int $datamode = QRCode::DATA_BYTE; @@ -31,11 +31,27 @@ final class Byte extends QRDataAbstract{ /** * @inheritdoc */ - protected function write(string $data):void{ - $i = 0; + public function getLengthInBits():int{ + return $this->getLength() * 8; + } - while($i < $this->strlen){ - $this->bitBuffer->put(ord($data[$i]), 8); + /** + * @inheritdoc + */ + public static function validateString(string $string):bool{ + return !empty($string); + } + + /** + * @inheritdoc + */ + public function write(int $version):void{ + $this->writeSegmentHeader($version); + $len = $this->getLength(); + $i = 0; + + while($i < $len){ + $this->bitBuffer->put(ord($this->data[$i]), 8); $i++; } diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index e106c50f1..91cd85175 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -12,9 +12,10 @@ namespace chillerlan\QRCode\Data; +use chillerlan\QRCode\Helpers\BitBuffer; use chillerlan\QRCode\QRCode; -use function mb_strlen, ord, sprintf, strlen; +use function mb_convert_encoding, mb_detect_encoding, mb_strlen, ord, sprintf, strlen; /** * Kanji mode: double-byte characters from the Shift JIS character set @@ -22,17 +23,51 @@ use function mb_strlen, ord, sprintf, strlen; * ISO/IEC 18004:2000 Section 8.3.5 * ISO/IEC 18004:2000 Section 8.4.5 */ -final class Kanji extends QRDataAbstract{ +final class Kanji extends QRDataModeAbstract{ protected int $datamode = QRCode::DATA_KANJI; protected array $lengthBits = [8, 10, 12]; + public function __construct(BitBuffer $bitBuffer, string $data){ + parent::__construct($bitBuffer, $data); + + /** @noinspection PhpFieldAssignmentTypeMismatchInspection */ + $this->data = mb_convert_encoding($this->data, 'SJIS', mb_detect_encoding($this->data)); + } + /** * @inheritdoc */ - protected function getLength(string $data):int{ - return mb_strlen($data, 'SJIS'); + protected function getLength():int{ + return mb_strlen($this->data, 'SJIS'); + } + + /** + * @inheritdoc + */ + public function getLengthInBits():int{ + return $this->getLength() * 13; + } + + /** + * checks if a string qualifies as Kanji + */ + public static function validateString(string $string):bool{ + $i = 0; + $len = strlen($string); + + while($i + 1 < $len){ + $c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1])); + + if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){ + return false; + } + + $i += 2; + } + + return $i >= $len; } /** @@ -40,11 +75,12 @@ final class Kanji extends QRDataAbstract{ * * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence */ - protected function write(string $data):void{ - $len = strlen($data); + public function write(int $version):void{ + $this->writeSegmentHeader($version); + $len = strlen($this->data); // not self::getLength() - we need 8-bit length for($i = 0; $i + 1 < $len; $i += 2){ - $c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1])); + $c = ((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[$i + 1])); if($c >= 0x8140 && $c <= 0x9FFC){ $c -= 0x8140; diff --git a/src/Data/MaskPatternTester.php b/src/Data/MaskPatternTester.php index 7874cb53d..d7cedbe23 100644 --- a/src/Data/MaskPatternTester.php +++ b/src/Data/MaskPatternTester.php @@ -17,7 +17,7 @@ namespace chillerlan\QRCode\Data; use function abs, array_search, call_user_func_array, min; /** - * Receives a QRDataInterface object and runs the mask pattern tests on it. + * Receives a QRData object and runs the mask pattern tests on it. * * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results * @@ -28,16 +28,16 @@ final class MaskPatternTester{ /** * The data interface that contains the data matrix to test */ - protected QRDataInterface $dataInterface; + protected QRData $qrData; /** - * Receives the QRDataInterface + * Receives the QRData object * * @see \chillerlan\QRCode\QROptions::$maskPattern * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern */ - public function __construct(QRDataInterface $dataInterface){ - $this->dataInterface = $dataInterface; + public function __construct(QRData $qrData){ + $this->qrData = $qrData; } /** @@ -62,7 +62,7 @@ final class MaskPatternTester{ * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern */ public function testPattern(int $pattern):int{ - $matrix = $this->dataInterface->initMatrix($pattern, true); + $matrix = $this->qrData->initMatrix($pattern, true); $penalty = 0; for($level = 1; $level <= 4; $level++){ diff --git a/src/Data/Number.php b/src/Data/Number.php index 0a905b13e..90f7d7632 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -14,7 +14,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\QRCode; -use function ord, sprintf, str_split, substr; +use function ceil, ord, sprintf, str_split, substr; /** * Numeric mode: decimal digits 0 to 9 @@ -22,7 +22,14 @@ use function ord, sprintf, str_split, substr; * ISO/IEC 18004:2000 Section 8.3.2 * ISO/IEC 18004:2000 Section 8.4.2 */ -final class Number extends QRDataAbstract{ +final class Number extends QRDataModeAbstract{ + + /** + * @var int[] + */ + protected const CHAR_MAP_NUMBER = [ + '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, + ]; protected int $datamode = QRCode::DATA_NUMBER; @@ -31,21 +38,44 @@ final class Number extends QRDataAbstract{ /** * @inheritdoc */ - protected function write(string $data):void{ - $i = 0; + public function getLengthInBits():int{ + return (int)ceil($this->getLength() * (10 / 3)); + } - while($i + 2 < $this->strlen){ - $this->bitBuffer->put($this->parseInt(substr($data, $i, 3)), 10); + /** + * @inheritdoc + */ + public static function validateString(string $string):bool{ + + foreach(str_split($string) as $chr){ + if(!isset(self::CHAR_MAP_NUMBER[$chr])){ + return false; + } + } + + return true; + } + + /** + * @inheritdoc + */ + public function write(int $version):void{ + $this->writeSegmentHeader($version); + $len = $this->getLength(); + $i = 0; + + while($i + 2 < $len){ + $this->bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10); $i += 3; } - if($i < $this->strlen){ + if($i < $len){ - if($this->strlen - $i === 1){ - $this->bitBuffer->put($this->parseInt(substr($data, $i, $i + 1)), 4); + if($len - $i === 1){ + $this->bitBuffer->put($this->parseInt(substr($this->data, $i, $i + 1)), 4); } - elseif($this->strlen - $i === 2){ - $this->bitBuffer->put($this->parseInt(substr($data, $i, $i + 2)), 7); + elseif($len - $i === 2){ + $this->bitBuffer->put($this->parseInt(substr($this->data, $i, $i + 2)), 7); } } @@ -63,7 +93,7 @@ final class Number extends QRDataAbstract{ foreach(str_split($string) as $chr){ $c = ord($chr); - if(!isset($this::CHAR_MAP_NUMBER[$chr])){ + if(!isset(self::CHAR_MAP_NUMBER[$chr])){ throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, $c)); } diff --git a/src/Data/QRData.php b/src/Data/QRData.php new file mode 100644 index 000000000..aea59ae3f --- /dev/null +++ b/src/Data/QRData.php @@ -0,0 +1,408 @@ + + * @copyright 2015 Smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial}; +use chillerlan\Settings\SettingsContainerInterface; + +use function array_column, array_combine, array_fill, array_keys, 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 + * + * @var int [][][] + */ + const RSBLOCKS = [ + 1 => [[ 1, 0, 26, 19], [ 1, 0, 26, 16], [ 1, 0, 26, 13], [ 1, 0, 26, 9]], + 2 => [[ 1, 0, 44, 34], [ 1, 0, 44, 28], [ 1, 0, 44, 22], [ 1, 0, 44, 16]], + 3 => [[ 1, 0, 70, 55], [ 1, 0, 70, 44], [ 2, 0, 35, 17], [ 2, 0, 35, 13]], + 4 => [[ 1, 0, 100, 80], [ 2, 0, 50, 32], [ 2, 0, 50, 24], [ 4, 0, 25, 9]], + 5 => [[ 1, 0, 134, 108], [ 2, 0, 67, 43], [ 2, 2, 33, 15], [ 2, 2, 33, 11]], + 6 => [[ 2, 0, 86, 68], [ 4, 0, 43, 27], [ 4, 0, 43, 19], [ 4, 0, 43, 15]], + 7 => [[ 2, 0, 98, 78], [ 4, 0, 49, 31], [ 2, 4, 32, 14], [ 4, 1, 39, 13]], + 8 => [[ 2, 0, 121, 97], [ 2, 2, 60, 38], [ 4, 2, 40, 18], [ 4, 2, 40, 14]], + 9 => [[ 2, 0, 146, 116], [ 3, 2, 58, 36], [ 4, 4, 36, 16], [ 4, 4, 36, 12]], + 10 => [[ 2, 2, 86, 68], [ 4, 1, 69, 43], [ 6, 2, 43, 19], [ 6, 2, 43, 15]], + 11 => [[ 4, 0, 101, 81], [ 1, 4, 80, 50], [ 4, 4, 50, 22], [ 3, 8, 36, 12]], + 12 => [[ 2, 2, 116, 92], [ 6, 2, 58, 36], [ 4, 6, 46, 20], [ 7, 4, 42, 14]], + 13 => [[ 4, 0, 133, 107], [ 8, 1, 59, 37], [ 8, 4, 44, 20], [12, 4, 33, 11]], + 14 => [[ 3, 1, 145, 115], [ 4, 5, 64, 40], [11, 5, 36, 16], [11, 5, 36, 12]], + 15 => [[ 5, 1, 109, 87], [ 5, 5, 65, 41], [ 5, 7, 54, 24], [11, 7, 36, 12]], + 16 => [[ 5, 1, 122, 98], [ 7, 3, 73, 45], [15, 2, 43, 19], [ 3, 13, 45, 15]], + 17 => [[ 1, 5, 135, 107], [10, 1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]], + 18 => [[ 5, 1, 150, 120], [ 9, 4, 69, 43], [17, 1, 50, 22], [ 2, 19, 42, 14]], + 19 => [[ 3, 4, 141, 113], [ 3, 11, 70, 44], [17, 4, 47, 21], [ 9, 16, 39, 13]], + 20 => [[ 3, 5, 135, 107], [ 3, 13, 67, 41], [15, 5, 54, 24], [15, 10, 43, 15]], + 21 => [[ 4, 4, 144, 116], [17, 0, 68, 42], [17, 6, 50, 22], [19, 6, 46, 16]], + 22 => [[ 2, 7, 139, 111], [17, 0, 74, 46], [ 7, 16, 54, 24], [34, 0, 37, 13]], + 23 => [[ 4, 5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]], + 24 => [[ 6, 4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30, 2, 46, 16]], + 25 => [[ 8, 4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]], + 26 => [[10, 2, 142, 114], [19, 4, 74, 46], [28, 6, 50, 22], [33, 4, 46, 16]], + 27 => [[ 8, 4, 152, 122], [22, 3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]], + 28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]], + 29 => [[ 7, 7, 146, 116], [21, 7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]], + 30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]], + 31 => [[13, 3, 145, 115], [ 2, 29, 74, 46], [42, 1, 54, 24], [23, 28, 45, 15]], + 32 => [[17, 0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]], + 33 => [[17, 1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]], + 34 => [[13, 6, 145, 115], [14, 23, 74, 46], [44, 7, 54, 24], [59, 1, 46, 16]], + 35 => [[12, 7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]], + 36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]], + 37 => [[17, 4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]], + 38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]], + 39 => [[20, 4, 147, 117], [40, 7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]], + 40 => [[19, 6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]], + ]; + + /** + * current QR Code version + */ + protected int $version; + + /** + * ECC temp data + */ + protected array $ecdata; + + /** + * ECC temp data + */ + protected array $dcdata; + + /** + * @var \chillerlan\QRCode\Data\QRDataModeInterface[] + */ + protected array $dataSegments = []; + + /** + * Max bits for the current ECC mode + * + * @var int[] + */ + protected array $maxBitsForEcc; + + /** + * the options instance + * + * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions + */ + protected SettingsContainerInterface $options; + + /** + * a BitBuffer instance + */ + protected BitBuffer $bitBuffer; + + /** + * QRData constructor. + * + * @param \chillerlan\Settings\SettingsContainerInterface $options + * @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]) + ); + + if(!empty($dataSegments)){ + $this->setData($dataSegments); + } + + } + + /** + * Sets the data string (internally called by the constructor) + */ + public function setData(array $dataSegments):QRData{ + + foreach($dataSegments as $segment){ + [$class, $data] = $segment; + + $this->dataSegments[] = new $class($this->bitBuffer, $data); + } + + $this->version = $this->options->version === QRCode::VERSION_AUTO + ? $this->getMinimumVersion() + : $this->options->version; + + $this->writeBitBuffer(); + + return $this; + } + + /** + * returns a fresh matrix object with the data written for the given $maskPattern + */ + public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{ + return (new QRMatrix($this->version, $this->options->eccLevel)) + ->init($maskPattern, $test) + ->mapData($this->maskECC(), $maskPattern) + ; + } + + /** + * estimates the total length of the several mode segments in order to guess the minimum version + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + protected function estimateTotalBitLength():int{ + $length = 0; + $margin = 0; + + foreach($this->dataSegments as $segment){ + // data length in bits of the current segment +4 bits for each mode descriptor + $length += ($segment->getLengthInBits() + $segment->getLengthBits(0) + 4); + // mode length bits margin to the next breakpoint + $margin += ($segment instanceof Byte ? 8 : 2); + } + + foreach([9, 26, 40] as $breakpoint){ + + // length bits for the first breakpoint have already been added + if($breakpoint > 9){ + $length += $margin; + } + + if($length < $this->maxBitsForEcc[$breakpoint]){ + return $length; + } + } + + throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length)); + } + + /** + * returns the minimum version number for the given string + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + protected function getMinimumVersion():int{ + $total = $this->estimateTotalBitLength(); + + // guess the version number within the given range + foreach(range($this->options->versionMin, $this->options->versionMax) as $version){ + + if($total <= $this->maxBitsForEcc[$version]){ + return $version; + } + + } + + // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first + throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore + } + + /** + * creates a BitBuffer and writes the string data to it + * + * @throws \chillerlan\QRCode\QRCodeException on data overflow + */ + protected function writeBitBuffer():void{ + $MAX_BITS = $this->maxBitsForEcc[$this->version]; + + foreach($this->dataSegments as $segment){ + $segment->write($this->version); + } + + // overflow, likely caused due to invalid version setting + if($this->bitBuffer->getLength() > $MAX_BITS){ + throw new QRCodeDataException( + sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS) + ); + } + + // add terminator (ISO/IEC 18004:2000 Table 2) + if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){ + $this->bitBuffer->put(0b0000, 4); + } + + // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion + while($this->bitBuffer->getLength() % 8 !== 0){ + $this->bitBuffer->putBit(false); + } + + while(true){ + + if($this->bitBuffer->getLength() >= $MAX_BITS){ + break; + } + + $this->bitBuffer->put(0b11101100, 8); + + if($this->bitBuffer->getLength() >= $MAX_BITS){ + break; + } + + $this->bitBuffer->put(0b00010001, 8); + } + + } + + /** + * ECC masking + * + * ISO/IEC 18004:2000 Section 8.5 ff + * + * @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]]; + + $rsBlocks = array_fill(0, $l1, [$b1, $b2]); + $rsCount = $l1 + $l2; + $this->ecdata = array_fill(0, $rsCount, []); + $this->dcdata = $this->ecdata; + + if($l2 > 0){ + $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1])); + } + + $totalCodeCount = 0; + $maxDcCount = 0; + $maxEcCount = 0; + $offset = 0; + + $bitBuffer = $this->bitBuffer->getBuffer(); + + foreach($rsBlocks as $key => $block){ + [$rsBlockTotal, $dcCount] = $block; + + $ecCount = $rsBlockTotal - $dcCount; + $maxDcCount = max($maxDcCount, $dcCount); + $maxEcCount = max($maxEcCount, $ecCount); + $this->dcdata[$key] = array_fill(0, $dcCount, null); + + foreach($this->dcdata[$key] as $a => $_z){ + $this->dcdata[$key][$a] = 0xff & $bitBuffer[$a + $offset]; + } + + [$num, $add] = $this->poly($key, $ecCount); + + foreach($this->ecdata[$key] as $c => $_){ + $modIndex = $c + $add; + $this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0; + } + + $offset += $dcCount; + $totalCodeCount += $rsBlockTotal; + } + + $data = array_fill(0, $totalCodeCount, null); + $index = 0; + + $mask = function(array $arr, int $count) use (&$data, &$index, $rsCount):void{ + for($x = 0; $x < $count; $x++){ + for($y = 0; $y < $rsCount; $y++){ + if($x < count($arr[$y])){ + $data[$index] = $arr[$y][$x]; + $index++; + } + } + } + }; + + $mask($this->dcdata, $maxDcCount); + $mask($this->ecdata, $maxEcCount); + + return $data; + } + + /** + * helper method for the polynomial operations + */ + protected function poly(int $key, int $count):array{ + $rsPoly = new Polynomial; + $modPoly = new Polynomial; + + for($i = 0; $i < $count; $i++){ + $modPoly->setNum([1, $modPoly->gexp($i)]); + $rsPoly->multiply($modPoly->getNum()); + } + + $rsPolyCount = count($rsPoly->getNum()); + + $modPoly + ->setNum($this->dcdata[$key], $rsPolyCount - 1) + ->mod($rsPoly->getNum()) + ; + + $this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null); + $num = $modPoly->getNum(); + + return [ + $num, + count($num) - count($this->ecdata[$key]), + ]; + } + +} diff --git a/src/Data/QRDataAbstract.php b/src/Data/QRDataAbstract.php deleted file mode 100644 index 72b67b7b9..000000000 --- a/src/Data/QRDataAbstract.php +++ /dev/null @@ -1,311 +0,0 @@ - - * @copyright 2015 Smiley - * @license MIT - */ - -namespace chillerlan\QRCode\Data; - -use chillerlan\QRCode\QRCode; -use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial}; -use chillerlan\Settings\SettingsContainerInterface; - -use function array_fill, array_merge, count, max, mb_convert_encoding, mb_detect_encoding, range, sprintf, strlen; - -/** - * Processes the binary data and maps it on a matrix which is then being returned - */ -abstract class QRDataAbstract implements QRDataInterface{ - - /** - * the string byte count - */ - protected ?int $strlen = null; - - /** - * the current data mode: Num, Alphanum, Kanji, Byte - */ - protected int $datamode; - - /** - * mode length bits for the version breakpoints 1-9, 10-26 and 27-40 - * - * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator - */ - protected array $lengthBits = [0, 0, 0]; - - /** - * current QR Code version - */ - protected int $version; - - /** - * ECC temp data - */ - protected array $ecdata; - - /** - * ECC temp data - */ - protected array $dcdata; - - /** - * the options instance - * - * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions - */ - protected SettingsContainerInterface $options; - - /** - * a BitBuffer instance - */ - protected BitBuffer $bitBuffer; - - /** - * QRDataInterface constructor. - */ - public function __construct(SettingsContainerInterface $options, string $data = null){ - $this->options = $options; - - if($data !== null){ - $this->setData($data); - } - } - - /** - * @inheritDoc - */ - public function setData(string $data):QRDataInterface{ - - if($this->datamode === QRCode::DATA_KANJI){ - $data = mb_convert_encoding($data, 'SJIS', mb_detect_encoding($data)); - } - - $this->strlen = $this->getLength($data); - $this->version = $this->options->version === QRCode::VERSION_AUTO - ? $this->getMinimumVersion() - : $this->options->version; - - $this->writeBitBuffer($data); - - return $this; - } - - /** - * @inheritDoc - */ - public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{ - return (new QRMatrix($this->version, $this->options->eccLevel)) - ->init($maskPattern, $test) - ->mapData($this->maskECC(), $maskPattern) - ; - } - - /** - * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40 - * - * @throws \chillerlan\QRCode\Data\QRCodeDataException - * @codeCoverageIgnore - */ - protected function getLengthBits():int{ - - foreach([9, 26, 40] as $key => $breakpoint){ - if($this->version <= $breakpoint){ - return $this->lengthBits[$key]; - } - } - - throw new QRCodeDataException(sprintf('invalid version number: %d', $this->version)); - } - - /** - * returns the byte count of the $data string - */ - protected function getLength(string $data):int{ - return strlen($data); - } - - /** - * returns the minimum version number for the given string - * - * @throws \chillerlan\QRCode\Data\QRCodeDataException - */ - protected function getMinimumVersion():int{ - $maxlength = 0; - - // guess the version number within the given range - $dataMode = QRCode::DATA_MODES[$this->datamode]; - $eccMode = QRCode::ECC_MODES[$this->options->eccLevel]; - - foreach(range($this->options->versionMin, $this->options->versionMax) as $version){ - $maxlength = $this::MAX_LENGTH[$version][$dataMode][$eccMode]; - - if($this->strlen <= $maxlength){ - return $version; - } - } - - throw new QRCodeDataException(sprintf('data exceeds %d characters', $maxlength)); - } - - /** - * writes the actual data string to the BitBuffer - * - * @see \chillerlan\QRCode\Data\QRDataAbstract::writeBitBuffer() - */ - abstract protected function write(string $data):void; - - /** - * creates a BitBuffer and writes the string data to it - * - * @throws \chillerlan\QRCode\QRCodeException on data overflow - */ - protected function writeBitBuffer(string $data):void{ - $this->bitBuffer = new BitBuffer; - - $MAX_BITS = $this::MAX_BITS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]]; - - $this->bitBuffer - ->put($this->datamode, 4) - ->put($this->strlen, $this->getLengthBits()) - ; - - $this->write($data); - - // overflow, likely caused due to invalid version setting - if($this->bitBuffer->getLength() > $MAX_BITS){ - throw new QRCodeDataException(sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)); - } - - // add terminator (ISO/IEC 18004:2000 Table 2) - if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){ - $this->bitBuffer->put(0, 4); - } - - // padding - while($this->bitBuffer->getLength() % 8 !== 0){ - $this->bitBuffer->putBit(false); - } - - // padding - while(true){ - - if($this->bitBuffer->getLength() >= $MAX_BITS){ - break; - } - - $this->bitBuffer->put(0xEC, 8); - - if($this->bitBuffer->getLength() >= $MAX_BITS){ - break; - } - - $this->bitBuffer->put(0x11, 8); - } - - } - - /** - * ECC masking - * - * ISO/IEC 18004:2000 Section 8.5 ff - * - * @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]]; - - $rsBlocks = array_fill(0, $l1, [$b1, $b2]); - $rsCount = $l1 + $l2; - $this->ecdata = array_fill(0, $rsCount, []); - $this->dcdata = $this->ecdata; - - if($l2 > 0){ - $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1])); - } - - $totalCodeCount = 0; - $maxDcCount = 0; - $maxEcCount = 0; - $offset = 0; - - $bitBuffer = $this->bitBuffer->getBuffer(); - - foreach($rsBlocks as $key => $block){ - [$rsBlockTotal, $dcCount] = $block; - - $ecCount = $rsBlockTotal - $dcCount; - $maxDcCount = max($maxDcCount, $dcCount); - $maxEcCount = max($maxEcCount, $ecCount); - $this->dcdata[$key] = array_fill(0, $dcCount, null); - - foreach($this->dcdata[$key] as $a => $_z){ - $this->dcdata[$key][$a] = 0xff & $bitBuffer[$a + $offset]; - } - - [$num, $add] = $this->poly($key, $ecCount); - - foreach($this->ecdata[$key] as $c => $_){ - $modIndex = $c + $add; - $this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0; - } - - $offset += $dcCount; - $totalCodeCount += $rsBlockTotal; - } - - $data = array_fill(0, $totalCodeCount, null); - $index = 0; - - $mask = function(array $arr, int $count) use (&$data, &$index, $rsCount):void{ - for($x = 0; $x < $count; $x++){ - for($y = 0; $y < $rsCount; $y++){ - if($x < count($arr[$y])){ - $data[$index] = $arr[$y][$x]; - $index++; - } - } - } - }; - - $mask($this->dcdata, $maxDcCount); - $mask($this->ecdata, $maxEcCount); - - return $data; - } - - /** - * helper method for the polynomial operations - */ - protected function poly(int $key, int $count):array{ - $rsPoly = new Polynomial; - $modPoly = new Polynomial; - - for($i = 0; $i < $count; $i++){ - $modPoly->setNum([1, $modPoly->gexp($i)]); - $rsPoly->multiply($modPoly->getNum()); - } - - $rsPolyCount = count($rsPoly->getNum()); - - $modPoly - ->setNum($this->dcdata[$key], $rsPolyCount - 1) - ->mod($rsPoly->getNum()) - ; - - $this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null); - $num = $modPoly->getNum(); - - return [ - $num, - count($num) - count($this->ecdata[$key]), - ]; - } - -} diff --git a/src/Data/QRDataInterface.php b/src/Data/QRDataInterface.php deleted file mode 100644 index 93ad6221d..000000000 --- a/src/Data/QRDataInterface.php +++ /dev/null @@ -1,200 +0,0 @@ - - * @copyright 2015 Smiley - * @license MIT - */ - -namespace chillerlan\QRCode\Data; - -/** - * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji) - * and holds version information in several constants - */ -interface QRDataInterface{ - - /** - * @var int[] - */ - const CHAR_MAP_NUMBER = [ - '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, - ]; - - /** - * ISO/IEC 18004:2000 Table 5 - * - * @var int[] - */ - const CHAR_MAP_ALPHANUM = [ - '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, - '8' => 8, '9' => 9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15, - 'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, - 'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31, - 'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39, - '+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44, - ]; - - /** - * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40 - * - * @see http://www.qrcode.com/en/about/version.html - * - * @var int [][][] - */ - const MAX_LENGTH =[ - // v => [NUMERIC => [L, M, Q, H ], ALPHANUM => [L, M, Q, H], BINARY => [L, M, Q, H ], KANJI => [L, M, Q, H ]] // modules - 1 => [[ 41, 34, 27, 17], [ 25, 20, 16, 10], [ 17, 14, 11, 7], [ 10, 8, 7, 4]], // 21 - 2 => [[ 77, 63, 48, 34], [ 47, 38, 29, 20], [ 32, 26, 20, 14], [ 20, 16, 12, 8]], // 25 - 3 => [[ 127, 101, 77, 58], [ 77, 61, 47, 35], [ 53, 42, 32, 24], [ 32, 26, 20, 15]], // 29 - 4 => [[ 187, 149, 111, 82], [ 114, 90, 67, 50], [ 78, 62, 46, 34], [ 48, 38, 28, 21]], // 33 - 5 => [[ 255, 202, 144, 106], [ 154, 122, 87, 64], [ 106, 84, 60, 44], [ 65, 52, 37, 27]], // 37 - 6 => [[ 322, 255, 178, 139], [ 195, 154, 108, 84], [ 134, 106, 74, 58], [ 82, 65, 45, 36]], // 41 - 7 => [[ 370, 293, 207, 154], [ 224, 178, 125, 93], [ 154, 122, 86, 64], [ 95, 75, 53, 39]], // 45 - 8 => [[ 461, 365, 259, 202], [ 279, 221, 157, 122], [ 192, 152, 108, 84], [ 118, 93, 66, 52]], // 49 - 9 => [[ 552, 432, 312, 235], [ 335, 262, 189, 143], [ 230, 180, 130, 98], [ 141, 111, 80, 60]], // 53 - 10 => [[ 652, 513, 364, 288], [ 395, 311, 221, 174], [ 271, 213, 151, 119], [ 167, 131, 93, 74]], // 57 - 11 => [[ 772, 604, 427, 331], [ 468, 366, 259, 200], [ 321, 251, 177, 137], [ 198, 155, 109, 85]], // 61 - 12 => [[ 883, 691, 489, 374], [ 535, 419, 296, 227], [ 367, 287, 203, 155], [ 226, 177, 125, 96]], // 65 - 13 => [[1022, 796, 580, 427], [ 619, 483, 352, 259], [ 425, 331, 241, 177], [ 262, 204, 149, 109]], // 69 NICE! - 14 => [[1101, 871, 621, 468], [ 667, 528, 376, 283], [ 458, 362, 258, 194], [ 282, 223, 159, 120]], // 73 - 15 => [[1250, 991, 703, 530], [ 758, 600, 426, 321], [ 520, 412, 292, 220], [ 320, 254, 180, 136]], // 77 - 16 => [[1408, 1082, 775, 602], [ 854, 656, 470, 365], [ 586, 450, 322, 250], [ 361, 277, 198, 154]], // 81 - 17 => [[1548, 1212, 876, 674], [ 938, 734, 531, 408], [ 644, 504, 364, 280], [ 397, 310, 224, 173]], // 85 - 18 => [[1725, 1346, 948, 746], [1046, 816, 574, 452], [ 718, 560, 394, 310], [ 442, 345, 243, 191]], // 89 - 19 => [[1903, 1500, 1063, 813], [1153, 909, 644, 493], [ 792, 624, 442, 338], [ 488, 384, 272, 208]], // 93 - 20 => [[2061, 1600, 1159, 919], [1249, 970, 702, 557], [ 858, 666, 482, 382], [ 528, 410, 297, 235]], // 97 - 21 => [[2232, 1708, 1224, 969], [1352, 1035, 742, 587], [ 929, 711, 509, 403], [ 572, 438, 314, 248]], // 101 - 22 => [[2409, 1872, 1358, 1056], [1460, 1134, 823, 640], [1003, 779, 565, 439], [ 618, 480, 348, 270]], // 105 - 23 => [[2620, 2059, 1468, 1108], [1588, 1248, 890, 672], [1091, 857, 611, 461], [ 672, 528, 376, 284]], // 109 - 24 => [[2812, 2188, 1588, 1228], [1704, 1326, 963, 744], [1171, 911, 661, 511], [ 721, 561, 407, 315]], // 113 - 25 => [[3057, 2395, 1718, 1286], [1853, 1451, 1041, 779], [1273, 997, 715, 535], [ 784, 614, 440, 330]], // 117 - 26 => [[3283, 2544, 1804, 1425], [1990, 1542, 1094, 864], [1367, 1059, 751, 593], [ 842, 652, 462, 365]], // 121 - 27 => [[3517, 2701, 1933, 1501], [2132, 1637, 1172, 910], [1465, 1125, 805, 625], [ 902, 692, 496, 385]], // 125 - 28 => [[3669, 2857, 2085, 1581], [2223, 1732, 1263, 958], [1528, 1190, 868, 658], [ 940, 732, 534, 405]], // 129 - 29 => [[3909, 3035, 2181, 1677], [2369, 1839, 1322, 1016], [1628, 1264, 908, 698], [1002, 778, 559, 430]], // 133 - 30 => [[4158, 3289, 2358, 1782], [2520, 1994, 1429, 1080], [1732, 1370, 982, 742], [1066, 843, 604, 457]], // 137 - 31 => [[4417, 3486, 2473, 1897], [2677, 2113, 1499, 1150], [1840, 1452, 1030, 790], [1132, 894, 634, 486]], // 141 - 32 => [[4686, 3693, 2670, 2022], [2840, 2238, 1618, 1226], [1952, 1538, 1112, 842], [1201, 947, 684, 518]], // 145 - 33 => [[4965, 3909, 2805, 2157], [3009, 2369, 1700, 1307], [2068, 1628, 1168, 898], [1273, 1002, 719, 553]], // 149 - 34 => [[5253, 4134, 2949, 2301], [3183, 2506, 1787, 1394], [2188, 1722, 1228, 958], [1347, 1060, 756, 590]], // 153 - 35 => [[5529, 4343, 3081, 2361], [3351, 2632, 1867, 1431], [2303, 1809, 1283, 983], [1417, 1113, 790, 605]], // 157 - 36 => [[5836, 4588, 3244, 2524], [3537, 2780, 1966, 1530], [2431, 1911, 1351, 1051], [1496, 1176, 832, 647]], // 161 - 37 => [[6153, 4775, 3417, 2625], [3729, 2894, 2071, 1591], [2563, 1989, 1423, 1093], [1577, 1224, 876, 673]], // 165 - 38 => [[6479, 5039, 3599, 2735], [3927, 3054, 2181, 1658], [2699, 2099, 1499, 1139], [1661, 1292, 923, 701]], // 169 - 39 => [[6743, 5313, 3791, 2927], [4087, 3220, 2298, 1774], [2809, 2213, 1579, 1219], [1729, 1362, 972, 750]], // 173 - 40 => [[7089, 5596, 3993, 3057], [4296, 3391, 2420, 1852], [2953, 2331, 1663, 1273], [1817, 1435, 1024, 784]], // 177 - ]; - - /** - * 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 - * - * @var int [][][] - */ - const RSBLOCKS = [ - 1 => [[ 1, 0, 26, 19], [ 1, 0, 26, 16], [ 1, 0, 26, 13], [ 1, 0, 26, 9]], - 2 => [[ 1, 0, 44, 34], [ 1, 0, 44, 28], [ 1, 0, 44, 22], [ 1, 0, 44, 16]], - 3 => [[ 1, 0, 70, 55], [ 1, 0, 70, 44], [ 2, 0, 35, 17], [ 2, 0, 35, 13]], - 4 => [[ 1, 0, 100, 80], [ 2, 0, 50, 32], [ 2, 0, 50, 24], [ 4, 0, 25, 9]], - 5 => [[ 1, 0, 134, 108], [ 2, 0, 67, 43], [ 2, 2, 33, 15], [ 2, 2, 33, 11]], - 6 => [[ 2, 0, 86, 68], [ 4, 0, 43, 27], [ 4, 0, 43, 19], [ 4, 0, 43, 15]], - 7 => [[ 2, 0, 98, 78], [ 4, 0, 49, 31], [ 2, 4, 32, 14], [ 4, 1, 39, 13]], - 8 => [[ 2, 0, 121, 97], [ 2, 2, 60, 38], [ 4, 2, 40, 18], [ 4, 2, 40, 14]], - 9 => [[ 2, 0, 146, 116], [ 3, 2, 58, 36], [ 4, 4, 36, 16], [ 4, 4, 36, 12]], - 10 => [[ 2, 2, 86, 68], [ 4, 1, 69, 43], [ 6, 2, 43, 19], [ 6, 2, 43, 15]], - 11 => [[ 4, 0, 101, 81], [ 1, 4, 80, 50], [ 4, 4, 50, 22], [ 3, 8, 36, 12]], - 12 => [[ 2, 2, 116, 92], [ 6, 2, 58, 36], [ 4, 6, 46, 20], [ 7, 4, 42, 14]], - 13 => [[ 4, 0, 133, 107], [ 8, 1, 59, 37], [ 8, 4, 44, 20], [12, 4, 33, 11]], - 14 => [[ 3, 1, 145, 115], [ 4, 5, 64, 40], [11, 5, 36, 16], [11, 5, 36, 12]], - 15 => [[ 5, 1, 109, 87], [ 5, 5, 65, 41], [ 5, 7, 54, 24], [11, 7, 36, 12]], - 16 => [[ 5, 1, 122, 98], [ 7, 3, 73, 45], [15, 2, 43, 19], [ 3, 13, 45, 15]], - 17 => [[ 1, 5, 135, 107], [10, 1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]], - 18 => [[ 5, 1, 150, 120], [ 9, 4, 69, 43], [17, 1, 50, 22], [ 2, 19, 42, 14]], - 19 => [[ 3, 4, 141, 113], [ 3, 11, 70, 44], [17, 4, 47, 21], [ 9, 16, 39, 13]], - 20 => [[ 3, 5, 135, 107], [ 3, 13, 67, 41], [15, 5, 54, 24], [15, 10, 43, 15]], - 21 => [[ 4, 4, 144, 116], [17, 0, 68, 42], [17, 6, 50, 22], [19, 6, 46, 16]], - 22 => [[ 2, 7, 139, 111], [17, 0, 74, 46], [ 7, 16, 54, 24], [34, 0, 37, 13]], - 23 => [[ 4, 5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]], - 24 => [[ 6, 4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30, 2, 46, 16]], - 25 => [[ 8, 4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]], - 26 => [[10, 2, 142, 114], [19, 4, 74, 46], [28, 6, 50, 22], [33, 4, 46, 16]], - 27 => [[ 8, 4, 152, 122], [22, 3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]], - 28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]], - 29 => [[ 7, 7, 146, 116], [21, 7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]], - 30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]], - 31 => [[13, 3, 145, 115], [ 2, 29, 74, 46], [42, 1, 54, 24], [23, 28, 45, 15]], - 32 => [[17, 0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]], - 33 => [[17, 1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]], - 34 => [[13, 6, 145, 115], [14, 23, 74, 46], [44, 7, 54, 24], [59, 1, 46, 16]], - 35 => [[12, 7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]], - 36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]], - 37 => [[17, 4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]], - 38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]], - 39 => [[20, 4, 147, 117], [40, 7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]], - 40 => [[19, 6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]], - ]; - - /** - * Sets the data string (internally called by the constructor) - */ - public function setData(string $data):QRDataInterface; - - /** - * returns a fresh matrix object with the data written for the given $maskPattern - */ - public function initMatrix(int $maskPattern, bool $test = null):QRMatrix; - -} diff --git a/src/Data/QRDataModeAbstract.php b/src/Data/QRDataModeAbstract.php new file mode 100644 index 000000000..8b652a72e --- /dev/null +++ b/src/Data/QRDataModeAbstract.php @@ -0,0 +1,101 @@ + + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\Helpers\BitBuffer; + +/** + */ +abstract class QRDataModeAbstract implements QRDataModeInterface{ + + /** + * the current data mode: Num, Alphanum, Kanji, Byte + */ + protected int $datamode; + + /** + * mode length bits for the version breakpoints 1-9, 10-26 and 27-40 + * + * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator + */ + protected array $lengthBits = [0, 0, 0]; + + /** + * The data to write + */ + protected string $data; + + /** + * a BitBuffer instance + */ + protected BitBuffer $bitBuffer; + + /** + * QRDataModeAbstract constructor. + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function __construct(BitBuffer $bitBuffer, string $data){ + // do we need this here? we check during write anyways. +# if(!static::validateString($data)){ +# throw new QRCodeDataException('invalid data string'); +# } + + $this->bitBuffer = $bitBuffer; + $this->data = $data; + } + + /** + * returns the character count of the $data string + */ + protected function getLength():int{ + return strlen($this->data); + } + + /** + * @inheritDoc + */ + public function getLengthBits(int $k):int{ + return $this->lengthBits[$k] ?? 0; + } + + /** + * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40 + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + * @codeCoverageIgnore + */ + protected function getLengthBitsForVersion(int $version):int{ + + foreach([9, 26, 40] as $key => $breakpoint){ + if($version <= $breakpoint){ + return $this->getLengthBits($key); + } + } + + throw new QRCodeDataException(sprintf('invalid version number: %d', $version)); + } + + /** + * + */ + protected function writeSegmentHeader(int $version):void{ + + $this->bitBuffer + ->put($this->datamode, 4) + ->put($this->getLength(), $this->getLengthBitsForVersion($version)) + ; + + } + +} diff --git a/src/Data/QRDataModeInterface.php b/src/Data/QRDataModeInterface.php new file mode 100644 index 000000000..d254b18cc --- /dev/null +++ b/src/Data/QRDataModeInterface.php @@ -0,0 +1,43 @@ + + * @copyright 2015 Smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +/** + * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji) + * and holds version information in several constants + */ +interface QRDataModeInterface{ + + /** + * returns the length bits for the given breakpoint [0,1,2] + */ + public function getLengthBits(int $k):int; + + /** + * retruns the length in bits of the data string + */ + public function getLengthInBits():int; + + /** + * checks if the given string qualifies for the encoder module + */ + public static function validateString(string $string):bool; + + /** + * writes the actual data string to the BitBuffer, uses the given version to determine the length bits + * + * @see \chillerlan\QRCode\Data\QRData::writeBitBuffer() + */ + public function write(int $version):void; + +} diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index ea4198174..2ba1e50a6 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -641,10 +641,10 @@ final class QRMatrix{ } /** - * Maps the binary $data array from QRDataInterface::maskECC() on the matrix, + * Maps the binary $data array from QRData::maskECC() on the matrix, * masking the data using $maskPattern (ISO/IEC 18004:2000 Section 8.8) * - * @see \chillerlan\QRCode\Data\QRDataAbstract::maskECC() + * @see \chillerlan\QRCode\Data\QRData::maskECC() * * @param int[] $data * @param int $maskPattern diff --git a/src/QRCode.php b/src/QRCode.php index 11842fa17..a2340704f 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -13,14 +13,14 @@ namespace chillerlan\QRCode; use chillerlan\QRCode\Data\{ - AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRCodeDataException, QRDataInterface, QRMatrix + AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRMatrix }; use chillerlan\QRCode\Output\{ QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString }; use chillerlan\Settings\SettingsContainerInterface; -use function call_user_func_array, class_exists, in_array, ord, strlen, strtolower, str_split; +use function class_exists, in_array; /** * Turns a text string into a Model 2 QR Code @@ -49,20 +49,6 @@ class QRCode{ /** @var int */ public const DATA_KANJI = 0b1000; - /** - * References to the keys of the following tables: - * - * @see \chillerlan\QRCode\Data\QRDataInterface::MAX_LENGTH - * - * @var int[] - */ - public const DATA_MODES = [ - self::DATA_NUMBER => 0, - self::DATA_ALPHANUM => 1, - self::DATA_BYTE => 2, - self::DATA_KANJI => 3, - ]; - // ISO/IEC 18004:2000 Tables 12, 25 /** @var int */ @@ -77,8 +63,8 @@ class QRCode{ /** * References to the keys of the following tables: * - * @see \chillerlan\QRCode\Data\QRDataInterface::MAX_BITS - * @see \chillerlan\QRCode\Data\QRDataInterface::RSBLOCKS + * @see \chillerlan\QRCode\Data\QRData::MAX_BITS + * @see \chillerlan\QRCode\Data\QRData::RSBLOCKS * @see \chillerlan\QRCode\Data\QRMatrix::formatPattern * * @var int[] @@ -134,22 +120,31 @@ class QRCode{ self::OUTPUT_IMAGICK, ], QRFpdf::class => [ - self::OUTPUT_FPDF - ] + self::OUTPUT_FPDF, + ], ]; /** - * Map of data mode => interface + * Map of data mode => interface (detection order) * * @var string[] */ protected const DATA_INTERFACES = [ - 'number' => Number::class, - 'alphanum' => AlphaNum::class, - 'kanji' => Kanji::class, - 'byte' => Byte::class, + self::DATA_NUMBER => Number::class, + self::DATA_ALPHANUM => AlphaNum::class, + self::DATA_KANJI => Kanji::class, + self::DATA_BYTE => Byte::class, ]; + /** + * A collection of one or more data segments of [classname, data] to write + * + * @see \chillerlan\QRCode\Data\QRDataModeInterface + * + * @var string[][]|int[][] + */ + protected array $dataSegments = []; + /** * The settings container * @@ -160,7 +155,7 @@ class QRCode{ /** * The selected data interface (Number, AlphaNum, Kanji, Byte) */ - protected QRDataInterface $dataInterface; + protected QRData $dataInterface; /** * QRCode constructor. @@ -176,22 +171,39 @@ class QRCode{ * * @return mixed */ - public function render(string $data, string $file = null){ - return $this->initOutputInterface($data)->dump($file); + public function render(string $data = null, string $file = null){ + + if($data !== null){ + /** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */ + foreach($this::DATA_INTERFACES as $dataInterface){ + + if($dataInterface::validateString($data)){ + $this->addSegment($data, $dataInterface); + + break; + } + + } + + } + + return $this->initOutputInterface()->dump($file); } + + /** * Returns a QRMatrix object for the given $data and current QROptions * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - public function getMatrix(string $data):QRMatrix{ + public function getMatrix():QRMatrix{ - if(empty($data)){ + if(empty($this->dataSegments)){ throw new QRCodeDataException('QRCode::getMatrix() No data given.'); } - $this->dataInterface = $this->initDataInterface($data); + $this->dataInterface = new QRData($this->options, $this->dataSegments); $maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO ? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern() @@ -206,47 +218,21 @@ class QRCode{ return $matrix; } - /** - * returns a fresh QRDataInterface for the given $data - * - * @throws \chillerlan\QRCode\Data\QRCodeDataException - */ - public function initDataInterface(string $data):QRDataInterface{ - - // allow forcing the data mode - // see https://github.com/chillerlan/php-qrcode/issues/39 - $interface = $this::DATA_INTERFACES[strtolower($this->options->dataModeOverride)] ?? null; - - if($interface !== null){ - return new $interface($this->options, $data); - } - - foreach($this::DATA_INTERFACES as $mode => $dataInterface){ - - if(call_user_func_array([$this, 'is'.$mode], [$data])){ - return new $dataInterface($this->options, $data); - } - - } - - throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore - } - /** * returns a fresh (built-in) QROutputInterface * * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ - protected function initOutputInterface(string $data):QROutputInterface{ + protected function initOutputInterface():QROutputInterface{ if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){ - return new $this->options->outputInterface($this->options, $this->getMatrix($data)); + return new $this->options->outputInterface($this->options, $this->getMatrix()); } foreach($this::OUTPUT_MODES as $outputInterface => $modes){ if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){ - return new $outputInterface($this->options, $this->getMatrix($data)); + return new $outputInterface($this->options, $this->getMatrix()); } } @@ -255,58 +241,67 @@ class QRCode{ } /** - * checks if a string qualifies as numeric + * checks if a string qualifies as numeric (convenience method) + * + * @see Number::validateString() */ public function isNumber(string $string):bool{ - return $this->checkString($string, QRDataInterface::CHAR_MAP_NUMBER); + return Number::validateString($string); } /** - * checks if a string qualifies as alphanumeric + * checks if a string qualifies as alphanumeric (convenience method) + * + * @see AlphaNum::validateString() */ public function isAlphaNum(string $string):bool{ - return $this->checkString($string, QRDataInterface::CHAR_MAP_ALPHANUM); + return AlphaNum::validateString($string); } /** - * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence. - */ - protected function checkString(string $string, array $charmap):bool{ - - foreach(str_split($string) as $chr){ - if(!isset($charmap[$chr])){ - return false; - } - } - - return true; - } - - /** - * checks if a string qualifies as Kanji + * checks if a string qualifies as Kanji (convenience method) + * + * @see Kanji::validateString() */ public function isKanji(string $string):bool{ - $i = 0; - $len = strlen($string); - - while($i + 1 < $len){ - $c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1])); - - if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){ - return false; - } - - $i += 2; - } - - return $i >= $len; + return Kanji::validateString($string); } /** - * a dummy + * a dummy (convenience method) + * + * @see Byte::validateString() */ - public function isByte(string $data):bool{ - return !empty($data); + public function isByte(string $string):bool{ + return Byte::validateString($string); + } + + protected function addSegment(string $data, string $classname):void{ + $this->dataSegments[] = [$classname, $data]; + } + + public function addNumberSegment(string $data):QRCode{ + $this->addSegment($data, Number::class); + + return $this; + } + + public function addAlphaNumSegment(string $data):QRCode{ + $this->addSegment($data, AlphaNum::class); + + return $this; + } + + public function addKanjiSegment(string $data):QRCode{ + $this->addSegment($data, Kanji::class); + + return $this; + } + + public function addByteSegment(string $data):QRCode{ + $this->addSegment($data, Byte::class); + + return $this; } } diff --git a/src/QROptions.php b/src/QROptions.php index e36f6701a..437eb5b90 100644 --- a/src/QROptions.php +++ b/src/QROptions.php @@ -24,7 +24,6 @@ use chillerlan\Settings\SettingsContainerAbstract; * @property int $maskPattern * @property bool $addQuietzone * @property int $quietzoneSize - * @property string|null $dataModeOverride * @property string $outputType * @property string|null $outputInterface * @property string|null $cachefile diff --git a/src/QROptionsTrait.php b/src/QROptionsTrait.php index 5ce144aba..204ec04bc 100644 --- a/src/QROptionsTrait.php +++ b/src/QROptionsTrait.php @@ -71,15 +71,6 @@ trait QROptionsTrait{ */ protected int $quietzoneSize = 4; - /** - * Use this to circumvent the data mode detection and force the usage of the given mode. - * - * valid modes are: Number, AlphaNum, Kanji, Byte (case insensitive) - * - * @see https://github.com/chillerlan/php-qrcode/issues/39 - */ - protected ?string $dataModeOverride = null; - /** * The output type * diff --git a/tests/Data/AlphaNumTest.php b/tests/Data/AlphaNumTest.php index 10847633c..6feab6ddc 100644 --- a/tests/Data/AlphaNumTest.php +++ b/tests/Data/AlphaNumTest.php @@ -12,8 +12,7 @@ namespace chillerlan\QRCodeTest\Data; -use chillerlan\QRCode\Data\{AlphaNum, QRCodeDataException, QRDataInterface}; -use chillerlan\QRCode\QROptions; +use chillerlan\QRCode\Data\{AlphaNum, QRCodeDataException}; /** * Tests the AlphaNum class @@ -21,7 +20,7 @@ use chillerlan\QRCode\QROptions; final class AlphaNumTest extends DatainterfaceTestAbstract{ /** @internal */ - protected string $testdata = '0 $%*+-./:'; + protected array $testdata = [AlphaNum::class, '0 $%*+-./:']; /** @internal */ protected array $expected = [ @@ -40,14 +39,6 @@ final class AlphaNumTest extends DatainterfaceTestAbstract{ 92, 112, 20, 198, 27 ]; - /** - * @inheritDoc - * @internal - */ - protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{ - return new AlphaNum($options); - } - /** * Tests if an exception is thrown when an invalid character is encountered */ @@ -55,7 +46,7 @@ final class AlphaNumTest extends DatainterfaceTestAbstract{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('illegal char: "#" [35]'); - $this->dataInterface->setData('#'); + $this->dataInterface->setData([[AlphaNum::class, '#']]); } } diff --git a/tests/Data/ByteTest.php b/tests/Data/ByteTest.php index 295603200..dacc523a5 100644 --- a/tests/Data/ByteTest.php +++ b/tests/Data/ByteTest.php @@ -13,8 +13,6 @@ namespace chillerlan\QRCodeTest\Data; use chillerlan\QRCode\Data\Byte; -use chillerlan\QRCode\Data\QRDataInterface; -use chillerlan\QRCode\QROptions; /** * Tests the Byte class @@ -22,7 +20,7 @@ use chillerlan\QRCode\QROptions; final class ByteTest extends DatainterfaceTestAbstract{ /** @internal */ - protected string $testdata = '[¯\_(ツ)_/¯]'; + protected array $testdata = [Byte::class, '[¯\_(ツ)_/¯]']; /** @internal */ protected array $expected = [ @@ -41,12 +39,4 @@ final class ByteTest extends DatainterfaceTestAbstract{ 21, 47, 250, 101 ]; - /** - * @inheritDoc - * @internal - */ - protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{ - return new Byte($options); - } - } diff --git a/tests/Data/DatainterfaceTestAbstract.php b/tests/Data/DatainterfaceTestAbstract.php index d533c3f17..b69fad70b 100644 --- a/tests/Data/DatainterfaceTestAbstract.php +++ b/tests/Data/DatainterfaceTestAbstract.php @@ -15,7 +15,7 @@ namespace chillerlan\QRCodeTest\Data; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; use PHPUnit\Framework\TestCase; -use chillerlan\QRCode\Data\{QRCodeDataException, QRDataInterface, QRMatrix}; +use chillerlan\QRCode\Data\{QRCodeDataException, QRData, QRMatrix}; use ReflectionClass; use function str_repeat; @@ -28,39 +28,32 @@ abstract class DatainterfaceTestAbstract extends TestCase{ /** @internal */ protected ReflectionClass $reflection; /** @internal */ - protected QRDataInterface $dataInterface; + protected QRData $dataInterface; /** @internal */ - protected string $testdata; + protected array $testdata; /** @internal */ - protected array $expected; + protected array $expected; /** * @internal */ protected function setUp():void{ - $this->dataInterface = $this->getDataInterfaceInstance(new QROptions(['version' => 4])); + $this->dataInterface = new QRData(new QROptions(['version' => 4]), []); $this->reflection = new ReflectionClass($this->dataInterface); } - /** - * Returns a data interface instance - * - * @internal - */ - abstract protected function getDataInterfaceInstance(QROptions $options):QRDataInterface; - /** * Verifies the data interface instance */ public function testInstance():void{ - $this::assertInstanceOf(QRDataInterface::class, $this->dataInterface); + $this::assertInstanceOf(QRData::class, $this->dataInterface); } /** * Tests ecc masking and verifies against a sample */ public function testMaskEcc():void{ - $this->dataInterface->setData($this->testdata); + $this->dataInterface->setData([$this->testdata]); $maskECC = $this->reflection->getMethod('maskECC'); $maskECC->setAccessible(true); @@ -83,7 +76,7 @@ abstract class DatainterfaceTestAbstract extends TestCase{ * @dataProvider MaskPatternProvider */ public function testInitMatrix(int $maskPattern):void{ - $this->dataInterface->setData($this->testdata); + $this->dataInterface->setData([$this->testdata]); $matrix = $this->dataInterface->initMatrix($maskPattern); @@ -95,7 +88,7 @@ abstract class DatainterfaceTestAbstract extends TestCase{ * Tests getting the minimum QR version for the given data */ public function testGetMinimumVersion():void{ - $this->dataInterface->setData($this->testdata); + $this->dataInterface->setData([$this->testdata]); $getMinimumVersion = $this->reflection->getMethod('getMinimumVersion'); $getMinimumVersion->setAccessible(true); @@ -109,9 +102,12 @@ abstract class DatainterfaceTestAbstract extends TestCase{ public function testGetMinimumVersionException():void{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('data exceeds'); + [$class, $data] = $this->testdata; - $this->dataInterface = $this->getDataInterfaceInstance(new QROptions(['version' => QRCode::VERSION_AUTO])); - $this->dataInterface->setData(str_repeat($this->testdata, 1337)); + $this->dataInterface = new QRData( + new QROptions(['version' => QRCode::VERSION_AUTO]), + [[$class, str_repeat($data, 1337)]] + ); } /** @@ -120,8 +116,9 @@ abstract class DatainterfaceTestAbstract extends TestCase{ public function testCodeLengthOverflowException():void{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('code length overflow'); + [$class, $data] = $this->testdata; - $this->dataInterface->setData(str_repeat($this->testdata, 1337)); + $this->dataInterface->setData([[$class, str_repeat($data, 1337)]]); } } diff --git a/tests/Data/KanjiTest.php b/tests/Data/KanjiTest.php index 484f388dc..36fa0c2dc 100644 --- a/tests/Data/KanjiTest.php +++ b/tests/Data/KanjiTest.php @@ -12,8 +12,7 @@ namespace chillerlan\QRCodeTest\Data; -use chillerlan\QRCode\QROptions; -use chillerlan\QRCode\Data\{Kanji, QRCodeDataException, QRDataInterface}; +use chillerlan\QRCode\Data\{Kanji, QRCodeDataException}; /** * Tests the Kanji class @@ -21,7 +20,7 @@ use chillerlan\QRCode\Data\{Kanji, QRCodeDataException, QRDataInterface}; final class KanjiTest extends DatainterfaceTestAbstract{ /** @internal */ - protected string $testdata = '茗荷茗荷茗荷茗荷茗荷'; + protected array $testdata = [Kanji::class, '茗荷茗荷茗荷茗荷茗荷']; /** @internal */ protected array $expected = [ @@ -40,14 +39,6 @@ final class KanjiTest extends DatainterfaceTestAbstract{ 96, 113, 54, 191 ]; - /** - * @inheritDoc - * @internal - */ - protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{ - return new Kanji($options); - } - /** * Tests if an exception is thrown when an invalid character is encountered */ @@ -55,7 +46,7 @@ final class KanjiTest extends DatainterfaceTestAbstract{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('illegal char at 1 [16191]'); - $this->dataInterface->setData('ÃÃ'); + $this->dataInterface->setData([[Kanji::class, 'ÃÃ']]); } /** @@ -65,7 +56,7 @@ final class KanjiTest extends DatainterfaceTestAbstract{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('illegal char at 1'); - $this->dataInterface->setData('Ã'); + $this->dataInterface->setData([[Kanji::class, 'Ã']]); } } diff --git a/tests/Data/MaskPatternTesterTest.php b/tests/Data/MaskPatternTesterTest.php index d286b41dc..46c98c99c 100644 --- a/tests/Data/MaskPatternTesterTest.php +++ b/tests/Data/MaskPatternTesterTest.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCodeTest\Data; use chillerlan\QRCode\QROptions; -use chillerlan\QRCode\Data\{Byte, MaskPatternTester}; +use chillerlan\QRCode\Data\{Byte, MaskPatternTester, QRData}; use PHPUnit\Framework\TestCase; /** @@ -25,7 +25,7 @@ final class MaskPatternTesterTest extends TestCase{ * Tests getting the best mask pattern */ public function testMaskpattern():void{ - $dataInterface = new Byte(new QROptions(['version' => 10]), 'test'); + $dataInterface = new QRData(new QROptions(['version' => 10]), [[Byte::class, 'test']]); $this::assertSame(3, (new MaskPatternTester($dataInterface))->getBestMaskPattern()); } @@ -34,7 +34,7 @@ final class MaskPatternTesterTest extends TestCase{ * Tests getting the penalty value for a given mask pattern */ public function testMaskpatternID():void{ - $dataInterface = new Byte(new QROptions(['version' => 10]), 'test'); + $dataInterface = new QRData(new QROptions(['version' => 10]), [[Byte::class, 'test']]); $this::assertSame(4243, (new MaskPatternTester($dataInterface))->testPattern(3)); } diff --git a/tests/Data/NumberTest.php b/tests/Data/NumberTest.php index dcd9507ab..070459cf7 100644 --- a/tests/Data/NumberTest.php +++ b/tests/Data/NumberTest.php @@ -12,8 +12,7 @@ namespace chillerlan\QRCodeTest\Data; -use chillerlan\QRCode\QROptions; -use chillerlan\QRCode\Data\{Number, QRCodeDataException, QRDataInterface}; +use chillerlan\QRCode\Data\{Number, QRCodeDataException}; /** * Tests the Number class @@ -21,7 +20,7 @@ use chillerlan\QRCode\Data\{Number, QRCodeDataException, QRDataInterface}; final class NumberTest extends DatainterfaceTestAbstract{ /** @internal */ - protected string $testdata = '0123456789'; + protected array $testdata = [Number::class, '0123456789']; /** @internal */ protected array $expected = [ @@ -40,14 +39,6 @@ final class NumberTest extends DatainterfaceTestAbstract{ 89, 63, 168, 151 ]; - /** - * @inheritDoc - * @internal - */ - protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{ - return new Number($options); - } - /** * Tests if an exception is thrown when an invalid character is encountered */ @@ -55,7 +46,7 @@ final class NumberTest extends DatainterfaceTestAbstract{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('illegal char: "#" [35]'); - $this->dataInterface->setData('#'); + $this->dataInterface->setData([[Number::class, '#']]); } } diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index 68b31a5de..046cc86bb 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -315,7 +315,7 @@ final class QRMatrixTest extends TestCase{ $o->eccLevel = QRCode::ECC_H; $o->addQuietzone = false; - $matrix = (new QRCode($o))->getMatrix('testdata'); + $matrix = (new QRCode($o))->addByteSegment('testdata')->getMatrix(); // also testing size adjustment to uneven numbers $matrix->setLogoSpace(20, 14); @@ -335,7 +335,7 @@ final class QRMatrixTest extends TestCase{ $o->addQuietzone = true; $o->quietzoneSize = 10; - $m = (new QRCode($o))->getMatrix('testdata'); + $m = (new QRCode($o))->addByteSegment('testdata')->getMatrix(); // logo space should not overwrite quiet zone & function patterns $m->setLogoSpace(21, 21, -10, -10); @@ -360,7 +360,7 @@ final class QRMatrixTest extends TestCase{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('ECC level "H" required to add logo space'); - (new QRCode)->getMatrix('testdata')->setLogoSpace(50, 50); + (new QRCode)->addByteSegment('testdata')->getMatrix()->setLogoSpace(50, 50); } public function testSetLogoSpaceMaxSizeException():void{ @@ -371,7 +371,7 @@ final class QRMatrixTest extends TestCase{ $o->version = 5; $o->eccLevel = QRCode::ECC_H; - (new QRCode($o))->getMatrix('testdata')->setLogoSpace(50, 50); + (new QRCode($o))->addByteSegment('testdata')->getMatrix()->setLogoSpace(50, 50); } } diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index 323fae908..b94c1f520 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCodeTest\Output; use chillerlan\QRCode\{QRCode, QROptions}; -use chillerlan\QRCode\Data\{Byte, QRMatrix}; +use chillerlan\QRCode\Data\{Byte, QRData, QRMatrix}; use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface}; use PHPUnit\Framework\TestCase; @@ -48,7 +48,7 @@ abstract class QROutputTestAbstract extends TestCase{ } $this->options = new QROptions; - $this->matrix = (new Byte($this->options, 'testdata'))->initMatrix(0); + $this->matrix = (new QRData($this->options, [[Byte::class, 'testdata']]))->initMatrix(0); $this->outputInterface = $this->getOutputInterface($this->options); } diff --git a/tests/QRCodeTest.php b/tests/QRCodeTest.php index 523d7eb8c..cdf984771 100755 --- a/tests/QRCodeTest.php +++ b/tests/QRCodeTest.php @@ -13,12 +13,10 @@ namespace chillerlan\QRCodeTest; use chillerlan\QRCode\{QROptions, QRCode}; -use chillerlan\QRCode\Data\{AlphaNum, Byte, Kanji, Number, QRCodeDataException}; +use chillerlan\QRCode\Data\QRCodeDataException; use chillerlan\QRCode\Output\QRCodeOutputException; use PHPUnit\Framework\TestCase; -use function random_bytes; - /** * Tests basic functions of the QRCode class */ @@ -97,55 +95,7 @@ class QRCodeTest extends TestCase{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('QRCode::getMatrix() No data given.'); - $this->qrcode->getMatrix(''); - } - - /** - * test whether stings are trimmed (they are not) - i'm still torn on that (see isByte) - */ - public function testAvoidTrimming():void{ - $m1 = $this->qrcode->getMatrix('hello')->matrix(); - $m2 = $this->qrcode->getMatrix('hello ')->matrix(); // added space - - $this::assertNotSame($m1, $m2); - } - - /** - * tests if the data mode is overriden if QROptions::$dataModeOverride is set to a valid value - * - * @see https://github.com/chillerlan/php-qrcode/issues/39 - */ - public function testDataModeOverride():void{ - - // no (or invalid) value set - auto detection - $this->options->dataModeOverride = 'foo'; - $this->qrcode = new QRCode; - - $this::assertInstanceOf(Number::class, $this->qrcode->initDataInterface('123')); - $this::assertInstanceOf(AlphaNum::class, $this->qrcode->initDataInterface('ABC123')); - $this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface(random_bytes(32))); - $this::assertInstanceOf(Kanji::class, $this->qrcode->initDataInterface('茗荷')); - - // data mode set: force the given data mode - $this->options->dataModeOverride = 'Byte'; - $this->qrcode = new QRCode($this->options); - - $this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('123')); - $this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('ABC123')); - $this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface(random_bytes(32))); - $this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('茗荷')); - } - - /** - * tests if an exception is thrown when an invalid character occurs when forcing a data mode other than Byte - */ - public function testDataModeOverrideError():void{ - $this->expectException(QRCodeDataException::class); - $this->expectExceptionMessage('illegal char:'); - - $this->options->dataModeOverride = 'AlphaNum'; - - (new QRCode($this->options))->initDataInterface(random_bytes(32)); + $this->qrcode->getMatrix(); } } From 8bd573ddfdb6b65c5a5ecf9bba4cae77d4ee7d8e Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 21 Nov 2020 18:40:53 +0100 Subject: [PATCH 02/78] :bath: data mode rework --- src/Data/AlphaNum.php | 21 ++++++++++++-------- src/Data/Byte.php | 20 +++++++++++-------- src/Data/Kanji.php | 24 +++++++++++----------- src/Data/Number.php | 31 ++++++++++++++++++----------- src/Data/QRData.php | 4 ++-- src/Data/QRDataModeAbstract.php | 34 ++------------------------------ src/Data/QRDataModeInterface.php | 4 +++- 7 files changed, 64 insertions(+), 74 deletions(-) diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index fd16000c6..a78d8fec3 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -12,6 +12,7 @@ namespace chillerlan\QRCode\Data; +use chillerlan\QRCode\Helpers\BitBuffer; use chillerlan\QRCode\QRCode; use function ceil, ord, sprintf, str_split; @@ -38,15 +39,13 @@ final class AlphaNum extends QRDataModeAbstract{ '+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44, ]; - protected int $datamode = QRCode::DATA_ALPHANUM; - protected array $lengthBits = [9, 11, 13]; /** * @inheritdoc */ public function getLengthInBits():int{ - return (int)ceil($this->getLength() * (11 / 2)); + return (int)ceil($this->getCharCount() * (11 / 2)); } /** @@ -66,16 +65,22 @@ final class AlphaNum extends QRDataModeAbstract{ /** * @inheritdoc */ - public function write(int $version):void{ - $this->writeSegmentHeader($version); - $len = $this->getLength(); + public function write(BitBuffer $bitBuffer, int $version):void{ + $len = $this->getCharCount(); + $bitBuffer + ->put(QRCode::DATA_ALPHANUM, 4) + ->put($len, $this->getLengthBitsForVersion($version)) + ; + + // encode 2 characters in 11 bits for($i = 0; $i + 1 < $len; $i += 2){ - $this->bitBuffer->put($this->getCharCode($this->data[$i]) * 45 + $this->getCharCode($this->data[$i + 1]), 11); + $bitBuffer->put($this->getCharCode($this->data[$i]) * 45 + $this->getCharCode($this->data[$i + 1]), 11); } + // encode a remaining character in 6 bits if($i < $len){ - $this->bitBuffer->put($this->getCharCode($this->data[$i]), 6); + $bitBuffer->put($this->getCharCode($this->data[$i]), 6); } } diff --git a/src/Data/Byte.php b/src/Data/Byte.php index c93a7f40e..738ec7d32 100644 --- a/src/Data/Byte.php +++ b/src/Data/Byte.php @@ -12,6 +12,7 @@ namespace chillerlan\QRCode\Data; +use chillerlan\QRCode\Helpers\BitBuffer; use chillerlan\QRCode\QRCode; use function ord; @@ -24,15 +25,13 @@ use function ord; */ final class Byte extends QRDataModeAbstract{ - protected int $datamode = QRCode::DATA_BYTE; - protected array $lengthBits = [8, 16, 16]; /** * @inheritdoc */ public function getLengthInBits():int{ - return $this->getLength() * 8; + return $this->getCharCount() * 8; } /** @@ -45,13 +44,18 @@ final class Byte extends QRDataModeAbstract{ /** * @inheritdoc */ - public function write(int $version):void{ - $this->writeSegmentHeader($version); - $len = $this->getLength(); - $i = 0; + public function write(BitBuffer $bitBuffer, int $version):void{ + $len = $this->getCharCount(); + + $bitBuffer + ->put(QRCode::DATA_BYTE, 4) + ->put($len, $this->getLengthBitsForVersion($version)) + ; + + $i = 0; while($i < $len){ - $this->bitBuffer->put(ord($this->data[$i]), 8); + $bitBuffer->put(ord($this->data[$i]), 8); $i++; } diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index 91cd85175..676fe2a93 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -25,12 +25,10 @@ use function mb_convert_encoding, mb_detect_encoding, mb_strlen, ord, sprintf, s */ final class Kanji extends QRDataModeAbstract{ - protected int $datamode = QRCode::DATA_KANJI; - protected array $lengthBits = [8, 10, 12]; - public function __construct(BitBuffer $bitBuffer, string $data){ - parent::__construct($bitBuffer, $data); + public function __construct(string $data){ + parent::__construct($data); /** @noinspection PhpFieldAssignmentTypeMismatchInspection */ $this->data = mb_convert_encoding($this->data, 'SJIS', mb_detect_encoding($this->data)); @@ -39,7 +37,7 @@ final class Kanji extends QRDataModeAbstract{ /** * @inheritdoc */ - protected function getLength():int{ + protected function getCharCount():int{ return mb_strlen($this->data, 'SJIS'); } @@ -47,7 +45,7 @@ final class Kanji extends QRDataModeAbstract{ * @inheritdoc */ public function getLengthInBits():int{ - return $this->getLength() * 13; + return $this->getCharCount() * 13; } /** @@ -75,9 +73,14 @@ final class Kanji extends QRDataModeAbstract{ * * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence */ - public function write(int $version):void{ - $this->writeSegmentHeader($version); - $len = strlen($this->data); // not self::getLength() - we need 8-bit length + public function write(BitBuffer $bitBuffer, int $version):void{ + + $bitBuffer + ->put(QRCode::DATA_KANJI, 4) + ->put($this->getCharCount(), $this->getLengthBitsForVersion($version)) + ; + + $len = strlen($this->data); for($i = 0; $i + 1 < $len; $i += 2){ $c = ((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[$i + 1])); @@ -92,8 +95,7 @@ final class Kanji extends QRDataModeAbstract{ throw new QRCodeDataException(sprintf('illegal char at %d [%d]', $i + 1, $c)); } - $this->bitBuffer->put(((($c >> 8) & 0xff) * 0xC0) + ($c & 0xff), 13); - + $bitBuffer->put(((($c >> 8) & 0xff) * 0xC0) + ($c & 0xff), 13); } if($i < $len){ diff --git a/src/Data/Number.php b/src/Data/Number.php index 90f7d7632..0fe616ff9 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -12,6 +12,7 @@ namespace chillerlan\QRCode\Data; +use chillerlan\QRCode\Helpers\BitBuffer; use chillerlan\QRCode\QRCode; use function ceil, ord, sprintf, str_split, substr; @@ -31,15 +32,13 @@ final class Number extends QRDataModeAbstract{ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, ]; - protected int $datamode = QRCode::DATA_NUMBER; - protected array $lengthBits = [10, 12, 14]; /** * @inheritdoc */ public function getLengthInBits():int{ - return (int)ceil($this->getLength() * (10 / 3)); + return (int)ceil($this->getCharCount() * (10 / 3)); } /** @@ -59,23 +58,31 @@ final class Number extends QRDataModeAbstract{ /** * @inheritdoc */ - public function write(int $version):void{ - $this->writeSegmentHeader($version); - $len = $this->getLength(); - $i = 0; + public function write(BitBuffer $bitBuffer, int $version):void{ + $len = $this->getCharCount(); + $bitBuffer + ->put(QRCode::DATA_NUMBER, 4) + ->put($len, $this->getLengthBitsForVersion($version)) + ; + + $i = 0; + + // encode numeric triplets in 10 bits while($i + 2 < $len){ - $this->bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10); + $bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10); $i += 3; } if($i < $len){ - if($len - $i === 1){ - $this->bitBuffer->put($this->parseInt(substr($this->data, $i, $i + 1)), 4); + // encode 2 remaining numbers in 7 bits + if($len - $i === 2){ + $bitBuffer->put($this->parseInt(substr($this->data, $i, 2)), 7); } - elseif($len - $i === 2){ - $this->bitBuffer->put($this->parseInt(substr($this->data, $i, $i + 2)), 7); + // encode one remaining number in 4 bits + elseif($len - $i === 1){ + $bitBuffer->put($this->parseInt(substr($this->data, $i, 1)), 4); } } diff --git a/src/Data/QRData.php b/src/Data/QRData.php index aea59ae3f..856901963 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -188,7 +188,7 @@ class QRData{ foreach($dataSegments as $segment){ [$class, $data] = $segment; - $this->dataSegments[] = new $class($this->bitBuffer, $data); + $this->dataSegments[] = new $class($data); } $this->version = $this->options->version === QRCode::VERSION_AUTO @@ -271,7 +271,7 @@ class QRData{ $MAX_BITS = $this->maxBitsForEcc[$this->version]; foreach($this->dataSegments as $segment){ - $segment->write($this->version); + $segment->write($this->bitBuffer, $this->version); } // overflow, likely caused due to invalid version setting diff --git a/src/Data/QRDataModeAbstract.php b/src/Data/QRDataModeAbstract.php index 8b652a72e..dadfb52a2 100644 --- a/src/Data/QRDataModeAbstract.php +++ b/src/Data/QRDataModeAbstract.php @@ -18,11 +18,6 @@ use chillerlan\QRCode\Helpers\BitBuffer; */ abstract class QRDataModeAbstract implements QRDataModeInterface{ - /** - * the current data mode: Num, Alphanum, Kanji, Byte - */ - protected int $datamode; - /** * mode length bits for the version breakpoints 1-9, 10-26 and 27-40 * @@ -35,30 +30,17 @@ abstract class QRDataModeAbstract implements QRDataModeInterface{ */ protected string $data; - /** - * a BitBuffer instance - */ - protected BitBuffer $bitBuffer; - /** * QRDataModeAbstract constructor. - * - * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - public function __construct(BitBuffer $bitBuffer, string $data){ - // do we need this here? we check during write anyways. -# if(!static::validateString($data)){ -# throw new QRCodeDataException('invalid data string'); -# } - - $this->bitBuffer = $bitBuffer; + public function __construct(string $data){ $this->data = $data; } /** * returns the character count of the $data string */ - protected function getLength():int{ + protected function getCharCount():int{ return strlen($this->data); } @@ -86,16 +68,4 @@ abstract class QRDataModeAbstract implements QRDataModeInterface{ throw new QRCodeDataException(sprintf('invalid version number: %d', $version)); } - /** - * - */ - protected function writeSegmentHeader(int $version):void{ - - $this->bitBuffer - ->put($this->datamode, 4) - ->put($this->getLength(), $this->getLengthBitsForVersion($version)) - ; - - } - } diff --git a/src/Data/QRDataModeInterface.php b/src/Data/QRDataModeInterface.php index d254b18cc..e24886ca4 100644 --- a/src/Data/QRDataModeInterface.php +++ b/src/Data/QRDataModeInterface.php @@ -12,6 +12,8 @@ namespace chillerlan\QRCode\Data; +use chillerlan\QRCode\Helpers\BitBuffer; + /** * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji) * and holds version information in several constants @@ -38,6 +40,6 @@ interface QRDataModeInterface{ * * @see \chillerlan\QRCode\Data\QRData::writeBitBuffer() */ - public function write(int $version):void; + public function write(BitBuffer $bitBuffer, int $version):void; } From 5a1777f391cf1b4dfbc28ea4ff6f8977326c3982 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 21 Nov 2020 19:01:24 +0100 Subject: [PATCH 03/78] :sparkles: +ECI mode --- src/Data/ECI.php | 93 +++++++++++++++++++++++++++++++++++++++++++++ src/Data/QRData.php | 7 +++- src/QRCode.php | 19 +++++++-- 3 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 src/Data/ECI.php diff --git a/src/Data/ECI.php b/src/Data/ECI.php new file mode 100644 index 000000000..1c0363cf2 --- /dev/null +++ b/src/Data/ECI.php @@ -0,0 +1,93 @@ + + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\Helpers\BitBuffer; +use chillerlan\QRCode\QRCode; + +/** + * Adds an ECI Designator + * + * Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment() + */ +class ECI extends QRDataModeAbstract{ + + public const CP437 = 0; // Code page 437, DOS Latin US + public const ISO_IEC_8859_1_GLI = 1; // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1 + public const CP437_WO_GLI = 2; // An equivalent code table to CP437, without the return-to-GLI 0 logic + public const ISO_IEC_8859_1 = 3; // Latin-1 (Default) + public const ISO_IEC_8859_2 = 4; // Latin-2 + public const ISO_IEC_8859_3 = 5; // Latin-3 + public const ISO_IEC_8859_4 = 6; // Latin-4 + public const ISO_IEC_8859_5 = 7; // Latin/Cyrillic + public const ISO_IEC_8859_6 = 8; // Latin/Arabic + public const ISO_IEC_8859_7 = 9; // Latin/Greek + public const ISO_IEC_8859_8 = 10; // Latin/Hebrew + public const ISO_IEC_8859_9 = 11; // Latin-5 + public const ISO_IEC_8859_10 = 12; // Latin-6 + public const ISO_IEC_8859_11 = 13; // Latin/Thai + // 14 reserved + public const ISO_IEC_8859_13 = 15; // Latin-7 (Baltic Rim) + public const ISO_IEC_8859_14 = 16; // Latin-8 (Celtic) + public const ISO_IEC_8859_15 = 17; // Latin-9 + public const ISO_IEC_8859_16 = 18; // Latin-10 + // 19 reserved + public const SHIFT_JIS = 20; // JIS X 0208 Annex 1 + JIS X 0201 + public const WINDOWS_1250_LATIN_2 = 21; // Superset of Latin-2, Central Europe + public const WINDOWS_1251_CYRILLIC = 22; // Latin/Cyrillic + public const WINDOWS_1252_LATIN_1 = 23; // Superset of Latin-1 + public const WINDOWS_1256_ARABIC = 24; + public const ISO_IEC_10646_UCS_2 = 25; // High order byte first (UTF-16BE) + public const ISO_IEC_10646_UTF_8 = 26; + public const ISO_IEC_646_1991 = 27; // International Reference Version of ISO 7-bit coded character set (US-ASCII) + public const BIG5 = 28; // Big 5 (Taiwan) Chinese Character Set + public const GB18030 = 29; // GB (PRC) Chinese Character Set + public const EUC_KR = 30; // Korean Character Set + + /** + * The current encoding + */ + protected int $encoding; + + /** + * @inheritDoc + */ + public function __construct(BitBuffer $bitBuffer, int $encoding){ + parent::__construct($bitBuffer, ''); + + $this->encoding = $encoding; + } + + /** + * @inheritDoc + */ + public function getLengthInBits():int{ + return 8; + } + + /** + * @inheritDoc + */ + public static function validateString(string $string):bool{ + return true; + } + + /** + * @inheritDoc + */ + public function write(int $version):void{ + $this->bitBuffer->put(QRCode::DATA_ECI, 4); + $this->bitBuffer->put($this->encoding, 8); + } + +} diff --git a/src/Data/QRData.php b/src/Data/QRData.php index 856901963..c4fa5a2f5 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -222,8 +222,11 @@ class QRData{ foreach($this->dataSegments as $segment){ // data length in bits of the current segment +4 bits for each mode descriptor $length += ($segment->getLengthInBits() + $segment->getLengthBits(0) + 4); - // mode length bits margin to the next breakpoint - $margin += ($segment instanceof Byte ? 8 : 2); + + if(!$segment instanceof ECI){ + // mode length bits margin to the next breakpoint + $margin += ($segment instanceof Byte ? 8 : 2); + } } foreach([9, 26, 40] as $breakpoint){ diff --git a/src/QRCode.php b/src/QRCode.php index a2340704f..5750c572c 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -12,9 +12,7 @@ namespace chillerlan\QRCode; -use chillerlan\QRCode\Data\{ - AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRMatrix -}; +use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRMatrix}; use chillerlan\QRCode\Output\{ QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString }; @@ -48,6 +46,8 @@ class QRCode{ public const DATA_BYTE = 0b0100; /** @var int */ public const DATA_KANJI = 0b1000; + /** @var int */ + public const DATA_ECI = 0b0111; // ISO/IEC 18004:2000 Tables 12, 25 @@ -276,7 +276,13 @@ class QRCode{ return Byte::validateString($string); } - protected function addSegment(string $data, string $classname):void{ + /** + * @param string|int $data + * @param string $classname + * + * @return void + */ + protected function addSegment($data, string $classname):void{ $this->dataSegments[] = [$classname, $data]; } @@ -304,4 +310,9 @@ class QRCode{ return $this; } + public function addEciDesignator(int $encoding):QRCode{ + $this->addSegment($encoding, ECI::class); + + return $this; + } } From cbf8a990ca730a34539cb3f00529f684056e5b29 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 21 Nov 2020 20:51:37 +0100 Subject: [PATCH 04/78] :bath: data mode rework --- src/Data/ECI.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Data/ECI.php b/src/Data/ECI.php index 1c0363cf2..d28555235 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -62,8 +62,8 @@ class ECI extends QRDataModeAbstract{ /** * @inheritDoc */ - public function __construct(BitBuffer $bitBuffer, int $encoding){ - parent::__construct($bitBuffer, ''); + public function __construct(int $encoding){ + parent::__construct(''); $this->encoding = $encoding; } @@ -85,9 +85,11 @@ class ECI extends QRDataModeAbstract{ /** * @inheritDoc */ - public function write(int $version):void{ - $this->bitBuffer->put(QRCode::DATA_ECI, 4); - $this->bitBuffer->put($this->encoding, 8); + public function write(BitBuffer $bitBuffer, int $version):void{ + $bitBuffer + ->put(QRCode::DATA_ECI, 4) + ->put($this->encoding, 8) + ; } } From 37a40571e6aa9b353d2b1acbaa3c16c455a82a66 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 21 Nov 2020 21:52:19 +0100 Subject: [PATCH 05/78] :bath: extract Mode --- src/Common/Mode.php | 88 ++++++++++++++++++++++++++++++++ src/Data/AlphaNum.php | 8 +-- src/Data/Byte.php | 8 +-- src/Data/ECI.php | 6 ++- src/Data/Kanji.php | 8 +-- src/Data/Number.php | 8 +-- src/Data/QRData.php | 3 +- src/Data/QRDataModeAbstract.php | 31 ++--------- src/Data/QRDataModeInterface.php | 4 +- src/QRCode.php | 28 +--------- tests/Helpers/BitBufferTest.php | 10 ++-- 11 files changed, 124 insertions(+), 78 deletions(-) create mode 100644 src/Common/Mode.php diff --git a/src/Common/Mode.php b/src/Common/Mode.php new file mode 100644 index 000000000..c53b4f348 --- /dev/null +++ b/src/Common/Mode.php @@ -0,0 +1,88 @@ + + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\Data\{AlphaNum, Byte, Kanji, Number}; +use chillerlan\QRCode\QRCodeException; + +/** + * ISO 18004:2006, 6.4.1, Tables 2 and 3 + */ +class Mode{ + + // ISO/IEC 18004:2000 Table 2 + + /** @var int */ + public const DATA_TERMINATOR = 0b0000; + /** @var int */ + public const DATA_NUMBER = 0b0001; + /** @var int */ + public const DATA_ALPHANUM = 0b0010; + /** @var int */ + public const DATA_BYTE = 0b0100; + /** @var int */ + public const DATA_KANJI = 0b1000; + /** @var int */ + public const DATA_STRCTURED_APPEND = 0b0011; + /** @var int */ + public const DATA_FNC1_FIRST = 0b0101; + /** @var int */ + public const DATA_FNC1_SECOND = 0b1001; + /** @var int */ + public const DATA_ECI = 0b0111; + + /** + * mode length bits for the version breakpoints 1-9, 10-26 and 27-40 + * + * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator + */ + public const LENGTH_BITS = [ + self::DATA_NUMBER => [10, 12, 14], + self::DATA_ALPHANUM => [9, 11, 13], + self::DATA_BYTE => [8, 16, 16], + self::DATA_KANJI => [8, 10, 12], + ]; + + /** + * Map of data mode => interface (detection order) + * + * @var string[] + */ + public const DATA_INTERFACES = [ + Mode::DATA_NUMBER => Number::class, + Mode::DATA_ALPHANUM => AlphaNum::class, + Mode::DATA_KANJI => Kanji::class, + Mode::DATA_BYTE => Byte::class, + ]; + + /** + * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40 + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public static function getLengthBitsForVersion(int $mode, int $version):int{ + + if(!isset(self::LENGTH_BITS[$mode])){ + throw new QRCodeException('invalid mode given'); + } + + foreach([9, 26, 40] as $key => $breakpoint){ + if($version <= $breakpoint){ + return self::LENGTH_BITS[$mode][$key]; + } + } + + throw new QRCodeException(sprintf('invalid version number: %d', $version)); + } + +} diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index a78d8fec3..8252b17d2 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\Common\Mode; use function ceil, ord, sprintf, str_split; @@ -39,7 +39,7 @@ final class AlphaNum extends QRDataModeAbstract{ '+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44, ]; - protected array $lengthBits = [9, 11, 13]; + protected int $datamode = Mode::DATA_ALPHANUM; /** * @inheritdoc @@ -69,8 +69,8 @@ final class AlphaNum extends QRDataModeAbstract{ $len = $this->getCharCount(); $bitBuffer - ->put(QRCode::DATA_ALPHANUM, 4) - ->put($len, $this->getLengthBitsForVersion($version)) + ->put($this->datamode, 4) + ->put($len, Mode::getLengthBitsForVersion($this->datamode, $version)) ; // encode 2 characters in 11 bits diff --git a/src/Data/Byte.php b/src/Data/Byte.php index 738ec7d32..0a57590cc 100644 --- a/src/Data/Byte.php +++ b/src/Data/Byte.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\Common\Mode; use function ord; @@ -25,7 +25,7 @@ use function ord; */ final class Byte extends QRDataModeAbstract{ - protected array $lengthBits = [8, 16, 16]; + protected int $datamode = Mode::DATA_BYTE; /** * @inheritdoc @@ -48,8 +48,8 @@ final class Byte extends QRDataModeAbstract{ $len = $this->getCharCount(); $bitBuffer - ->put(QRCode::DATA_BYTE, 4) - ->put($len, $this->getLengthBitsForVersion($version)) + ->put($this->datamode, 4) + ->put($len, Mode::getLengthBitsForVersion($this->datamode, $version)) ; $i = 0; diff --git a/src/Data/ECI.php b/src/Data/ECI.php index d28555235..54a6867ec 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\Common\Mode; /** * Adds an ECI Designator @@ -59,6 +59,8 @@ class ECI extends QRDataModeAbstract{ */ protected int $encoding; + protected int $datamode = Mode::DATA_ECI; + /** * @inheritDoc */ @@ -87,7 +89,7 @@ class ECI extends QRDataModeAbstract{ */ public function write(BitBuffer $bitBuffer, int $version):void{ $bitBuffer - ->put(QRCode::DATA_ECI, 4) + ->put($this->datamode, 4) ->put($this->encoding, 8) ; } diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index 676fe2a93..8cb87c4f6 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\Common\Mode; use function mb_convert_encoding, mb_detect_encoding, mb_strlen, ord, sprintf, strlen; @@ -25,7 +25,7 @@ use function mb_convert_encoding, mb_detect_encoding, mb_strlen, ord, sprintf, s */ final class Kanji extends QRDataModeAbstract{ - protected array $lengthBits = [8, 10, 12]; + protected int $datamode = Mode::DATA_KANJI; public function __construct(string $data){ parent::__construct($data); @@ -76,8 +76,8 @@ final class Kanji extends QRDataModeAbstract{ public function write(BitBuffer $bitBuffer, int $version):void{ $bitBuffer - ->put(QRCode::DATA_KANJI, 4) - ->put($this->getCharCount(), $this->getLengthBitsForVersion($version)) + ->put($this->datamode, 4) + ->put($this->getCharCount(), Mode::getLengthBitsForVersion($this->datamode, $version)) ; $len = strlen($this->data); diff --git a/src/Data/Number.php b/src/Data/Number.php index 0fe616ff9..cbcd78cd7 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\Common\Mode; use function ceil, ord, sprintf, str_split, substr; @@ -32,7 +32,7 @@ final class Number extends QRDataModeAbstract{ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, ]; - protected array $lengthBits = [10, 12, 14]; + protected int $datamode = Mode::DATA_NUMBER; /** * @inheritdoc @@ -62,8 +62,8 @@ final class Number extends QRDataModeAbstract{ $len = $this->getCharCount(); $bitBuffer - ->put(QRCode::DATA_NUMBER, 4) - ->put($len, $this->getLengthBitsForVersion($version)) + ->put($this->datamode, 4) + ->put($len, Mode::getLengthBitsForVersion($this->datamode, $version)) ; $i = 0; diff --git a/src/Data/QRData.php b/src/Data/QRData.php index c4fa5a2f5..3e9726fe0 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -14,6 +14,7 @@ namespace chillerlan\QRCode\Data; 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; @@ -221,7 +222,7 @@ class QRData{ foreach($this->dataSegments as $segment){ // data length in bits of the current segment +4 bits for each mode descriptor - $length += ($segment->getLengthInBits() + $segment->getLengthBits(0) + 4); + $length += ($segment->getLengthInBits() + Mode::LENGTH_BITS[$segment->getDataMode()][0] + 4); if(!$segment instanceof ECI){ // mode length bits margin to the next breakpoint diff --git a/src/Data/QRDataModeAbstract.php b/src/Data/QRDataModeAbstract.php index dadfb52a2..062707d7d 100644 --- a/src/Data/QRDataModeAbstract.php +++ b/src/Data/QRDataModeAbstract.php @@ -12,18 +12,14 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Helpers\BitBuffer; - /** */ abstract class QRDataModeAbstract implements QRDataModeInterface{ /** - * mode length bits for the version breakpoints 1-9, 10-26 and 27-40 - * - * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator + * the current data mode: Num, Alphanum, Kanji, Byte */ - protected array $lengthBits = [0, 0, 0]; + protected int $datamode; /** * The data to write @@ -34,7 +30,7 @@ abstract class QRDataModeAbstract implements QRDataModeInterface{ * QRDataModeAbstract constructor. */ public function __construct(string $data){ - $this->data = $data; + $this->data = $data; } /** @@ -47,25 +43,8 @@ abstract class QRDataModeAbstract implements QRDataModeInterface{ /** * @inheritDoc */ - public function getLengthBits(int $k):int{ - return $this->lengthBits[$k] ?? 0; - } - - /** - * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40 - * - * @throws \chillerlan\QRCode\Data\QRCodeDataException - * @codeCoverageIgnore - */ - protected function getLengthBitsForVersion(int $version):int{ - - foreach([9, 26, 40] as $key => $breakpoint){ - if($version <= $breakpoint){ - return $this->getLengthBits($key); - } - } - - throw new QRCodeDataException(sprintf('invalid version number: %d', $version)); + public function getDataMode():int{ + return $this->datamode; } } diff --git a/src/Data/QRDataModeInterface.php b/src/Data/QRDataModeInterface.php index e24886ca4..91839a442 100644 --- a/src/Data/QRDataModeInterface.php +++ b/src/Data/QRDataModeInterface.php @@ -21,9 +21,9 @@ use chillerlan\QRCode\Helpers\BitBuffer; interface QRDataModeInterface{ /** - * returns the length bits for the given breakpoint [0,1,2] + * returns the current data mode constant */ - public function getLengthBits(int $k):int; + public function getDataMode():int; /** * retruns the length in bits of the data string diff --git a/src/QRCode.php b/src/QRCode.php index 5750c572c..5a29d5f22 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\Mode; use chillerlan\QRCode\Output\{ QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString }; @@ -36,19 +37,6 @@ class QRCode{ /** @var int */ public const MASK_PATTERN_AUTO = -1; - // ISO/IEC 18004:2000 Table 2 - - /** @var int */ - public const DATA_NUMBER = 0b0001; - /** @var int */ - public const DATA_ALPHANUM = 0b0010; - /** @var int */ - public const DATA_BYTE = 0b0100; - /** @var int */ - public const DATA_KANJI = 0b1000; - /** @var int */ - public const DATA_ECI = 0b0111; - // ISO/IEC 18004:2000 Tables 12, 25 /** @var int */ @@ -124,18 +112,6 @@ class QRCode{ ], ]; - /** - * Map of data mode => interface (detection order) - * - * @var string[] - */ - protected const DATA_INTERFACES = [ - self::DATA_NUMBER => Number::class, - self::DATA_ALPHANUM => AlphaNum::class, - self::DATA_KANJI => Kanji::class, - self::DATA_BYTE => Byte::class, - ]; - /** * A collection of one or more data segments of [classname, data] to write * @@ -175,7 +151,7 @@ class QRCode{ if($data !== null){ /** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */ - foreach($this::DATA_INTERFACES as $dataInterface){ + foreach(Mode::DATA_INTERFACES as $dataInterface){ if($dataInterface::validateString($data)){ $this->addSegment($data, $dataInterface); diff --git a/tests/Helpers/BitBufferTest.php b/tests/Helpers/BitBufferTest.php index e9479a5c6..886ffbb1c 100644 --- a/tests/Helpers/BitBufferTest.php +++ b/tests/Helpers/BitBufferTest.php @@ -12,8 +12,8 @@ namespace chillerlan\QRCodeTest\Helpers; -use chillerlan\QRCode\QRCode; use chillerlan\QRCode\Helpers\BitBuffer; +use chillerlan\QRCode\Common\Mode; use PHPUnit\Framework\TestCase; /** @@ -29,10 +29,10 @@ final class BitBufferTest extends TestCase{ public function bitProvider():array{ return [ - 'number' => [QRCode::DATA_NUMBER, 16], - 'alphanum' => [QRCode::DATA_ALPHANUM, 32], - 'byte' => [QRCode::DATA_BYTE, 64], - 'kanji' => [QRCode::DATA_KANJI, 128], + 'number' => [Mode::DATA_NUMBER, 16], + 'alphanum' => [Mode::DATA_ALPHANUM, 32], + 'byte' => [Mode::DATA_BYTE, 64], + 'kanji' => [Mode::DATA_KANJI, 128], ]; } From 1f1a363d9dbf8e5fe51471a91b80a60bf7337971 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 22 Nov 2020 00:35:58 +0100 Subject: [PATCH 06/78] :bath: extract Mode --- src/Common/Mode.php | 7 +++++++ src/Data/QRData.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Common/Mode.php b/src/Common/Mode.php index c53b4f348..d36e84beb 100644 --- a/src/Common/Mode.php +++ b/src/Common/Mode.php @@ -85,4 +85,11 @@ class Mode{ throw new QRCodeException(sprintf('invalid version number: %d', $version)); } + /** + * returns the array of length bits for the given mode + */ + public static function getLengthBitsForMode(int $mode):array{ + return self::LENGTH_BITS[$mode]; + } + } diff --git a/src/Data/QRData.php b/src/Data/QRData.php index 3e9726fe0..a3972b1ec 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -222,7 +222,7 @@ class QRData{ foreach($this->dataSegments as $segment){ // data length in bits of the current segment +4 bits for each mode descriptor - $length += ($segment->getLengthInBits() + Mode::LENGTH_BITS[$segment->getDataMode()][0] + 4); + $length += ($segment->getLengthInBits() + Mode::getLengthBitsForMode($segment->getDataMode())[0] + 4); if(!$segment instanceof ECI){ // mode length bits margin to the next breakpoint From 6d8705b19bd6e93cbc7bef792398997b67932ece Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 22 Nov 2020 00:47:13 +0100 Subject: [PATCH 07/78] :bath: extract Version --- src/Common/Version.php | 209 ++++++++++++++++++++++++++++++++++++ src/Data/QRData.php | 76 +++---------- src/Data/QRMatrix.php | 132 ++++------------------- src/QRCode.php | 2 +- tests/Data/QRMatrixTest.php | 17 +-- 5 files changed, 245 insertions(+), 191 deletions(-) create mode 100644 src/Common/Version.php 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){ From d58c2044f1834ce29efd1c7c5caec0d989a5ee39 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 22 Nov 2020 03:04:42 +0100 Subject: [PATCH 08/78] :bath: extract EccLevel --- src/Common/EccLevel.php | 254 ++++++++++++++++++++++++++++++++++ src/Common/Version.php | 56 -------- src/Data/QRData.php | 59 +------- src/Data/QRMatrix.php | 87 ++---------- src/QRCode.php | 27 ---- src/QROptionsTrait.php | 6 +- tests/Data/QRMatrixTest.php | 28 ++-- tests/Output/QRStringTest.php | 4 +- 8 files changed, 290 insertions(+), 231 deletions(-) create mode 100644 src/Common/EccLevel.php diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php new file mode 100644 index 000000000..4091843ed --- /dev/null +++ b/src/Common/EccLevel.php @@ -0,0 +1,254 @@ + + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; + +/** + */ +class EccLevel{ + + // ISO/IEC 18004:2000 Tables 12, 25 + + /** @var int */ + public const L = 0b01; // 7%. + /** @var int */ + public const M = 0b00; // 15%. + /** @var int */ + public const Q = 0b11; // 25%. + /** @var int */ + public const H = 0b10; // 30%. + + /** + * References to the keys of the following tables: + * + * @see \chillerlan\QRCode\Common\Version::MAX_BITS + * @see \chillerlan\QRCode\Common\EccLevel::RSBLOCKS + * @see \chillerlan\QRCode\Data\QRMatrix::formatPattern + * + * @var int[] + */ + public const MODES = [ + self::L => 0, + self::M => 1, + self::Q => 2, + self::H => 3, + ]; + + /** + * ISO/IEC 18004:2000 Tables 13-22 + * + * @see http://www.thonky.com/qr-code-tutorial/error-correction-table + * + * @var int [][][] + */ + const RSBLOCKS = [ + 1 => [[ 1, 0, 26, 19], [ 1, 0, 26, 16], [ 1, 0, 26, 13], [ 1, 0, 26, 9]], + 2 => [[ 1, 0, 44, 34], [ 1, 0, 44, 28], [ 1, 0, 44, 22], [ 1, 0, 44, 16]], + 3 => [[ 1, 0, 70, 55], [ 1, 0, 70, 44], [ 2, 0, 35, 17], [ 2, 0, 35, 13]], + 4 => [[ 1, 0, 100, 80], [ 2, 0, 50, 32], [ 2, 0, 50, 24], [ 4, 0, 25, 9]], + 5 => [[ 1, 0, 134, 108], [ 2, 0, 67, 43], [ 2, 2, 33, 15], [ 2, 2, 33, 11]], + 6 => [[ 2, 0, 86, 68], [ 4, 0, 43, 27], [ 4, 0, 43, 19], [ 4, 0, 43, 15]], + 7 => [[ 2, 0, 98, 78], [ 4, 0, 49, 31], [ 2, 4, 32, 14], [ 4, 1, 39, 13]], + 8 => [[ 2, 0, 121, 97], [ 2, 2, 60, 38], [ 4, 2, 40, 18], [ 4, 2, 40, 14]], + 9 => [[ 2, 0, 146, 116], [ 3, 2, 58, 36], [ 4, 4, 36, 16], [ 4, 4, 36, 12]], + 10 => [[ 2, 2, 86, 68], [ 4, 1, 69, 43], [ 6, 2, 43, 19], [ 6, 2, 43, 15]], + 11 => [[ 4, 0, 101, 81], [ 1, 4, 80, 50], [ 4, 4, 50, 22], [ 3, 8, 36, 12]], + 12 => [[ 2, 2, 116, 92], [ 6, 2, 58, 36], [ 4, 6, 46, 20], [ 7, 4, 42, 14]], + 13 => [[ 4, 0, 133, 107], [ 8, 1, 59, 37], [ 8, 4, 44, 20], [12, 4, 33, 11]], + 14 => [[ 3, 1, 145, 115], [ 4, 5, 64, 40], [11, 5, 36, 16], [11, 5, 36, 12]], + 15 => [[ 5, 1, 109, 87], [ 5, 5, 65, 41], [ 5, 7, 54, 24], [11, 7, 36, 12]], + 16 => [[ 5, 1, 122, 98], [ 7, 3, 73, 45], [15, 2, 43, 19], [ 3, 13, 45, 15]], + 17 => [[ 1, 5, 135, 107], [10, 1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]], + 18 => [[ 5, 1, 150, 120], [ 9, 4, 69, 43], [17, 1, 50, 22], [ 2, 19, 42, 14]], + 19 => [[ 3, 4, 141, 113], [ 3, 11, 70, 44], [17, 4, 47, 21], [ 9, 16, 39, 13]], + 20 => [[ 3, 5, 135, 107], [ 3, 13, 67, 41], [15, 5, 54, 24], [15, 10, 43, 15]], + 21 => [[ 4, 4, 144, 116], [17, 0, 68, 42], [17, 6, 50, 22], [19, 6, 46, 16]], + 22 => [[ 2, 7, 139, 111], [17, 0, 74, 46], [ 7, 16, 54, 24], [34, 0, 37, 13]], + 23 => [[ 4, 5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]], + 24 => [[ 6, 4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30, 2, 46, 16]], + 25 => [[ 8, 4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]], + 26 => [[10, 2, 142, 114], [19, 4, 74, 46], [28, 6, 50, 22], [33, 4, 46, 16]], + 27 => [[ 8, 4, 152, 122], [22, 3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]], + 28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]], + 29 => [[ 7, 7, 146, 116], [21, 7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]], + 30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]], + 31 => [[13, 3, 145, 115], [ 2, 29, 74, 46], [42, 1, 54, 24], [23, 28, 45, 15]], + 32 => [[17, 0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]], + 33 => [[17, 1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]], + 34 => [[13, 6, 145, 115], [14, 23, 74, 46], [44, 7, 54, 24], [59, 1, 46, 16]], + 35 => [[12, 7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]], + 36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]], + 37 => [[17, 4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]], + 38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]], + 39 => [[20, 4, 147, 117], [40, 7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]], + 40 => [[19, 6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]], + ]; + + /** + * 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 Section 8.9 - Format Information + * + * ECC level -> mask pattern + * + * @var int[][] + */ + protected const formatPattern = [ + [ // L + 0b111011111000100, + 0b111001011110011, + 0b111110110101010, + 0b111100010011101, + 0b110011000101111, + 0b110001100011000, + 0b110110001000001, + 0b110100101110110, + ], + [ // M + 0b101010000010010, + 0b101000100100101, + 0b101111001111100, + 0b101101101001011, + 0b100010111111001, + 0b100000011001110, + 0b100111110010111, + 0b100101010100000, + ], + [ // Q + 0b011010101011111, + 0b011000001101000, + 0b011111100110001, + 0b011101000000110, + 0b010010010110100, + 0b010000110000011, + 0b010111011011010, + 0b010101111101101, + ], + [ // H + 0b001011010001001, + 0b001001110111110, + 0b001110011100111, + 0b001100111010000, + 0b000011101100010, + 0b000001001010101, + 0b000110100001100, + 0b000100000111011, + ], + ]; + + private int $eccLevel; + + /** + * @param int $eccLevel containing the two bits encoding a QR Code's error correction level + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function __construct(int $eccLevel){ + + if((0b11 & $eccLevel) !== $eccLevel){ + throw new QRCodeException('invalid ECC level'); + } + + $this->eccLevel = $eccLevel; + } + + public function getOrdinal():int{ + return $this->eccLevel; + } + + /** + * returns ECC block information for the given $version and $eccLevel + * + * @return int[] + * @throws \chillerlan\QRCode\QRCodeException + */ + public function getRSBlocks(int $version):array{ + + if($version < 1 || $version > 40){ + throw new QRCodeException('invalid version'); + } + + return self::RSBLOCKS[$version][self::MODES[$this->eccLevel]]; + } + + /** + * returns the format pattern for the given $eccLevel and $maskPattern + * + * @return int + * @throws \chillerlan\QRCode\QRCodeException + */ + public function getformatPattern(int $maskPattern):int{ + + if((0b111 & $maskPattern) !== $maskPattern){ + throw new QRCodeException('invalid mask pattern'); + } + + return self::formatPattern[self::MODES[$this->eccLevel]][$maskPattern]; + } + + public function getMaxBits():array{ + return array_combine( + array_keys(self::MAX_BITS), + array_column(self::MAX_BITS, self::MODES[$this->eccLevel]) + ); + } + +} diff --git a/src/Common/Version.php b/src/Common/Version.php index 4fd7bf6b2..9c75914b1 100644 --- a/src/Common/Version.php +++ b/src/Common/Version.php @@ -21,55 +21,6 @@ use function array_column, array_combine, array_keys; */ 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 * @@ -199,11 +150,4 @@ class Version{ 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 16b6dcf40..c934c063b 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{Mode, Version}; +use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial}; use chillerlan\Settings\SettingsContainerInterface; @@ -24,54 +24,6 @@ use function array_fill, array_merge, count, max, range, sprintf; */ class QRData{ - /** - * @see http://www.thonky.com/qr-code-tutorial/error-correction-table - * - * @var int [][][] - */ - const RSBLOCKS = [ - 1 => [[ 1, 0, 26, 19], [ 1, 0, 26, 16], [ 1, 0, 26, 13], [ 1, 0, 26, 9]], - 2 => [[ 1, 0, 44, 34], [ 1, 0, 44, 28], [ 1, 0, 44, 22], [ 1, 0, 44, 16]], - 3 => [[ 1, 0, 70, 55], [ 1, 0, 70, 44], [ 2, 0, 35, 17], [ 2, 0, 35, 13]], - 4 => [[ 1, 0, 100, 80], [ 2, 0, 50, 32], [ 2, 0, 50, 24], [ 4, 0, 25, 9]], - 5 => [[ 1, 0, 134, 108], [ 2, 0, 67, 43], [ 2, 2, 33, 15], [ 2, 2, 33, 11]], - 6 => [[ 2, 0, 86, 68], [ 4, 0, 43, 27], [ 4, 0, 43, 19], [ 4, 0, 43, 15]], - 7 => [[ 2, 0, 98, 78], [ 4, 0, 49, 31], [ 2, 4, 32, 14], [ 4, 1, 39, 13]], - 8 => [[ 2, 0, 121, 97], [ 2, 2, 60, 38], [ 4, 2, 40, 18], [ 4, 2, 40, 14]], - 9 => [[ 2, 0, 146, 116], [ 3, 2, 58, 36], [ 4, 4, 36, 16], [ 4, 4, 36, 12]], - 10 => [[ 2, 2, 86, 68], [ 4, 1, 69, 43], [ 6, 2, 43, 19], [ 6, 2, 43, 15]], - 11 => [[ 4, 0, 101, 81], [ 1, 4, 80, 50], [ 4, 4, 50, 22], [ 3, 8, 36, 12]], - 12 => [[ 2, 2, 116, 92], [ 6, 2, 58, 36], [ 4, 6, 46, 20], [ 7, 4, 42, 14]], - 13 => [[ 4, 0, 133, 107], [ 8, 1, 59, 37], [ 8, 4, 44, 20], [12, 4, 33, 11]], - 14 => [[ 3, 1, 145, 115], [ 4, 5, 64, 40], [11, 5, 36, 16], [11, 5, 36, 12]], - 15 => [[ 5, 1, 109, 87], [ 5, 5, 65, 41], [ 5, 7, 54, 24], [11, 7, 36, 12]], - 16 => [[ 5, 1, 122, 98], [ 7, 3, 73, 45], [15, 2, 43, 19], [ 3, 13, 45, 15]], - 17 => [[ 1, 5, 135, 107], [10, 1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]], - 18 => [[ 5, 1, 150, 120], [ 9, 4, 69, 43], [17, 1, 50, 22], [ 2, 19, 42, 14]], - 19 => [[ 3, 4, 141, 113], [ 3, 11, 70, 44], [17, 4, 47, 21], [ 9, 16, 39, 13]], - 20 => [[ 3, 5, 135, 107], [ 3, 13, 67, 41], [15, 5, 54, 24], [15, 10, 43, 15]], - 21 => [[ 4, 4, 144, 116], [17, 0, 68, 42], [17, 6, 50, 22], [19, 6, 46, 16]], - 22 => [[ 2, 7, 139, 111], [17, 0, 74, 46], [ 7, 16, 54, 24], [34, 0, 37, 13]], - 23 => [[ 4, 5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]], - 24 => [[ 6, 4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30, 2, 46, 16]], - 25 => [[ 8, 4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]], - 26 => [[10, 2, 142, 114], [19, 4, 74, 46], [28, 6, 50, 22], [33, 4, 46, 16]], - 27 => [[ 8, 4, 152, 122], [22, 3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]], - 28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]], - 29 => [[ 7, 7, 146, 116], [21, 7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]], - 30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]], - 31 => [[13, 3, 145, 115], [ 2, 29, 74, 46], [42, 1, 54, 24], [23, 28, 45, 15]], - 32 => [[17, 0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]], - 33 => [[17, 1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]], - 34 => [[13, 6, 145, 115], [14, 23, 74, 46], [44, 7, 54, 24], [59, 1, 46, 16]], - 35 => [[12, 7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]], - 36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]], - 37 => [[17, 4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]], - 38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]], - 39 => [[20, 4, 147, 117], [40, 7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]], - 40 => [[19, 6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]], - ]; - /** * current QR Code version */ @@ -111,6 +63,8 @@ class QRData{ */ protected BitBuffer $bitBuffer; + protected EccLevel $eccLevel; + /** * QRData constructor. * @@ -120,7 +74,8 @@ class QRData{ public function __construct(SettingsContainerInterface $options, array $dataSegments = null){ $this->options = $options; $this->bitBuffer = new BitBuffer; - $this->maxBitsForEcc = Version::getMaxBitsForEcc($this->options->eccLevel); + $this->eccLevel = new EccLevel($this->options->eccLevel); + $this->maxBitsForEcc = $this->eccLevel->getMaxBits(); if(!empty($dataSegments)){ $this->setData($dataSegments); @@ -154,7 +109,7 @@ class QRData{ * returns a fresh matrix object with the data written for the given $maskPattern */ public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{ - return (new QRMatrix($this->version, $this->options->eccLevel)) + return (new QRMatrix($this->version, $this->eccLevel)) ->init($maskPattern, $test) ->mapData($this->maskECC(), $maskPattern) ; @@ -270,7 +225,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->getVersionNumber()][QRCode::ECC_MODES[$this->options->eccLevel]]; + [$l1, $l2, $b1, $b2] = $this->eccLevel->getRSBlocks($this->version->getVersionNumber()); $rsBlocks = array_fill(0, $l1, [$b1, $b2]); $rsCount = $l1 + $l2; diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index 742bc8f71..d4cbae5b4 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -12,11 +12,11 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\Version; +use chillerlan\QRCode\Common\{EccLevel, Version}; use chillerlan\QRCode\QRCode; use Closure; -use function array_fill, array_key_exists, array_push, array_unshift, count, floor, max, min, range; +use function array_fill, array_push, array_unshift, count, floor, max, min, range; /** * Holds a numerical representation of the final QR Code; @@ -53,61 +53,6 @@ final class QRMatrix{ /** @var int */ public const M_TEST = 0xff; - /** - * ISO/IEC 18004:2000 Section 8.9 - Format Information - * - * ECC level -> mask pattern - * - * @var int[][] - */ - protected const formatPattern = [ - [ // L - 0b111011111000100, - 0b111001011110011, - 0b111110110101010, - 0b111100010011101, - 0b110011000101111, - 0b110001100011000, - 0b110110001000001, - 0b110100101110110, - ], - [ // M - 0b101010000010010, - 0b101000100100101, - 0b101111001111100, - 0b101101101001011, - 0b100010111111001, - 0b100000011001110, - 0b100111110010111, - 0b100101010100000, - ], - [ // Q - 0b011010101011111, - 0b011000001101000, - 0b011111100110001, - 0b011101000000110, - 0b010010010110100, - 0b010000110000011, - 0b010111011011010, - 0b010101111101101, - ], - [ // H - 0b001011010001001, - 0b001001110111110, - 0b001110011100111, - 0b001100111010000, - 0b000011101100010, - 0b000001001010101, - 0b000110100001100, - 0b000100000111011, - ], - ]; - - /** - * the current ECC level - */ - protected int $eclevel; - /** * the used mask pattern, set via QRMatrix::mapData() */ @@ -125,6 +70,11 @@ final class QRMatrix{ */ protected array $matrix; + /** + * the current ECC level + */ + protected EccLevel $ecclevel; + /** * a Version instance */ @@ -132,17 +82,10 @@ final class QRMatrix{ /** * QRMatrix constructor. - * - * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - public function __construct(Version $version, int $eclevel){ - - if(!array_key_exists($eclevel, QRCode::ECC_MODES)){ - throw new QRCodeDataException('invalid ecc level'); - } - + public function __construct(Version $version, EccLevel $eclevel){ $this->version = $version; - $this->eclevel = $eclevel; + $this->ecclevel = $eclevel; $this->moduleCount = $this->version->getDimension(); $this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL)); } @@ -189,15 +132,15 @@ final class QRMatrix{ /** * Returns the current version number */ - public function version():int{ - return $this->version->getVersionNumber(); + public function version():Version{ + return $this->version; } /** * Returns the current ECC level */ - public function eccLevel():int{ - return $this->eclevel; + public function eccLevel():EccLevel{ + return $this->ecclevel; } /** @@ -406,7 +349,7 @@ final class QRMatrix{ * ISO/IEC 18004:2000 Section 8.9 */ public function setFormatInfo(int $maskPattern, bool $test = null):QRMatrix{ - $bits = $this::formatPattern[QRCode::ECC_MODES[$this->eclevel]][$maskPattern] ?? 0; + $bits = $this->ecclevel->getformatPattern($maskPattern); for($i = 0; $i < 15; $i++){ $v = !$test && (($bits >> $i) & 1) === 1; @@ -495,7 +438,7 @@ final class QRMatrix{ public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):QRMatrix{ // for logos we operate in ECC H (30%) only - if($this->eclevel !== QRCode::ECC_H){ + if($this->ecclevel->getOrdinal() !== EccLevel::H){ throw new QRCodeDataException('ECC level "H" required to add logo space'); } diff --git a/src/QRCode.php b/src/QRCode.php index 5de32ce4a..b8dffce50 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -37,33 +37,6 @@ class QRCode{ /** @var int */ public const MASK_PATTERN_AUTO = -1; - // ISO/IEC 18004:2000 Tables 12, 25 - - /** @var int */ - public const ECC_L = 0b01; // 7%. - /** @var int */ - public const ECC_M = 0b00; // 15%. - /** @var int */ - public const ECC_Q = 0b11; // 25%. - /** @var int */ - public const ECC_H = 0b10; // 30%. - - /** - * References to the keys of the following tables: - * - * @see \chillerlan\QRCode\Common\Version::MAX_BITS - * @see \chillerlan\QRCode\Data\QRData::RSBLOCKS - * @see \chillerlan\QRCode\Data\QRMatrix::formatPattern - * - * @var int[] - */ - public const ECC_MODES = [ - self::ECC_L => 0, - self::ECC_M => 1, - self::ECC_Q => 2, - self::ECC_H => 3, - ]; - /** @var string */ public const OUTPUT_MARKUP_HTML = 'html'; /** @var string */ diff --git a/src/QROptionsTrait.php b/src/QROptionsTrait.php index 204ec04bc..0bae75f02 100644 --- a/src/QROptionsTrait.php +++ b/src/QROptionsTrait.php @@ -14,6 +14,8 @@ namespace chillerlan\QRCode; +use chillerlan\QRCode\Common\EccLevel; + use function array_values, count, in_array, is_numeric, max, min, sprintf, strtolower; /** @@ -50,7 +52,7 @@ trait QROptionsTrait{ * - Q => 25% * - H => 30% */ - protected int $eccLevel = QRCode::ECC_L; + protected int $eccLevel = EccLevel::L; /** * Mask Pattern to use @@ -251,7 +253,7 @@ trait QROptionsTrait{ */ protected function set_eccLevel(int $eccLevel):void{ - if(!isset(QRCode::ECC_MODES[$eccLevel])){ + if(!isset(EccLevel::MODES[$eccLevel])){ throw new QRCodeException(sprintf('Invalid error correct level: %s', $eccLevel)); } diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index 77d9813d4..bcf6312c0 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -12,12 +12,10 @@ namespace chillerlan\QRCodeTest\Data; -use chillerlan\QRCode\Common\Version; -use chillerlan\QRCode\QRCode; -use chillerlan\QRCode\QROptions; +use chillerlan\QRCode\Common\{EccLevel, Version}; +use chillerlan\QRCode\{QRCode, QROptions}; use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix}; use PHPUnit\Framework\TestCase; -use ReflectionClass; /** * Tests the QRMatix class @@ -44,7 +42,7 @@ final class QRMatrixTest extends TestCase{ * @internal */ protected function getMatrix(int $version):QRMatrix{ - return new QRMatrix(new Version($version), QRCode::ECC_L); + return new QRMatrix(new Version($version), new EccLevel(EccLevel::L)); } /** @@ -54,16 +52,6 @@ final class QRMatrixTest extends TestCase{ $this::assertInstanceOf(QRMatrix::class, $this->matrix); } - /** - * Tests if an exception is thrown when an invalid ECC level was given - */ - public function testInvalidEccException():void{ - $this->expectException(QRCodeDataException::class); - $this->expectExceptionMessage('invalid ecc level'); - - $this->matrix = new QRMatrix(new Version(1), 42); - } - /** * Tests if size() returns the actual matrix size/count */ @@ -75,14 +63,14 @@ final class QRMatrixTest extends TestCase{ * Tests if version() returns the current (given) version */ public function testVersion():void{ - $this::assertSame($this::version, $this->matrix->version()); + $this::assertSame($this::version, $this->matrix->version()->getVersionNumber()); } /** * Tests if eccLevel() returns the current (given) ECC level */ public function testECC():void{ - $this::assertSame(QRCode::ECC_L, $this->matrix->eccLevel()); + $this::assertSame(EccLevel::L, $this->matrix->eccLevel()->getOrdinal()); } /** @@ -303,7 +291,7 @@ final class QRMatrixTest extends TestCase{ public function testSetLogoSpaceOrientation():void{ $o = new QROptions; $o->version = 10; - $o->eccLevel = QRCode::ECC_H; + $o->eccLevel = EccLevel::H; $o->addQuietzone = false; $matrix = (new QRCode($o))->addByteSegment('testdata')->getMatrix(); @@ -322,7 +310,7 @@ final class QRMatrixTest extends TestCase{ public function testSetLogoSpacePosition():void{ $o = new QROptions; $o->version = 10; - $o->eccLevel = QRCode::ECC_H; + $o->eccLevel = EccLevel::H; $o->addQuietzone = true; $o->quietzoneSize = 10; @@ -360,7 +348,7 @@ final class QRMatrixTest extends TestCase{ $o = new QROptions; $o->version = 5; - $o->eccLevel = QRCode::ECC_H; + $o->eccLevel = EccLevel::H; (new QRCode($o))->addByteSegment('testdata')->getMatrix()->setLogoSpace(50, 50); } diff --git a/tests/Output/QRStringTest.php b/tests/Output/QRStringTest.php index c41d109be..9206c8f10 100644 --- a/tests/Output/QRStringTest.php +++ b/tests/Output/QRStringTest.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCodeTest\Output; use chillerlan\QRCodeExamples\MyCustomOutput; -use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\{Common\EccLevel, QRCode, QROptions}; use chillerlan\QRCode\Output\{QROutputInterface, QRString}; /** @@ -63,7 +63,7 @@ class QRStringTest extends QROutputTestAbstract{ */ public function testCustomOutput():void{ $this->options->version = 5; - $this->options->eccLevel = QRCode::ECC_L; + $this->options->eccLevel = EccLevel::L; $this->options->outputType = QRCode::OUTPUT_CUSTOM; $this->options->outputInterface = MyCustomOutput::class; From 4aec4d2c3cc78beb46d353b9aa8aab384bb46b9f Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 22 Nov 2020 10:12:20 +0100 Subject: [PATCH 09/78] :bath: fix examples --- examples/MyCustomOutput.php | 2 +- examples/QRImageWithLogo.php | 10 +++++----- examples/custom_output.php | 5 +++-- examples/fpdf.php | 3 ++- examples/html.php | 7 ++++--- examples/image.php | 3 ++- examples/imageWithLogo.php | 15 ++++++++------- examples/imagick.php | 3 ++- examples/svg.php | 15 ++++++++------- examples/text.php | 5 +++-- 10 files changed, 38 insertions(+), 30 deletions(-) diff --git a/examples/MyCustomOutput.php b/examples/MyCustomOutput.php index 3664989b8..c48dcf912 100644 --- a/examples/MyCustomOutput.php +++ b/examples/MyCustomOutput.php @@ -20,7 +20,7 @@ class MyCustomOutput extends QROutputAbstract{ // TODO: Implement setModuleValues() method. } - public function dump(string $file = null){ + public function dump(string $file = null):string{ $output = ''; diff --git a/examples/QRImageWithLogo.php b/examples/QRImageWithLogo.php index f9d94ae34..9ba06815a 100644 --- a/examples/QRImageWithLogo.php +++ b/examples/QRImageWithLogo.php @@ -41,9 +41,9 @@ class QRImageWithLogo extends QRImage{ } $this->matrix->setLogoSpace( - $this->options->logoWidth, - $this->options->logoHeight - // not utilizing the position here + $this->options->logoSpaceWidth, + $this->options->logoSpaceHeight + // not utilizing the position here ); // there's no need to save the result of dump() into $this->image here @@ -56,8 +56,8 @@ class QRImageWithLogo extends QRImage{ $h = imagesy($im); // set new logo size, leave a border of 1 module - $lw = ($this->options->logoWidth - 2) * $this->options->scale; - $lh = ($this->options->logoHeight - 2) * $this->options->scale; + $lw = ($this->options->logoSpaceWidth - 2) * $this->options->scale; + $lh = ($this->options->logoSpaceHeight - 2) * $this->options->scale; // get the qrcode size $ql = $this->matrix->size() * $this->options->scale; diff --git a/examples/custom_output.php b/examples/custom_output.php index e21ce1073..f55f69b41 100644 --- a/examples/custom_output.php +++ b/examples/custom_output.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -19,7 +20,7 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; // invoke the QROutputInterface manually $options = new QROptions([ 'version' => 5, - 'eccLevel' => QRCode::ECC_L, + 'eccLevel' => EccLevel::L, ]); $qrcode = new QRCode($options); @@ -33,7 +34,7 @@ var_dump($qrOutputInterface->dump()); // or just $options = new QROptions([ 'version' => 5, - 'eccLevel' => QRCode::ECC_L, + 'eccLevel' => EccLevel::L, 'outputType' => QRCode::OUTPUT_CUSTOM, 'outputInterface' => MyCustomOutput::class, ]); diff --git a/examples/fpdf.php b/examples/fpdf.php index 9c690a7f7..b231e49e0 100644 --- a/examples/fpdf.php +++ b/examples/fpdf.php @@ -3,6 +3,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; require_once __DIR__ . '/../vendor/autoload.php'; @@ -11,7 +12,7 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; $options = new QROptions([ 'version' => 7, 'outputType' => QRCode::OUTPUT_FPDF, - 'eccLevel' => QRCode::ECC_L, + 'eccLevel' => EccLevel::L, 'scale' => 5, 'imageBase64' => false, 'moduleValues' => [ diff --git a/examples/html.php b/examples/html.php index aa5305d24..34140671c 100644 --- a/examples/html.php +++ b/examples/html.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; require_once '../vendor/autoload.php'; @@ -60,9 +61,9 @@ header('Content-Type: text/html; charset=utf-8'); $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; $options = new QROptions([ - 'version' => 5, - 'outputType' => QRCode::OUTPUT_MARKUP_HTML, - 'eccLevel' => QRCode::ECC_L, + 'version' => 5, + 'outputType' => QRCode::OUTPUT_MARKUP_HTML, + 'eccLevel' => EccLevel::L, 'moduleValues' => [ // finder 1536 => '#A71111', // dark (true) diff --git a/examples/image.php b/examples/image.php index 54426c68a..99d3ceab8 100644 --- a/examples/image.php +++ b/examples/image.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -19,7 +20,7 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; $options = new QROptions([ 'version' => 10, 'outputType' => QRCode::OUTPUT_IMAGE_PNG, - 'eccLevel' => QRCode::ECC_H, + 'eccLevel' => EccLevel::L, 'scale' => 5, 'imageBase64' => false, 'moduleValues' => [ diff --git a/examples/imageWithLogo.php b/examples/imageWithLogo.php index bfdf11f42..622c74bed 100644 --- a/examples/imageWithLogo.php +++ b/examples/imageWithLogo.php @@ -11,28 +11,29 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; /** - * @property int $logoWidth - * @property int $logoHeight + * @property int $logoSpaceWidth + * @property int $logoSpaceHeight * * @noinspection PhpIllegalPsrClassPathInspection */ class LogoOptions extends QROptions{ - protected int $logoWidth; - protected int $logoHeight; + protected int $logoSpaceWidth; + protected int $logoSpaceHeight; } $options = new LogoOptions; $options->version = 7; -$options->eccLevel = QRCode::ECC_H; +$options->eccLevel = EccLevel::H; $options->imageBase64 = false; -$options->logoWidth = 13; -$options->logoHeight = 13; +$options->logoSpaceWidth = 13; +$options->logoSpaceHeight = 13; $options->scale = 5; $options->imageTransparent = false; diff --git a/examples/imagick.php b/examples/imagick.php index 6bec4d02e..38162972f 100644 --- a/examples/imagick.php +++ b/examples/imagick.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -19,7 +20,7 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; $options = new QROptions([ 'version' => 7, 'outputType' => QRCode::OUTPUT_IMAGICK, - 'eccLevel' => QRCode::ECC_L, + 'eccLevel' => EccLevel::L, 'scale' => 5, 'moduleValues' => [ // finder diff --git a/examples/svg.php b/examples/svg.php index a7a159d70..e8dd5ad12 100644 --- a/examples/svg.php +++ b/examples/svg.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -18,14 +19,14 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; $gzip = true; $options = new QROptions([ - 'version' => 7, - 'outputType' => QRCode::OUTPUT_MARKUP_SVG, - 'eccLevel' => QRCode::ECC_L, + 'version' => 7, + 'outputType' => QRCode::OUTPUT_MARKUP_SVG, + 'eccLevel' => EccLevel::L, 'svgViewBoxSize' => 530, - 'addQuietzone' => true, - 'cssClass' => 'my-css-class', - 'svgOpacity' => 1.0, - 'svgDefs' => ' + 'addQuietzone' => true, + 'cssClass' => 'my-css-class', + 'svgOpacity' => 1.0, + 'svgDefs' => ' diff --git a/examples/text.php b/examples/text.php index 9bdf154f0..d0a6ec2bc 100644 --- a/examples/text.php +++ b/examples/text.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -19,7 +20,7 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; $options = new QROptions([ 'version' => 5, 'outputType' => QRCode::OUTPUT_STRING_TEXT, - 'eccLevel' => QRCode::ECC_L, + 'eccLevel' => EccLevel::L, ]); //
 to view it in a browser
@@ -30,7 +31,7 @@ echo '
'.(new QRCode($options))->ren
 $options = new QROptions([
 	'version'      => 5,
 	'outputType'   => QRCode::OUTPUT_STRING_TEXT,
-	'eccLevel'     => QRCode::ECC_L,
+	'eccLevel'     => EccLevel::L,
 	'moduleValues' => [
 		// finder
 		1536 => 'A', // dark (true)

From 6dd66bf6203052b954b50f2798338a5b878d6f9c Mon Sep 17 00:00:00 2001
From: codemasher 
Date: Sun, 22 Nov 2020 10:28:51 +0100
Subject: [PATCH 10/78] :shower:

---
 src/Common/EccLevel.php | 2 ++
 src/Common/Version.php  | 6 ++----
 src/Data/QRData.php     | 6 ++++++
 3 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php
index 4091843ed..900d8c59b 100644
--- a/src/Common/EccLevel.php
+++ b/src/Common/EccLevel.php
@@ -14,6 +14,8 @@ namespace chillerlan\QRCode\Common;
 
 use chillerlan\QRCode\QRCodeException;
 
+use function array_column, array_combine, array_keys;
+
 /**
  */
 class EccLevel{
diff --git a/src/Common/Version.php b/src/Common/Version.php
index 9c75914b1..31de87649 100644
--- a/src/Common/Version.php
+++ b/src/Common/Version.php
@@ -1,5 +1,6 @@
 bitBuffer->getLength() % 8 !== 0){
 			$this->bitBuffer->putBit(false);
 		}
 
+		// The message bit stream shall then be extended to fill the data capacity of the symbol
+		// corresponding to the Version and Error Correction Level, by the addition of the Pad
+		// Codewords 11101100 and 00010001 alternately.
 		while(true){
 
 			if($this->bitBuffer->getLength() >= $MAX_BITS){

From 9cabb83ad317ab4034b6eabde7b8faf15775edc5 Mon Sep 17 00:00:00 2001
From: codemasher 
Date: Sun, 22 Nov 2020 11:17:30 +0100
Subject: [PATCH 11/78] :shower:

---
 examples/QRImageWithLogo.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/QRImageWithLogo.php b/examples/QRImageWithLogo.php
index 9ba06815a..76aa5ced7 100644
--- a/examples/QRImageWithLogo.php
+++ b/examples/QRImageWithLogo.php
@@ -55,7 +55,7 @@ class QRImageWithLogo extends QRImage{
 		$w = imagesx($im);
 		$h = imagesy($im);
 
-		// set new logo size, leave a border of 1 module
+		// set new logo size, leave a border of 1 module (no proportional resize/centering)
 		$lw = ($this->options->logoSpaceWidth - 2) * $this->options->scale;
 		$lh = ($this->options->logoSpaceHeight - 2) * $this->options->scale;
 

From 51ca0964d4b0ec96edba887a5c02f537e8730a15 Mon Sep 17 00:00:00 2001
From: codemasher 
Date: Sun, 22 Nov 2020 11:19:44 +0100
Subject: [PATCH 12/78] :shower:

---
 src/Common/EccLevel.php |  7 ++++++-
 src/Common/Mode.php     |  8 ++++----
 src/Common/Version.php  | 14 ++++++++++++++
 3 files changed, 24 insertions(+), 5 deletions(-)

diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php
index 900d8c59b..9c3b9b94b 100644
--- a/src/Common/EccLevel.php
+++ b/src/Common/EccLevel.php
@@ -212,6 +212,9 @@ class EccLevel{
 		$this->eccLevel = $eccLevel;
 	}
 
+	/**
+	 * returns the ordinal value of the current ECC level
+	 */
 	public function getOrdinal():int{
 		return $this->eccLevel;
 	}
@@ -234,7 +237,6 @@ class EccLevel{
 	/**
 	 * returns the format pattern for the given $eccLevel and $maskPattern
 	 *
-	 * @return int
 	 * @throws \chillerlan\QRCode\QRCodeException
 	 */
 	public function getformatPattern(int $maskPattern):int{
@@ -246,6 +248,9 @@ class EccLevel{
 		return self::formatPattern[self::MODES[$this->eccLevel]][$maskPattern];
 	}
 
+	/**
+	 * returns an array wit the max bit lengths for version 1-40 and the current ECC level
+	 */
 	public function getMaxBits():array{
 		return array_combine(
 			array_keys(self::MAX_BITS),
diff --git a/src/Common/Mode.php b/src/Common/Mode.php
index d36e84beb..67921a6dd 100644
--- a/src/Common/Mode.php
+++ b/src/Common/Mode.php
@@ -59,10 +59,10 @@ class Mode{
 	 * @var string[]
 	 */
 	public const DATA_INTERFACES = [
-		Mode::DATA_NUMBER   => Number::class,
-		Mode::DATA_ALPHANUM => AlphaNum::class,
-		Mode::DATA_KANJI    => Kanji::class,
-		Mode::DATA_BYTE     => Byte::class,
+		self::DATA_NUMBER   => Number::class,
+		self::DATA_ALPHANUM => AlphaNum::class,
+		self::DATA_KANJI    => Kanji::class,
+		self::DATA_BYTE     => Byte::class,
 	];
 
 	/**
diff --git a/src/Common/Version.php b/src/Common/Version.php
index 31de87649..34a2a9537 100644
--- a/src/Common/Version.php
+++ b/src/Common/Version.php
@@ -132,18 +132,32 @@ class Version{
 		$this->version = $version;
 	}
 
+	/**
+	 * returns the current version number
+	 */
 	public function getVersionNumber():int{
 		return $this->version;
 	}
 
+	/**
+	 * the matrix size for the given version
+	 */
 	public function getDimension():int{
 		return $this->version * 4 + 17;
 	}
 
+	/**
+	 * the version pattern for the given version
+	 */
 	public function getVersionPattern():?int{
 		return self::VERSION_PATTERN[$this->version] ?? null;
 	}
 
+	/**
+	 * the alignment patterns for the current version
+	 *
+	 * @return int[]
+	 */
 	public function getAlignmentPattern():array{
 		return self::ALIGNMENT_PATTERN[$this->version];
 	}

From 63f2bfd337beafb29669adade65181cff19da416 Mon Sep 17 00:00:00 2001
From: codemasher 
Date: Sun, 22 Nov 2020 11:25:44 +0100
Subject: [PATCH 13/78] :shower:

---
 src/Common/Mode.php | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/Common/Mode.php b/src/Common/Mode.php
index 67921a6dd..5afd0dc3c 100644
--- a/src/Common/Mode.php
+++ b/src/Common/Mode.php
@@ -48,9 +48,9 @@ class Mode{
 	 */
 	public const LENGTH_BITS = [
 		self::DATA_NUMBER   => [10, 12, 14],
-		self::DATA_ALPHANUM => [9, 11, 13],
-		self::DATA_BYTE     => [8, 16, 16],
-		self::DATA_KANJI    => [8, 10, 12],
+		self::DATA_ALPHANUM => [ 9, 11, 13],
+		self::DATA_BYTE     => [ 8, 16, 16],
+		self::DATA_KANJI    => [ 8, 10, 12],
 	];
 
 	/**
@@ -76,10 +76,15 @@ class Mode{
 			throw new QRCodeException('invalid mode given');
 		}
 
+		$minVersion = 0;
+
 		foreach([9, 26, 40] as $key => $breakpoint){
-			if($version <= $breakpoint){
+
+			if($version > $minVersion && $version <= $breakpoint){
 				return self::LENGTH_BITS[$mode][$key];
 			}
+
+			$minVersion = $breakpoint;
 		}
 
 		throw new QRCodeException(sprintf('invalid version number: %d', $version));

From 9921331e86cd8520b7aab124c39dd59c95dea5a8 Mon Sep 17 00:00:00 2001
From: codemasher 
Date: Thu, 17 Dec 2020 18:31:08 +0100
Subject: [PATCH 14/78] :shower:

---
 src/Common/EccLevel.php | 11 +++----
 src/Common/Mode.php     |  2 +-
 src/Common/Version.php  | 64 +++++++++++++++++++++++++++++++++++++++--
 src/Data/QRMatrix.php   | 12 ++++----
 4 files changed, 74 insertions(+), 15 deletions(-)

diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php
index 9c3b9b94b..a1ffb51f2 100644
--- a/src/Common/EccLevel.php
+++ b/src/Common/EccLevel.php
@@ -17,8 +17,9 @@ use chillerlan\QRCode\QRCodeException;
 use function array_column, array_combine, array_keys;
 
 /**
+ *
  */
-class EccLevel{
+final class EccLevel{
 
 	// ISO/IEC 18004:2000 Tables 12, 25
 
@@ -54,7 +55,7 @@ class EccLevel{
 	 *
 	 * @var int [][][]
 	 */
-	const RSBLOCKS = [
+	private const RSBLOCKS = [
 		1  => [[ 1,  0,  26,  19], [ 1,  0, 26, 16], [ 1,  0, 26, 13], [ 1,  0, 26,  9]],
 		2  => [[ 1,  0,  44,  34], [ 1,  0, 44, 28], [ 1,  0, 44, 22], [ 1,  0, 44, 16]],
 		3  => [[ 1,  0,  70,  55], [ 1,  0, 70, 44], [ 2,  0, 35, 17], [ 2,  0, 35, 13]],
@@ -102,7 +103,7 @@ class EccLevel{
 	 *
 	 * @var int [][]
 	 */
-	const MAX_BITS = [
+	private const MAX_BITS = [
 	//  v  => [    L,     M,     Q,     H]  // modules
 		1  => [  152,   128,   104,    72], //  21
 		2  => [  272,   224,   176,   128], //  25
@@ -153,7 +154,7 @@ class EccLevel{
 	 *
 	 * @var int[][]
 	 */
-	protected const formatPattern = [
+	private const FORMAT_PATTERN = [
 		[ // L
 		  0b111011111000100,
 		  0b111001011110011,
@@ -245,7 +246,7 @@ class EccLevel{
 			throw new QRCodeException('invalid mask pattern');
 		}
 
-		return self::formatPattern[self::MODES[$this->eccLevel]][$maskPattern];
+		return self::FORMAT_PATTERN[self::MODES[$this->eccLevel]][$maskPattern];
 	}
 
 	/**
diff --git a/src/Common/Mode.php b/src/Common/Mode.php
index 5afd0dc3c..edb2a8282 100644
--- a/src/Common/Mode.php
+++ b/src/Common/Mode.php
@@ -18,7 +18,7 @@ use chillerlan\QRCode\QRCodeException;
 /**
  * ISO 18004:2006, 6.4.1, Tables 2 and 3
  */
-class Mode{
+final class Mode{
 
 	// ISO/IEC 18004:2000 Table 2
 
diff --git a/src/Common/Version.php b/src/Common/Version.php
index 34a2a9537..d1735b5f7 100644
--- a/src/Common/Version.php
+++ b/src/Common/Version.php
@@ -17,7 +17,7 @@ use chillerlan\QRCode\QRCodeException;
 /**
  *
  */
-class Version{
+final class Version{
 
 	/**
 	 * ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns
@@ -26,7 +26,7 @@ class Version{
 	 *
 	 * @var int[][]
 	 */
-	public const ALIGNMENT_PATTERN = [
+	private const ALIGNMENT_PATTERN = [
 		1  => [],
 		2  => [6, 18],
 		3  => [6, 22],
@@ -76,7 +76,7 @@ class Version{
 	 *
 	 * @var int[]
 	 */
-	protected const VERSION_PATTERN = [
+	private const VERSION_PATTERN = [
 		7  => 0b000111110010010100,
 		8  => 0b001000010110111100,
 		9  => 0b001001101010011001,
@@ -113,6 +113,57 @@ class Version{
 		40 => 0b101000110001101001,
 	];
 
+	/**
+	 * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
+	 *
+	 * @see http://www.qrcode.com/en/about/version.html
+	 *
+	 * @var int [][][]
+	 */
+	private const MAX_LENGTH =[
+	//	v  => [NUMERIC => [L, M, Q, H ], ALPHANUM => [L, M, Q, H], BINARY => [L, M, Q, H  ], KANJI => [L, M, Q, H   ]]
+		1  => [[  41,   34,   27,   17], [  25,   20,   16,   10], [  17,   14,   11,    7], [  10,    8,    7,    4]],
+		2  => [[  77,   63,   48,   34], [  47,   38,   29,   20], [  32,   26,   20,   14], [  20,   16,   12,    8]],
+		3  => [[ 127,  101,   77,   58], [  77,   61,   47,   35], [  53,   42,   32,   24], [  32,   26,   20,   15]],
+		4  => [[ 187,  149,  111,   82], [ 114,   90,   67,   50], [  78,   62,   46,   34], [  48,   38,   28,   21]],
+		5  => [[ 255,  202,  144,  106], [ 154,  122,   87,   64], [ 106,   84,   60,   44], [  65,   52,   37,   27]],
+		6  => [[ 322,  255,  178,  139], [ 195,  154,  108,   84], [ 134,  106,   74,   58], [  82,   65,   45,   36]],
+		7  => [[ 370,  293,  207,  154], [ 224,  178,  125,   93], [ 154,  122,   86,   64], [  95,   75,   53,   39]],
+		8  => [[ 461,  365,  259,  202], [ 279,  221,  157,  122], [ 192,  152,  108,   84], [ 118,   93,   66,   52]],
+		9  => [[ 552,  432,  312,  235], [ 335,  262,  189,  143], [ 230,  180,  130,   98], [ 141,  111,   80,   60]],
+		10 => [[ 652,  513,  364,  288], [ 395,  311,  221,  174], [ 271,  213,  151,  119], [ 167,  131,   93,   74]],
+		11 => [[ 772,  604,  427,  331], [ 468,  366,  259,  200], [ 321,  251,  177,  137], [ 198,  155,  109,   85]],
+		12 => [[ 883,  691,  489,  374], [ 535,  419,  296,  227], [ 367,  287,  203,  155], [ 226,  177,  125,   96]],
+		13 => [[1022,  796,  580,  427], [ 619,  483,  352,  259], [ 425,  331,  241,  177], [ 262,  204,  149,  109]],
+		14 => [[1101,  871,  621,  468], [ 667,  528,  376,  283], [ 458,  362,  258,  194], [ 282,  223,  159,  120]],
+		15 => [[1250,  991,  703,  530], [ 758,  600,  426,  321], [ 520,  412,  292,  220], [ 320,  254,  180,  136]],
+		16 => [[1408, 1082,  775,  602], [ 854,  656,  470,  365], [ 586,  450,  322,  250], [ 361,  277,  198,  154]],
+		17 => [[1548, 1212,  876,  674], [ 938,  734,  531,  408], [ 644,  504,  364,  280], [ 397,  310,  224,  173]],
+		18 => [[1725, 1346,  948,  746], [1046,  816,  574,  452], [ 718,  560,  394,  310], [ 442,  345,  243,  191]],
+		19 => [[1903, 1500, 1063,  813], [1153,  909,  644,  493], [ 792,  624,  442,  338], [ 488,  384,  272,  208]],
+		20 => [[2061, 1600, 1159,  919], [1249,  970,  702,  557], [ 858,  666,  482,  382], [ 528,  410,  297,  235]],
+		21 => [[2232, 1708, 1224,  969], [1352, 1035,  742,  587], [ 929,  711,  509,  403], [ 572,  438,  314,  248]],
+		22 => [[2409, 1872, 1358, 1056], [1460, 1134,  823,  640], [1003,  779,  565,  439], [ 618,  480,  348,  270]],
+		23 => [[2620, 2059, 1468, 1108], [1588, 1248,  890,  672], [1091,  857,  611,  461], [ 672,  528,  376,  284]],
+		24 => [[2812, 2188, 1588, 1228], [1704, 1326,  963,  744], [1171,  911,  661,  511], [ 721,  561,  407,  315]],
+		25 => [[3057, 2395, 1718, 1286], [1853, 1451, 1041,  779], [1273,  997,  715,  535], [ 784,  614,  440,  330]],
+		26 => [[3283, 2544, 1804, 1425], [1990, 1542, 1094,  864], [1367, 1059,  751,  593], [ 842,  652,  462,  365]],
+		27 => [[3517, 2701, 1933, 1501], [2132, 1637, 1172,  910], [1465, 1125,  805,  625], [ 902,  692,  496,  385]],
+		28 => [[3669, 2857, 2085, 1581], [2223, 1732, 1263,  958], [1528, 1190,  868,  658], [ 940,  732,  534,  405]],
+		29 => [[3909, 3035, 2181, 1677], [2369, 1839, 1322, 1016], [1628, 1264,  908,  698], [1002,  778,  559,  430]],
+		30 => [[4158, 3289, 2358, 1782], [2520, 1994, 1429, 1080], [1732, 1370,  982,  742], [1066,  843,  604,  457]],
+		31 => [[4417, 3486, 2473, 1897], [2677, 2113, 1499, 1150], [1840, 1452, 1030,  790], [1132,  894,  634,  486]],
+		32 => [[4686, 3693, 2670, 2022], [2840, 2238, 1618, 1226], [1952, 1538, 1112,  842], [1201,  947,  684,  518]],
+		33 => [[4965, 3909, 2805, 2157], [3009, 2369, 1700, 1307], [2068, 1628, 1168,  898], [1273, 1002,  719,  553]],
+		34 => [[5253, 4134, 2949, 2301], [3183, 2506, 1787, 1394], [2188, 1722, 1228,  958], [1347, 1060,  756,  590]],
+		35 => [[5529, 4343, 3081, 2361], [3351, 2632, 1867, 1431], [2303, 1809, 1283,  983], [1417, 1113,  790,  605]],
+		36 => [[5836, 4588, 3244, 2524], [3537, 2780, 1966, 1530], [2431, 1911, 1351, 1051], [1496, 1176,  832,  647]],
+		37 => [[6153, 4775, 3417, 2625], [3729, 2894, 2071, 1591], [2563, 1989, 1423, 1093], [1577, 1224,  876,  673]],
+		38 => [[6479, 5039, 3599, 2735], [3927, 3054, 2181, 1658], [2699, 2099, 1499, 1139], [1661, 1292,  923,  701]],
+		39 => [[6743, 5313, 3791, 2927], [4087, 3220, 2298, 1774], [2809, 2213, 1579, 1219], [1729, 1362,  972,  750]],
+		40 => [[7089, 5596, 3993, 3057], [4296, 3391, 2420, 1852], [2953, 2331, 1663, 1273], [1817, 1435, 1024,  784]],
+	];
+
 	/**
 	 * QR Code version number
 	 */
@@ -162,4 +213,11 @@ class Version{
 		return self::ALIGNMENT_PATTERN[$this->version];
 	}
 
+	/**
+	 * the maximum character count for the given $mode and $eccLevel
+	 */
+	public function getMaxLengthForMode(int $mode, int $eccLevel):?int{
+		return self::MAX_LENGTH[$this->version][$mode][$eccLevel] ?? null;
+	}
+
 }
diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php
index d4cbae5b4..185089853 100755
--- a/src/Data/QRMatrix.php
+++ b/src/Data/QRMatrix.php
@@ -73,7 +73,7 @@ final class QRMatrix{
 	/**
 	 * the current ECC level
 	 */
-	protected EccLevel $ecclevel;
+	protected EccLevel $eccLevel;
 
 	/**
 	 * a Version instance
@@ -83,9 +83,9 @@ final class QRMatrix{
 	/**
 	 * QRMatrix constructor.
 	 */
-	public function __construct(Version $version, EccLevel $eclevel){
+	public function __construct(Version $version, EccLevel $eccLevel){
 		$this->version     = $version;
-		$this->ecclevel    = $eclevel;
+		$this->eccLevel    = $eccLevel;
 		$this->moduleCount = $this->version->getDimension();
 		$this->matrix      = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
 	}
@@ -140,7 +140,7 @@ final class QRMatrix{
 	 * Returns the current ECC level
 	 */
 	public function eccLevel():EccLevel{
-		return $this->ecclevel;
+		return $this->eccLevel;
 	}
 
 	/**
@@ -349,7 +349,7 @@ final class QRMatrix{
 	 * ISO/IEC 18004:2000 Section 8.9
 	 */
 	public function setFormatInfo(int $maskPattern, bool $test = null):QRMatrix{
-		$bits = $this->ecclevel->getformatPattern($maskPattern);
+		$bits = $this->eccLevel->getformatPattern($maskPattern);
 
 		for($i = 0; $i < 15; $i++){
 			$v = !$test && (($bits >> $i) & 1) === 1;
@@ -438,7 +438,7 @@ final class QRMatrix{
 	public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):QRMatrix{
 
 		// for logos we operate in ECC H (30%) only
-		if($this->ecclevel->getOrdinal() !== EccLevel::H){
+		if($this->eccLevel->getOrdinal() !== EccLevel::H){
 			throw new QRCodeDataException('ECC level "H" required to add logo space');
 		}
 

From 1b6b22c191223d546c341fb8d670c51fa60d3c58 Mon Sep 17 00:00:00 2001
From: codemasher 
Date: Sun, 3 Jan 2021 13:13:11 +0100
Subject: [PATCH 15/78] :octocat:

---
 .scrutinizer.yml |  5 +++++
 README.md        | 17 ++++++++++-------
 composer.json    |  2 +-
 3 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index 0be10f364..2a7e7028b 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -4,8 +4,13 @@ build:
       tests:
         override:
           - php-scrutinizer-run
+  environment:
+    php: 8.0.0
+
 filter:
   excluded_paths:
     - examples/*
     - tests/*
     - vendor/*
+    - .github/*
+    - .phan/*
diff --git a/README.md b/README.md
index 31fb15575..115cbb51f 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
 A PHP 7.4+ QR Code library based on the [implementation](https://github.com/kazuhikoarase/qrcode-generator) by [Kazuhiko Arase](https://github.com/kazuhikoarase),
 namespaced, cleaned up, improved and other stuff.
 
+[![PHP Version Support][php-badge]][php]
 [![Packagist version][packagist-badge]][packagist]
 [![License][license-badge]][license]
 [![Travis CI][travis-badge]][travis]
@@ -13,19 +14,21 @@ namespaced, cleaned up, improved and other stuff.
 
 [![Continuous Integration][gh-action-badge]][gh-action] [![phpDocs][gh-docs-badge]][gh-docs]
 
-[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg?style=flat-square
+[php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-qrcode?logo=php&color=8892BF
+[php]: https://www.php.net/supported-versions.php
+[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg
 [packagist]: https://packagist.org/packages/chillerlan/php-qrcode
-[license-badge]: https://img.shields.io/github/license/chillerlan/php-qrcode.svg?style=flat-square
+[license-badge]: https://img.shields.io/github/license/chillerlan/php-qrcode.svg
 [license]: https://github.com/chillerlan/php-qrcode/blob/main/LICENSE
-[travis-badge]: https://img.shields.io/travis/chillerlan/php-qrcode.svg?style=flat-square
+[travis-badge]: https://img.shields.io/travis/chillerlan/php-qrcode.svg?logo=travis
 [travis]: https://travis-ci.org/chillerlan/php-qrcode
-[coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-qrcode.svg?style=flat-square
+[coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-qrcode.svg?logo=codecov
 [coverage]: https://codecov.io/github/chillerlan/php-qrcode
-[scrutinizer-badge]: https://img.shields.io/scrutinizer/g/chillerlan/php-qrcode.svg?style=flat-square
+[scrutinizer-badge]: https://img.shields.io/scrutinizer/g/chillerlan/php-qrcode.svg?logo=scrutinizer
 [scrutinizer]: https://scrutinizer-ci.com/g/chillerlan/php-qrcode
-[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode.svg?style=flat-square
+[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode.svg
 [downloads]: https://packagist.org/packages/chillerlan/php-qrcode/stats
-[donate-badge]: https://img.shields.io/badge/donate-paypal-ff33aa.svg?style=flat-square
+[donate-badge]: https://img.shields.io/badge/-donate-ff33aa.svg?logo=paypal
 [donate]: https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4
 [gh-action-badge]: https://github.com/chillerlan/php-qrcode/workflows/Continuous%20Integration/badge.svg
 [gh-action]: https://github.com/chillerlan/php-qrcode/actions?query=workflow%3A%22Continuous+Integration%22
diff --git a/composer.json b/composer.json
index c7f140ef9..32be91d91 100644
--- a/composer.json
+++ b/composer.json
@@ -29,7 +29,7 @@
 		"chillerlan/php-settings-container": "^2.1"
 	},
 	"require-dev": {
-		"phpunit/phpunit": "^9.4",
+		"phpunit/phpunit": "^9.5",
 		"phan/phan": "^3.2.2",
 		"setasign/fpdf": "^1.8.2"
 	},

From 92282edd9945d185ed775a2b77177b1623920652 Mon Sep 17 00:00:00 2001
From: codemasher 
Date: Sun, 3 Jan 2021 13:20:36 +0100
Subject: [PATCH 16/78] :octocat:

---
 README.md | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 115cbb51f..f4250cc21 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,7 @@ namespaced, cleaned up, improved and other stuff.
 [![Travis CI][travis-badge]][travis]
 [![CodeCov][coverage-badge]][coverage]
 [![Scrunitizer CI][scrutinizer-badge]][scrutinizer]
-[![Packagist downloads][downloads-badge]][downloads]
-[![PayPal donate][donate-badge]][donate]
-
+[![Packagist downloads][downloads-badge]][downloads]
[![Continuous Integration][gh-action-badge]][gh-action] [![phpDocs][gh-docs-badge]][gh-docs] [php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-qrcode?logo=php&color=8892BF @@ -28,8 +26,6 @@ namespaced, cleaned up, improved and other stuff. [scrutinizer]: https://scrutinizer-ci.com/g/chillerlan/php-qrcode [downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode.svg [downloads]: https://packagist.org/packages/chillerlan/php-qrcode/stats -[donate-badge]: https://img.shields.io/badge/-donate-ff33aa.svg?logo=paypal -[donate]: https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4 [gh-action-badge]: https://github.com/chillerlan/php-qrcode/workflows/Continuous%20Integration/badge.svg [gh-action]: https://github.com/chillerlan/php-qrcode/actions?query=workflow%3A%22Continuous+Integration%22 [gh-docs-badge]: https://github.com/chillerlan/php-qrcode/workflows/Docs/badge.svg From f2f195c4ed18a0b219f1488626e9c13835365263 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 3 Jan 2021 13:40:41 +0100 Subject: [PATCH 17/78] :octocat: --- .travis.yml | 1 + README.md | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5628430b9..b992b2e32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ env: matrix: include: - php: 7.4 + - php: 8.0 - php: nightly allow_failures: - php: nightly diff --git a/README.md b/README.md index f4250cc21..cea5509a2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ namespaced, cleaned up, improved and other stuff. [![CodeCov][coverage-badge]][coverage] [![Scrunitizer CI][scrutinizer-badge]][scrutinizer] [![Packagist downloads][downloads-badge]][downloads]
-[![Continuous Integration][gh-action-badge]][gh-action] [![phpDocs][gh-docs-badge]][gh-docs] +[![Continuous Integration][gh-action-badge]][gh-action] +[![phpDocs][gh-docs-badge]][gh-docs] [php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-qrcode?logo=php&color=8892BF [php]: https://www.php.net/supported-versions.php @@ -18,8 +19,8 @@ namespaced, cleaned up, improved and other stuff. [packagist]: https://packagist.org/packages/chillerlan/php-qrcode [license-badge]: https://img.shields.io/github/license/chillerlan/php-qrcode.svg [license]: https://github.com/chillerlan/php-qrcode/blob/main/LICENSE -[travis-badge]: https://img.shields.io/travis/chillerlan/php-qrcode.svg?logo=travis -[travis]: https://travis-ci.org/chillerlan/php-qrcode +[travis-badge]: https://img.shields.io/travis/com/chillerlan/php-qrcode/main?logo=travis +[travis]: https://travis-ci.com/github/chillerlan/php-qrcode [coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-qrcode.svg?logo=codecov [coverage]: https://codecov.io/github/chillerlan/php-qrcode [scrutinizer-badge]: https://img.shields.io/scrutinizer/g/chillerlan/php-qrcode.svg?logo=scrutinizer From 5a9bb67d2edaeeaa3b64675259688c9f0aa106a8 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 3 Jan 2021 13:44:26 +0100 Subject: [PATCH 18/78] :octocat: imagick plz --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b992b2e32..5de8f4e39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - pecl channel-update pecl.php.net - pecl install ast - | - if [ $TRAVIS_PHP_VERSION != 'nightly' ]; then + if [ $TRAVIS_PHP_VERSION == '7.4' ]; then printf "\n" | pecl install imagick; fi From c25a1a832987f3824a7c2facbcd4ec8f4e334911 Mon Sep 17 00:00:00 2001 From: codemasher Date: Tue, 5 Jan 2021 22:21:28 +0100 Subject: [PATCH 19/78] :shower: fix svg example --- examples/svg.php | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/svg.php b/examples/svg.php index e8dd5ad12..517e393ed 100644 --- a/examples/svg.php +++ b/examples/svg.php @@ -21,6 +21,7 @@ $gzip = true; $options = new QROptions([ 'version' => 7, 'outputType' => QRCode::OUTPUT_MARKUP_SVG, + 'imageBase64' => false, 'eccLevel' => EccLevel::L, 'svgViewBoxSize' => 530, 'addQuietzone' => true, From 0c1fdb92c345ce4d87bc4f297e2517073b1af796 Mon Sep 17 00:00:00 2001 From: codemasher Date: Wed, 6 Jan 2021 13:27:45 +0100 Subject: [PATCH 20/78] :sparkles: PHPStorm code style & inspection settings --- .gitignore | 14 +- .idea/codeStyles/Project.xml | 863 +++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/inspectionProfiles/Project_Default.xml | 28 + 4 files changed, 909 insertions(+), 1 deletion(-) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml diff --git a/.gitignore b/.gitignore index 8f74009cd..964d223f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,17 @@ -.build/* +# IDE - IntelliJ .idea/* +# Keep the code styles. +!.idea/codeStyles +.idea/codeStyles/* +!.idea/codeStyles/Project.xml +!.idea/codeStyles/codeStyleConfig.xml +# Keep the inspection levels +!.idea/inspectionProfiles +.idea/inspectionProfiles/* +!.idea/inspectionProfiles/Project_Default.xml + +# project stuff +.build/* docs/* vendor/* composer.lock diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..511ae8d84 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,863 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..fcf7b674d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,28 @@ + + + + \ No newline at end of file From 6241c2d90ba8ee3e213888eb1c04eeb2445200cd Mon Sep 17 00:00:00 2001 From: codemasher Date: Wed, 6 Jan 2021 13:57:07 +0100 Subject: [PATCH 21/78] :fire_engine: test fix --- tests/Data/QRMatrixTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index bcf6312c0..4181a4539 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -169,7 +169,7 @@ final class QRMatrixTest extends TestCase{ ->setAlignmentPattern() ; - $alignmentPattern = Version::ALIGNMENT_PATTERN[$version]; + $alignmentPattern = (new Version($version))->getAlignmentPattern(); foreach($alignmentPattern as $py){ foreach($alignmentPattern as $px){ From 759262032b2a97e88b4e75c2d13223a2b59ef07c Mon Sep 17 00:00:00 2001 From: codemasher Date: Wed, 6 Jan 2021 13:58:20 +0100 Subject: [PATCH 22/78] :wrench: fix windows test? --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c849fe80b..c13f6b6a5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -89,7 +89,7 @@ jobs: run: composer update --no-ansi --no-interaction --no-progress --no-suggest - name: "Run tests with phpunit" - run: php vendor/bin/phpunit --configuration=phpunit.xml + run: php vendor/phpunit/phpunit/phpunit --configuration=phpunit.xml - name: "Send code coverage report to Codecov.io" uses: codecov/codecov-action@v1 From 72d46ba7b7ddb3436a8ebfa78caea2d3f9305409 Mon Sep 17 00:00:00 2001 From: codemasher Date: Wed, 6 Jan 2021 14:14:37 +0100 Subject: [PATCH 23/78] :wrench: --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c13f6b6a5..2263dd2cc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,9 +54,9 @@ jobs: - "8.0" steps: - - name: "Configure git to avoid issues with line endings" - if: matrix.os == 'windows-latest' - run: git config --global core.autocrlf false +# - name: "Configure git to avoid issues with line endings" +# if: matrix.os == 'windows-latest' +# run: git config --global core.autocrlf false - name: "Checkout" uses: actions/checkout@v2 From aa12ef4d36ff876cd4b7f9761a75b78c4f5f3691 Mon Sep 17 00:00:00 2001 From: codemasher Date: Wed, 6 Jan 2021 15:41:21 +0100 Subject: [PATCH 24/78] :octocat: --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index df877c1b3..1fea31626 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ Date: Wed, 6 Jan 2021 17:33:12 +0100 Subject: [PATCH 25/78] :octocat: phan 4.x --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 32be91d91..5ec4031be 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ }, "require-dev": { "phpunit/phpunit": "^9.5", - "phan/phan": "^3.2.2", + "phan/phan": "^4.0", "setasign/fpdf": "^1.8.2" }, "suggest": { From 0c318f7ef63ab09740f8f39073096bc63d87dc10 Mon Sep 17 00:00:00 2001 From: codemasher Date: Wed, 6 Jan 2021 19:13:34 +0100 Subject: [PATCH 26/78] :octocat: +max length table reference --- src/Common/Mode.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Common/Mode.php b/src/Common/Mode.php index edb2a8282..fd73f6783 100644 --- a/src/Common/Mode.php +++ b/src/Common/Mode.php @@ -65,6 +65,20 @@ final class Mode{ self::DATA_BYTE => Byte::class, ]; + /** + * References to the keys of the following table(s): + * + * @see \chillerlan\QRCode\Common\Version::MAX_LENGTH + * + * @var int[] + */ + public const DATA_MODES = [ + self::DATA_NUMBER => 0, + self::DATA_ALPHANUM => 1, + self::DATA_BYTE => 2, + self::DATA_KANJI => 3, + ]; + /** * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40 * From 96135bfedaf3c57f8c93fad8526a1267aa14454e Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 7 Jan 2021 11:03:46 +0100 Subject: [PATCH 27/78] :wrench: --- src/Common/EccLevel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php index a1ffb51f2..7f2a7d196 100644 --- a/src/Common/EccLevel.php +++ b/src/Common/EccLevel.php @@ -217,7 +217,7 @@ final class EccLevel{ * returns the ordinal value of the current ECC level */ public function getOrdinal():int{ - return $this->eccLevel; + return self::MODES[$this->eccLevel]; } /** From 4167286f65bcb1a477c4153edf03ab2413138dbd Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 7 Jan 2021 11:17:51 +0100 Subject: [PATCH 28/78] :wrench: --- src/Common/EccLevel.php | 7 +++++++ src/Data/QRMatrix.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php index 7f2a7d196..1c8693822 100644 --- a/src/Common/EccLevel.php +++ b/src/Common/EccLevel.php @@ -213,6 +213,13 @@ final class EccLevel{ $this->eccLevel = $eccLevel; } + /** + * returns the current ECC level + */ + public function getLevel():int{ + return $this->eccLevel; + } + /** * returns the ordinal value of the current ECC level */ diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index 185089853..cb9def0f0 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -438,7 +438,7 @@ final class QRMatrix{ public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):QRMatrix{ // for logos we operate in ECC H (30%) only - if($this->eccLevel->getOrdinal() !== EccLevel::H){ + if($this->eccLevel->getLevel() !== EccLevel::H){ throw new QRCodeDataException('ECC level "H" required to add logo space'); } From 28d116f4e451ecd1f5f2cf8dde048d36aa8bb305 Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 7 Jan 2021 11:18:21 +0100 Subject: [PATCH 29/78] :wrench: --- tests/Data/QRMatrixTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index 4181a4539..3b94b974d 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -70,7 +70,7 @@ final class QRMatrixTest extends TestCase{ * Tests if eccLevel() returns the current (given) ECC level */ public function testECC():void{ - $this::assertSame(EccLevel::L, $this->matrix->eccLevel()->getOrdinal()); + $this::assertSame(EccLevel::MODES[EccLevel::L], $this->matrix->eccLevel()->getOrdinal()); } /** From eb2ec6f0899f38ca5a04db29095199f07e7c5b4c Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 7 Jan 2021 12:19:13 +0100 Subject: [PATCH 30/78] :octocat: +eec string representation --- src/Common/EccLevel.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php index 1c8693822..b1247f822 100644 --- a/src/Common/EccLevel.php +++ b/src/Common/EccLevel.php @@ -37,7 +37,7 @@ final class EccLevel{ * * @see \chillerlan\QRCode\Common\Version::MAX_BITS * @see \chillerlan\QRCode\Common\EccLevel::RSBLOCKS - * @see \chillerlan\QRCode\Data\QRMatrix::formatPattern + * @see \chillerlan\QRCode\Common\EccLevel::formatPattern * * @var int[] */ @@ -48,6 +48,13 @@ final class EccLevel{ self::H => 3, ]; + public const MODES_STRING = [ + self::L => 'L', + self::M => 'M', + self::Q => 'Q', + self::H => 'H', + ]; + /** * ISO/IEC 18004:2000 Tables 13-22 * @@ -213,6 +220,13 @@ final class EccLevel{ $this->eccLevel = $eccLevel; } + /** + * returns the string representation of the current ECC level + */ + public function __toString():string{ + return self::MODES_STRING[$this->eccLevel]; + } + /** * returns the current ECC level */ From 92f563b762362fa97ae8a14f423636fbdd4f709f Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 7 Jan 2021 16:35:30 +0100 Subject: [PATCH 31/78] :fire_engine: i have no idea why this breaks the GH windows runner --- tests/Output/QROutputTestAbstract.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index b94c1f520..633b99206 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -70,6 +70,14 @@ abstract class QROutputTestAbstract extends TestCase{ * Tests if an exception is thrown when trying to write a cache file to an invalid destination */ public function testSaveException():void{ + + if(PHP_OS_FAMILY === 'Windows'){ + $this::markTestSkipped('why does this fail on CI??'); + + /** @noinspection PhpUnreachableStatementInspection */ + return; + } + $this->expectException(QRCodeOutputException::class); $this->expectExceptionMessage('Could not write data to cache file: /foo'); From 36cb9d6bc13b790acdbc5b18481cfd711c2a6f75 Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 7 Jan 2021 21:19:40 +0100 Subject: [PATCH 32/78] :octocat: move rsblock data to Version --- src/Common/EccLevel.php | 65 ---------------------- src/Common/Version.php | 116 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 65 deletions(-) diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php index b1247f822..a5c468ecb 100644 --- a/src/Common/EccLevel.php +++ b/src/Common/EccLevel.php @@ -55,56 +55,6 @@ final class EccLevel{ self::H => 'H', ]; - /** - * ISO/IEC 18004:2000 Tables 13-22 - * - * @see http://www.thonky.com/qr-code-tutorial/error-correction-table - * - * @var int [][][] - */ - private const RSBLOCKS = [ - 1 => [[ 1, 0, 26, 19], [ 1, 0, 26, 16], [ 1, 0, 26, 13], [ 1, 0, 26, 9]], - 2 => [[ 1, 0, 44, 34], [ 1, 0, 44, 28], [ 1, 0, 44, 22], [ 1, 0, 44, 16]], - 3 => [[ 1, 0, 70, 55], [ 1, 0, 70, 44], [ 2, 0, 35, 17], [ 2, 0, 35, 13]], - 4 => [[ 1, 0, 100, 80], [ 2, 0, 50, 32], [ 2, 0, 50, 24], [ 4, 0, 25, 9]], - 5 => [[ 1, 0, 134, 108], [ 2, 0, 67, 43], [ 2, 2, 33, 15], [ 2, 2, 33, 11]], - 6 => [[ 2, 0, 86, 68], [ 4, 0, 43, 27], [ 4, 0, 43, 19], [ 4, 0, 43, 15]], - 7 => [[ 2, 0, 98, 78], [ 4, 0, 49, 31], [ 2, 4, 32, 14], [ 4, 1, 39, 13]], - 8 => [[ 2, 0, 121, 97], [ 2, 2, 60, 38], [ 4, 2, 40, 18], [ 4, 2, 40, 14]], - 9 => [[ 2, 0, 146, 116], [ 3, 2, 58, 36], [ 4, 4, 36, 16], [ 4, 4, 36, 12]], - 10 => [[ 2, 2, 86, 68], [ 4, 1, 69, 43], [ 6, 2, 43, 19], [ 6, 2, 43, 15]], - 11 => [[ 4, 0, 101, 81], [ 1, 4, 80, 50], [ 4, 4, 50, 22], [ 3, 8, 36, 12]], - 12 => [[ 2, 2, 116, 92], [ 6, 2, 58, 36], [ 4, 6, 46, 20], [ 7, 4, 42, 14]], - 13 => [[ 4, 0, 133, 107], [ 8, 1, 59, 37], [ 8, 4, 44, 20], [12, 4, 33, 11]], - 14 => [[ 3, 1, 145, 115], [ 4, 5, 64, 40], [11, 5, 36, 16], [11, 5, 36, 12]], - 15 => [[ 5, 1, 109, 87], [ 5, 5, 65, 41], [ 5, 7, 54, 24], [11, 7, 36, 12]], - 16 => [[ 5, 1, 122, 98], [ 7, 3, 73, 45], [15, 2, 43, 19], [ 3, 13, 45, 15]], - 17 => [[ 1, 5, 135, 107], [10, 1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]], - 18 => [[ 5, 1, 150, 120], [ 9, 4, 69, 43], [17, 1, 50, 22], [ 2, 19, 42, 14]], - 19 => [[ 3, 4, 141, 113], [ 3, 11, 70, 44], [17, 4, 47, 21], [ 9, 16, 39, 13]], - 20 => [[ 3, 5, 135, 107], [ 3, 13, 67, 41], [15, 5, 54, 24], [15, 10, 43, 15]], - 21 => [[ 4, 4, 144, 116], [17, 0, 68, 42], [17, 6, 50, 22], [19, 6, 46, 16]], - 22 => [[ 2, 7, 139, 111], [17, 0, 74, 46], [ 7, 16, 54, 24], [34, 0, 37, 13]], - 23 => [[ 4, 5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]], - 24 => [[ 6, 4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30, 2, 46, 16]], - 25 => [[ 8, 4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]], - 26 => [[10, 2, 142, 114], [19, 4, 74, 46], [28, 6, 50, 22], [33, 4, 46, 16]], - 27 => [[ 8, 4, 152, 122], [22, 3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]], - 28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]], - 29 => [[ 7, 7, 146, 116], [21, 7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]], - 30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]], - 31 => [[13, 3, 145, 115], [ 2, 29, 74, 46], [42, 1, 54, 24], [23, 28, 45, 15]], - 32 => [[17, 0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]], - 33 => [[17, 1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]], - 34 => [[13, 6, 145, 115], [14, 23, 74, 46], [44, 7, 54, 24], [59, 1, 46, 16]], - 35 => [[12, 7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]], - 36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]], - 37 => [[17, 4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]], - 38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]], - 39 => [[20, 4, 147, 117], [40, 7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]], - 40 => [[19, 6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]], - ]; - /** * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40 * @@ -241,21 +191,6 @@ final class EccLevel{ return self::MODES[$this->eccLevel]; } - /** - * returns ECC block information for the given $version and $eccLevel - * - * @return int[] - * @throws \chillerlan\QRCode\QRCodeException - */ - public function getRSBlocks(int $version):array{ - - if($version < 1 || $version > 40){ - throw new QRCodeException('invalid version'); - } - - return self::RSBLOCKS[$version][self::MODES[$this->eccLevel]]; - } - /** * returns the format pattern for the given $eccLevel and $maskPattern * diff --git a/src/Common/Version.php b/src/Common/Version.php index d1735b5f7..3dc8c3aff 100644 --- a/src/Common/Version.php +++ b/src/Common/Version.php @@ -164,6 +164,99 @@ final class Version{ 40 => [[7089, 5596, 3993, 3057], [4296, 3391, 2420, 1852], [2953, 2331, 1663, 1273], [1817, 1435, 1024, 784]], ]; + /** + * ISO/IEC 18004:2000 Tables 13-22 + * + * @see http://www.thonky.com/qr-code-tutorial/error-correction-table + * + * @var int [][][] + */ + private const RSBLOCKS = [ + 1 => [[ 1, 0, 26, 19], [ 1, 0, 26, 16], [ 1, 0, 26, 13], [ 1, 0, 26, 9]], + 2 => [[ 1, 0, 44, 34], [ 1, 0, 44, 28], [ 1, 0, 44, 22], [ 1, 0, 44, 16]], + 3 => [[ 1, 0, 70, 55], [ 1, 0, 70, 44], [ 2, 0, 35, 17], [ 2, 0, 35, 13]], + 4 => [[ 1, 0, 100, 80], [ 2, 0, 50, 32], [ 2, 0, 50, 24], [ 4, 0, 25, 9]], + 5 => [[ 1, 0, 134, 108], [ 2, 0, 67, 43], [ 2, 2, 33, 15], [ 2, 2, 33, 11]], + 6 => [[ 2, 0, 86, 68], [ 4, 0, 43, 27], [ 4, 0, 43, 19], [ 4, 0, 43, 15]], + 7 => [[ 2, 0, 98, 78], [ 4, 0, 49, 31], [ 2, 4, 32, 14], [ 4, 1, 39, 13]], + 8 => [[ 2, 0, 121, 97], [ 2, 2, 60, 38], [ 4, 2, 40, 18], [ 4, 2, 40, 14]], + 9 => [[ 2, 0, 146, 116], [ 3, 2, 58, 36], [ 4, 4, 36, 16], [ 4, 4, 36, 12]], + 10 => [[ 2, 2, 86, 68], [ 4, 1, 69, 43], [ 6, 2, 43, 19], [ 6, 2, 43, 15]], + 11 => [[ 4, 0, 101, 81], [ 1, 4, 80, 50], [ 4, 4, 50, 22], [ 3, 8, 36, 12]], + 12 => [[ 2, 2, 116, 92], [ 6, 2, 58, 36], [ 4, 6, 46, 20], [ 7, 4, 42, 14]], + 13 => [[ 4, 0, 133, 107], [ 8, 1, 59, 37], [ 8, 4, 44, 20], [12, 4, 33, 11]], + 14 => [[ 3, 1, 145, 115], [ 4, 5, 64, 40], [11, 5, 36, 16], [11, 5, 36, 12]], + 15 => [[ 5, 1, 109, 87], [ 5, 5, 65, 41], [ 5, 7, 54, 24], [11, 7, 36, 12]], + 16 => [[ 5, 1, 122, 98], [ 7, 3, 73, 45], [15, 2, 43, 19], [ 3, 13, 45, 15]], + 17 => [[ 1, 5, 135, 107], [10, 1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]], + 18 => [[ 5, 1, 150, 120], [ 9, 4, 69, 43], [17, 1, 50, 22], [ 2, 19, 42, 14]], + 19 => [[ 3, 4, 141, 113], [ 3, 11, 70, 44], [17, 4, 47, 21], [ 9, 16, 39, 13]], + 20 => [[ 3, 5, 135, 107], [ 3, 13, 67, 41], [15, 5, 54, 24], [15, 10, 43, 15]], + 21 => [[ 4, 4, 144, 116], [17, 0, 68, 42], [17, 6, 50, 22], [19, 6, 46, 16]], + 22 => [[ 2, 7, 139, 111], [17, 0, 74, 46], [ 7, 16, 54, 24], [34, 0, 37, 13]], + 23 => [[ 4, 5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]], + 24 => [[ 6, 4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30, 2, 46, 16]], + 25 => [[ 8, 4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]], + 26 => [[10, 2, 142, 114], [19, 4, 74, 46], [28, 6, 50, 22], [33, 4, 46, 16]], + 27 => [[ 8, 4, 152, 122], [22, 3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]], + 28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]], + 29 => [[ 7, 7, 146, 116], [21, 7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]], + 30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]], + 31 => [[13, 3, 145, 115], [ 2, 29, 74, 46], [42, 1, 54, 24], [23, 28, 45, 15]], + 32 => [[17, 0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]], + 33 => [[17, 1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]], + 34 => [[13, 6, 145, 115], [14, 23, 74, 46], [44, 7, 54, 24], [59, 1, 46, 16]], + 35 => [[12, 7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]], + 36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]], + 37 => [[17, 4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]], + 38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]], + 39 => [[20, 4, 147, 117], [40, 7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]], + 40 => [[19, 6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]], + ]; + + private const TOTAL_CODEWORDS = [ + 1 => 26, + 2 => 44, + 3 => 70, + 4 => 100, + 5 => 134, + 6 => 172, + 7 => 196, + 8 => 242, + 9 => 292, + 10 => 346, + 11 => 404, + 12 => 466, + 13 => 532, + 14 => 581, + 15 => 655, + 16 => 733, + 17 => 815, + 18 => 901, + 19 => 991, + 20 => 1085, + 21 => 1156, + 22 => 1258, + 23 => 1364, + 24 => 1474, + 25 => 1588, + 26 => 1706, + 27 => 1828, + 28 => 1921, + 29 => 2051, + 30 => 2185, + 31 => 2323, + 32 => 2465, + 33 => 2611, + 34 => 2761, + 35 => 2876, + 36 => 3034, + 37 => 3196, + 38 => 3362, + 39 => 3532, + 40 => 3706, + ]; + /** * QR Code version number */ @@ -220,4 +313,27 @@ final class Version{ return self::MAX_LENGTH[$this->version][$mode][$eccLevel] ?? null; } + /** + * returns ECC block information for the given $version and $eccLevel + * + * @return int[] + * @throws \chillerlan\QRCode\QRCodeException + */ + public function getRSBlocks(int $eccLevel):array{ + + if((0b11 & $eccLevel) !== $eccLevel){ + throw new QRCodeException('invalid ECC level'); + } + + return self::RSBLOCKS[$this->version][EccLevel::MODES[$eccLevel]]; + } + + /** + * returns the maximum codewords for the current version + */ + public function getTotalCodewords():int{ + return self::TOTAL_CODEWORDS[$this->version]; + } + + } From 26536de7f0180bedf64efbb070570834b4e738ef Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 9 Jan 2021 20:41:51 +0100 Subject: [PATCH 33/78] :octocat: extract ECC/RS operations --- src/Common/ReedSolomon.php | 125 +++++++++++++++++++++++ src/Data/QRData.php | 118 ++------------------- src/Data/QRMatrix.php | 9 +- tests/Data/DatainterfaceTestAbstract.php | 10 +- 4 files changed, 144 insertions(+), 118 deletions(-) create mode 100644 src/Common/ReedSolomon.php diff --git a/src/Common/ReedSolomon.php b/src/Common/ReedSolomon.php new file mode 100644 index 000000000..855ecfbcd --- /dev/null +++ b/src/Common/ReedSolomon.php @@ -0,0 +1,125 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\Helpers\BitBuffer; +use chillerlan\QRCode\Helpers\Polynomial; + +use SplFixedArray; +use function array_fill, array_merge, count, max; + +/** + * ISO/IEC 18004:2000 Section 8.5 ff + * + * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding + */ +final class ReedSolomon{ + + private Version $version; + private EccLevel $eccLevel; + + private SplFixedArray $interleavedData; + private int $interleavedDataIndex; + + /** + * ReedSolomon constructor. + */ + public function __construct(Version $version, EccLevel $eccLevel){ + $this->version = $version; + $this->eccLevel = $eccLevel; + } + + /** + * ECC interleaving + * + * @return \SplFixedArray + */ + public function interleaveEcBytes(BitBuffer $bitBuffer):SplFixedArray{ + [$l1, $l2, $b1, $b2] = $this->version->getRSBlocks($this->eccLevel->getLevel()); + + $numRsBlocks = $l1 + $l2; + $ecBytes = new SplFixedArray($numRsBlocks); + $rsBlocks = array_fill(0, $l1, [$b1, $b2]); + + if($l2 > 0){ + $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1])); + } + + $dataBytes = SplFixedArray::fromArray($rsBlocks); + $maxDataBytes = 0; + $maxEcBytes = 0; + $dataByteOffset = 0; + $bitBufferData = $bitBuffer->getBuffer(); + + foreach($rsBlocks as $key => $block){ + [$rsBlockTotal, $dataByteCount] = $block; + + $ecByteCount = $rsBlockTotal - $dataByteCount; + $maxDataBytes = max($maxDataBytes, $dataByteCount); + $maxEcBytes = max($maxEcBytes, $ecByteCount); + $dataBytes[$key] = new SplFixedArray($dataByteCount); + + foreach($dataBytes[$key] as $i => $_){ + $dataBytes[$key][$i] = $bitBufferData[$i + $dataByteOffset] & 0xff; + } + + $rsPoly = new Polynomial; + $modPoly = new Polynomial; + + for($i = 0; $i < $ecByteCount; $i++){ + $modPoly->setNum([1, $modPoly->gexp($i)]); + $rsPoly->multiply($modPoly->getNum()); + } + + $rsPolyCount = count($rsPoly->getNum()) - 1; + + $modPoly + ->setNum($dataBytes[$key]->toArray(), $rsPolyCount) + ->mod($rsPoly->getNum()) + ; + + $ecBytes[$key] = new SplFixedArray($rsPolyCount); + $num = $modPoly->getNum(); + $count = count($num) - count($ecBytes[$key]); + + foreach($ecBytes[$key] as $i => $_){ + $modIndex = $i + $count; + $ecBytes[$key][$i] = $modIndex >= 0 ? $num[$modIndex] : 0; + } + + $dataByteOffset += $dataByteCount; + } + + $this->interleavedData = new SplFixedArray($this->version->getTotalCodewords()); + $this->interleavedDataIndex = 0; + + $this->interleave($dataBytes, $maxDataBytes, $numRsBlocks); + $this->interleave($ecBytes, $maxEcBytes, $numRsBlocks); + + return $this->interleavedData; + } + + /** + * + */ + private function interleave(SplFixedArray $byteArray, int $maxBytes, int $numRsBlocks):void{ + for($x = 0; $x < $maxBytes; $x++){ + for($y = 0; $y < $numRsBlocks; $y++){ + if($x < count($byteArray[$y])){ + $this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x]; + } + } + } + } + +} diff --git a/src/Data/QRData.php b/src/Data/QRData.php index f42000333..7df57874d 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -12,12 +12,12 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; +use chillerlan\QRCode\Common\{EccLevel, Mode, ReedSolomon, Version}; use chillerlan\QRCode\QRCode; -use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial}; +use chillerlan\QRCode\Helpers\BitBuffer; use chillerlan\Settings\SettingsContainerInterface; -use function array_fill, array_merge, count, max, range, sprintf; +use function range, sprintf; /** * Processes the binary data and maps it on a matrix which is then being returned @@ -29,16 +29,6 @@ class QRData{ */ protected Version $version; - /** - * ECC temp data - */ - protected array $ecdata; - - /** - * ECC temp data - */ - protected array $dcdata; - /** * @var \chillerlan\QRCode\Data\QRDataModeInterface[] */ @@ -109,9 +99,12 @@ class QRData{ * returns a fresh matrix object with the data written for the given $maskPattern */ public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{ + $rs = new ReedSolomon($this->version, $this->eccLevel); + $data = $rs->interleaveEcBytes($this->bitBuffer); + return (new QRMatrix($this->version, $this->eccLevel)) ->init($maskPattern, $test) - ->mapData($this->maskECC(), $maskPattern) + ->mapData($data, $maskPattern) ; } @@ -223,101 +216,4 @@ class QRData{ } - /** - * ECC masking - * - * ISO/IEC 18004:2000 Section 8.5 ff - * - * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding - */ - protected function maskECC():array{ - [$l1, $l2, $b1, $b2] = $this->eccLevel->getRSBlocks($this->version->getVersionNumber()); - - $rsBlocks = array_fill(0, $l1, [$b1, $b2]); - $rsCount = $l1 + $l2; - $this->ecdata = array_fill(0, $rsCount, []); - $this->dcdata = $this->ecdata; - - if($l2 > 0){ - $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1])); - } - - $totalCodeCount = 0; - $maxDcCount = 0; - $maxEcCount = 0; - $offset = 0; - - $bitBuffer = $this->bitBuffer->getBuffer(); - - foreach($rsBlocks as $key => $block){ - [$rsBlockTotal, $dcCount] = $block; - - $ecCount = $rsBlockTotal - $dcCount; - $maxDcCount = max($maxDcCount, $dcCount); - $maxEcCount = max($maxEcCount, $ecCount); - $this->dcdata[$key] = array_fill(0, $dcCount, null); - - foreach($this->dcdata[$key] as $a => $_z){ - $this->dcdata[$key][$a] = 0xff & $bitBuffer[$a + $offset]; - } - - [$num, $add] = $this->poly($key, $ecCount); - - foreach($this->ecdata[$key] as $c => $_){ - $modIndex = $c + $add; - $this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0; - } - - $offset += $dcCount; - $totalCodeCount += $rsBlockTotal; - } - - $data = array_fill(0, $totalCodeCount, null); - $index = 0; - - $mask = function(array $arr, int $count) use (&$data, &$index, $rsCount):void{ - for($x = 0; $x < $count; $x++){ - for($y = 0; $y < $rsCount; $y++){ - if($x < count($arr[$y])){ - $data[$index] = $arr[$y][$x]; - $index++; - } - } - } - }; - - $mask($this->dcdata, $maxDcCount); - $mask($this->ecdata, $maxEcCount); - - return $data; - } - - /** - * helper method for the polynomial operations - */ - protected function poly(int $key, int $count):array{ - $rsPoly = new Polynomial; - $modPoly = new Polynomial; - - for($i = 0; $i < $count; $i++){ - $modPoly->setNum([1, $modPoly->gexp($i)]); - $rsPoly->multiply($modPoly->getNum()); - } - - $rsPolyCount = count($rsPoly->getNum()); - - $modPoly - ->setNum($this->dcdata[$key], $rsPolyCount - 1) - ->mod($rsPoly->getNum()) - ; - - $this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null); - $num = $modPoly->getNum(); - - return [ - $num, - count($num) - count($this->ecdata[$key]), - ]; - } - } diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index cb9def0f0..bb7bf82d3 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -16,6 +16,7 @@ use chillerlan\QRCode\Common\{EccLevel, Version}; use chillerlan\QRCode\QRCode; use Closure; +use SplFixedArray; use function array_fill, array_push, array_unshift, count, floor, max, min, range; /** @@ -493,14 +494,14 @@ final class QRMatrix{ * * @see \chillerlan\QRCode\Data\QRData::maskECC() * - * @param int[] $data - * @param int $maskPattern + * @param \SplFixedArray $data + * @param int $maskPattern * * @return \chillerlan\QRCode\Data\QRMatrix */ - public function mapData(array $data, int $maskPattern):QRMatrix{ + public function mapData(SplFixedArray $data, int $maskPattern):QRMatrix{ $this->maskPattern = $maskPattern; - $byteCount = count($data); + $byteCount = $data->count(); $y = $this->moduleCount - 1; $inc = -1; $byteIndex = 0; diff --git a/tests/Data/DatainterfaceTestAbstract.php b/tests/Data/DatainterfaceTestAbstract.php index b69fad70b..0e0361dfc 100644 --- a/tests/Data/DatainterfaceTestAbstract.php +++ b/tests/Data/DatainterfaceTestAbstract.php @@ -52,14 +52,18 @@ abstract class DatainterfaceTestAbstract extends TestCase{ /** * Tests ecc masking and verifies against a sample */ - public function testMaskEcc():void{ +/* public function testMaskEcc():void{ $this->dataInterface->setData([$this->testdata]); $maskECC = $this->reflection->getMethod('maskECC'); $maskECC->setAccessible(true); - $this::assertSame($this->expected, $maskECC->invoke($this->dataInterface)); - } + $bitBuffer = $this->reflection->getProperty('bitBuffer'); + $bitBuffer->setAccessible(true); + $bb = $bitBuffer->getValue($this->dataInterface); + + $this::assertSame($this->expected, $maskECC->invokeArgs($this->dataInterface, [$bb->getBuffer()])); + }*/ /** * @see testInitMatrix() From f657785da2c577ba591b5f1d5b542aa9dd0aebb1 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 10 Jan 2021 09:11:21 +0100 Subject: [PATCH 34/78] :octocat: de-clutter rs block table --- src/Common/ReedSolomon.php | 6 +-- src/Common/Version.php | 96 +++++++++++++++++--------------------- 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/src/Common/ReedSolomon.php b/src/Common/ReedSolomon.php index 855ecfbcd..efacf4b68 100644 --- a/src/Common/ReedSolomon.php +++ b/src/Common/ReedSolomon.php @@ -45,14 +45,14 @@ final class ReedSolomon{ * @return \SplFixedArray */ public function interleaveEcBytes(BitBuffer $bitBuffer):SplFixedArray{ - [$l1, $l2, $b1, $b2] = $this->version->getRSBlocks($this->eccLevel->getLevel()); + [$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $this->version->getRSBlocks($this->eccLevel); $numRsBlocks = $l1 + $l2; $ecBytes = new SplFixedArray($numRsBlocks); - $rsBlocks = array_fill(0, $l1, [$b1, $b2]); + $rsBlocks = array_fill(0, $l1, [$numEccCodewords + $b1, $b1]); if($l2 > 0){ - $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1])); + $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$numEccCodewords + $b2, $b2])); } $dataBytes = SplFixedArray::fromArray($rsBlocks); diff --git a/src/Common/Version.php b/src/Common/Version.php index 3dc8c3aff..662154300 100644 --- a/src/Common/Version.php +++ b/src/Common/Version.php @@ -168,50 +168,48 @@ final class Version{ * ISO/IEC 18004:2000 Tables 13-22 * * @see http://www.thonky.com/qr-code-tutorial/error-correction-table - * - * @var int [][][] */ private const RSBLOCKS = [ - 1 => [[ 1, 0, 26, 19], [ 1, 0, 26, 16], [ 1, 0, 26, 13], [ 1, 0, 26, 9]], - 2 => [[ 1, 0, 44, 34], [ 1, 0, 44, 28], [ 1, 0, 44, 22], [ 1, 0, 44, 16]], - 3 => [[ 1, 0, 70, 55], [ 1, 0, 70, 44], [ 2, 0, 35, 17], [ 2, 0, 35, 13]], - 4 => [[ 1, 0, 100, 80], [ 2, 0, 50, 32], [ 2, 0, 50, 24], [ 4, 0, 25, 9]], - 5 => [[ 1, 0, 134, 108], [ 2, 0, 67, 43], [ 2, 2, 33, 15], [ 2, 2, 33, 11]], - 6 => [[ 2, 0, 86, 68], [ 4, 0, 43, 27], [ 4, 0, 43, 19], [ 4, 0, 43, 15]], - 7 => [[ 2, 0, 98, 78], [ 4, 0, 49, 31], [ 2, 4, 32, 14], [ 4, 1, 39, 13]], - 8 => [[ 2, 0, 121, 97], [ 2, 2, 60, 38], [ 4, 2, 40, 18], [ 4, 2, 40, 14]], - 9 => [[ 2, 0, 146, 116], [ 3, 2, 58, 36], [ 4, 4, 36, 16], [ 4, 4, 36, 12]], - 10 => [[ 2, 2, 86, 68], [ 4, 1, 69, 43], [ 6, 2, 43, 19], [ 6, 2, 43, 15]], - 11 => [[ 4, 0, 101, 81], [ 1, 4, 80, 50], [ 4, 4, 50, 22], [ 3, 8, 36, 12]], - 12 => [[ 2, 2, 116, 92], [ 6, 2, 58, 36], [ 4, 6, 46, 20], [ 7, 4, 42, 14]], - 13 => [[ 4, 0, 133, 107], [ 8, 1, 59, 37], [ 8, 4, 44, 20], [12, 4, 33, 11]], - 14 => [[ 3, 1, 145, 115], [ 4, 5, 64, 40], [11, 5, 36, 16], [11, 5, 36, 12]], - 15 => [[ 5, 1, 109, 87], [ 5, 5, 65, 41], [ 5, 7, 54, 24], [11, 7, 36, 12]], - 16 => [[ 5, 1, 122, 98], [ 7, 3, 73, 45], [15, 2, 43, 19], [ 3, 13, 45, 15]], - 17 => [[ 1, 5, 135, 107], [10, 1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]], - 18 => [[ 5, 1, 150, 120], [ 9, 4, 69, 43], [17, 1, 50, 22], [ 2, 19, 42, 14]], - 19 => [[ 3, 4, 141, 113], [ 3, 11, 70, 44], [17, 4, 47, 21], [ 9, 16, 39, 13]], - 20 => [[ 3, 5, 135, 107], [ 3, 13, 67, 41], [15, 5, 54, 24], [15, 10, 43, 15]], - 21 => [[ 4, 4, 144, 116], [17, 0, 68, 42], [17, 6, 50, 22], [19, 6, 46, 16]], - 22 => [[ 2, 7, 139, 111], [17, 0, 74, 46], [ 7, 16, 54, 24], [34, 0, 37, 13]], - 23 => [[ 4, 5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]], - 24 => [[ 6, 4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30, 2, 46, 16]], - 25 => [[ 8, 4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]], - 26 => [[10, 2, 142, 114], [19, 4, 74, 46], [28, 6, 50, 22], [33, 4, 46, 16]], - 27 => [[ 8, 4, 152, 122], [22, 3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]], - 28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]], - 29 => [[ 7, 7, 146, 116], [21, 7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]], - 30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]], - 31 => [[13, 3, 145, 115], [ 2, 29, 74, 46], [42, 1, 54, 24], [23, 28, 45, 15]], - 32 => [[17, 0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]], - 33 => [[17, 1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]], - 34 => [[13, 6, 145, 115], [14, 23, 74, 46], [44, 7, 54, 24], [59, 1, 46, 16]], - 35 => [[12, 7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]], - 36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]], - 37 => [[17, 4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]], - 38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]], - 39 => [[20, 4, 147, 117], [40, 7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]], - 40 => [[19, 6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]], + 1 => [[ 7, [[ 1, 19], [ 0, 0]]], [10, [[ 1, 16], [ 0, 0]]], [13, [[ 1, 13], [ 0, 0]]], [17, [[ 1, 9], [ 0, 0]]]], + 2 => [[10, [[ 1, 34], [ 0, 0]]], [16, [[ 1, 28], [ 0, 0]]], [22, [[ 1, 22], [ 0, 0]]], [28, [[ 1, 16], [ 0, 0]]]], + 3 => [[15, [[ 1, 55], [ 0, 0]]], [26, [[ 1, 44], [ 0, 0]]], [18, [[ 2, 17], [ 0, 0]]], [22, [[ 2, 13], [ 0, 0]]]], + 4 => [[20, [[ 1, 80], [ 0, 0]]], [18, [[ 2, 32], [ 0, 0]]], [26, [[ 2, 24], [ 0, 0]]], [16, [[ 4, 9], [ 0, 0]]]], + 5 => [[26, [[ 1, 108], [ 0, 0]]], [24, [[ 2, 43], [ 0, 0]]], [18, [[ 2, 15], [ 2, 16]]], [22, [[ 2, 11], [ 2, 12]]]], + 6 => [[18, [[ 2, 68], [ 0, 0]]], [16, [[ 4, 27], [ 0, 0]]], [24, [[ 4, 19], [ 0, 0]]], [28, [[ 4, 15], [ 0, 0]]]], + 7 => [[20, [[ 2, 78], [ 0, 0]]], [18, [[ 4, 31], [ 0, 0]]], [18, [[ 2, 14], [ 4, 15]]], [26, [[ 4, 13], [ 1, 14]]]], + 8 => [[24, [[ 2, 97], [ 0, 0]]], [22, [[ 2, 38], [ 2, 39]]], [22, [[ 4, 18], [ 2, 19]]], [26, [[ 4, 14], [ 2, 15]]]], + 9 => [[30, [[ 2, 116], [ 0, 0]]], [22, [[ 3, 36], [ 2, 37]]], [20, [[ 4, 16], [ 4, 17]]], [24, [[ 4, 12], [ 4, 13]]]], + 10 => [[18, [[ 2, 68], [ 2, 69]]], [26, [[ 4, 43], [ 1, 44]]], [24, [[ 6, 19], [ 2, 20]]], [28, [[ 6, 15], [ 2, 16]]]], + 11 => [[20, [[ 4, 81], [ 0, 0]]], [30, [[ 1, 50], [ 4, 51]]], [28, [[ 4, 22], [ 4, 23]]], [24, [[ 3, 12], [ 8, 13]]]], + 12 => [[24, [[ 2, 92], [ 2, 93]]], [22, [[ 6, 36], [ 2, 37]]], [26, [[ 4, 20], [ 6, 21]]], [28, [[ 7, 14], [ 4, 15]]]], + 13 => [[26, [[ 4, 107], [ 0, 0]]], [22, [[ 8, 37], [ 1, 38]]], [24, [[ 8, 20], [ 4, 21]]], [22, [[12, 11], [ 4, 12]]]], + 14 => [[30, [[ 3, 115], [ 1, 116]]], [24, [[ 4, 40], [ 5, 41]]], [20, [[11, 16], [ 5, 17]]], [24, [[11, 12], [ 5, 13]]]], + 15 => [[22, [[ 5, 87], [ 1, 88]]], [24, [[ 5, 41], [ 5, 42]]], [30, [[ 5, 24], [ 7, 25]]], [24, [[11, 12], [ 7, 13]]]], + 16 => [[24, [[ 5, 98], [ 1, 99]]], [28, [[ 7, 45], [ 3, 46]]], [24, [[15, 19], [ 2, 20]]], [30, [[ 3, 15], [13, 16]]]], + 17 => [[28, [[ 1, 107], [ 5, 108]]], [28, [[10, 46], [ 1, 47]]], [28, [[ 1, 22], [15, 23]]], [28, [[ 2, 14], [17, 15]]]], + 18 => [[30, [[ 5, 120], [ 1, 121]]], [26, [[ 9, 43], [ 4, 44]]], [28, [[17, 22], [ 1, 23]]], [28, [[ 2, 14], [19, 15]]]], + 19 => [[28, [[ 3, 113], [ 4, 114]]], [26, [[ 3, 44], [11, 45]]], [26, [[17, 21], [ 4, 22]]], [26, [[ 9, 13], [16, 14]]]], + 20 => [[28, [[ 3, 107], [ 5, 108]]], [26, [[ 3, 41], [13, 42]]], [30, [[15, 24], [ 5, 25]]], [28, [[15, 15], [10, 16]]]], + 21 => [[28, [[ 4, 116], [ 4, 117]]], [26, [[17, 42], [ 0, 0]]], [28, [[17, 22], [ 6, 23]]], [30, [[19, 16], [ 6, 17]]]], + 22 => [[28, [[ 2, 111], [ 7, 112]]], [28, [[17, 46], [ 0, 0]]], [30, [[ 7, 24], [16, 25]]], [24, [[34, 13], [ 0, 0]]]], + 23 => [[30, [[ 4, 121], [ 5, 122]]], [28, [[ 4, 47], [14, 48]]], [30, [[11, 24], [14, 25]]], [30, [[16, 15], [14, 16]]]], + 24 => [[30, [[ 6, 117], [ 4, 118]]], [28, [[ 6, 45], [14, 46]]], [30, [[11, 24], [16, 25]]], [30, [[30, 16], [ 2, 17]]]], + 25 => [[26, [[ 8, 106], [ 4, 107]]], [28, [[ 8, 47], [13, 48]]], [30, [[ 7, 24], [22, 25]]], [30, [[22, 15], [13, 16]]]], + 26 => [[28, [[10, 114], [ 2, 115]]], [28, [[19, 46], [ 4, 47]]], [28, [[28, 22], [ 6, 23]]], [30, [[33, 16], [ 4, 17]]]], + 27 => [[30, [[ 8, 122], [ 4, 123]]], [28, [[22, 45], [ 3, 46]]], [30, [[ 8, 23], [26, 24]]], [30, [[12, 15], [28, 16]]]], + 28 => [[30, [[ 3, 117], [10, 118]]], [28, [[ 3, 45], [23, 46]]], [30, [[ 4, 24], [31, 25]]], [30, [[11, 15], [31, 16]]]], + 29 => [[30, [[ 7, 116], [ 7, 117]]], [28, [[21, 45], [ 7, 46]]], [30, [[ 1, 23], [37, 24]]], [30, [[19, 15], [26, 16]]]], + 30 => [[30, [[ 5, 115], [10, 116]]], [28, [[19, 47], [10, 48]]], [30, [[15, 24], [25, 25]]], [30, [[23, 15], [25, 16]]]], + 31 => [[30, [[13, 115], [ 3, 116]]], [28, [[ 2, 46], [29, 47]]], [30, [[42, 24], [ 1, 25]]], [30, [[23, 15], [28, 16]]]], + 32 => [[30, [[17, 115], [ 0, 0]]], [28, [[10, 46], [23, 47]]], [30, [[10, 24], [35, 25]]], [30, [[19, 15], [35, 16]]]], + 33 => [[30, [[17, 115], [ 1, 116]]], [28, [[14, 46], [21, 47]]], [30, [[29, 24], [19, 25]]], [30, [[11, 15], [46, 16]]]], + 34 => [[30, [[13, 115], [ 6, 116]]], [28, [[14, 46], [23, 47]]], [30, [[44, 24], [ 7, 25]]], [30, [[59, 16], [ 1, 17]]]], + 35 => [[30, [[12, 121], [ 7, 122]]], [28, [[12, 47], [26, 48]]], [30, [[39, 24], [14, 25]]], [30, [[22, 15], [41, 16]]]], + 36 => [[30, [[ 6, 121], [14, 122]]], [28, [[ 6, 47], [34, 48]]], [30, [[46, 24], [10, 25]]], [30, [[ 2, 15], [64, 16]]]], + 37 => [[30, [[17, 122], [ 4, 123]]], [28, [[29, 46], [14, 47]]], [30, [[49, 24], [10, 25]]], [30, [[24, 15], [46, 16]]]], + 38 => [[30, [[ 4, 122], [18, 123]]], [28, [[13, 46], [32, 47]]], [30, [[48, 24], [14, 25]]], [30, [[42, 15], [32, 16]]]], + 39 => [[30, [[20, 117], [ 4, 118]]], [28, [[40, 47], [ 7, 48]]], [30, [[43, 24], [22, 25]]], [30, [[10, 15], [67, 16]]]], + 40 => [[30, [[19, 118], [ 6, 119]]], [28, [[18, 47], [31, 48]]], [30, [[34, 24], [34, 25]]], [30, [[20, 15], [61, 16]]]], ]; private const TOTAL_CODEWORDS = [ @@ -309,23 +307,17 @@ final class Version{ /** * the maximum character count for the given $mode and $eccLevel */ - public function getMaxLengthForMode(int $mode, int $eccLevel):?int{ - return self::MAX_LENGTH[$this->version][$mode][$eccLevel] ?? null; + public function getMaxLengthForMode(int $mode, EccLevel $eccLevel):?int{ + return self::MAX_LENGTH[$this->version][$mode][$eccLevel->getOrdinal()] ?? null; } /** * returns ECC block information for the given $version and $eccLevel * * @return int[] - * @throws \chillerlan\QRCode\QRCodeException */ - public function getRSBlocks(int $eccLevel):array{ - - if((0b11 & $eccLevel) !== $eccLevel){ - throw new QRCodeException('invalid ECC level'); - } - - return self::RSBLOCKS[$this->version][EccLevel::MODES[$eccLevel]]; + public function getRSBlocks(EccLevel $eccLevel):array{ + return self::RSBLOCKS[$this->version][$eccLevel->getOrdinal()]; } /** From 0f50b90b96a80cee62025bc87edbab3a3b7aedca Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 10 Jan 2021 09:14:48 +0100 Subject: [PATCH 35/78] :shower: clarify param name --- src/Data/AlphaNum.php | 4 ++-- src/Data/Byte.php | 4 ++-- src/Data/ECI.php | 2 +- src/Data/Kanji.php | 4 ++-- src/Data/Number.php | 4 ++-- src/Data/QRDataModeInterface.php | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index 8252b17d2..797713ebd 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -65,12 +65,12 @@ final class AlphaNum extends QRDataModeAbstract{ /** * @inheritdoc */ - public function write(BitBuffer $bitBuffer, int $version):void{ + public function write(BitBuffer $bitBuffer, int $versionNumber):void{ $len = $this->getCharCount(); $bitBuffer ->put($this->datamode, 4) - ->put($len, Mode::getLengthBitsForVersion($this->datamode, $version)) + ->put($len, Mode::getLengthBitsForVersion($this->datamode, $versionNumber)) ; // encode 2 characters in 11 bits diff --git a/src/Data/Byte.php b/src/Data/Byte.php index 0a57590cc..57e4ca64e 100644 --- a/src/Data/Byte.php +++ b/src/Data/Byte.php @@ -44,12 +44,12 @@ final class Byte extends QRDataModeAbstract{ /** * @inheritdoc */ - public function write(BitBuffer $bitBuffer, int $version):void{ + public function write(BitBuffer $bitBuffer, int $versionNumber):void{ $len = $this->getCharCount(); $bitBuffer ->put($this->datamode, 4) - ->put($len, Mode::getLengthBitsForVersion($this->datamode, $version)) + ->put($len, Mode::getLengthBitsForVersion($this->datamode, $versionNumber)) ; $i = 0; diff --git a/src/Data/ECI.php b/src/Data/ECI.php index 54a6867ec..8997dfe3c 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -87,7 +87,7 @@ class ECI extends QRDataModeAbstract{ /** * @inheritDoc */ - public function write(BitBuffer $bitBuffer, int $version):void{ + public function write(BitBuffer $bitBuffer, int $versionNumber):void{ $bitBuffer ->put($this->datamode, 4) ->put($this->encoding, 8) diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index 8cb87c4f6..27ae28615 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -73,11 +73,11 @@ final class Kanji extends QRDataModeAbstract{ * * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence */ - public function write(BitBuffer $bitBuffer, int $version):void{ + public function write(BitBuffer $bitBuffer, int $versionNumber):void{ $bitBuffer ->put($this->datamode, 4) - ->put($this->getCharCount(), Mode::getLengthBitsForVersion($this->datamode, $version)) + ->put($this->getCharCount(), Mode::getLengthBitsForVersion($this->datamode, $versionNumber)) ; $len = strlen($this->data); diff --git a/src/Data/Number.php b/src/Data/Number.php index cbcd78cd7..8505614fd 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -58,12 +58,12 @@ final class Number extends QRDataModeAbstract{ /** * @inheritdoc */ - public function write(BitBuffer $bitBuffer, int $version):void{ + public function write(BitBuffer $bitBuffer, int $versionNumber):void{ $len = $this->getCharCount(); $bitBuffer ->put($this->datamode, 4) - ->put($len, Mode::getLengthBitsForVersion($this->datamode, $version)) + ->put($len, Mode::getLengthBitsForVersion($this->datamode, $versionNumber)) ; $i = 0; diff --git a/src/Data/QRDataModeInterface.php b/src/Data/QRDataModeInterface.php index 91839a442..a53b633ce 100644 --- a/src/Data/QRDataModeInterface.php +++ b/src/Data/QRDataModeInterface.php @@ -40,6 +40,6 @@ interface QRDataModeInterface{ * * @see \chillerlan\QRCode\Data\QRData::writeBitBuffer() */ - public function write(BitBuffer $bitBuffer, int $version):void; + public function write(BitBuffer $bitBuffer, int $versionNumber):void; } From a80b9b21404eb719cf901d6c81b9bc49c8e418d0 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 10 Jan 2021 09:16:32 +0100 Subject: [PATCH 36/78] :shower: --- src/Data/QRMatrix.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index bb7bf82d3..74077513f 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -17,7 +17,7 @@ use chillerlan\QRCode\QRCode; use Closure; use SplFixedArray; -use function array_fill, array_push, array_unshift, count, floor, max, min, range; +use function array_fill, array_push, array_unshift, floor, max, min, range; /** * Holds a numerical representation of the final QR Code; From 043137491a3cd138ab55aa3c1ff59d5a1e923611 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 10 Jan 2021 09:18:40 +0100 Subject: [PATCH 37/78] :shower: --- src/Common/Version.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Common/Version.php b/src/Common/Version.php index 662154300..37c91bb22 100644 --- a/src/Common/Version.php +++ b/src/Common/Version.php @@ -313,8 +313,6 @@ final class Version{ /** * returns ECC block information for the given $version and $eccLevel - * - * @return int[] */ public function getRSBlocks(EccLevel $eccLevel):array{ return self::RSBLOCKS[$this->version][$eccLevel->getOrdinal()]; From 32b6a5ed778bdc8af64e7c431908857075df5e83 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 16 Jan 2021 19:25:13 +0100 Subject: [PATCH 38/78] :sparkles: --- NOTICE | 13 ++ README.md | 3 + composer.json | 12 +- src/Common/GF256.php | 152 +++++++++++++++++++ src/Common/GenericGFPoly.php | 278 +++++++++++++++++++++++++++++++++++ 5 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 NOTICE create mode 100644 src/Common/GF256.php create mode 100644 src/Common/GenericGFPoly.php diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..98764e2ba --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +Copyright 2007 ZXing authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index cea5509a2..fa36b5044 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,9 @@ Hi, please check out my other projects that are way cooler than qrcodes! ### Disclaimer! I don't take responsibility for molten CPUs, misled applications, failed log-ins etc.. Use at your own risk! +#### License notice +Parts of this code are [ported to php](https://github.com/khanamiryan/php-qrcode-detector-decoder) from the [ZXing project](https://github.com/zxing/zxing) and licensed under the Apache License, Version 2.0. + #### Trademark Notice The word "QR Code" is registered trademark of *DENSO WAVE INCORPORATED*
diff --git a/composer.json b/composer.json index 5ec4031be..536bdf56f 100644 --- a/composer.json +++ b/composer.json @@ -6,12 +6,20 @@ "minimum-stability": "stable", "type": "library", "keywords": [ - "QR code", "qrcode", "qr", "qrcode-generator", "phpqrcode" + "QR code", "qrcode", "qr", "qrcode-generator", "phpqrcode", "qrcode-reader" ], "authors": [ { "name": "Kazuhiko Arase", - "homepage": "https://github.com/kazuhikoarase" + "homepage": "https://github.com/kazuhikoarase/qrcode-generator" + }, + { + "name":"ZXing Authors", + "homepage": "https://github.com/zxing/zxing" + }, + { + "name": "Ashot Khanamiryan", + "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder" }, { "name": "Smiley", diff --git a/src/Common/GF256.php b/src/Common/GF256.php new file mode 100644 index 000000000..12c4ad099 --- /dev/null +++ b/src/Common/GF256.php @@ -0,0 +1,152 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use InvalidArgumentException; + +use function array_fill; + +/** + *

This class contains utility methods for performing mathematical operations over + * the Galois Fields. Operations use a given primitive polynomial in calculations.

+ * + *

Throughout this package, elements of the GF are represented as an {@code int} + * for convenience and speed (but at the cost of memory). + *

+ * + * @author Sean Owen + * @author David Olivier + */ +final class GF256{ + + /** + * irreducible polynomial whose coefficients are represented by the bits of an int, + * where the least-significant bit represents the constant coefficient + */ +# private int $primitive = 0x011D; + + private const logTable = [ + null, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, + 4, 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, + 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, + 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, + 6, 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, + 54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, + 30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, + 202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, + 7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, + 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, + 55, 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, + 242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, + 31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, + 108, 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, + 203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215, + 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175, + ]; + + private const expTable = [ + 1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, + 76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192, + 157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, + 70, 140, 5, 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, + 95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240, + 253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, + 217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206, + 129, 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, + 133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, + 168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, + 230, 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, + 227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, + 130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, + 81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, + 18, 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, + 44, 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1, + ]; + + /** + * Implements both addition and subtraction -- they are the same in GF(size). + * + * @return int sum/difference of a and b + */ + public static function addOrSubtract(int $a, int $b):int{ + return $a ^ $b; + } + + /** + * @return GenericGFPoly the monomial representing coefficient * x^degree + */ + public static function buildMonomial(int $degree, int $coefficient):GenericGFPoly{ + + if($degree < 0){ + throw new InvalidArgumentException(); + } + + $coefficients = array_fill(0, $degree + 1, 0); + $coefficients[0] = $coefficient; + + return new GenericGFPoly($coefficients); + } + + /** + * @return int 2 to the power of a in GF(size) + */ + public static function exp(int $a):int{ + + if($a < 0){ + $a += 255; + } + elseif($a >= 256){ + $a -= 255; + } + + return self::expTable[$a]; + } + + /** + * @return int base 2 log of a in GF(size) + */ + public static function log(int $a):int{ + + if($a < 1){ + throw new InvalidArgumentException(); + } + + return self::logTable[$a]; + } + + /** + * @return int multiplicative inverse of a + */ + public static function inverse(int $a):int{ + + if($a === 0){ + throw new InvalidArgumentException(); + } + + return self::expTable[256 - self::logTable[$a] - 1]; + } + + /** + * @return int product of a and b in GF(size) + */ + public static function multiply(int $a, int $b):int{ + + if($a === 0 || $b === 0){ + return 0; + } + + return self::expTable[(self::logTable[$a] + self::logTable[$b]) % 255]; + } + +} diff --git a/src/Common/GenericGFPoly.php b/src/Common/GenericGFPoly.php new file mode 100644 index 000000000..29078b672 --- /dev/null +++ b/src/Common/GenericGFPoly.php @@ -0,0 +1,278 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use InvalidArgumentException; + +use function array_fill, array_slice, array_splice, count; + +/** + *

Represents a polynomial whose coefficients are elements of a GF. + * Instances of this class are immutable.

+ * + *

Much credit is due to William Rucklidge since portions of this code are an indirect + * port of his C++ Reed-Solomon implementation.

+ * + * @author Sean Owen + */ +final class GenericGFPoly{ + + private array $coefficients; + + /** + * @param array|null $coefficients array coefficients as ints representing elements of GF(size), arranged + * from most significant (highest-power term) coefficient to least significant + * @param int|null $degree + * + * @throws \InvalidArgumentException if argument is null or empty, or if leading coefficient is 0 and this is not a + * constant polynomial (that is, it is not the monomial "0") + */ + public function __construct(array $coefficients, int $degree = null){ + $degree ??= 0; + + if(empty($coefficients)){ + throw new InvalidArgumentException('arg $coefficients is empty'); + } + + if($degree < 0){ + throw new InvalidArgumentException('negative degree'); + } + + $coefficientsLength = count($coefficients); + + // Leading term must be non-zero for anything except the constant polynomial "0" + $firstNonZero = 0; + + while($firstNonZero < $coefficientsLength && $coefficients[$firstNonZero] === 0){ + $firstNonZero++; + } + + if($firstNonZero === $coefficientsLength){ + $this->coefficients = [0]; + } + else{ + $this->coefficients = array_fill(0, $coefficientsLength - $firstNonZero + $degree, 0); + + for($i = 0; $i < $coefficientsLength - $firstNonZero; $i++){ + $this->coefficients[$i] = $coefficients[$i + $firstNonZero]; + } + } + } + + /** + * @return int $coefficient of x^degree term in this polynomial + */ + public function getCoefficient(int $degree):int{ + return $this->coefficients[count($this->coefficients) - 1 - $degree]; + } + + /** + * @return int[] + */ + public function getCoefficients():array{ + return $this->coefficients; + } + + /** + * @return int $degree of this polynomial + */ + public function getDegree():int{ + return count($this->coefficients) - 1; + } + + /** + * @return bool true if this polynomial is the monomial "0" + */ + public function isZero():bool{ + return $this->coefficients[0] === 0; + } + + /** + * @return int evaluation of this polynomial at a given point + */ + public function evaluateAt(int $a):int{ + + if($a === 0){ + // Just return the x^0 coefficient + return $this->getCoefficient(0); + } + + $result = 0; + + foreach($this->coefficients as $c){ + // if $a === 1 just the sum of the coefficients + $result = GF256::addOrSubtract(($a === 1 ? $result : GF256::multiply($a, $result)), $c); + } + + return $result; + } + + /** + * @param \chillerlan\QRCode\Common\GenericGFPoly $other + * + * @return \chillerlan\QRCode\Common\GenericGFPoly + */ + public function multiply(GenericGFPoly $other):GenericGFPoly{ + + if($this->isZero() || $other->isZero()){ + return new self([0]); + } + + $product = array_fill(0, count($this->coefficients) + count($other->coefficients) - 1, 0); + + foreach($this->coefficients as $i => $aCoeff){ + foreach($other->coefficients as $j => $bCoeff){ + $product[$i + $j] ^= GF256::multiply($aCoeff, $bCoeff); + } + } + + return new self($product); + } + + /** + * @param \chillerlan\QRCode\Common\GenericGFPoly $other + * + * @return \chillerlan\QRCode\Common\GenericGFPoly[] [quotient, remainder] + */ + public function divide(GenericGFPoly $other):array{ + + if($other->isZero()){ + throw new InvalidArgumentException('Division by 0'); + } + + $quotient = new self([0]); + $remainder = clone $this; + + $denominatorLeadingTerm = $other->getCoefficient($other->getDegree()); + $inverseDenominatorLeadingTerm = GF256::inverse($denominatorLeadingTerm); + + while($remainder->getDegree() >= $other->getDegree() && !$remainder->isZero()){ + $scale = GF256::multiply($remainder->getCoefficient($remainder->getDegree()), $inverseDenominatorLeadingTerm); + $diff = $remainder->getDegree() - $other->getDegree(); + $quotient = $quotient->addOrSubtract(GF256::buildMonomial($diff, $scale)); + $remainder = $remainder->addOrSubtract($other->multiplyByMonomial($diff, $scale)); + } + + return [$quotient, $remainder]; + + } + + /** + * @param int $scalar + * + * @return \chillerlan\QRCode\Common\GenericGFPoly + */ + public function multiplyInt(int $scalar):GenericGFPoly{ + + if($scalar === 0){ + return new self([0]); + } + + if($scalar === 1){ + return $this; + } + + $product = array_fill(0, count($this->coefficients), 0); + + foreach($this->coefficients as $i => $c){ + $product[$i] = GF256::multiply($c, $scalar); + } + + return new self($product); + } + + /** + * @param int $degree + * @param int $coefficient + * + * @return \chillerlan\QRCode\Common\GenericGFPoly + */ + public function multiplyByMonomial(int $degree, int $coefficient):GenericGFPoly{ + + if($degree < 0){ + throw new InvalidArgumentException(); + } + + if($coefficient === 0){ + return new self([0]); + } + + $product = array_fill(0, count($this->coefficients) + $degree, 0); + + foreach($this->coefficients as $i => $c){ + $product[$i] = GF256::multiply($c, $coefficient); + } + + return new self($product); + } + + /** + * @param \chillerlan\QRCode\Common\GenericGFPoly $other + * + * @return \chillerlan\QRCode\Common\GenericGFPoly + */ + public function mod(GenericGFPoly $other):GenericGFPoly{ + + if(count($this->coefficients) - count($other->coefficients) < 0){ + return $this; + } + + $ratio = GF256::log($this->coefficients[0]) - GF256::log($other->coefficients[0]); + + foreach($other->coefficients as $i => $c){ + $this->coefficients[$i] ^= GF256::exp(GF256::log($c) + $ratio); + } + + return (new self($this->coefficients))->mod($other); + } + + /** + * @param \chillerlan\QRCode\Common\GenericGFPoly $other + * + * @return \chillerlan\QRCode\Common\GenericGFPoly + */ + public function addOrSubtract(GenericGFPoly $other):GenericGFPoly{ + + if($this->isZero()){ + return $other; + } + + if($other->isZero()){ + return $this; + } + + $smallerCoefficients = $this->coefficients; + $largerCoefficients = $other->coefficients; + + if(count($smallerCoefficients) > count($largerCoefficients)){ + $temp = $smallerCoefficients; + $smallerCoefficients = $largerCoefficients; + $largerCoefficients = $temp; + } + + $sumDiff = array_fill(0, count($largerCoefficients), 0); + $lengthDiff = count($largerCoefficients) - count($smallerCoefficients); + // Copy high-order terms only found in higher-degree polynomial's coefficients + array_splice($sumDiff, 0, $lengthDiff, array_slice($largerCoefficients, 0, $lengthDiff)); + + $countLargerCoefficients = count($largerCoefficients); + + for($i = $lengthDiff; $i < $countLargerCoefficients; $i++){ + $sumDiff[$i] = GF256::addOrSubtract($smallerCoefficients[$i - $lengthDiff], $largerCoefficients[$i]); + } + + return new self($sumDiff); + } + +} From 4ce932aee3085d39301aea653da96f7539b4d73b Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 16 Jan 2021 19:45:30 +0100 Subject: [PATCH 39/78] :octocat: use ZXing classes for RS encoding, cleanup --- src/Common/ReedSolomon.php | 125 --------------------- src/Common/ReedSolomonEncoder.php | 118 ++++++++++++++++++++ src/Data/QRData.php | 5 +- src/Helpers/Polynomial.php | 178 ------------------------------ tests/Helpers/PolynomialTest.php | 42 ------- 5 files changed, 120 insertions(+), 348 deletions(-) delete mode 100644 src/Common/ReedSolomon.php create mode 100644 src/Common/ReedSolomonEncoder.php delete mode 100644 src/Helpers/Polynomial.php delete mode 100644 tests/Helpers/PolynomialTest.php diff --git a/src/Common/ReedSolomon.php b/src/Common/ReedSolomon.php deleted file mode 100644 index efacf4b68..000000000 --- a/src/Common/ReedSolomon.php +++ /dev/null @@ -1,125 +0,0 @@ - - * @copyright 2021 smiley - * @license MIT - */ - -namespace chillerlan\QRCode\Common; - -use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\Helpers\Polynomial; - -use SplFixedArray; -use function array_fill, array_merge, count, max; - -/** - * ISO/IEC 18004:2000 Section 8.5 ff - * - * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding - */ -final class ReedSolomon{ - - private Version $version; - private EccLevel $eccLevel; - - private SplFixedArray $interleavedData; - private int $interleavedDataIndex; - - /** - * ReedSolomon constructor. - */ - public function __construct(Version $version, EccLevel $eccLevel){ - $this->version = $version; - $this->eccLevel = $eccLevel; - } - - /** - * ECC interleaving - * - * @return \SplFixedArray - */ - public function interleaveEcBytes(BitBuffer $bitBuffer):SplFixedArray{ - [$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $this->version->getRSBlocks($this->eccLevel); - - $numRsBlocks = $l1 + $l2; - $ecBytes = new SplFixedArray($numRsBlocks); - $rsBlocks = array_fill(0, $l1, [$numEccCodewords + $b1, $b1]); - - if($l2 > 0){ - $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$numEccCodewords + $b2, $b2])); - } - - $dataBytes = SplFixedArray::fromArray($rsBlocks); - $maxDataBytes = 0; - $maxEcBytes = 0; - $dataByteOffset = 0; - $bitBufferData = $bitBuffer->getBuffer(); - - foreach($rsBlocks as $key => $block){ - [$rsBlockTotal, $dataByteCount] = $block; - - $ecByteCount = $rsBlockTotal - $dataByteCount; - $maxDataBytes = max($maxDataBytes, $dataByteCount); - $maxEcBytes = max($maxEcBytes, $ecByteCount); - $dataBytes[$key] = new SplFixedArray($dataByteCount); - - foreach($dataBytes[$key] as $i => $_){ - $dataBytes[$key][$i] = $bitBufferData[$i + $dataByteOffset] & 0xff; - } - - $rsPoly = new Polynomial; - $modPoly = new Polynomial; - - for($i = 0; $i < $ecByteCount; $i++){ - $modPoly->setNum([1, $modPoly->gexp($i)]); - $rsPoly->multiply($modPoly->getNum()); - } - - $rsPolyCount = count($rsPoly->getNum()) - 1; - - $modPoly - ->setNum($dataBytes[$key]->toArray(), $rsPolyCount) - ->mod($rsPoly->getNum()) - ; - - $ecBytes[$key] = new SplFixedArray($rsPolyCount); - $num = $modPoly->getNum(); - $count = count($num) - count($ecBytes[$key]); - - foreach($ecBytes[$key] as $i => $_){ - $modIndex = $i + $count; - $ecBytes[$key][$i] = $modIndex >= 0 ? $num[$modIndex] : 0; - } - - $dataByteOffset += $dataByteCount; - } - - $this->interleavedData = new SplFixedArray($this->version->getTotalCodewords()); - $this->interleavedDataIndex = 0; - - $this->interleave($dataBytes, $maxDataBytes, $numRsBlocks); - $this->interleave($ecBytes, $maxEcBytes, $numRsBlocks); - - return $this->interleavedData; - } - - /** - * - */ - private function interleave(SplFixedArray $byteArray, int $maxBytes, int $numRsBlocks):void{ - for($x = 0; $x < $maxBytes; $x++){ - for($y = 0; $y < $numRsBlocks; $y++){ - if($x < count($byteArray[$y])){ - $this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x]; - } - } - } - } - -} diff --git a/src/Common/ReedSolomonEncoder.php b/src/Common/ReedSolomonEncoder.php new file mode 100644 index 000000000..e82e2355b --- /dev/null +++ b/src/Common/ReedSolomonEncoder.php @@ -0,0 +1,118 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\Helpers\BitBuffer; +use SplFixedArray; + +use function array_fill, array_merge, count, max; + +/** + * ISO/IEC 18004:2000 Section 8.5 ff + * + * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding + */ +final class ReedSolomonEncoder{ + + private SplFixedArray $interleavedData; + private int $interleavedDataIndex; + + /** + * ECC interleaving + * + * @return \SplFixedArray + */ + public function interleaveEcBytes(BitBuffer $bitBuffer, Version $version, EccLevel $eccLevel):SplFixedArray{ + [$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $version->getRSBlocks($eccLevel); + + $rsBlocks = array_fill(0, $l1, [$numEccCodewords + $b1, $b1]); + + if($l2 > 0){ + $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$numEccCodewords + $b2, $b2])); + } + + $bitBufferData = $bitBuffer->getBuffer(); + $dataBytes = []; + $ecBytes = []; + $maxDataBytes = 0; + $maxEcBytes = 0; + $dataByteOffset = 0; + + foreach($rsBlocks as $key => $block){ + [$rsBlockTotal, $dataByteCount] = $block; + + $dataBytes[$key] = []; + + for($i = 0; $i < $dataByteCount; $i++){ + $dataBytes[$key][$i] = $bitBufferData[$i + $dataByteOffset] & 0xff; + } + + $ecByteCount = $rsBlockTotal - $dataByteCount; + $ecBytes[$key] = $this->generateEcBytes($dataBytes[$key], $ecByteCount); + $maxDataBytes = max($maxDataBytes, $dataByteCount); + $maxEcBytes = max($maxEcBytes, $ecByteCount); + $dataByteOffset += $dataByteCount; + } + + $this->interleavedData = new SplFixedArray($version->getTotalCodewords()); + $this->interleavedDataIndex = 0; + $numRsBlocks = $l1 + $l2; + + $this->interleave($dataBytes, $maxDataBytes, $numRsBlocks); + $this->interleave($ecBytes, $maxEcBytes, $numRsBlocks); + + return $this->interleavedData; + } + + /** + * + */ + private function generateEcBytes(array $dataBytes, int $ecByteCount):array{ + $rsPoly = new GenericGFPoly([1]); + + for($i = 0; $i < $ecByteCount; $i++){ + $rsPoly = $rsPoly->multiply(new GenericGFPoly([1, GF256::exp($i)])); + } + + $rsPolyDegree = $rsPoly->getDegree(); + + $num = (new GenericGFPoly($dataBytes, $rsPolyDegree)) + ->mod($rsPoly) + ->getCoefficients() + ; + + $ecBytes = array_fill(0, $rsPolyDegree, 0); + $count = count($num) - count($ecBytes); + + foreach($ecBytes as $i => &$val){ + $modIndex = $i + $count; + $val = $modIndex >= 0 ? $num[$modIndex] : 0; + } + + return $ecBytes; + } + + /** + * + */ + private function interleave(array $byteArray, int $maxBytes, int $numRsBlocks):void{ + for($x = 0; $x < $maxBytes; $x++){ + for($y = 0; $y < $numRsBlocks; $y++){ + if($x < count($byteArray[$y])){ + $this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x]; + } + } + } + } + +} diff --git a/src/Data/QRData.php b/src/Data/QRData.php index 7df57874d..9d9795164 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{EccLevel, Mode, ReedSolomon, Version}; +use chillerlan\QRCode\Common\{EccLevel, Mode, ReedSolomonEncoder, Version}; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\Helpers\BitBuffer; use chillerlan\Settings\SettingsContainerInterface; @@ -99,8 +99,7 @@ class QRData{ * returns a fresh matrix object with the data written for the given $maskPattern */ public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{ - $rs = new ReedSolomon($this->version, $this->eccLevel); - $data = $rs->interleaveEcBytes($this->bitBuffer); + $data = (new ReedSolomonEncoder)->interleaveEcBytes($this->bitBuffer, $this->version, $this->eccLevel); return (new QRMatrix($this->version, $this->eccLevel)) ->init($maskPattern, $test) diff --git a/src/Helpers/Polynomial.php b/src/Helpers/Polynomial.php deleted file mode 100644 index c42e0831c..000000000 --- a/src/Helpers/Polynomial.php +++ /dev/null @@ -1,178 +0,0 @@ - - * @copyright 2015 Smiley - * @license MIT - */ - -namespace chillerlan\QRCode\Helpers; - -use chillerlan\QRCode\QRCodeException; - -use function array_fill, count, sprintf; - -/** - * Polynomial long division helpers - * - * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding - */ -final class Polynomial{ - - /** - * @see http://www.thonky.com/qr-code-tutorial/log-antilog-table - */ - protected const table = [ - [ 1, 0], [ 2, 0], [ 4, 1], [ 8, 25], [ 16, 2], [ 32, 50], [ 64, 26], [128, 198], - [ 29, 3], [ 58, 223], [116, 51], [232, 238], [205, 27], [135, 104], [ 19, 199], [ 38, 75], - [ 76, 4], [152, 100], [ 45, 224], [ 90, 14], [180, 52], [117, 141], [234, 239], [201, 129], - [143, 28], [ 3, 193], [ 6, 105], [ 12, 248], [ 24, 200], [ 48, 8], [ 96, 76], [192, 113], - [157, 5], [ 39, 138], [ 78, 101], [156, 47], [ 37, 225], [ 74, 36], [148, 15], [ 53, 33], - [106, 53], [212, 147], [181, 142], [119, 218], [238, 240], [193, 18], [159, 130], [ 35, 69], - [ 70, 29], [140, 181], [ 5, 194], [ 10, 125], [ 20, 106], [ 40, 39], [ 80, 249], [160, 185], - [ 93, 201], [186, 154], [105, 9], [210, 120], [185, 77], [111, 228], [222, 114], [161, 166], - [ 95, 6], [190, 191], [ 97, 139], [194, 98], [153, 102], [ 47, 221], [ 94, 48], [188, 253], - [101, 226], [202, 152], [137, 37], [ 15, 179], [ 30, 16], [ 60, 145], [120, 34], [240, 136], - [253, 54], [231, 208], [211, 148], [187, 206], [107, 143], [214, 150], [177, 219], [127, 189], - [254, 241], [225, 210], [223, 19], [163, 92], [ 91, 131], [182, 56], [113, 70], [226, 64], - [217, 30], [175, 66], [ 67, 182], [134, 163], [ 17, 195], [ 34, 72], [ 68, 126], [136, 110], - [ 13, 107], [ 26, 58], [ 52, 40], [104, 84], [208, 250], [189, 133], [103, 186], [206, 61], - [129, 202], [ 31, 94], [ 62, 155], [124, 159], [248, 10], [237, 21], [199, 121], [147, 43], - [ 59, 78], [118, 212], [236, 229], [197, 172], [151, 115], [ 51, 243], [102, 167], [204, 87], - [133, 7], [ 23, 112], [ 46, 192], [ 92, 247], [184, 140], [109, 128], [218, 99], [169, 13], - [ 79, 103], [158, 74], [ 33, 222], [ 66, 237], [132, 49], [ 21, 197], [ 42, 254], [ 84, 24], - [168, 227], [ 77, 165], [154, 153], [ 41, 119], [ 82, 38], [164, 184], [ 85, 180], [170, 124], - [ 73, 17], [146, 68], [ 57, 146], [114, 217], [228, 35], [213, 32], [183, 137], [115, 46], - [230, 55], [209, 63], [191, 209], [ 99, 91], [198, 149], [145, 188], [ 63, 207], [126, 205], - [252, 144], [229, 135], [215, 151], [179, 178], [123, 220], [246, 252], [241, 190], [255, 97], - [227, 242], [219, 86], [171, 211], [ 75, 171], [150, 20], [ 49, 42], [ 98, 93], [196, 158], - [149, 132], [ 55, 60], [110, 57], [220, 83], [165, 71], [ 87, 109], [174, 65], [ 65, 162], - [130, 31], [ 25, 45], [ 50, 67], [100, 216], [200, 183], [141, 123], [ 7, 164], [ 14, 118], - [ 28, 196], [ 56, 23], [112, 73], [224, 236], [221, 127], [167, 12], [ 83, 111], [166, 246], - [ 81, 108], [162, 161], [ 89, 59], [178, 82], [121, 41], [242, 157], [249, 85], [239, 170], - [195, 251], [155, 96], [ 43, 134], [ 86, 177], [172, 187], [ 69, 204], [138, 62], [ 9, 90], - [ 18, 203], [ 36, 89], [ 72, 95], [144, 176], [ 61, 156], [122, 169], [244, 160], [245, 81], - [247, 11], [243, 245], [251, 22], [235, 235], [203, 122], [139, 117], [ 11, 44], [ 22, 215], - [ 44, 79], [ 88, 174], [176, 213], [125, 233], [250, 230], [233, 231], [207, 173], [131, 232], - [ 27, 116], [ 54, 214], [108, 244], [216, 234], [173, 168], [ 71, 80], [142, 88], [ 1, 175], - ]; - - /** - * @var int[] - */ - protected array $num = []; - - /** - * Polynomial constructor. - */ - public function __construct(array $num = null, int $shift = null){ - $this->setNum($num ?? [1], $shift); - } - - /** - * - */ - public function getNum():array{ - return $this->num; - } - - /** - * @param int[] $num - * @param int|null $shift - * - * @return \chillerlan\QRCode\Helpers\Polynomial - */ - public function setNum(array $num, int $shift = null):Polynomial{ - $offset = 0; - $numCount = count($num); - - while($offset < $numCount && $num[$offset] === 0){ - $offset++; - } - - $this->num = array_fill(0, $numCount - $offset + ($shift ?? 0), 0); - - for($i = 0; $i < $numCount - $offset; $i++){ - $this->num[$i] = $num[$i + $offset]; - } - - return $this; - } - - /** - * @param int[] $e - * - * @return \chillerlan\QRCode\Helpers\Polynomial - */ - public function multiply(array $e):Polynomial{ - $n = array_fill(0, count($this->num) + count($e) - 1, 0); - - foreach($this->num as $i => $vi){ - $vi = $this->glog($vi); - - foreach($e as $j => $vj){ - $n[$i + $j] ^= $this->gexp($vi + $this->glog($vj)); - } - - } - - $this->setNum($n); - - return $this; - } - - /** - * @param int[] $e - * - * @return \chillerlan\QRCode\Helpers\Polynomial - */ - public function mod(array $e):Polynomial{ - $n = $this->num; - - if(count($n) - count($e) < 0){ - return $this; - } - - $ratio = $this->glog($n[0]) - $this->glog($e[0]); - - foreach($e as $i => $v){ - $n[$i] ^= $this->gexp($this->glog($v) + $ratio); - } - - $this->setNum($n)->mod($e); - - return $this; - } - - /** - * @throws \chillerlan\QRCode\QRCodeException - */ - public function glog(int $n):int{ - - if($n < 1){ - throw new QRCodeException(sprintf('log(%s)', $n)); - } - - return Polynomial::table[$n][1]; - } - - /** - * - */ - public function gexp(int $n):int{ - - if($n < 0){ - $n += 255; - } - elseif($n >= 256){ - $n -= 255; - } - - return Polynomial::table[$n][0]; - } - -} diff --git a/tests/Helpers/PolynomialTest.php b/tests/Helpers/PolynomialTest.php deleted file mode 100644 index b0f3f4aa7..000000000 --- a/tests/Helpers/PolynomialTest.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright 2015 Smiley - * @license MIT - */ - -namespace chillerlan\QRCodeTest\Helpers; - -use chillerlan\QRCode\Helpers\Polynomial; -use chillerlan\QRCode\QRCodeException; -use PHPUnit\Framework\TestCase; - -/** - * Polynomial coverage test - */ -final class PolynomialTest extends TestCase{ - - protected Polynomial $polynomial; - - protected function setUp():void{ - $this->polynomial = new Polynomial; - } - - public function testGexp():void{ - $this::assertSame(142, $this->polynomial->gexp(-1)); - $this::assertSame(133, $this->polynomial->gexp(128)); - $this::assertSame(2, $this->polynomial->gexp(256)); - } - - public function testGlogException():void{ - $this->expectException(QRCodeException::class); - $this->expectExceptionMessage('log(0)'); - - $this->polynomial->glog(0); - } -} From 37d4b9faad55dd376cbc6ea68f0e540f99d7ca11 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 16 Jan 2021 19:52:12 +0100 Subject: [PATCH 40/78] :octocat: move BitBuffer to Common --- src/{Helpers => Common}/BitBuffer.php | 4 ++-- src/Common/ReedSolomonEncoder.php | 1 - src/Data/AlphaNum.php | 3 +-- src/Data/Byte.php | 3 +-- src/Data/ECI.php | 3 +-- src/Data/Kanji.php | 3 +-- src/Data/Number.php | 3 +-- src/Data/QRData.php | 3 +-- src/Data/QRDataModeInterface.php | 2 +- tests/{Helpers => Common}/BitBufferTest.php | 7 +++---- 10 files changed, 12 insertions(+), 20 deletions(-) rename src/{Helpers => Common}/BitBuffer.php (94%) rename tests/{Helpers => Common}/BitBufferTest.php (86%) diff --git a/src/Helpers/BitBuffer.php b/src/Common/BitBuffer.php similarity index 94% rename from src/Helpers/BitBuffer.php rename to src/Common/BitBuffer.php index de47f20f4..3f8ff7163 100644 --- a/src/Helpers/BitBuffer.php +++ b/src/Common/BitBuffer.php @@ -4,13 +4,13 @@ * * @filesource BitBuffer.php * @created 25.11.2015 - * @package chillerlan\QRCode\Helpers + * @package chillerlan\QRCode\Common * @author Smiley * @copyright 2015 Smiley * @license MIT */ -namespace chillerlan\QRCode\Helpers; +namespace chillerlan\QRCode\Common; use function count, floor; diff --git a/src/Common/ReedSolomonEncoder.php b/src/Common/ReedSolomonEncoder.php index e82e2355b..46a16eef6 100644 --- a/src/Common/ReedSolomonEncoder.php +++ b/src/Common/ReedSolomonEncoder.php @@ -12,7 +12,6 @@ namespace chillerlan\QRCode\Common; -use chillerlan\QRCode\Helpers\BitBuffer; use SplFixedArray; use function array_fill, array_merge, count, max; diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index 797713ebd..9852a54e3 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -12,8 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\Common\Mode; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; use function ceil, ord, sprintf, str_split; diff --git a/src/Data/Byte.php b/src/Data/Byte.php index 57e4ca64e..50a172bff 100644 --- a/src/Data/Byte.php +++ b/src/Data/Byte.php @@ -12,8 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\Common\Mode; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; use function ord; diff --git a/src/Data/ECI.php b/src/Data/ECI.php index 8997dfe3c..daeba6981 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -12,8 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\Common\Mode; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; /** * Adds an ECI Designator diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index 27ae28615..69972592a 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -12,8 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\Common\Mode; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; use function mb_convert_encoding, mb_detect_encoding, mb_strlen, ord, sprintf, strlen; diff --git a/src/Data/Number.php b/src/Data/Number.php index 8505614fd..b627dbded 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -12,8 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\Common\Mode; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; use function ceil, ord, sprintf, str_split, substr; diff --git a/src/Data/QRData.php b/src/Data/QRData.php index 9d9795164..a07f1be99 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -12,9 +12,8 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{EccLevel, Mode, ReedSolomonEncoder, Version}; +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, ReedSolomonEncoder, Version}; use chillerlan\QRCode\QRCode; -use chillerlan\QRCode\Helpers\BitBuffer; use chillerlan\Settings\SettingsContainerInterface; use function range, sprintf; diff --git a/src/Data/QRDataModeInterface.php b/src/Data/QRDataModeInterface.php index a53b633ce..cb791c7ef 100644 --- a/src/Data/QRDataModeInterface.php +++ b/src/Data/QRDataModeInterface.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Helpers\BitBuffer; +use chillerlan\QRCode\Common\BitBuffer; /** * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji) diff --git a/tests/Helpers/BitBufferTest.php b/tests/Common/BitBufferTest.php similarity index 86% rename from tests/Helpers/BitBufferTest.php rename to tests/Common/BitBufferTest.php index 886ffbb1c..e863c8443 100644 --- a/tests/Helpers/BitBufferTest.php +++ b/tests/Common/BitBufferTest.php @@ -4,16 +4,15 @@ * * @filesource BitBufferTest.php * @created 08.02.2016 - * @package chillerlan\QRCodeTest\Helpers + * @package chillerlan\QRCodeTest\Common * @author Smiley * @copyright 2015 Smiley * @license MIT */ -namespace chillerlan\QRCodeTest\Helpers; +namespace chillerlan\QRCodeTest\Common; -use chillerlan\QRCode\Helpers\BitBuffer; -use chillerlan\QRCode\Common\Mode; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; use PHPUnit\Framework\TestCase; /** From b11f4ef61e03c17274d3b48ef23efd04d096b8e3 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 16 Jan 2021 20:01:29 +0100 Subject: [PATCH 41/78] :shower: --- src/Common/ReedSolomonEncoder.php | 6 +++--- src/Data/QRData.php | 32 ++++++++++++++++--------------- src/Data/QRMatrix.php | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Common/ReedSolomonEncoder.php b/src/Common/ReedSolomonEncoder.php index 46a16eef6..ca8e0e03b 100644 --- a/src/Common/ReedSolomonEncoder.php +++ b/src/Common/ReedSolomonEncoder.php @@ -85,17 +85,17 @@ final class ReedSolomonEncoder{ $rsPolyDegree = $rsPoly->getDegree(); - $num = (new GenericGFPoly($dataBytes, $rsPolyDegree)) + $modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree)) ->mod($rsPoly) ->getCoefficients() ; $ecBytes = array_fill(0, $rsPolyDegree, 0); - $count = count($num) - count($ecBytes); + $count = count($modCoefficients) - $rsPolyDegree; foreach($ecBytes as $i => &$val){ $modIndex = $i + $count; - $val = $modIndex >= 0 ? $num[$modIndex] : 0; + $val = $modIndex >= 0 ? $modCoefficients[$modIndex] : 0; } return $ecBytes; diff --git a/src/Data/QRData.php b/src/Data/QRData.php index a07f1be99..79fa99123 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -23,6 +23,23 @@ use function range, sprintf; */ class QRData{ + /** + * the options instance + * + * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions + */ + protected SettingsContainerInterface $options; + + /** + * a BitBuffer instance + */ + protected BitBuffer $bitBuffer; + + /** + * an EccLevel instance + */ + protected EccLevel $eccLevel; + /** * current QR Code version */ @@ -40,20 +57,6 @@ class QRData{ */ protected array $maxBitsForEcc; - /** - * the options instance - * - * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions - */ - protected SettingsContainerInterface $options; - - /** - * a BitBuffer instance - */ - protected BitBuffer $bitBuffer; - - protected EccLevel $eccLevel; - /** * QRData constructor. * @@ -154,7 +157,6 @@ class QRData{ if($total <= $this->maxBitsForEcc[$version]){ return $version; } - } // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index 74077513f..2d9f74b28 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -495,7 +495,7 @@ final class QRMatrix{ * @see \chillerlan\QRCode\Data\QRData::maskECC() * * @param \SplFixedArray $data - * @param int $maskPattern + * @param int $maskPattern * * @return \chillerlan\QRCode\Data\QRMatrix */ From bde04254de76acdf9ffd4917223d6e2c4c9cea0b Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 16 Jan 2021 20:07:57 +0100 Subject: [PATCH 42/78] :octocat: renamed QRData:initMatrix() to writeMatrix() --- src/Data/MaskPatternTester.php | 2 +- src/Data/QRData.php | 2 +- src/QRCode.php | 2 +- tests/Data/DatainterfaceTestAbstract.php | 2 +- tests/Output/QROutputTestAbstract.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Data/MaskPatternTester.php b/src/Data/MaskPatternTester.php index d7cedbe23..8e470f57b 100644 --- a/src/Data/MaskPatternTester.php +++ b/src/Data/MaskPatternTester.php @@ -62,7 +62,7 @@ final class MaskPatternTester{ * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern */ public function testPattern(int $pattern):int{ - $matrix = $this->qrData->initMatrix($pattern, true); + $matrix = $this->qrData->writeMatrix($pattern, true); $penalty = 0; for($level = 1; $level <= 4; $level++){ diff --git a/src/Data/QRData.php b/src/Data/QRData.php index 79fa99123..28ed1ad76 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -100,7 +100,7 @@ class QRData{ /** * returns a fresh matrix object with the data written for the given $maskPattern */ - public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{ + public function writeMatrix(int $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/QRCode.php b/src/QRCode.php index b8dffce50..d654d2a3b 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -158,7 +158,7 @@ class QRCode{ ? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern() : $this->options->maskPattern; - $matrix = $this->dataInterface->initMatrix($maskPattern); + $matrix = $this->dataInterface->writeMatrix($maskPattern); if((bool)$this->options->addQuietzone){ $matrix->setQuietZone($this->options->quietzoneSize); diff --git a/tests/Data/DatainterfaceTestAbstract.php b/tests/Data/DatainterfaceTestAbstract.php index 0e0361dfc..e0faee38a 100644 --- a/tests/Data/DatainterfaceTestAbstract.php +++ b/tests/Data/DatainterfaceTestAbstract.php @@ -82,7 +82,7 @@ abstract class DatainterfaceTestAbstract extends TestCase{ public function testInitMatrix(int $maskPattern):void{ $this->dataInterface->setData([$this->testdata]); - $matrix = $this->dataInterface->initMatrix($maskPattern); + $matrix = $this->dataInterface->writeMatrix($maskPattern); $this::assertInstanceOf(QRMatrix::class, $matrix); $this::assertSame($maskPattern, $matrix->maskPattern()); diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index 633b99206..b6eec6b99 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -48,7 +48,7 @@ abstract class QROutputTestAbstract extends TestCase{ } $this->options = new QROptions; - $this->matrix = (new QRData($this->options, [[Byte::class, 'testdata']]))->initMatrix(0); + $this->matrix = (new QRData($this->options, [[Byte::class, 'testdata']]))->writeMatrix(0); $this->outputInterface = $this->getOutputInterface($this->options); } From 387beea8924199162d34c7786fed3b641460bfd8 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 16 Jan 2021 20:10:21 +0100 Subject: [PATCH 43/78] :shower: final --- src/Data/ECI.php | 2 +- src/Data/QRData.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Data/ECI.php b/src/Data/ECI.php index daeba6981..1ca8c78bc 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -19,7 +19,7 @@ use chillerlan\QRCode\Common\{BitBuffer, Mode}; * * Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment() */ -class ECI extends QRDataModeAbstract{ +final class ECI extends QRDataModeAbstract{ public const CP437 = 0; // Code page 437, DOS Latin US public const ISO_IEC_8859_1_GLI = 1; // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1 diff --git a/src/Data/QRData.php b/src/Data/QRData.php index 28ed1ad76..c02d50812 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -21,7 +21,7 @@ use function range, sprintf; /** * Processes the binary data and maps it on a matrix which is then being returned */ -class QRData{ +final class QRData{ /** * the options instance From 902486edc393a5894befe38352d1f4d7d58b8496 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 16 Jan 2021 20:38:39 +0100 Subject: [PATCH 44/78] :fire_engine: phan happy --- src/Common/GF256.php | 3 ++- src/Common/GenericGFPoly.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Common/GF256.php b/src/Common/GF256.php index 12c4ad099..e7f4cc1a2 100644 --- a/src/Common/GF256.php +++ b/src/Common/GF256.php @@ -37,7 +37,8 @@ final class GF256{ # private int $primitive = 0x011D; private const logTable = [ - null, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, + 0, // the first value is never returned, index starts at 1 + 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, diff --git a/src/Common/GenericGFPoly.php b/src/Common/GenericGFPoly.php index 29078b672..87e8efd8f 100644 --- a/src/Common/GenericGFPoly.php +++ b/src/Common/GenericGFPoly.php @@ -31,7 +31,7 @@ final class GenericGFPoly{ private array $coefficients; /** - * @param array|null $coefficients array coefficients as ints representing elements of GF(size), arranged + * @param array $coefficients array coefficients as ints representing elements of GF(size), arranged * from most significant (highest-power term) coefficient to least significant * @param int|null $degree * From 307b6462f65ae9d1cb63db77d6876ff0fcd748c7 Mon Sep 17 00:00:00 2001 From: codemasher Date: Tue, 19 Jan 2021 00:24:16 +0100 Subject: [PATCH 45/78] :octocat: QRMatrix::M_*TYPE rework, separate masking --- examples/fpdf.php | 31 +++++----- examples/html.php | 49 ++++++---------- examples/image.php | 39 +++++++------ examples/imagick.php | 34 +++++------ examples/svg.php | 34 +++++------ examples/text.php | 32 ++++++----- src/Data/QRData.php | 3 +- src/Data/QRMatrix.php | 99 +++++++++++++++++++++----------- src/Output/QROutputInterface.php | 40 ++++++------- tests/Data/QRMatrixTest.php | 59 +++++++++++++------ tests/Output/QRMarkupTest.php | 6 +- tests/Output/QRStringTest.php | 10 ++-- tests/Output/samples/json | 2 +- tests/Output/samples/svg | 2 +- 14 files changed, 246 insertions(+), 194 deletions(-) diff --git a/examples/fpdf.php b/examples/fpdf.php index b231e49e0..a693df725 100644 --- a/examples/fpdf.php +++ b/examples/fpdf.php @@ -3,6 +3,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Common\EccLevel; require_once __DIR__ . '/../vendor/autoload.php'; @@ -17,29 +18,29 @@ $options = new QROptions([ 'imageBase64' => false, 'moduleValues' => [ // finder - 1536 => [0, 63, 255], // dark (true) - 6 => [255, 255, 255], // light (false), white is the transparency color and is enabled by default + QRMatrix::M_FINDER | QRMatrix::IS_DARK => [0, 63, 255], // dark (true) + QRMatrix::M_FINDER => [255, 255, 255], // light (false), white is the transparency color and is enabled by default // alignment - 2560 => [255, 0, 255], - 10 => [255, 255, 255], + QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK => [255, 0, 255], + QRMatrix::M_ALIGNMENT => [255, 255, 255], // timing - 3072 => [255, 0, 0], - 12 => [255, 255, 255], + QRMatrix::M_TIMING | QRMatrix::IS_DARK => [255, 0, 0], + QRMatrix::M_TIMING => [255, 255, 255], // format - 3584 => [67, 191, 84], - 14 => [255, 255, 255], + QRMatrix::M_FORMAT | QRMatrix::IS_DARK => [67, 191, 84], + QRMatrix::M_FORMAT => [255, 255, 255], // version - 4096 => [62, 174, 190], - 16 => [255, 255, 255], + QRMatrix::M_VERSION | QRMatrix::IS_DARK => [62, 174, 190], + QRMatrix::M_VERSION => [255, 255, 255], // data - 1024 => [0, 0, 0], - 4 => [255, 255, 255], + QRMatrix::M_DATA | QRMatrix::IS_DARK => [0, 0, 0], + QRMatrix::M_DATA => [255, 255, 255], // darkmodule - 512 => [0, 0, 0], + QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => [0, 0, 0], // separator - 8 => [255, 255, 255], + QRMatrix::M_SEPARATOR => [255, 255, 255], // quietzone - 18 => [255, 255, 255], + QRMatrix::M_QUIETZONE => [255, 255, 255], ], ]); diff --git a/examples/html.php b/examples/html.php index 34140671c..e7d658961 100644 --- a/examples/html.php +++ b/examples/html.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Common\EccLevel; require_once '../vendor/autoload.php'; @@ -25,20 +26,12 @@ header('Content-Type: text/html; charset=utf-8'); QRCode test -
5, 'outputType' => QRCode::OUTPUT_MARKUP_HTML, 'eccLevel' => EccLevel::L, + 'cssClass' => 'qrcode', 'moduleValues' => [ // finder - 1536 => '#A71111', // dark (true) - 6 => '#FFBFBF', // light (false) + QRMatrix::M_FINDER | QRMatrix::IS_DARK => '#A71111', // dark (true) + QRMatrix::M_FINDER => '#FFBFBF', // light (false) + QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => '#A71111', // finder dot, dark (true) // alignment - 2560 => '#A70364', - 10 => '#FFC9C9', + QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK => '#A70364', + QRMatrix::M_ALIGNMENT => '#FFC9C9', // timing - 3072 => '#98005D', - 12 => '#FFB8E9', + QRMatrix::M_TIMING | QRMatrix::IS_DARK => '#98005D', + QRMatrix::M_TIMING => '#FFB8E9', // format - 3584 => '#003804', - 14 => '#00FB12', + QRMatrix::M_FORMAT | QRMatrix::IS_DARK => '#003804', + QRMatrix::M_FORMAT => '#00FB12', // version - 4096 => '#650098', - 16 => '#E0B8FF', + QRMatrix::M_VERSION | QRMatrix::IS_DARK => '#650098', + QRMatrix::M_VERSION => '#E0B8FF', // data - 1024 => '#4A6000', - 4 => '#ECF9BE', + QRMatrix::M_DATA | QRMatrix::IS_DARK => '#4A6000', + QRMatrix::M_DATA => '#ECF9BE', // darkmodule - 512 => '#080063', + QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => '#080063', // separator - 8 => '#AFBFBF', + QRMatrix::M_SEPARATOR => '#AFBFBF', // quietzone - 18 => '#FFFFFF', + QRMatrix::M_QUIETZONE => '#DDDDDD', ], ]); echo (new QRCode($options))->render($data); ?> -
diff --git a/examples/image.php b/examples/image.php index 99d3ceab8..6d9069f33 100644 --- a/examples/image.php +++ b/examples/image.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -18,39 +19,39 @@ require_once __DIR__.'/../vendor/autoload.php'; $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'; $options = new QROptions([ - 'version' => 10, + 'version' => 5, 'outputType' => QRCode::OUTPUT_IMAGE_PNG, 'eccLevel' => EccLevel::L, 'scale' => 5, 'imageBase64' => false, 'moduleValues' => [ // finder - 1536 => [0, 63, 255], // dark (true) - 6 => [255, 255, 255], // light (false), white is the transparency color and is enabled by default - 5632 => [241, 28, 163], // finder dot, dark (true) + QRMatrix::M_FINDER | QRMatrix::IS_DARK => [0, 63, 255], // dark (true) + QRMatrix::M_FINDER => [255, 255, 255], // light (false), white is the transparency color and is enabled by default + QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => [241, 28, 163], // finder dot, dark (true) // alignment - 2560 => [255, 0, 255], - 10 => [255, 255, 255], + QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK => [255, 0, 255], + QRMatrix::M_ALIGNMENT => [255, 255, 255], // timing - 3072 => [255, 0, 0], - 12 => [255, 255, 255], + QRMatrix::M_TIMING | QRMatrix::IS_DARK => [255, 0, 0], + QRMatrix::M_TIMING => [255, 255, 255], // format - 3584 => [67, 99, 84], - 14 => [255, 255, 255], + QRMatrix::M_FORMAT | QRMatrix::IS_DARK => [67, 99, 84], + QRMatrix::M_FORMAT => [255, 255, 255], // version - 4096 => [62, 174, 190], - 16 => [255, 255, 255], + QRMatrix::M_VERSION | QRMatrix::IS_DARK => [62, 174, 190], + QRMatrix::M_VERSION => [255, 255, 255], // data - 1024 => [0, 0, 0], - 4 => [255, 255, 255], + QRMatrix::M_DATA | QRMatrix::IS_DARK => [0, 0, 0], + QRMatrix::M_DATA => [255, 255, 255], // darkmodule - 512 => [0, 0, 0], + QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => [0, 0, 0], // separator - 8 => [255, 255, 255], + QRMatrix::M_SEPARATOR => [255, 255, 255], // quietzone - 18 => [255, 255, 255], - // logo (requires a call to QRMatrix::setLogoSpace()) - 20 => [255, 255, 255], + QRMatrix::M_QUIETZONE => [255, 255, 255], + // logo (requires a call to QRMatrix::setLogoSpace()), see QRImageWithLogo + QRMatrix::M_LOGO => [255, 255, 255], ], ]); diff --git a/examples/imagick.php b/examples/imagick.php index 38162972f..fb0561f06 100644 --- a/examples/imagick.php +++ b/examples/imagick.php @@ -1,7 +1,7 @@ * @copyright 2017 Smiley @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -24,29 +25,30 @@ $options = new QROptions([ 'scale' => 5, 'moduleValues' => [ // finder - 1536 => '#A71111', // dark (true) - 6 => '#FFBFBF', // light (false) + QRMatrix::M_FINDER | QRMatrix::IS_DARK => '#A71111', // dark (true) + QRMatrix::M_FINDER => '#FFBFBF', // light (false) + QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => '#A71111', // finder dot, dark (true) // alignment - 2560 => '#A70364', - 10 => '#FFC9C9', + QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK => '#A70364', + QRMatrix::M_ALIGNMENT => '#FFC9C9', // timing - 3072 => '#98005D', - 12 => '#FFB8E9', + QRMatrix::M_TIMING | QRMatrix::IS_DARK => '#98005D', + QRMatrix::M_TIMING => '#FFB8E9', // format - 3584 => '#003804', - 14 => '#00FB12', + QRMatrix::M_FORMAT | QRMatrix::IS_DARK => '#003804', + QRMatrix::M_FORMAT => '#00FB12', // version - 4096 => '#650098', - 16 => '#E0B8FF', + QRMatrix::M_VERSION | QRMatrix::IS_DARK => '#650098', + QRMatrix::M_VERSION => '#E0B8FF', // data - 1024 => '#4A6000', - 4 => '#ECF9BE', + QRMatrix::M_DATA | QRMatrix::IS_DARK => '#4A6000', + QRMatrix::M_DATA => '#ECF9BE', // darkmodule - 512 => '#080063', + QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => '#080063', // separator - 8 => '#DDDDDD', + QRMatrix::M_SEPARATOR => '#DDDDDD', // quietzone - 18 => '#DDDDDD', + QRMatrix::M_QUIETZONE => '#DDDDDD', ], ]); diff --git a/examples/svg.php b/examples/svg.php index 517e393ed..b02040dda 100644 --- a/examples/svg.php +++ b/examples/svg.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -39,29 +40,30 @@ $options = new QROptions([ ', 'moduleValues' => [ // finder - 1536 => 'url(#g1)', // dark (true) - 6 => '#fff', // light (false) + QRMatrix::M_FINDER | QRMatrix::IS_DARK => 'url(#g1)', // dark (true) + QRMatrix::M_FINDER => '#fff', // light (false) + QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => 'url(#g2)', // finder dot, dark (true) // alignment - 2560 => 'url(#g1)', - 10 => '#fff', + QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK => 'url(#g1)', + QRMatrix::M_ALIGNMENT => '#fff', // timing - 3072 => 'url(#g1)', - 12 => '#fff', + QRMatrix::M_TIMING | QRMatrix::IS_DARK => 'url(#g1)', + QRMatrix::M_TIMING => '#fff', // format - 3584 => 'url(#g1)', - 14 => '#fff', + QRMatrix::M_FORMAT | QRMatrix::IS_DARK => 'url(#g1)', + QRMatrix::M_FORMAT => '#fff', // version - 4096 => 'url(#g1)', - 16 => '#fff', + QRMatrix::M_VERSION | QRMatrix::IS_DARK => 'url(#g1)', + QRMatrix::M_VERSION => '#fff', // data - 1024 => 'url(#g2)', - 4 => '#fff', + QRMatrix::M_DATA | QRMatrix::IS_DARK => 'url(#g2)', + QRMatrix::M_DATA => '#fff', // darkmodule - 512 => 'url(#g1)', + QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => 'url(#g1)', // separator - 8 => '#fff', + QRMatrix::M_SEPARATOR => '#fff', // quietzone - 18 => '#fff', + QRMatrix::M_QUIETZONE => '#fff', ], ]); @@ -72,7 +74,7 @@ header('Content-type: image/svg+xml'); if($gzip === true){ header('Vary: Accept-Encoding'); header('Content-Encoding: gzip'); - $qrcode = gzencode($qrcode ,9); + $qrcode = gzencode($qrcode, 9); } echo $qrcode; diff --git a/examples/text.php b/examples/text.php index d0a6ec2bc..89854db6a 100644 --- a/examples/text.php +++ b/examples/text.php @@ -11,6 +11,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Common\EccLevel; require_once __DIR__.'/../vendor/autoload.php'; @@ -34,29 +35,30 @@ $options = new QROptions([ 'eccLevel' => EccLevel::L, 'moduleValues' => [ // finder - 1536 => 'A', // dark (true) - 6 => 'a', // light (false) + QRMatrix::M_FINDER | QRMatrix::IS_DARK => 'A', // dark (true) + QRMatrix::M_FINDER => 'a', // light (false) + QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => 'ä', // finder dot, dark (true) // alignment - 2560 => 'B', - 10 => 'b', + QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK => 'B', + QRMatrix::M_ALIGNMENT => 'b', // timing - 3072 => 'C', - 12 => 'c', + QRMatrix::M_TIMING | QRMatrix::IS_DARK => 'C', + QRMatrix::M_TIMING => 'c', // format - 3584 => 'D', - 14 => 'd', + QRMatrix::M_FORMAT | QRMatrix::IS_DARK => 'D', + QRMatrix::M_FORMAT => 'd', // version - 4096 => 'E', - 16 => 'e', + QRMatrix::M_VERSION | QRMatrix::IS_DARK => 'E', + QRMatrix::M_VERSION => 'e', // data - 1024 => 'F', - 4 => 'f', + QRMatrix::M_DATA | QRMatrix::IS_DARK => 'F', + QRMatrix::M_DATA => 'f', // darkmodule - 512 => 'G', + QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => 'G', // separator - 8 => 'h', + QRMatrix::M_SEPARATOR => 'h', // quietzone - 18 => 'i', + QRMatrix::M_QUIETZONE => 'i', ], ]); diff --git a/src/Data/QRData.php b/src/Data/QRData.php index c02d50812..6f62cdb4e 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -105,7 +105,8 @@ final class QRData{ return (new QRMatrix($this->version, $this->eccLevel)) ->init($maskPattern, $test) - ->mapData($data, $maskPattern) + ->mapData($data) + ->mask($maskPattern) ; } diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index 2d9f74b28..d06aa36bc 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -28,31 +28,33 @@ use function array_fill, array_push, array_unshift, floor, max, min, range; final class QRMatrix{ /** @var int */ - public const M_NULL = 0x00; + public const M_NULL = 0b000000000000; /** @var int */ - public const M_DARKMODULE = 0x02; + public const M_DARKMODULE = 0b000000000001; /** @var int */ - public const M_DATA = 0x04; + public const M_DATA = 0b000000000010; /** @var int */ - public const M_FINDER = 0x06; + public const M_FINDER = 0b000000000100; /** @var int */ - public const M_SEPARATOR = 0x08; + public const M_SEPARATOR = 0b000000001000; /** @var int */ - public const M_ALIGNMENT = 0x0a; + public const M_ALIGNMENT = 0b000000010000; /** @var int */ - public const M_TIMING = 0x0c; + public const M_TIMING = 0b000000100000; /** @var int */ - public const M_FORMAT = 0x0e; + public const M_FORMAT = 0b000001000000; /** @var int */ - public const M_VERSION = 0x10; + public const M_VERSION = 0b000010000000; /** @var int */ - public const M_QUIETZONE = 0x12; + public const M_QUIETZONE = 0b000100000000; /** @var int */ - public const M_LOGO = 0x14; + public const M_LOGO = 0b001000000000; /** @var int */ - public const M_FINDER_DOT = 0x16; + public const M_FINDER_DOT = 0b010000000000; /** @var int */ - public const M_TEST = 0xff; + public const M_TEST = 0b011111111111; + /** @var int */ + public const IS_DARK = 0b100000000000; /** * the used mask pattern, set via QRMatrix::mapData() @@ -123,7 +125,7 @@ final class QRMatrix{ $matrix[$y] = []; foreach($row as $x => $val){ - $matrix[$y][$x] = ($val >> 8) > 0; + $matrix[$y][$x] = ($val & $this::IS_DARK) === $this::IS_DARK; } } @@ -170,28 +172,42 @@ final class QRMatrix{ /** * Sets the $M_TYPE value for the module at position [$x, $y] * - * true => $M_TYPE << 8 + * true => $M_TYPE | 0x800 * false => $M_TYPE */ public function set(int $x, int $y, bool $value, int $M_TYPE):QRMatrix{ - $this->matrix[$y][$x] = $M_TYPE << ($value ? 8 : 0); + $this->matrix[$y][$x] = $M_TYPE | ($value ? $this::IS_DARK : 0); return $this; } /** - * Checks whether a module is true (dark) or false (light) - * - * true => $value >> 8 === $M_TYPE - * $value >> 8 > 0 - * - * false => $value === $M_TYPE - * $value >> 8 === 0 + * Flips the value of the module */ - public function check(int $x, int $y):bool{ - return ($this->matrix[$y][$x] >> 8) > 0; + public function flip(int $x, int $y):QRMatrix{ + $this->matrix[$y][$x] ^= $this::IS_DARK; + + return $this; } + /** + * Checks whether a module is of the given $M_TYPE + * + * true => $value & $M_TYPE === $M_TYPE + */ + public function checkType(int $x, int $y, int $M_TYPE):bool{ + return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE; + } + + /** + * Checks whether a module is true (dark) or false (light) + * + * true => $value & 0x800 === 0x800 + * false => $value & 0x800 === 0 + */ + public function check(int $x, int $y):bool{ + return $this->checkType($x, $y, $this::IS_DARK); + } /** * Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder @@ -331,7 +347,7 @@ final class QRMatrix{ if($bits !== null){ for($i = 0; $i < 18; $i++){ - $a = (int)floor($i / 3); + $a = (int)($i / 3); $b = $i % 3 + $this->moduleCount - 8 - 3; $v = !$test && (($bits >> $i) & 1) === 1; @@ -495,18 +511,15 @@ final class QRMatrix{ * @see \chillerlan\QRCode\Data\QRData::maskECC() * * @param \SplFixedArray $data - * @param int $maskPattern * * @return \chillerlan\QRCode\Data\QRMatrix */ - public function mapData(SplFixedArray $data, int $maskPattern):QRMatrix{ - $this->maskPattern = $maskPattern; + public function mapData(SplFixedArray $data):QRMatrix{ $byteCount = $data->count(); $y = $this->moduleCount - 1; $inc = -1; $byteIndex = 0; $bitIndex = 7; - $mask = $this->getMask($this->maskPattern); for($i = $y; $i > 0; $i -= 2){ @@ -525,11 +538,7 @@ final class QRMatrix{ $v = (($data[$byteIndex] >> $bitIndex) & 1) === 1; } - if($mask($x, $y) === 0){ - $v = !$v; - } - - $this->matrix[$y][$x] = $this::M_DATA << ($v ? 8 : 0); + $this->matrix[$y][$x] = $this::M_DATA | ($v ? $this::IS_DARK : 0); $bitIndex--; if($bitIndex === -1){ @@ -555,6 +564,26 @@ final class QRMatrix{ return $this; } + /** + * Applies the mask pattern + * + * ISO/IEC 18004:2000 Section 8.8.1 + */ + public function mask(int $maskPattern):QRMatrix{ + $this->maskPattern = $maskPattern; + $mask = $this->getMask($this->maskPattern); + + foreach($this->matrix as $y => &$row){ + foreach($row as $x => &$val){ + if($mask($x, $y) === 0 && ($val & $this::M_DATA) === $this::M_DATA){ + $val ^= $this::IS_DARK; + } + } + } + + return $this; + } + /** * ISO/IEC 18004:2000 Section 8.8.1 * diff --git a/src/Output/QROutputInterface.php b/src/Output/QROutputInterface.php index b07b8e7a5..847b2b754 100644 --- a/src/Output/QROutputInterface.php +++ b/src/Output/QROutputInterface.php @@ -21,27 +21,27 @@ interface QROutputInterface{ const DEFAULT_MODULE_VALUES = [ // light - QRMatrix::M_NULL => false, // 0 - QRMatrix::M_DATA => false, // 4 - QRMatrix::M_FINDER => false, // 6 - QRMatrix::M_SEPARATOR => false, // 8 - QRMatrix::M_ALIGNMENT => false, // 10 - QRMatrix::M_TIMING => false, // 12 - QRMatrix::M_FORMAT => false, // 14 - QRMatrix::M_VERSION => false, // 16 - QRMatrix::M_QUIETZONE => false, // 18 - QRMatrix::M_LOGO => false, // 20 - QRMatrix::M_TEST => false, // 255 + QRMatrix::M_NULL => false, + QRMatrix::M_DATA => false, + QRMatrix::M_FINDER => false, + QRMatrix::M_SEPARATOR => false, + QRMatrix::M_ALIGNMENT => false, + QRMatrix::M_TIMING => false, + QRMatrix::M_FORMAT => false, + QRMatrix::M_VERSION => false, + QRMatrix::M_QUIETZONE => false, + QRMatrix::M_LOGO => false, + QRMatrix::M_TEST => false, // dark - QRMatrix::M_DARKMODULE << 8 => true, // 512 - QRMatrix::M_DATA << 8 => true, // 1024 - QRMatrix::M_FINDER << 8 => true, // 1536 - QRMatrix::M_ALIGNMENT << 8 => true, // 2560 - QRMatrix::M_TIMING << 8 => true, // 3072 - QRMatrix::M_FORMAT << 8 => true, // 3584 - QRMatrix::M_VERSION << 8 => true, // 4096 - QRMatrix::M_FINDER_DOT << 8 => true, // 5632 - QRMatrix::M_TEST << 8 => true, // 65280 + QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => true, + QRMatrix::M_DATA | QRMatrix::IS_DARK => true, + QRMatrix::M_FINDER | QRMatrix::IS_DARK => true, + QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK => true, + QRMatrix::M_TIMING | QRMatrix::IS_DARK => true, + QRMatrix::M_FORMAT | QRMatrix::IS_DARK => true, + QRMatrix::M_VERSION | QRMatrix::IS_DARK => true, + QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => true, + QRMatrix::M_TEST | QRMatrix::IS_DARK => true, ]; /** diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index 3b94b974d..53ec8ea3c 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -87,11 +87,11 @@ final class QRMatrixTest extends TestCase{ */ public function testGetSetCheck():void{ $this->matrix->set(10, 10, true, QRMatrix::M_TEST); - $this::assertSame(65280, $this->matrix->get(10, 10)); + $this::assertSame(QRMatrix::M_TEST | QRMatrix::IS_DARK, $this->matrix->get(10, 10)); $this::assertTrue($this->matrix->check(10, 10)); $this->matrix->set(20, 20, false, QRMatrix::M_TEST); - $this::assertSame(255, $this->matrix->get(20, 20)); + $this::assertSame(QRMatrix::M_TEST, $this->matrix->get(20, 20)); $this::assertFalse($this->matrix->check(20, 20)); } @@ -119,7 +119,7 @@ final class QRMatrixTest extends TestCase{ public function testSetDarkModule(int $version):void{ $matrix = $this->getMatrix($version)->setDarkModule(); - $this::assertSame(QRMatrix::M_DARKMODULE << 8, $matrix->get(8, $matrix->size() - 8)); + $this::assertSame(QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK, $matrix->get(8, $matrix->size() - 8)); } /** @@ -130,9 +130,9 @@ final class QRMatrixTest extends TestCase{ public function testSetFinderPattern(int $version):void{ $matrix = $this->getMatrix($version)->setFinderPattern(); - $this::assertSame(QRMatrix::M_FINDER << 8, $matrix->get(0, 0)); - $this::assertSame(QRMatrix::M_FINDER << 8, $matrix->get(0, $matrix->size() - 1)); - $this::assertSame(QRMatrix::M_FINDER << 8, $matrix->get($matrix->size() - 1, 0)); + $this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $matrix->get(0, 0)); + $this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $matrix->get(0, $matrix->size() - 1)); + $this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $matrix->get($matrix->size() - 1, 0)); } /** @@ -174,12 +174,12 @@ final class QRMatrixTest extends TestCase{ foreach($alignmentPattern as $py){ foreach($alignmentPattern as $px){ - if($matrix->get($px, $py) === QRMatrix::M_FINDER << 8){ - $this::assertSame(QRMatrix::M_FINDER << 8, $matrix->get($px, $py), 'skipped finder pattern'); + if($matrix->get($px, $py) === (QRMatrix::M_FINDER | QRMatrix::IS_DARK)){ + $this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $matrix->get($px, $py), 'skipped finder pattern'); continue; } - $this::assertSame(QRMatrix::M_ALIGNMENT << 8, $matrix->get($px, $py)); + $this::assertSame(QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK, $matrix->get($px, $py)); } } @@ -204,13 +204,13 @@ final class QRMatrixTest extends TestCase{ if($i % 2 === 0){ $p1 = $matrix->get(6, $i); - if($p1 === QRMatrix::M_ALIGNMENT << 8){ - $this::assertSame(QRMatrix::M_ALIGNMENT << 8, $p1, 'skipped alignment pattern'); + if($p1 === (QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK)){ + $this::assertSame(QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK, $p1, 'skipped alignment pattern'); continue; } - $this::assertSame(QRMatrix::M_TIMING << 8, $p1); - $this::assertSame(QRMatrix::M_TIMING << 8, $matrix->get($i, 6)); + $this::assertSame(QRMatrix::M_TIMING | QRMatrix::IS_DARK, $p1); + $this::assertSame(QRMatrix::M_TIMING | QRMatrix::IS_DARK, $matrix->get($i, 6)); } } } @@ -274,8 +274,8 @@ final class QRMatrixTest extends TestCase{ $this::assertSame(QRMatrix::M_QUIETZONE, $matrix->get(0, 0)); $this::assertSame(QRMatrix::M_QUIETZONE, $matrix->get($size - 1, $size - 1)); - $this::assertSame(QRMatrix::M_TEST << 8, $matrix->get($q, $q)); - $this::assertSame(QRMatrix::M_TEST << 8, $matrix->get($size - 1 - $q, $size - 1 - $q)); + $this::assertSame(QRMatrix::M_TEST | QRMatrix::IS_DARK, $matrix->get($q, $q)); + $this::assertSame(QRMatrix::M_TEST | QRMatrix::IS_DARK, $matrix->get($size - 1 - $q, $size - 1 - $q)); } /** @@ -319,10 +319,10 @@ final class QRMatrixTest extends TestCase{ // logo space should not overwrite quiet zone & function patterns $m->setLogoSpace(21, 21, -10, -10); $this::assertSame(QRMatrix::M_QUIETZONE, $m->get(9, 9)); - $this::assertSame(QRMatrix::M_FINDER << 8, $m->get(10, 10)); - $this::assertSame(QRMatrix::M_FINDER << 8, $m->get(16, 16)); + $this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $m->get(10, 10)); + $this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $m->get(16, 16)); $this::assertSame(QRMatrix::M_SEPARATOR, $m->get(17, 17)); - $this::assertSame(QRMatrix::M_FORMAT << 8, $m->get(18, 18)); + $this::assertSame(QRMatrix::M_FORMAT | QRMatrix::IS_DARK, $m->get(18, 18)); $this::assertSame(QRMatrix::M_LOGO, $m->get(19, 19)); $this::assertSame(QRMatrix::M_LOGO, $m->get(20, 20)); $this::assertNotSame(QRMatrix::M_LOGO, $m->get(21, 21)); @@ -353,4 +353,27 @@ final class QRMatrixTest extends TestCase{ (new QRCode($o))->addByteSegment('testdata')->getMatrix()->setLogoSpace(50, 50); } + /** + * Tests flipping the value of a module + */ + public function testFlip():void{ + // using the dark module here because i'm lazy + $matrix = $this->getMatrix(10)->setDarkModule(); + $x = 8; + $y = $matrix->size() - 8; + + // cover checkType() + $this::assertTrue($matrix->checkType($x, $y, QRMatrix::M_DARKMODULE)); + // verify the current state (dark) + $this::assertSame(QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK, $matrix->get($x, $y)); + // flip + $matrix->flip($x, $y); + // verify flip + $this::assertSame(QRMatrix::M_DARKMODULE, $matrix->get($x, $y)); + // flip again + $matrix->flip($x, $y); + // verify flip + $this::assertSame(QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK, $matrix->get($x, $y)); + } + } diff --git a/tests/Output/QRMarkupTest.php b/tests/Output/QRMarkupTest.php index cc18077d5..b928c5062 100644 --- a/tests/Output/QRMarkupTest.php +++ b/tests/Output/QRMarkupTest.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCodeTest\Output; -use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\{Data\QRMatrix, QRCode, QROptions}; use chillerlan\QRCode\Output\{QROutputInterface, QRMarkup}; /** @@ -46,8 +46,8 @@ class QRMarkupTest extends QROutputTestAbstract{ $this->options->imageBase64 = false; $this->options->moduleValues = [ // data - 1024 => '#4A6000', - 4 => '#ECF9BE', + QRMatrix::M_DATA | QRMatrix::IS_DARK => '#4A6000', + QRMatrix::M_DATA => '#ECF9BE', ]; $this->outputInterface = $this->getOutputInterface($this->options); diff --git a/tests/Output/QRStringTest.php b/tests/Output/QRStringTest.php index 9206c8f10..c40694ec8 100644 --- a/tests/Output/QRStringTest.php +++ b/tests/Output/QRStringTest.php @@ -12,9 +12,11 @@ namespace chillerlan\QRCodeTest\Output; -use chillerlan\QRCodeExamples\MyCustomOutput; -use chillerlan\QRCode\{Common\EccLevel, QRCode, QROptions}; +use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\EccLevel; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Output\{QROutputInterface, QRString}; +use chillerlan\QRCodeExamples\MyCustomOutput; /** * Tests the QRString output module @@ -47,8 +49,8 @@ class QRStringTest extends QROutputTestAbstract{ $this->options->moduleValues = [ // data - 1024 => 'A', - 4 => 'B', + QRMatrix::M_DATA | QRMatrix::IS_DARK => 'A', + QRMatrix::M_DATA => 'B', ]; $this->outputInterface = $this->getOutputInterface($this->options); diff --git a/tests/Output/samples/json b/tests/Output/samples/json index f8423f75b..56837ea1b 100644 --- a/tests/Output/samples/json +++ b/tests/Output/samples/json @@ -1 +1 @@ -[[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18],[18,18,18,18,1536,1536,1536,1536,1536,1536,1536,8,3584,1024,4,4,1024,8,1536,1536,1536,1536,1536,1536,1536,18,18,18,18],[18,18,18,18,1536,6,6,6,6,6,1536,8,14,1024,4,4,1024,8,1536,6,6,6,6,6,1536,18,18,18,18],[18,18,18,18,1536,6,5632,5632,5632,6,1536,8,3584,4,1024,4,1024,8,1536,6,5632,5632,5632,6,1536,18,18,18,18],[18,18,18,18,1536,6,5632,5632,5632,6,1536,8,3584,4,4,1024,4,8,1536,6,5632,5632,5632,6,1536,18,18,18,18],[18,18,18,18,1536,6,5632,5632,5632,6,1536,8,3584,1024,1024,4,4,8,1536,6,5632,5632,5632,6,1536,18,18,18,18],[18,18,18,18,1536,6,6,6,6,6,1536,8,14,4,4,4,4,8,1536,6,6,6,6,6,1536,18,18,18,18],[18,18,18,18,1536,1536,1536,1536,1536,1536,1536,8,3072,12,3072,12,3072,8,1536,1536,1536,1536,1536,1536,1536,18,18,18,18],[18,18,18,18,8,8,8,8,8,8,8,8,14,1024,1024,4,4,8,8,8,8,8,8,8,8,18,18,18,18],[18,18,18,18,3584,3584,3584,3584,14,14,3072,14,3584,4,1024,4,4,3584,14,14,3584,3584,3584,14,3584,18,18,18,18],[18,18,18,18,1024,4,4,1024,1024,1024,12,1024,4,4,1024,4,1024,4,4,1024,4,1024,1024,4,1024,18,18,18,18],[18,18,18,18,1024,4,1024,4,1024,1024,3072,4,4,4,1024,4,1024,1024,1024,1024,1024,4,4,1024,1024,18,18,18,18],[18,18,18,18,1024,4,1024,1024,4,4,12,1024,1024,4,1024,4,1024,1024,4,4,4,1024,4,1024,4,18,18,18,18],[18,18,18,18,4,1024,1024,4,1024,4,3072,4,4,1024,1024,1024,4,1024,4,4,1024,1024,4,1024,4,18,18,18,18],[18,18,18,18,8,8,8,8,8,8,8,8,512,4,1024,4,4,1024,4,1024,4,1024,4,1024,4,18,18,18,18],[18,18,18,18,1536,1536,1536,1536,1536,1536,1536,8,14,4,1024,4,4,1024,1024,4,1024,1024,1024,4,4,18,18,18,18],[18,18,18,18,1536,6,6,6,6,6,1536,8,14,1024,4,1024,1024,1024,1024,4,4,1024,1024,4,4,18,18,18,18],[18,18,18,18,1536,6,5632,5632,5632,6,1536,8,14,1024,1024,1024,4,4,1024,4,4,4,1024,1024,1024,18,18,18,18],[18,18,18,18,1536,6,5632,5632,5632,6,1536,8,3584,1024,4,4,4,1024,4,4,1024,1024,4,1024,4,18,18,18,18],[18,18,18,18,1536,6,5632,5632,5632,6,1536,8,3584,4,1024,1024,4,1024,4,4,1024,4,1024,4,4,18,18,18,18],[18,18,18,18,1536,6,6,6,6,6,1536,8,3584,1024,1024,1024,1024,4,1024,4,1024,1024,4,4,1024,18,18,18,18],[18,18,18,18,1536,1536,1536,1536,1536,1536,1536,8,3584,1024,4,1024,1024,4,4,1024,4,4,4,4,4,18,18,18,18],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18]] +[[256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256],[256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256],[256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256],[256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256],[256,256,256,256,2052,2052,2052,2052,2052,2052,2052,8,2112,2050,2,2,2050,8,2052,2052,2052,2052,2052,2052,2052,256,256,256,256],[256,256,256,256,2052,4,4,4,4,4,2052,8,64,2050,2,2,2050,8,2052,4,4,4,4,4,2052,256,256,256,256],[256,256,256,256,2052,4,3072,3072,3072,4,2052,8,2112,2,2050,2,2050,8,2052,4,3072,3072,3072,4,2052,256,256,256,256],[256,256,256,256,2052,4,3072,3072,3072,4,2052,8,2112,2,2,2050,2,8,2052,4,3072,3072,3072,4,2052,256,256,256,256],[256,256,256,256,2052,4,3072,3072,3072,4,2052,8,2112,2050,2050,2,2,8,2052,4,3072,3072,3072,4,2052,256,256,256,256],[256,256,256,256,2052,4,4,4,4,4,2052,8,64,2,2,2,2,8,2052,4,4,4,4,4,2052,256,256,256,256],[256,256,256,256,2052,2052,2052,2052,2052,2052,2052,8,2080,32,2080,32,2080,8,2052,2052,2052,2052,2052,2052,2052,256,256,256,256],[256,256,256,256,8,8,8,8,8,8,8,8,64,2050,2050,2,2,8,8,8,8,8,8,8,8,256,256,256,256],[256,256,256,256,2112,2112,2112,2112,64,64,2080,64,2112,2,2050,2,2,2112,64,64,2112,2112,2112,64,2112,256,256,256,256],[256,256,256,256,2050,2,2,2050,2050,2050,32,2050,2,2,2050,2,2050,2,2,2050,2,2050,2050,2,2050,256,256,256,256],[256,256,256,256,2050,2,2050,2,2050,2050,2080,2,2,2,2050,2,2050,2050,2050,2050,2050,2,2,2050,2050,256,256,256,256],[256,256,256,256,2050,2,2050,2050,2,2,32,2050,2050,2,2050,2,2050,2050,2,2,2,2050,2,2050,2,256,256,256,256],[256,256,256,256,2,2050,2050,2,2050,2,2080,2,2,2050,2050,2050,2,2050,2,2,2050,2050,2,2050,2,256,256,256,256],[256,256,256,256,8,8,8,8,8,8,8,8,2049,2,2050,2,2,2050,2,2050,2,2050,2,2050,2,256,256,256,256],[256,256,256,256,2052,2052,2052,2052,2052,2052,2052,8,64,2,2050,2,2,2050,2050,2,2050,2050,2050,2,2,256,256,256,256],[256,256,256,256,2052,4,4,4,4,4,2052,8,64,2050,2,2050,2050,2050,2050,2,2,2050,2050,2,2,256,256,256,256],[256,256,256,256,2052,4,3072,3072,3072,4,2052,8,64,2050,2050,2050,2,2,2050,2,2,2,2050,2050,2050,256,256,256,256],[256,256,256,256,2052,4,3072,3072,3072,4,2052,8,2112,2050,2,2,2,2050,2,2,2050,2050,2,2050,2,256,256,256,256],[256,256,256,256,2052,4,3072,3072,3072,4,2052,8,2112,2,2050,2050,2,2050,2,2,2050,2,2050,2,2,256,256,256,256],[256,256,256,256,2052,4,4,4,4,4,2052,8,2112,2050,2050,2050,2050,2,2050,2,2050,2050,2,2,2050,256,256,256,256],[256,256,256,256,2052,2052,2052,2052,2052,2052,2052,8,2112,2050,2,2050,2050,2,2,2050,2,2,2,2,2,256,256,256,256],[256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256],[256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256],[256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256],[256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256]] diff --git a/tests/Output/samples/svg b/tests/Output/samples/svg index 54aceb9ec..10d895a93 100644 --- a/tests/Output/samples/svg +++ b/tests/Output/samples/svg @@ -1 +1 @@ -data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJxci1zdmcgIiBzdHlsZT0id2lkdGg6IDEwMCU7IGhlaWdodDogYXV0bzsiIHZpZXdCb3g9IjAgMCAyOSAyOSI+DQo8ZGVmcz48c3R5bGU+cmVjdHtzaGFwZS1yZW5kZXJpbmc6Y3Jpc3BFZGdlc308L3N0eWxlPjwvZGVmcz4NCjxwYXRoIGNsYXNzPSJxci00ICIgc3Ryb2tlPSJ0cmFuc3BhcmVudCIgZmlsbD0iI2ZmZiIgZmlsbC1vcGFjaXR5PSIxIiBkPSJNMTQgNCBoMiB2MSBoLTJaIE0xNCA1IGgyIHYxIGgtMlogTTEzIDYgaDEgdjEgaC0xWiBNMTUgNiBoMSB2MSBoLTFaIE0xMyA3IGgyIHYxIGgtMlogTTE2IDcgaDEgdjEgaC0xWiBNMTUgOCBoMiB2MSBoLTJaIE0xMyA5IGg0IHYxIGgtNFogTTE1IDExIGgyIHYxIGgtMlogTTEzIDEyIGgxIHYxIGgtMVogTTE1IDEyIGgyIHYxIGgtMlogTTUgMTMgaDIgdjEgaC0yWiBNMTIgMTMgaDIgdjEgaC0yWiBNMTUgMTMgaDEgdjEgaC0xWiBNMTcgMTMgaDIgdjEgaC0yWiBNMjAgMTMgaDEgdjEgaC0xWiBNMjMgMTMgaDEgdjEgaC0xWiBNNSAxNCBoMSB2MSBoLTFaIE03IDE0IGgxIHYxIGgtMVogTTExIDE0IGgzIHYxIGgtM1ogTTE1IDE0IGgxIHYxIGgtMVogTTIxIDE0IGgyIHYxIGgtMlogTTUgMTUgaDEgdjEgaC0xWiBNOCAxNSBoMiB2MSBoLTJaIE0xMyAxNSBoMSB2MSBoLTFaIE0xNSAxNSBoMSB2MSBoLTFaIE0xOCAxNSBoMyB2MSBoLTNaIE0yMiAxNSBoMSB2MSBoLTFaIE0yNCAxNSBoMSB2MSBoLTFaIE00IDE2IGgxIHYxIGgtMVogTTcgMTYgaDEgdjEgaC0xWiBNOSAxNiBoMSB2MSBoLTFaIE0xMSAxNiBoMiB2MSBoLTJaIE0xNiAxNiBoMSB2MSBoLTFaIE0xOCAxNiBoMiB2MSBoLTJaIE0yMiAxNiBoMSB2MSBoLTFaIE0yNCAxNiBoMSB2MSBoLTFaIE0xMyAxNyBoMSB2MSBoLTFaIE0xNSAxNyBoMiB2MSBoLTJaIE0xOCAxNyBoMSB2MSBoLTFaIE0yMCAxNyBoMSB2MSBoLTFaIE0yMiAxNyBoMSB2MSBoLTFaIE0yNCAxNyBoMSB2MSBoLTFaIE0xMyAxOCBoMSB2MSBoLTFaIE0xNSAxOCBoMiB2MSBoLTJaIE0xOSAxOCBoMSB2MSBoLTFaIE0yMyAxOCBoMiB2MSBoLTJaIE0xNCAxOSBoMSB2MSBoLTFaIE0xOSAxOSBoMiB2MSBoLTJaIE0yMyAxOSBoMiB2MSBoLTJaIE0xNiAyMCBoMiB2MSBoLTJaIE0xOSAyMCBoMyB2MSBoLTNaIE0xNCAyMSBoMyB2MSBoLTNaIE0xOCAyMSBoMiB2MSBoLTJaIE0yMiAyMSBoMSB2MSBoLTFaIE0yNCAyMSBoMSB2MSBoLTFaIE0xMyAyMiBoMSB2MSBoLTFaIE0xNiAyMiBoMSB2MSBoLTFaIE0xOCAyMiBoMiB2MSBoLTJaIE0yMSAyMiBoMSB2MSBoLTFaIE0yMyAyMiBoMiB2MSBoLTJaIE0xNyAyMyBoMSB2MSBoLTFaIE0xOSAyMyBoMSB2MSBoLTFaIE0yMiAyMyBoMiB2MSBoLTJaIE0xNCAyNCBoMSB2MSBoLTFaIE0xNyAyNCBoMiB2MSBoLTJaIE0yMCAyNCBoNSB2MSBoLTVaICIgLz48cGF0aCBjbGFzcz0icXItNiAiIHN0cm9rZT0idHJhbnNwYXJlbnQiIGZpbGw9IiNmZmYiIGZpbGwtb3BhY2l0eT0iMSIgZD0iTTUgNSBoNSB2MSBoLTVaIE0xOSA1IGg1IHYxIGgtNVogTTUgNiBoMSB2MSBoLTFaIE05IDYgaDEgdjEgaC0xWiBNMTkgNiBoMSB2MSBoLTFaIE0yMyA2IGgxIHYxIGgtMVogTTUgNyBoMSB2MSBoLTFaIE05IDcgaDEgdjEgaC0xWiBNMTkgNyBoMSB2MSBoLTFaIE0yMyA3IGgxIHYxIGgtMVogTTUgOCBoMSB2MSBoLTFaIE05IDggaDEgdjEgaC0xWiBNMTkgOCBoMSB2MSBoLTFaIE0yMyA4IGgxIHYxIGgtMVogTTUgOSBoNSB2MSBoLTVaIE0xOSA5IGg1IHYxIGgtNVogTTUgMTkgaDUgdjEgaC01WiBNNSAyMCBoMSB2MSBoLTFaIE05IDIwIGgxIHYxIGgtMVogTTUgMjEgaDEgdjEgaC0xWiBNOSAyMSBoMSB2MSBoLTFaIE01IDIyIGgxIHYxIGgtMVogTTkgMjIgaDEgdjEgaC0xWiBNNSAyMyBoNSB2MSBoLTVaICIgLz48cGF0aCBjbGFzcz0icXItOCAiIHN0cm9rZT0idHJhbnNwYXJlbnQiIGZpbGw9IiNmZmYiIGZpbGwtb3BhY2l0eT0iMSIgZD0iTTExIDQgaDEgdjEgaC0xWiBNMTcgNCBoMSB2MSBoLTFaIE0xMSA1IGgxIHYxIGgtMVogTTE3IDUgaDEgdjEgaC0xWiBNMTEgNiBoMSB2MSBoLTFaIE0xNyA2IGgxIHYxIGgtMVogTTExIDcgaDEgdjEgaC0xWiBNMTcgNyBoMSB2MSBoLTFaIE0xMSA4IGgxIHYxIGgtMVogTTE3IDggaDEgdjEgaC0xWiBNMTEgOSBoMSB2MSBoLTFaIE0xNyA5IGgxIHYxIGgtMVogTTExIDEwIGgxIHYxIGgtMVogTTE3IDEwIGgxIHYxIGgtMVogTTQgMTEgaDggdjEgaC04WiBNMTcgMTEgaDggdjEgaC04WiBNNCAxNyBoOCB2MSBoLThaIE0xMSAxOCBoMSB2MSBoLTFaIE0xMSAxOSBoMSB2MSBoLTFaIE0xMSAyMCBoMSB2MSBoLTFaIE0xMSAyMSBoMSB2MSBoLTFaIE0xMSAyMiBoMSB2MSBoLTFaIE0xMSAyMyBoMSB2MSBoLTFaIE0xMSAyNCBoMSB2MSBoLTFaICIgLz48cGF0aCBjbGFzcz0icXItMTIgIiBzdHJva2U9InRyYW5zcGFyZW50IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjEiIGQ9Ik0xMyAxMCBoMSB2MSBoLTFaIE0xNSAxMCBoMSB2MSBoLTFaIE0xMCAxMyBoMSB2MSBoLTFaIE0xMCAxNSBoMSB2MSBoLTFaICIgLz48cGF0aCBjbGFzcz0icXItMTQgIiBzdHJva2U9InRyYW5zcGFyZW50IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjEiIGQ9Ik0xMiA1IGgxIHYxIGgtMVogTTEyIDkgaDEgdjEgaC0xWiBNMTIgMTEgaDEgdjEgaC0xWiBNOCAxMiBoMiB2MSBoLTJaIE0xMSAxMiBoMSB2MSBoLTFaIE0xOCAxMiBoMiB2MSBoLTJaIE0yMyAxMiBoMSB2MSBoLTFaIE0xMiAxOCBoMSB2MSBoLTFaIE0xMiAxOSBoMSB2MSBoLTFaIE0xMiAyMCBoMSB2MSBoLTFaICIgLz48cGF0aCBjbGFzcz0icXItMTggIiBzdHJva2U9InRyYW5zcGFyZW50IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjEiIGQ9Ik0wIDAgaDI5IHYxIGgtMjlaIE0wIDEgaDI5IHYxIGgtMjlaIE0wIDIgaDI5IHYxIGgtMjlaIE0wIDMgaDI5IHYxIGgtMjlaIE0wIDQgaDQgdjEgaC00WiBNMjUgNCBoNCB2MSBoLTRaIE0wIDUgaDQgdjEgaC00WiBNMjUgNSBoNCB2MSBoLTRaIE0wIDYgaDQgdjEgaC00WiBNMjUgNiBoNCB2MSBoLTRaIE0wIDcgaDQgdjEgaC00WiBNMjUgNyBoNCB2MSBoLTRaIE0wIDggaDQgdjEgaC00WiBNMjUgOCBoNCB2MSBoLTRaIE0wIDkgaDQgdjEgaC00WiBNMjUgOSBoNCB2MSBoLTRaIE0wIDEwIGg0IHYxIGgtNFogTTI1IDEwIGg0IHYxIGgtNFogTTAgMTEgaDQgdjEgaC00WiBNMjUgMTEgaDQgdjEgaC00WiBNMCAxMiBoNCB2MSBoLTRaIE0yNSAxMiBoNCB2MSBoLTRaIE0wIDEzIGg0IHYxIGgtNFogTTI1IDEzIGg0IHYxIGgtNFogTTAgMTQgaDQgdjEgaC00WiBNMjUgMTQgaDQgdjEgaC00WiBNMCAxNSBoNCB2MSBoLTRaIE0yNSAxNSBoNCB2MSBoLTRaIE0wIDE2IGg0IHYxIGgtNFogTTI1IDE2IGg0IHYxIGgtNFogTTAgMTcgaDQgdjEgaC00WiBNMjUgMTcgaDQgdjEgaC00WiBNMCAxOCBoNCB2MSBoLTRaIE0yNSAxOCBoNCB2MSBoLTRaIE0wIDE5IGg0IHYxIGgtNFogTTI1IDE5IGg0IHYxIGgtNFogTTAgMjAgaDQgdjEgaC00WiBNMjUgMjAgaDQgdjEgaC00WiBNMCAyMSBoNCB2MSBoLTRaIE0yNSAyMSBoNCB2MSBoLTRaIE0wIDIyIGg0IHYxIGgtNFogTTI1IDIyIGg0IHYxIGgtNFogTTAgMjMgaDQgdjEgaC00WiBNMjUgMjMgaDQgdjEgaC00WiBNMCAyNCBoNCB2MSBoLTRaIE0yNSAyNCBoNCB2MSBoLTRaIE0wIDI1IGgyOSB2MSBoLTI5WiBNMCAyNiBoMjkgdjEgaC0yOVogTTAgMjcgaDI5IHYxIGgtMjlaIE0wIDI4IGgyOSB2MSBoLTI5WiAiIC8+PHBhdGggY2xhc3M9InFyLTUxMiAiIHN0cm9rZT0idHJhbnNwYXJlbnQiIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iMSIgZD0iTTEyIDE3IGgxIHYxIGgtMVogIiAvPjxwYXRoIGNsYXNzPSJxci0xMDI0ICIgc3Ryb2tlPSJ0cmFuc3BhcmVudCIgZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIxIiBkPSJNMTMgNCBoMSB2MSBoLTFaIE0xNiA0IGgxIHYxIGgtMVogTTEzIDUgaDEgdjEgaC0xWiBNMTYgNSBoMSB2MSBoLTFaIE0xNCA2IGgxIHYxIGgtMVogTTE2IDYgaDEgdjEgaC0xWiBNMTUgNyBoMSB2MSBoLTFaIE0xMyA4IGgyIHYxIGgtMlogTTEzIDExIGgyIHYxIGgtMlogTTE0IDEyIGgxIHYxIGgtMVogTTQgMTMgaDEgdjEgaC0xWiBNNyAxMyBoMyB2MSBoLTNaIE0xMSAxMyBoMSB2MSBoLTFaIE0xNCAxMyBoMSB2MSBoLTFaIE0xNiAxMyBoMSB2MSBoLTFaIE0xOSAxMyBoMSB2MSBoLTFaIE0yMSAxMyBoMiB2MSBoLTJaIE0yNCAxMyBoMSB2MSBoLTFaIE00IDE0IGgxIHYxIGgtMVogTTYgMTQgaDEgdjEgaC0xWiBNOCAxNCBoMiB2MSBoLTJaIE0xNCAxNCBoMSB2MSBoLTFaIE0xNiAxNCBoNSB2MSBoLTVaIE0yMyAxNCBoMiB2MSBoLTJaIE00IDE1IGgxIHYxIGgtMVogTTYgMTUgaDIgdjEgaC0yWiBNMTEgMTUgaDIgdjEgaC0yWiBNMTQgMTUgaDEgdjEgaC0xWiBNMTYgMTUgaDIgdjEgaC0yWiBNMjEgMTUgaDEgdjEgaC0xWiBNMjMgMTUgaDEgdjEgaC0xWiBNNSAxNiBoMiB2MSBoLTJaIE04IDE2IGgxIHYxIGgtMVogTTEzIDE2IGgzIHYxIGgtM1ogTTE3IDE2IGgxIHYxIGgtMVogTTIwIDE2IGgyIHYxIGgtMlogTTIzIDE2IGgxIHYxIGgtMVogTTE0IDE3IGgxIHYxIGgtMVogTTE3IDE3IGgxIHYxIGgtMVogTTE5IDE3IGgxIHYxIGgtMVogTTIxIDE3IGgxIHYxIGgtMVogTTIzIDE3IGgxIHYxIGgtMVogTTE0IDE4IGgxIHYxIGgtMVogTTE3IDE4IGgyIHYxIGgtMlogTTIwIDE4IGgzIHYxIGgtM1ogTTEzIDE5IGgxIHYxIGgtMVogTTE1IDE5IGg0IHYxIGgtNFogTTIxIDE5IGgyIHYxIGgtMlogTTEzIDIwIGgzIHYxIGgtM1ogTTE4IDIwIGgxIHYxIGgtMVogTTIyIDIwIGgzIHYxIGgtM1ogTTEzIDIxIGgxIHYxIGgtMVogTTE3IDIxIGgxIHYxIGgtMVogTTIwIDIxIGgyIHYxIGgtMlogTTIzIDIxIGgxIHYxIGgtMVogTTE0IDIyIGgyIHYxIGgtMlogTTE3IDIyIGgxIHYxIGgtMVogTTIwIDIyIGgxIHYxIGgtMVogTTIyIDIyIGgxIHYxIGgtMVogTTEzIDIzIGg0IHYxIGgtNFogTTE4IDIzIGgxIHYxIGgtMVogTTIwIDIzIGgyIHYxIGgtMlogTTI0IDIzIGgxIHYxIGgtMVogTTEzIDI0IGgxIHYxIGgtMVogTTE1IDI0IGgyIHYxIGgtMlogTTE5IDI0IGgxIHYxIGgtMVogIiAvPjxwYXRoIGNsYXNzPSJxci0xNTM2ICIgc3Ryb2tlPSJ0cmFuc3BhcmVudCIgZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIxIiBkPSJNNCA0IGg3IHYxIGgtN1ogTTE4IDQgaDcgdjEgaC03WiBNNCA1IGgxIHYxIGgtMVogTTEwIDUgaDEgdjEgaC0xWiBNMTggNSBoMSB2MSBoLTFaIE0yNCA1IGgxIHYxIGgtMVogTTQgNiBoMSB2MSBoLTFaIE0xMCA2IGgxIHYxIGgtMVogTTE4IDYgaDEgdjEgaC0xWiBNMjQgNiBoMSB2MSBoLTFaIE00IDcgaDEgdjEgaC0xWiBNMTAgNyBoMSB2MSBoLTFaIE0xOCA3IGgxIHYxIGgtMVogTTI0IDcgaDEgdjEgaC0xWiBNNCA4IGgxIHYxIGgtMVogTTEwIDggaDEgdjEgaC0xWiBNMTggOCBoMSB2MSBoLTFaIE0yNCA4IGgxIHYxIGgtMVogTTQgOSBoMSB2MSBoLTFaIE0xMCA5IGgxIHYxIGgtMVogTTE4IDkgaDEgdjEgaC0xWiBNMjQgOSBoMSB2MSBoLTFaIE00IDEwIGg3IHYxIGgtN1ogTTE4IDEwIGg3IHYxIGgtN1ogTTQgMTggaDcgdjEgaC03WiBNNCAxOSBoMSB2MSBoLTFaIE0xMCAxOSBoMSB2MSBoLTFaIE00IDIwIGgxIHYxIGgtMVogTTEwIDIwIGgxIHYxIGgtMVogTTQgMjEgaDEgdjEgaC0xWiBNMTAgMjEgaDEgdjEgaC0xWiBNNCAyMiBoMSB2MSBoLTFaIE0xMCAyMiBoMSB2MSBoLTFaIE00IDIzIGgxIHYxIGgtMVogTTEwIDIzIGgxIHYxIGgtMVogTTQgMjQgaDcgdjEgaC03WiAiIC8+PHBhdGggY2xhc3M9InFyLTMwNzIgIiBzdHJva2U9InRyYW5zcGFyZW50IiBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9IjEiIGQ9Ik0xMiAxMCBoMSB2MSBoLTFaIE0xNCAxMCBoMSB2MSBoLTFaIE0xNiAxMCBoMSB2MSBoLTFaIE0xMCAxMiBoMSB2MSBoLTFaIE0xMCAxNCBoMSB2MSBoLTFaIE0xMCAxNiBoMSB2MSBoLTFaICIgLz48cGF0aCBjbGFzcz0icXItMzU4NCAiIHN0cm9rZT0idHJhbnNwYXJlbnQiIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iMSIgZD0iTTEyIDQgaDEgdjEgaC0xWiBNMTIgNiBoMSB2MSBoLTFaIE0xMiA3IGgxIHYxIGgtMVogTTEyIDggaDEgdjEgaC0xWiBNNCAxMiBoNCB2MSBoLTRaIE0xMiAxMiBoMSB2MSBoLTFaIE0xNyAxMiBoMSB2MSBoLTFaIE0yMCAxMiBoMyB2MSBoLTNaIE0yNCAxMiBoMSB2MSBoLTFaIE0xMiAyMSBoMSB2MSBoLTFaIE0xMiAyMiBoMSB2MSBoLTFaIE0xMiAyMyBoMSB2MSBoLTFaIE0xMiAyNCBoMSB2MSBoLTFaICIgLz48cGF0aCBjbGFzcz0icXItNTYzMiAiIHN0cm9rZT0idHJhbnNwYXJlbnQiIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iMSIgZD0iTTYgNiBoMyB2MSBoLTNaIE0yMCA2IGgzIHYxIGgtM1ogTTYgNyBoMyB2MSBoLTNaIE0yMCA3IGgzIHYxIGgtM1ogTTYgOCBoMyB2MSBoLTNaIE0yMCA4IGgzIHYxIGgtM1ogTTYgMjAgaDMgdjEgaC0zWiBNNiAyMSBoMyB2MSBoLTNaIE02IDIyIGgzIHYxIGgtM1ogIiAvPjwvc3ZnPg0K +data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJxci1zdmcgIiBzdHlsZT0id2lkdGg6IDEwMCU7IGhlaWdodDogYXV0bzsiIHZpZXdCb3g9IjAgMCAyOSAyOSI+DQo8ZGVmcz48c3R5bGU+cmVjdHtzaGFwZS1yZW5kZXJpbmc6Y3Jpc3BFZGdlc308L3N0eWxlPjwvZGVmcz4NCjxwYXRoIGNsYXNzPSJxci0yICIgc3Ryb2tlPSJ0cmFuc3BhcmVudCIgZmlsbD0iI2ZmZiIgZmlsbC1vcGFjaXR5PSIxIiBkPSJNMTQgNCBoMiB2MSBoLTJaIE0xNCA1IGgyIHYxIGgtMlogTTEzIDYgaDEgdjEgaC0xWiBNMTUgNiBoMSB2MSBoLTFaIE0xMyA3IGgyIHYxIGgtMlogTTE2IDcgaDEgdjEgaC0xWiBNMTUgOCBoMiB2MSBoLTJaIE0xMyA5IGg0IHYxIGgtNFogTTE1IDExIGgyIHYxIGgtMlogTTEzIDEyIGgxIHYxIGgtMVogTTE1IDEyIGgyIHYxIGgtMlogTTUgMTMgaDIgdjEgaC0yWiBNMTIgMTMgaDIgdjEgaC0yWiBNMTUgMTMgaDEgdjEgaC0xWiBNMTcgMTMgaDIgdjEgaC0yWiBNMjAgMTMgaDEgdjEgaC0xWiBNMjMgMTMgaDEgdjEgaC0xWiBNNSAxNCBoMSB2MSBoLTFaIE03IDE0IGgxIHYxIGgtMVogTTExIDE0IGgzIHYxIGgtM1ogTTE1IDE0IGgxIHYxIGgtMVogTTIxIDE0IGgyIHYxIGgtMlogTTUgMTUgaDEgdjEgaC0xWiBNOCAxNSBoMiB2MSBoLTJaIE0xMyAxNSBoMSB2MSBoLTFaIE0xNSAxNSBoMSB2MSBoLTFaIE0xOCAxNSBoMyB2MSBoLTNaIE0yMiAxNSBoMSB2MSBoLTFaIE0yNCAxNSBoMSB2MSBoLTFaIE00IDE2IGgxIHYxIGgtMVogTTcgMTYgaDEgdjEgaC0xWiBNOSAxNiBoMSB2MSBoLTFaIE0xMSAxNiBoMiB2MSBoLTJaIE0xNiAxNiBoMSB2MSBoLTFaIE0xOCAxNiBoMiB2MSBoLTJaIE0yMiAxNiBoMSB2MSBoLTFaIE0yNCAxNiBoMSB2MSBoLTFaIE0xMyAxNyBoMSB2MSBoLTFaIE0xNSAxNyBoMiB2MSBoLTJaIE0xOCAxNyBoMSB2MSBoLTFaIE0yMCAxNyBoMSB2MSBoLTFaIE0yMiAxNyBoMSB2MSBoLTFaIE0yNCAxNyBoMSB2MSBoLTFaIE0xMyAxOCBoMSB2MSBoLTFaIE0xNSAxOCBoMiB2MSBoLTJaIE0xOSAxOCBoMSB2MSBoLTFaIE0yMyAxOCBoMiB2MSBoLTJaIE0xNCAxOSBoMSB2MSBoLTFaIE0xOSAxOSBoMiB2MSBoLTJaIE0yMyAxOSBoMiB2MSBoLTJaIE0xNiAyMCBoMiB2MSBoLTJaIE0xOSAyMCBoMyB2MSBoLTNaIE0xNCAyMSBoMyB2MSBoLTNaIE0xOCAyMSBoMiB2MSBoLTJaIE0yMiAyMSBoMSB2MSBoLTFaIE0yNCAyMSBoMSB2MSBoLTFaIE0xMyAyMiBoMSB2MSBoLTFaIE0xNiAyMiBoMSB2MSBoLTFaIE0xOCAyMiBoMiB2MSBoLTJaIE0yMSAyMiBoMSB2MSBoLTFaIE0yMyAyMiBoMiB2MSBoLTJaIE0xNyAyMyBoMSB2MSBoLTFaIE0xOSAyMyBoMSB2MSBoLTFaIE0yMiAyMyBoMiB2MSBoLTJaIE0xNCAyNCBoMSB2MSBoLTFaIE0xNyAyNCBoMiB2MSBoLTJaIE0yMCAyNCBoNSB2MSBoLTVaICIgLz48cGF0aCBjbGFzcz0icXItNCAiIHN0cm9rZT0idHJhbnNwYXJlbnQiIGZpbGw9IiNmZmYiIGZpbGwtb3BhY2l0eT0iMSIgZD0iTTUgNSBoNSB2MSBoLTVaIE0xOSA1IGg1IHYxIGgtNVogTTUgNiBoMSB2MSBoLTFaIE05IDYgaDEgdjEgaC0xWiBNMTkgNiBoMSB2MSBoLTFaIE0yMyA2IGgxIHYxIGgtMVogTTUgNyBoMSB2MSBoLTFaIE05IDcgaDEgdjEgaC0xWiBNMTkgNyBoMSB2MSBoLTFaIE0yMyA3IGgxIHYxIGgtMVogTTUgOCBoMSB2MSBoLTFaIE05IDggaDEgdjEgaC0xWiBNMTkgOCBoMSB2MSBoLTFaIE0yMyA4IGgxIHYxIGgtMVogTTUgOSBoNSB2MSBoLTVaIE0xOSA5IGg1IHYxIGgtNVogTTUgMTkgaDUgdjEgaC01WiBNNSAyMCBoMSB2MSBoLTFaIE05IDIwIGgxIHYxIGgtMVogTTUgMjEgaDEgdjEgaC0xWiBNOSAyMSBoMSB2MSBoLTFaIE01IDIyIGgxIHYxIGgtMVogTTkgMjIgaDEgdjEgaC0xWiBNNSAyMyBoNSB2MSBoLTVaICIgLz48cGF0aCBjbGFzcz0icXItOCAiIHN0cm9rZT0idHJhbnNwYXJlbnQiIGZpbGw9IiNmZmYiIGZpbGwtb3BhY2l0eT0iMSIgZD0iTTExIDQgaDEgdjEgaC0xWiBNMTcgNCBoMSB2MSBoLTFaIE0xMSA1IGgxIHYxIGgtMVogTTE3IDUgaDEgdjEgaC0xWiBNMTEgNiBoMSB2MSBoLTFaIE0xNyA2IGgxIHYxIGgtMVogTTExIDcgaDEgdjEgaC0xWiBNMTcgNyBoMSB2MSBoLTFaIE0xMSA4IGgxIHYxIGgtMVogTTE3IDggaDEgdjEgaC0xWiBNMTEgOSBoMSB2MSBoLTFaIE0xNyA5IGgxIHYxIGgtMVogTTExIDEwIGgxIHYxIGgtMVogTTE3IDEwIGgxIHYxIGgtMVogTTQgMTEgaDggdjEgaC04WiBNMTcgMTEgaDggdjEgaC04WiBNNCAxNyBoOCB2MSBoLThaIE0xMSAxOCBoMSB2MSBoLTFaIE0xMSAxOSBoMSB2MSBoLTFaIE0xMSAyMCBoMSB2MSBoLTFaIE0xMSAyMSBoMSB2MSBoLTFaIE0xMSAyMiBoMSB2MSBoLTFaIE0xMSAyMyBoMSB2MSBoLTFaIE0xMSAyNCBoMSB2MSBoLTFaICIgLz48cGF0aCBjbGFzcz0icXItMzIgIiBzdHJva2U9InRyYW5zcGFyZW50IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjEiIGQ9Ik0xMyAxMCBoMSB2MSBoLTFaIE0xNSAxMCBoMSB2MSBoLTFaIE0xMCAxMyBoMSB2MSBoLTFaIE0xMCAxNSBoMSB2MSBoLTFaICIgLz48cGF0aCBjbGFzcz0icXItNjQgIiBzdHJva2U9InRyYW5zcGFyZW50IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjEiIGQ9Ik0xMiA1IGgxIHYxIGgtMVogTTEyIDkgaDEgdjEgaC0xWiBNMTIgMTEgaDEgdjEgaC0xWiBNOCAxMiBoMiB2MSBoLTJaIE0xMSAxMiBoMSB2MSBoLTFaIE0xOCAxMiBoMiB2MSBoLTJaIE0yMyAxMiBoMSB2MSBoLTFaIE0xMiAxOCBoMSB2MSBoLTFaIE0xMiAxOSBoMSB2MSBoLTFaIE0xMiAyMCBoMSB2MSBoLTFaICIgLz48cGF0aCBjbGFzcz0icXItMjU2ICIgc3Ryb2tlPSJ0cmFuc3BhcmVudCIgZmlsbD0iI2ZmZiIgZmlsbC1vcGFjaXR5PSIxIiBkPSJNMCAwIGgyOSB2MSBoLTI5WiBNMCAxIGgyOSB2MSBoLTI5WiBNMCAyIGgyOSB2MSBoLTI5WiBNMCAzIGgyOSB2MSBoLTI5WiBNMCA0IGg0IHYxIGgtNFogTTI1IDQgaDQgdjEgaC00WiBNMCA1IGg0IHYxIGgtNFogTTI1IDUgaDQgdjEgaC00WiBNMCA2IGg0IHYxIGgtNFogTTI1IDYgaDQgdjEgaC00WiBNMCA3IGg0IHYxIGgtNFogTTI1IDcgaDQgdjEgaC00WiBNMCA4IGg0IHYxIGgtNFogTTI1IDggaDQgdjEgaC00WiBNMCA5IGg0IHYxIGgtNFogTTI1IDkgaDQgdjEgaC00WiBNMCAxMCBoNCB2MSBoLTRaIE0yNSAxMCBoNCB2MSBoLTRaIE0wIDExIGg0IHYxIGgtNFogTTI1IDExIGg0IHYxIGgtNFogTTAgMTIgaDQgdjEgaC00WiBNMjUgMTIgaDQgdjEgaC00WiBNMCAxMyBoNCB2MSBoLTRaIE0yNSAxMyBoNCB2MSBoLTRaIE0wIDE0IGg0IHYxIGgtNFogTTI1IDE0IGg0IHYxIGgtNFogTTAgMTUgaDQgdjEgaC00WiBNMjUgMTUgaDQgdjEgaC00WiBNMCAxNiBoNCB2MSBoLTRaIE0yNSAxNiBoNCB2MSBoLTRaIE0wIDE3IGg0IHYxIGgtNFogTTI1IDE3IGg0IHYxIGgtNFogTTAgMTggaDQgdjEgaC00WiBNMjUgMTggaDQgdjEgaC00WiBNMCAxOSBoNCB2MSBoLTRaIE0yNSAxOSBoNCB2MSBoLTRaIE0wIDIwIGg0IHYxIGgtNFogTTI1IDIwIGg0IHYxIGgtNFogTTAgMjEgaDQgdjEgaC00WiBNMjUgMjEgaDQgdjEgaC00WiBNMCAyMiBoNCB2MSBoLTRaIE0yNSAyMiBoNCB2MSBoLTRaIE0wIDIzIGg0IHYxIGgtNFogTTI1IDIzIGg0IHYxIGgtNFogTTAgMjQgaDQgdjEgaC00WiBNMjUgMjQgaDQgdjEgaC00WiBNMCAyNSBoMjkgdjEgaC0yOVogTTAgMjYgaDI5IHYxIGgtMjlaIE0wIDI3IGgyOSB2MSBoLTI5WiBNMCAyOCBoMjkgdjEgaC0yOVogIiAvPjxwYXRoIGNsYXNzPSJxci0yMDQ5ICIgc3Ryb2tlPSJ0cmFuc3BhcmVudCIgZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIxIiBkPSJNMTIgMTcgaDEgdjEgaC0xWiAiIC8+PHBhdGggY2xhc3M9InFyLTIwNTAgIiBzdHJva2U9InRyYW5zcGFyZW50IiBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9IjEiIGQ9Ik0xMyA0IGgxIHYxIGgtMVogTTE2IDQgaDEgdjEgaC0xWiBNMTMgNSBoMSB2MSBoLTFaIE0xNiA1IGgxIHYxIGgtMVogTTE0IDYgaDEgdjEgaC0xWiBNMTYgNiBoMSB2MSBoLTFaIE0xNSA3IGgxIHYxIGgtMVogTTEzIDggaDIgdjEgaC0yWiBNMTMgMTEgaDIgdjEgaC0yWiBNMTQgMTIgaDEgdjEgaC0xWiBNNCAxMyBoMSB2MSBoLTFaIE03IDEzIGgzIHYxIGgtM1ogTTExIDEzIGgxIHYxIGgtMVogTTE0IDEzIGgxIHYxIGgtMVogTTE2IDEzIGgxIHYxIGgtMVogTTE5IDEzIGgxIHYxIGgtMVogTTIxIDEzIGgyIHYxIGgtMlogTTI0IDEzIGgxIHYxIGgtMVogTTQgMTQgaDEgdjEgaC0xWiBNNiAxNCBoMSB2MSBoLTFaIE04IDE0IGgyIHYxIGgtMlogTTE0IDE0IGgxIHYxIGgtMVogTTE2IDE0IGg1IHYxIGgtNVogTTIzIDE0IGgyIHYxIGgtMlogTTQgMTUgaDEgdjEgaC0xWiBNNiAxNSBoMiB2MSBoLTJaIE0xMSAxNSBoMiB2MSBoLTJaIE0xNCAxNSBoMSB2MSBoLTFaIE0xNiAxNSBoMiB2MSBoLTJaIE0yMSAxNSBoMSB2MSBoLTFaIE0yMyAxNSBoMSB2MSBoLTFaIE01IDE2IGgyIHYxIGgtMlogTTggMTYgaDEgdjEgaC0xWiBNMTMgMTYgaDMgdjEgaC0zWiBNMTcgMTYgaDEgdjEgaC0xWiBNMjAgMTYgaDIgdjEgaC0yWiBNMjMgMTYgaDEgdjEgaC0xWiBNMTQgMTcgaDEgdjEgaC0xWiBNMTcgMTcgaDEgdjEgaC0xWiBNMTkgMTcgaDEgdjEgaC0xWiBNMjEgMTcgaDEgdjEgaC0xWiBNMjMgMTcgaDEgdjEgaC0xWiBNMTQgMTggaDEgdjEgaC0xWiBNMTcgMTggaDIgdjEgaC0yWiBNMjAgMTggaDMgdjEgaC0zWiBNMTMgMTkgaDEgdjEgaC0xWiBNMTUgMTkgaDQgdjEgaC00WiBNMjEgMTkgaDIgdjEgaC0yWiBNMTMgMjAgaDMgdjEgaC0zWiBNMTggMjAgaDEgdjEgaC0xWiBNMjIgMjAgaDMgdjEgaC0zWiBNMTMgMjEgaDEgdjEgaC0xWiBNMTcgMjEgaDEgdjEgaC0xWiBNMjAgMjEgaDIgdjEgaC0yWiBNMjMgMjEgaDEgdjEgaC0xWiBNMTQgMjIgaDIgdjEgaC0yWiBNMTcgMjIgaDEgdjEgaC0xWiBNMjAgMjIgaDEgdjEgaC0xWiBNMjIgMjIgaDEgdjEgaC0xWiBNMTMgMjMgaDQgdjEgaC00WiBNMTggMjMgaDEgdjEgaC0xWiBNMjAgMjMgaDIgdjEgaC0yWiBNMjQgMjMgaDEgdjEgaC0xWiBNMTMgMjQgaDEgdjEgaC0xWiBNMTUgMjQgaDIgdjEgaC0yWiBNMTkgMjQgaDEgdjEgaC0xWiAiIC8+PHBhdGggY2xhc3M9InFyLTIwNTIgIiBzdHJva2U9InRyYW5zcGFyZW50IiBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9IjEiIGQ9Ik00IDQgaDcgdjEgaC03WiBNMTggNCBoNyB2MSBoLTdaIE00IDUgaDEgdjEgaC0xWiBNMTAgNSBoMSB2MSBoLTFaIE0xOCA1IGgxIHYxIGgtMVogTTI0IDUgaDEgdjEgaC0xWiBNNCA2IGgxIHYxIGgtMVogTTEwIDYgaDEgdjEgaC0xWiBNMTggNiBoMSB2MSBoLTFaIE0yNCA2IGgxIHYxIGgtMVogTTQgNyBoMSB2MSBoLTFaIE0xMCA3IGgxIHYxIGgtMVogTTE4IDcgaDEgdjEgaC0xWiBNMjQgNyBoMSB2MSBoLTFaIE00IDggaDEgdjEgaC0xWiBNMTAgOCBoMSB2MSBoLTFaIE0xOCA4IGgxIHYxIGgtMVogTTI0IDggaDEgdjEgaC0xWiBNNCA5IGgxIHYxIGgtMVogTTEwIDkgaDEgdjEgaC0xWiBNMTggOSBoMSB2MSBoLTFaIE0yNCA5IGgxIHYxIGgtMVogTTQgMTAgaDcgdjEgaC03WiBNMTggMTAgaDcgdjEgaC03WiBNNCAxOCBoNyB2MSBoLTdaIE00IDE5IGgxIHYxIGgtMVogTTEwIDE5IGgxIHYxIGgtMVogTTQgMjAgaDEgdjEgaC0xWiBNMTAgMjAgaDEgdjEgaC0xWiBNNCAyMSBoMSB2MSBoLTFaIE0xMCAyMSBoMSB2MSBoLTFaIE00IDIyIGgxIHYxIGgtMVogTTEwIDIyIGgxIHYxIGgtMVogTTQgMjMgaDEgdjEgaC0xWiBNMTAgMjMgaDEgdjEgaC0xWiBNNCAyNCBoNyB2MSBoLTdaICIgLz48cGF0aCBjbGFzcz0icXItMjA4MCAiIHN0cm9rZT0idHJhbnNwYXJlbnQiIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iMSIgZD0iTTEyIDEwIGgxIHYxIGgtMVogTTE0IDEwIGgxIHYxIGgtMVogTTE2IDEwIGgxIHYxIGgtMVogTTEwIDEyIGgxIHYxIGgtMVogTTEwIDE0IGgxIHYxIGgtMVogTTEwIDE2IGgxIHYxIGgtMVogIiAvPjxwYXRoIGNsYXNzPSJxci0yMTEyICIgc3Ryb2tlPSJ0cmFuc3BhcmVudCIgZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIxIiBkPSJNMTIgNCBoMSB2MSBoLTFaIE0xMiA2IGgxIHYxIGgtMVogTTEyIDcgaDEgdjEgaC0xWiBNMTIgOCBoMSB2MSBoLTFaIE00IDEyIGg0IHYxIGgtNFogTTEyIDEyIGgxIHYxIGgtMVogTTE3IDEyIGgxIHYxIGgtMVogTTIwIDEyIGgzIHYxIGgtM1ogTTI0IDEyIGgxIHYxIGgtMVogTTEyIDIxIGgxIHYxIGgtMVogTTEyIDIyIGgxIHYxIGgtMVogTTEyIDIzIGgxIHYxIGgtMVogTTEyIDI0IGgxIHYxIGgtMVogIiAvPjxwYXRoIGNsYXNzPSJxci0zMDcyICIgc3Ryb2tlPSJ0cmFuc3BhcmVudCIgZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIxIiBkPSJNNiA2IGgzIHYxIGgtM1ogTTIwIDYgaDMgdjEgaC0zWiBNNiA3IGgzIHYxIGgtM1ogTTIwIDcgaDMgdjEgaC0zWiBNNiA4IGgzIHYxIGgtM1ogTTIwIDggaDMgdjEgaC0zWiBNNiAyMCBoMyB2MSBoLTNaIE02IDIxIGgzIHYxIGgtM1ogTTYgMjIgaDMgdjEgaC0zWiAiIC8+PC9zdmc+DQo= From 182ebf4e70a78c893deaeada5c209ce8e4c24fce Mon Sep 17 00:00:00 2001 From: codemasher Date: Tue, 19 Jan 2021 01:15:34 +0100 Subject: [PATCH 46/78] :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); } From 0a1ceecdd8c622523f250391f800fd09f9d2ef42 Mon Sep 17 00:00:00 2001 From: codemasher Date: Tue, 19 Jan 2021 17:34:45 +0100 Subject: [PATCH 47/78] :shower: --- README.md | 4 ++-- src/Common/MaskPattern.php | 34 +++++++++++++++++++--------------- src/QRCode.php | 36 +++++++++++++++++++++--------------- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index fa36b5044..bab593b18 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,9 @@ via terminal: `composer require chillerlan/php-qrcode` } ``` -Note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^3.2` - see [releases](https://github.com/chillerlan/php-qrcode/releases) for valid versions. +Note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^4.3` - see [releases](https://github.com/chillerlan/php-qrcode/releases) for valid versions. For PHP version ... - - 7.4+ use `^4.2` + - 7.4+ use `^4.3` - 7.2+ use `^3.3` - 7.0+ use `^2.0` (PHP 7.0 and 7.1 are EOL!) - 5.6+ use `^1.0` (please let PHP 5 die!) diff --git a/src/Common/MaskPattern.php b/src/Common/MaskPattern.php index a036f93b5..e9e628a6a 100644 --- a/src/Common/MaskPattern.php +++ b/src/Common/MaskPattern.php @@ -16,9 +16,9 @@ use chillerlan\QRCode\QRCodeException; use Closure; /** - * + * ISO/IEC 18004:2000 Section 8.8.1 */ -class MaskPattern{ +final class MaskPattern{ public const PATTERN_000 = 0b000; public const PATTERN_001 = 0b001; @@ -45,39 +45,43 @@ class 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 + throw new QRCodeException('invalid mask pattern'); } $this->maskPattern = $maskPattern; } + /** + * Returns the current mask pattern + */ public function getPattern():int{ return $this->maskPattern; } /** - * ISO/IEC 18004:2000 Section 8.8.1 + * Returns a closure that applies the mask for the chosen mask pattern. * * 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) + * The information below has been corrected. + * + * @see https://www.thonky.com/qr-code-tutorial/mask-patterns */ public function getMask():Closure{ + // $x = column (width), $y = row (height) 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, + self::PATTERN_000 => fn(int $x, int $y):int => ($x + $y) % 2, + self::PATTERN_001 => fn(int $x, int $y):int => $y % 2, + self::PATTERN_010 => fn(int $x, int $y):int => $x % 3, + self::PATTERN_011 => fn(int $x, int $y):int => ($x + $y) % 3, + self::PATTERN_100 => fn(int $x, int $y):int => ((int)($y / 2) + (int)($x / 3)) % 2, + self::PATTERN_101 => fn(int $x, int $y):int => (($x * $y) % 2) + (($x * $y) % 3), + self::PATTERN_110 => fn(int $x, int $y):int => ((($x * $y) % 2) + (($x * $y) % 3)) % 2, + self::PATTERN_111 => fn(int $x, int $y):int => ((($x * $y) % 3) + (($x + $y) % 2)) % 2, ][$this->maskPattern]; } diff --git a/src/QRCode.php b/src/QRCode.php index 7dd57d949..5df31ab35 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -13,11 +13,8 @@ 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 -}; +use chillerlan\QRCode\Common\{MaskPattern, Mode}; +use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString}; use chillerlan\Settings\SettingsContainerInterface; use function class_exists, in_array; @@ -140,8 +137,6 @@ class QRCode{ return $this->initOutputInterface()->dump($file); } - - /** * Returns a QRMatrix object for the given $data and current QROptions * @@ -192,8 +187,6 @@ class QRCode{ /** * checks if a string qualifies as numeric (convenience method) - * - * @see Number::validateString() */ public function isNumber(string $string):bool{ return Number::validateString($string); @@ -201,8 +194,6 @@ class QRCode{ /** * checks if a string qualifies as alphanumeric (convenience method) - * - * @see AlphaNum::validateString() */ public function isAlphaNum(string $string):bool{ return AlphaNum::validateString($string); @@ -210,8 +201,6 @@ class QRCode{ /** * checks if a string qualifies as Kanji (convenience method) - * - * @see Kanji::validateString() */ public function isKanji(string $string):bool{ return Kanji::validateString($string); @@ -219,14 +208,15 @@ class QRCode{ /** * a dummy (convenience method) - * - * @see Byte::validateString() */ public function isByte(string $string):bool{ return Byte::validateString($string); } /** + * ISO/IEC 18004:2000 8.3.6 - Mixing modes + * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length + * * @param string|int $data * @param string $classname * @@ -236,33 +226,49 @@ class QRCode{ $this->dataSegments[] = [$classname, $data]; } + /** + * ISO/IEC 18004:2000 8.3.2 - Numeric Mode + */ public function addNumberSegment(string $data):QRCode{ $this->addSegment($data, Number::class); return $this; } + /** + * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode + */ public function addAlphaNumSegment(string $data):QRCode{ $this->addSegment($data, AlphaNum::class); return $this; } + /** + * ISO/IEC 18004:2000 8.3.5 - Kanji Mode + */ public function addKanjiSegment(string $data):QRCode{ $this->addSegment($data, Kanji::class); return $this; } + /** + * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode + */ public function addByteSegment(string $data):QRCode{ $this->addSegment($data, Byte::class); return $this; } + /** + * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode + */ public function addEciDesignator(int $encoding):QRCode{ $this->addSegment($encoding, ECI::class); return $this; } + } From abf6c2fd94fba7bf1cfb9937a95a9e06d7c4bac8 Mon Sep 17 00:00:00 2001 From: codemasher Date: Tue, 19 Jan 2021 20:10:12 +0100 Subject: [PATCH 48/78] :octocat: inject QRDataModeInterface --- src/Data/QRData.php | 11 +++------- src/QRCode.php | 27 +++++++++++------------- tests/Data/AlphaNumTest.php | 6 ++++-- tests/Data/DatainterfaceTestAbstract.php | 14 ++++++++---- tests/Data/KanjiTest.php | 8 +++++-- tests/Data/MaskPatternTesterTest.php | 4 ++-- tests/Data/NumberTest.php | 4 +++- tests/Output/QROutputTestAbstract.php | 2 +- 8 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/Data/QRData.php b/src/Data/QRData.php index f717e29ca..bce994c70 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -60,8 +60,8 @@ final class QRData{ /** * QRData constructor. * - * @param \chillerlan\Settings\SettingsContainerInterface $options - * @param array|null $dataSegments + * @param \chillerlan\Settings\SettingsContainerInterface $options + * @param \chillerlan\QRCode\Data\QRDataModeInterface[]|null $dataSegments */ public function __construct(SettingsContainerInterface $options, array $dataSegments = null){ $this->options = $options; @@ -79,12 +79,7 @@ final class QRData{ * Sets the data string (internally called by the constructor) */ public function setData(array $dataSegments):QRData{ - - foreach($dataSegments as $segment){ - [$class, $data] = $segment; - - $this->dataSegments[] = new $class($data); - } + $this->dataSegments = $dataSegments; $version = $this->options->version === QRCode::VERSION_AUTO ? $this->getMinimumVersion() diff --git a/src/QRCode.php b/src/QRCode.php index 5df31ab35..0998b0797 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -12,7 +12,9 @@ namespace chillerlan\QRCode; -use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRMatrix}; +use chillerlan\QRCode\Data\{ + AlphaNum, Byte, ECI, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix +}; use chillerlan\QRCode\Common\{MaskPattern, Mode}; use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString}; use chillerlan\Settings\SettingsContainerInterface; @@ -88,7 +90,7 @@ class QRCode{ * * @see \chillerlan\QRCode\Data\QRDataModeInterface * - * @var string[][]|int[][] + * @var \chillerlan\QRCode\Data\QRDataModeInterface[] */ protected array $dataSegments = []; @@ -125,7 +127,7 @@ class QRCode{ foreach(Mode::DATA_INTERFACES as $dataInterface){ if($dataInterface::validateString($data)){ - $this->addSegment($data, $dataInterface); + $this->addSegment(new $dataInterface($data)); break; } @@ -216,21 +218,16 @@ class QRCode{ /** * ISO/IEC 18004:2000 8.3.6 - Mixing modes * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length - * - * @param string|int $data - * @param string $classname - * - * @return void */ - protected function addSegment($data, string $classname):void{ - $this->dataSegments[] = [$classname, $data]; + protected function addSegment(QRDataModeInterface $segment):void{ + $this->dataSegments[] = $segment; } /** * ISO/IEC 18004:2000 8.3.2 - Numeric Mode */ public function addNumberSegment(string $data):QRCode{ - $this->addSegment($data, Number::class); + $this->addSegment(new Number($data)); return $this; } @@ -239,7 +236,7 @@ class QRCode{ * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode */ public function addAlphaNumSegment(string $data):QRCode{ - $this->addSegment($data, AlphaNum::class); + $this->addSegment(new AlphaNum($data)); return $this; } @@ -248,7 +245,7 @@ class QRCode{ * ISO/IEC 18004:2000 8.3.5 - Kanji Mode */ public function addKanjiSegment(string $data):QRCode{ - $this->addSegment($data, Kanji::class); + $this->addSegment(new Kanji($data)); return $this; } @@ -257,7 +254,7 @@ class QRCode{ * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode */ public function addByteSegment(string $data):QRCode{ - $this->addSegment($data, Byte::class); + $this->addSegment(new Byte($data)); return $this; } @@ -266,7 +263,7 @@ class QRCode{ * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode */ public function addEciDesignator(int $encoding):QRCode{ - $this->addSegment($encoding, ECI::class); + $this->addSegment(new ECI($encoding)); return $this; } diff --git a/tests/Data/AlphaNumTest.php b/tests/Data/AlphaNumTest.php index 6feab6ddc..80a6ff7df 100644 --- a/tests/Data/AlphaNumTest.php +++ b/tests/Data/AlphaNumTest.php @@ -20,7 +20,7 @@ use chillerlan\QRCode\Data\{AlphaNum, QRCodeDataException}; final class AlphaNumTest extends DatainterfaceTestAbstract{ /** @internal */ - protected array $testdata = [AlphaNum::class, '0 $%*+-./:']; + protected array $testdata = [AlphaNum::class, '0 $%*+-./:']; /** @internal */ protected array $expected = [ @@ -46,7 +46,9 @@ final class AlphaNumTest extends DatainterfaceTestAbstract{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('illegal char: "#" [35]'); - $this->dataInterface->setData([[AlphaNum::class, '#']]); + $this->testdata = [AlphaNum::class, '#']; + + $this->setTestData(); } } diff --git a/tests/Data/DatainterfaceTestAbstract.php b/tests/Data/DatainterfaceTestAbstract.php index 60fb3f548..5fda9946c 100644 --- a/tests/Data/DatainterfaceTestAbstract.php +++ b/tests/Data/DatainterfaceTestAbstract.php @@ -43,6 +43,11 @@ abstract class DatainterfaceTestAbstract extends TestCase{ $this->reflection = new ReflectionClass($this->dataInterface); } + protected function setTestData():void{ + [$class, $data] = $this->testdata; + $this->dataInterface->setData([new $class($data)]); + } + /** * Verifies the data interface instance */ @@ -81,7 +86,7 @@ abstract class DatainterfaceTestAbstract extends TestCase{ * @dataProvider MaskPatternProvider */ public function testInitMatrix(int $maskPattern):void{ - $this->dataInterface->setData([$this->testdata]); + $this->setTestData(); $matrix = $this->dataInterface->writeMatrix(new MaskPattern($maskPattern)); @@ -93,7 +98,7 @@ abstract class DatainterfaceTestAbstract extends TestCase{ * Tests getting the minimum QR version for the given data */ public function testGetMinimumVersion():void{ - $this->dataInterface->setData([$this->testdata]); + $this->setTestData(); $getMinimumVersion = $this->reflection->getMethod('getMinimumVersion'); $getMinimumVersion->setAccessible(true); @@ -111,7 +116,7 @@ abstract class DatainterfaceTestAbstract extends TestCase{ $this->dataInterface = new QRData( new QROptions(['version' => QRCode::VERSION_AUTO]), - [[$class, str_repeat($data, 1337)]] + [new $class(str_repeat($data, 1337))] ); } @@ -122,8 +127,9 @@ abstract class DatainterfaceTestAbstract extends TestCase{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('code length overflow'); [$class, $data] = $this->testdata; + $this->testdata = [$class, str_repeat($data, 1337)]; - $this->dataInterface->setData([[$class, str_repeat($data, 1337)]]); + $this->setTestData(); } } diff --git a/tests/Data/KanjiTest.php b/tests/Data/KanjiTest.php index 36fa0c2dc..a34df88fa 100644 --- a/tests/Data/KanjiTest.php +++ b/tests/Data/KanjiTest.php @@ -46,7 +46,9 @@ final class KanjiTest extends DatainterfaceTestAbstract{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('illegal char at 1 [16191]'); - $this->dataInterface->setData([[Kanji::class, 'ÃÃ']]); + $this->testdata = [Kanji::class, 'ÃÃ']; + + $this->setTestData(); } /** @@ -56,7 +58,9 @@ final class KanjiTest extends DatainterfaceTestAbstract{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('illegal char at 1'); - $this->dataInterface->setData([[Kanji::class, 'Ã']]); + $this->testdata = [Kanji::class, 'Ã']; + + $this->setTestData(); } } diff --git a/tests/Data/MaskPatternTesterTest.php b/tests/Data/MaskPatternTesterTest.php index e7571c24f..49b1c4924 100644 --- a/tests/Data/MaskPatternTesterTest.php +++ b/tests/Data/MaskPatternTesterTest.php @@ -26,7 +26,7 @@ final class MaskPatternTesterTest extends TestCase{ * Tests getting the best mask pattern */ public function testMaskpattern():void{ - $dataInterface = new QRData(new QROptions(['version' => 10]), [[Byte::class, 'test']]); + $dataInterface = new QRData(new QROptions(['version' => 10]), [new Byte('test')]); $this::assertSame(3, (new MaskPatternTester($dataInterface))->getBestMaskPattern()->getPattern()); } @@ -35,7 +35,7 @@ final class MaskPatternTesterTest extends TestCase{ * Tests getting the penalty value for a given mask pattern */ public function testMaskpatternID():void{ - $dataInterface = new QRData(new QROptions(['version' => 10]), [[Byte::class, 'test']]); + $dataInterface = new QRData(new QROptions(['version' => 10]), [new Byte('test')]); $this::assertSame(4243, (new MaskPatternTester($dataInterface))->testPattern(new MaskPattern(MaskPattern::PATTERN_011))); } diff --git a/tests/Data/NumberTest.php b/tests/Data/NumberTest.php index 070459cf7..ccbe6f65e 100644 --- a/tests/Data/NumberTest.php +++ b/tests/Data/NumberTest.php @@ -46,7 +46,9 @@ final class NumberTest extends DatainterfaceTestAbstract{ $this->expectException(QRCodeDataException::class); $this->expectExceptionMessage('illegal char: "#" [35]'); - $this->dataInterface->setData([[Number::class, '#']]); + $this->testdata = [Number::class, '#']; + + $this->setTestData(); } } diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index 6e6d9f469..21946d334 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -49,7 +49,7 @@ abstract class QROutputTestAbstract extends TestCase{ } $this->options = new QROptions; - $this->matrix = (new QRData($this->options, [[Byte::class, 'testdata']])) + $this->matrix = (new QRData($this->options, [new Byte('testdata')])) ->writeMatrix(new MaskPattern(MaskPattern::PATTERN_010)); $this->outputInterface = $this->getOutputInterface($this->options); } From e06c4a38f7aef495ef02e582bf02d1504710f758 Mon Sep 17 00:00:00 2001 From: codemasher Date: Wed, 20 Jan 2021 16:19:48 +0100 Subject: [PATCH 49/78] :octocat: simplify base64 URI creation --- examples/QRImageWithLogo.php | 2 +- examples/QRImageWithText.php | 4 ++-- src/Output/QRFpdf.php | 2 +- src/Output/QRImage.php | 6 +++--- src/Output/QRMarkup.php | 2 +- src/Output/QROutputAbstract.php | 10 ++++++++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/examples/QRImageWithLogo.php b/examples/QRImageWithLogo.php index 76aa5ced7..76f9e40ec 100644 --- a/examples/QRImageWithLogo.php +++ b/examples/QRImageWithLogo.php @@ -72,7 +72,7 @@ class QRImageWithLogo extends QRImage{ } if($this->options->imageBase64){ - $imageData = 'data:image/'.$this->options->outputType.';base64,'.base64_encode($imageData); + $imageData = $this->base64encode($imageData, 'image/'.$this->options->outputType); } return $imageData; diff --git a/examples/QRImageWithText.php b/examples/QRImageWithText.php index fe6b962a9..e3b2d8c62 100644 --- a/examples/QRImageWithText.php +++ b/examples/QRImageWithText.php @@ -20,7 +20,7 @@ namespace chillerlan\QRCodeExamples; use chillerlan\QRCode\Output\QRImage; -use function base64_encode, imagechar, imagecolorallocate, imagecolortransparent, imagecopymerge, imagecreatetruecolor, +use function imagechar, imagecolorallocate, imagecolortransparent, imagecopymerge, imagecreatetruecolor, imagedestroy, imagefilledrectangle, imagefontwidth, in_array, round, str_split, strlen; class QRImageWithText extends QRImage{ @@ -50,7 +50,7 @@ class QRImageWithText extends QRImage{ } if($this->options->imageBase64){ - $imageData = 'data:image/'.$this->options->outputType.';base64,'.base64_encode($imageData); + $imageData = $this->base64encode($imageData, 'image/'.$this->options->outputType); } return $imageData; diff --git a/src/Output/QRFpdf.php b/src/Output/QRFpdf.php index 0f56dfeb9..1c3b99d4e 100644 --- a/src/Output/QRFpdf.php +++ b/src/Output/QRFpdf.php @@ -103,7 +103,7 @@ class QRFpdf extends QROutputAbstract{ } if($this->options->imageBase64){ - $pdfData = sprintf('data:application/pdf;base64,%s', base64_encode($pdfData)); + $pdfData = $this->base64encode($pdfData, 'application/pdf'); } return $pdfData; diff --git a/src/Output/QRImage.php b/src/Output/QRImage.php index 967b4cd13..098a7825d 100644 --- a/src/Output/QRImage.php +++ b/src/Output/QRImage.php @@ -19,9 +19,9 @@ use chillerlan\QRCode\{QRCode, QRCodeException}; use chillerlan\Settings\SettingsContainerInterface; use Exception; -use function array_values, base64_encode, call_user_func, count, extension_loaded, imagecolorallocate, imagecolortransparent, +use function array_values, call_user_func, count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor, imagedestroy, imagefilledrectangle, imagegif, imagejpeg, imagepng, in_array, - is_array, ob_end_clean, ob_get_contents, ob_start, range, sprintf; + is_array, ob_end_clean, ob_get_contents, ob_start, range; /** * Converts the matrix into GD images, raw or base64 output (requires ext-gd) @@ -127,7 +127,7 @@ class QRImage extends QROutputAbstract{ } if($this->options->imageBase64){ - $imageData = sprintf('data:image/%s;base64,%s', $this->options->outputType, base64_encode($imageData)); + $imageData = $this->base64encode($imageData, 'image/'.$this->options->outputType); } return $imageData; diff --git a/src/Output/QRMarkup.php b/src/Output/QRMarkup.php index 06d6e88cb..a351c8cf4 100644 --- a/src/Output/QRMarkup.php +++ b/src/Output/QRMarkup.php @@ -151,7 +151,7 @@ class QRMarkup extends QROutputAbstract{ } if($this->options->imageBase64){ - $svg = sprintf('data:image/svg+xml;base64,%s', base64_encode($svg)); + $svg = $this->base64encode($svg, 'image/svg+xml'); } return $svg; diff --git a/src/Output/QROutputAbstract.php b/src/Output/QROutputAbstract.php index d4ed3d0c9..a115cf722 100644 --- a/src/Output/QROutputAbstract.php +++ b/src/Output/QROutputAbstract.php @@ -14,8 +14,7 @@ namespace chillerlan\QRCode\Output; use chillerlan\QRCode\{Data\QRMatrix, QRCode}; use chillerlan\Settings\SettingsContainerInterface; - -use function call_user_func_array, dirname, file_put_contents, get_called_class, in_array, is_writable, sprintf; +use function base64_encode, call_user_func_array, dirname, file_put_contents, get_called_class, in_array, is_writable, sprintf; /** * common output abstract @@ -92,6 +91,13 @@ abstract class QROutputAbstract implements QROutputInterface{ */ abstract protected function setModuleValues():void; + /** + * Returns a base64 data URI for the given string and mime type + */ + protected function base64encode(string $data, string $mime):string{ + return sprintf('data:%s;base64,%s', $mime, base64_encode($data)); + } + /** * saves the qr data to a file * From 67ce00b25dc867542ad038eefb3b1ab8b10e20c0 Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 21 Jan 2021 22:50:44 +0100 Subject: [PATCH 50/78] :octocat: ECI encoding (1st draft) --- src/Common/ECICharset.php | 108 ++++++++++++++++++++++++++++++++++++++ src/Data/ECI.php | 48 ++++------------- src/QRCode.php | 28 ++++++++-- 3 files changed, 142 insertions(+), 42 deletions(-) create mode 100644 src/Common/ECICharset.php diff --git a/src/Common/ECICharset.php b/src/Common/ECICharset.php new file mode 100644 index 000000000..39d6b1afb --- /dev/null +++ b/src/Common/ECICharset.php @@ -0,0 +1,108 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use InvalidArgumentException; +use function array_key_exists; + +class ECICharset{ + + public const CP437 = 0; // Code page 437, DOS Latin US + public const ISO_IEC_8859_1_GLI = 1; // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1 + public const CP437_WO_GLI = 2; // An equivalent code table to CP437, without the return-to-GLI 0 logic + public const ISO_IEC_8859_1 = 3; // Latin-1 (Default) + public const ISO_IEC_8859_2 = 4; // Latin-2 + public const ISO_IEC_8859_3 = 5; // Latin-3 + public const ISO_IEC_8859_4 = 6; // Latin-4 + public const ISO_IEC_8859_5 = 7; // Latin/Cyrillic + public const ISO_IEC_8859_6 = 8; // Latin/Arabic + public const ISO_IEC_8859_7 = 9; // Latin/Greek + public const ISO_IEC_8859_8 = 10; // Latin/Hebrew + public const ISO_IEC_8859_9 = 11; // Latin-5 + public const ISO_IEC_8859_10 = 12; // Latin-6 + public const ISO_IEC_8859_11 = 13; // Latin/Thai + // 14 reserved + public const ISO_IEC_8859_13 = 15; // Latin-7 (Baltic Rim) + public const ISO_IEC_8859_14 = 16; // Latin-8 (Celtic) + public const ISO_IEC_8859_15 = 17; // Latin-9 + public const ISO_IEC_8859_16 = 18; // Latin-10 + // 19 reserved + public const SHIFT_JIS = 20; // JIS X 0208 Annex 1 + JIS X 0201 + public const WINDOWS_1250_LATIN_2 = 21; // Superset of Latin-2, Central Europe + public const WINDOWS_1251_CYRILLIC = 22; // Latin/Cyrillic + public const WINDOWS_1252_LATIN_1 = 23; // Superset of Latin-1 + public const WINDOWS_1256_ARABIC = 24; + public const ISO_IEC_10646_UCS_2 = 25; // High order byte first (UTF-16BE) + public const ISO_IEC_10646_UTF_8 = 26; // UTF-8 + public const ISO_IEC_646_1991 = 27; // International Reference Version of ISO 7-bit coded character set (US-ASCII) + public const BIG5 = 28; // Big 5 (Taiwan) Chinese Character Set + public const GB18030 = 29; // GB (PRC) Chinese Character Set + public const EUC_KR = 30; // Korean Character Set + + /** + * map of charset id -> name + * + * @see \mb_list_encodings() + */ + public const MB_ENCODINGS = [ + self::CP437 => null, + self::ISO_IEC_8859_1_GLI => null, + self::CP437_WO_GLI => null, + self::ISO_IEC_8859_1 => 'ISO-8859-1', + self::ISO_IEC_8859_2 => 'ISO-8859-2', + self::ISO_IEC_8859_3 => 'ISO-8859-3', + self::ISO_IEC_8859_4 => 'ISO-8859-4', + self::ISO_IEC_8859_5 => 'ISO-8859-5', + self::ISO_IEC_8859_6 => 'ISO-8859-6', + self::ISO_IEC_8859_7 => 'ISO-8859-7', + self::ISO_IEC_8859_8 => 'ISO-8859-8', + self::ISO_IEC_8859_9 => 'ISO-8859-9', + self::ISO_IEC_8859_10 => 'ISO-8859-10', + self::ISO_IEC_8859_11 => null, + self::ISO_IEC_8859_13 => 'ISO-8859-13', + self::ISO_IEC_8859_14 => 'ISO-8859-14', + self::ISO_IEC_8859_15 => 'ISO-8859-15', + self::ISO_IEC_8859_16 => 'ISO-8859-16', + self::SHIFT_JIS => 'SJIS', + self::WINDOWS_1250_LATIN_2 => null, // @see https://www.php.net/manual/en/function.mb-convert-encoding.php#112547 + self::WINDOWS_1251_CYRILLIC => 'Windows-1251', + self::WINDOWS_1252_LATIN_1 => 'Windows-1252', + self::WINDOWS_1256_ARABIC => null, // @see https://stackoverflow.com/a/8592995 + self::ISO_IEC_10646_UCS_2 => 'UTF-16BE', + self::ISO_IEC_10646_UTF_8 => 'UTF-8', + self::ISO_IEC_646_1991 => 'ASCII', + self::BIG5 => 'BIG-5', + self::GB18030 => 'GB18030', + self::EUC_KR => 'EUC-KR', + ]; + + private int $charsetID; + + public function __construct(int $charsetID){ + + if(!array_key_exists($charsetID, self::MB_ENCODINGS)){ + throw new InvalidArgumentException('invalid charset id: '.$charsetID); + } + + $this->charsetID = $charsetID; + } + + public function getID():int{ + return $this->charsetID; + } + + public function getName():?string{ + return self::MB_ENCODINGS[$this->charsetID]; + } + +} diff --git a/src/Data/ECI.php b/src/Data/ECI.php index 1ca8c78bc..20af7aecd 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -21,51 +21,18 @@ use chillerlan\QRCode\Common\{BitBuffer, Mode}; */ final class ECI extends QRDataModeAbstract{ - public const CP437 = 0; // Code page 437, DOS Latin US - public const ISO_IEC_8859_1_GLI = 1; // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1 - public const CP437_WO_GLI = 2; // An equivalent code table to CP437, without the return-to-GLI 0 logic - public const ISO_IEC_8859_1 = 3; // Latin-1 (Default) - public const ISO_IEC_8859_2 = 4; // Latin-2 - public const ISO_IEC_8859_3 = 5; // Latin-3 - public const ISO_IEC_8859_4 = 6; // Latin-4 - public const ISO_IEC_8859_5 = 7; // Latin/Cyrillic - public const ISO_IEC_8859_6 = 8; // Latin/Arabic - public const ISO_IEC_8859_7 = 9; // Latin/Greek - public const ISO_IEC_8859_8 = 10; // Latin/Hebrew - public const ISO_IEC_8859_9 = 11; // Latin-5 - public const ISO_IEC_8859_10 = 12; // Latin-6 - public const ISO_IEC_8859_11 = 13; // Latin/Thai - // 14 reserved - public const ISO_IEC_8859_13 = 15; // Latin-7 (Baltic Rim) - public const ISO_IEC_8859_14 = 16; // Latin-8 (Celtic) - public const ISO_IEC_8859_15 = 17; // Latin-9 - public const ISO_IEC_8859_16 = 18; // Latin-10 - // 19 reserved - public const SHIFT_JIS = 20; // JIS X 0208 Annex 1 + JIS X 0201 - public const WINDOWS_1250_LATIN_2 = 21; // Superset of Latin-2, Central Europe - public const WINDOWS_1251_CYRILLIC = 22; // Latin/Cyrillic - public const WINDOWS_1252_LATIN_1 = 23; // Superset of Latin-1 - public const WINDOWS_1256_ARABIC = 24; - public const ISO_IEC_10646_UCS_2 = 25; // High order byte first (UTF-16BE) - public const ISO_IEC_10646_UTF_8 = 26; - public const ISO_IEC_646_1991 = 27; // International Reference Version of ISO 7-bit coded character set (US-ASCII) - public const BIG5 = 28; // Big 5 (Taiwan) Chinese Character Set - public const GB18030 = 29; // GB (PRC) Chinese Character Set - public const EUC_KR = 30; // Korean Character Set - - /** - * The current encoding - */ - protected int $encoding; - protected int $datamode = Mode::DATA_ECI; + /** + * The current ECI encoding id + */ + protected int $encoding; + /** * @inheritDoc + * @noinspection PhpMissingParentConstructorInspection */ public function __construct(int $encoding){ - parent::__construct(''); - $this->encoding = $encoding; } @@ -77,7 +44,10 @@ final class ECI extends QRDataModeAbstract{ } /** + * Unused, but required as per interface + * * @inheritDoc + * @codeCoverageIgnore */ public static function validateString(string $string):bool{ return true; diff --git a/src/QRCode.php b/src/QRCode.php index 0998b0797..aa455af0a 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -15,11 +15,10 @@ namespace chillerlan\QRCode; use chillerlan\QRCode\Data\{ AlphaNum, Byte, ECI, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix }; -use chillerlan\QRCode\Common\{MaskPattern, Mode}; +use chillerlan\QRCode\Common\{ECICharset, MaskPattern, Mode}; use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString}; use chillerlan\Settings\SettingsContainerInterface; - -use function class_exists, in_array; +use function class_exists, in_array, mb_convert_encoding, mb_internal_encoding; /** * Turns a text string into a Model 2 QR Code @@ -268,4 +267,27 @@ class QRCode{ return $this; } + /** + * i hate this somehow but i'll leave it for now + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function addEciSegment(int $encoding, string $data):QRCode{ + // validate the encoding id + $eciCharset = new ECICharset($encoding); + // get charset name + $eciCharsetName = $eciCharset->getName(); + // convert the string to the given charset + if($eciCharsetName !== null){ + $data = mb_convert_encoding($data, $eciCharsetName, mb_internal_encoding()); + // add ECI designator + $this->addSegment(new ECI($eciCharset->getID())); + $this->addSegment(new Byte($data)); + + return $this; + } + + throw new QRCodeException('unable to add ECI segment'); + } + } From 81dcab5b6629aae9d0953354dad9e2f44d67b5c4 Mon Sep 17 00:00:00 2001 From: codemasher Date: Thu, 21 Jan 2021 22:55:42 +0100 Subject: [PATCH 51/78] :octocat: detect source encoding --- src/QRCode.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/QRCode.php b/src/QRCode.php index aa455af0a..91761221c 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -18,7 +18,7 @@ use chillerlan\QRCode\Data\{ use chillerlan\QRCode\Common\{ECICharset, MaskPattern, Mode}; use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString}; use chillerlan\Settings\SettingsContainerInterface; -use function class_exists, in_array, mb_convert_encoding, mb_internal_encoding; +use function class_exists, in_array, mb_convert_encoding, mb_detect_encoding; /** * Turns a text string into a Model 2 QR Code @@ -279,7 +279,7 @@ class QRCode{ $eciCharsetName = $eciCharset->getName(); // convert the string to the given charset if($eciCharsetName !== null){ - $data = mb_convert_encoding($data, $eciCharsetName, mb_internal_encoding()); + $data = mb_convert_encoding($data, $eciCharsetName, mb_detect_encoding($data)); // add ECI designator $this->addSegment(new ECI($eciCharset->getID())); $this->addSegment(new Byte($data)); From 700af4b53ea40b37b477b02f53ce263b6a87ee97 Mon Sep 17 00:00:00 2001 From: codemasher Date: Fri, 22 Jan 2021 15:46:43 +0100 Subject: [PATCH 52/78] :octocat: BitBuffer: +read functionality (from ZXing) --- src/Common/BitBuffer.php | 81 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/src/Common/BitBuffer.php b/src/Common/BitBuffer.php index 3f8ff7163..094e363b0 100644 --- a/src/Common/BitBuffer.php +++ b/src/Common/BitBuffer.php @@ -12,6 +12,7 @@ namespace chillerlan\QRCode\Common; +use InvalidArgumentException; use function count, floor; /** @@ -24,12 +25,23 @@ final class BitBuffer{ * * @var int[] */ - protected array $buffer = []; + private array $buffer; /** * Length of the content (bits) */ - protected int $length = 0; + private int $length; + + private int $bytesRead = 0; + private int $bitsRead = 0; + + /** + * BitBuffer constructor. + */ + public function __construct(array $bytes = null){ + $this->buffer = $bytes ?? []; + $this->length = count($this->buffer); + } /** * clears the buffer @@ -57,14 +69,14 @@ final class BitBuffer{ * appends a single bit */ public function putBit(bool $bit):BitBuffer{ - $bufIndex = floor($this->length / 8); + $bufIndex = (int)floor($this->length / 8); if(count($this->buffer) <= $bufIndex){ $this->buffer[] = 0; } if($bit === true){ - $this->buffer[(int)$bufIndex] |= (0x80 >> ($this->length % 8)); + $this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8)); } $this->length++; @@ -86,4 +98,65 @@ final class BitBuffer{ return $this->buffer; } + /** + * @return int number of bits that can be read successfully + */ + public function available():int{ + return 8 * ($this->length - $this->bytesRead) - $this->bitsRead; + } + + /** + * @author Sean Owen, ZXing + * + * @param int $numBits number of bits to read + * + * @return int representing the bits read. The bits will appear as the least-significant + * bits of the int + * @throws InvalidArgumentException if numBits isn't in [1,32] or more than is available + */ + public function read(int $numBits):int{ + + if($numBits < 1 || $numBits > 32 || $numBits > $this->available()){ + throw new InvalidArgumentException('invalid $numBits: '.$numBits); + } + + $result = 0; + + // First, read remainder from current byte + if($this->bitsRead > 0){ + $bitsLeft = 8 - $this->bitsRead; + $toRead = $numBits < $bitsLeft ? $numBits : $bitsLeft; + $bitsToNotRead = $bitsLeft - $toRead; + $mask = (0xff >> (8 - $toRead)) << $bitsToNotRead; + $result = ($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead; + $numBits -= $toRead; + $this->bitsRead += $toRead; + + if($this->bitsRead == 8){ + $this->bitsRead = 0; + $this->bytesRead++; + } + } + + // Next read whole bytes + if($numBits > 0){ + + while($numBits >= 8){ + $result = ($result << 8) | ($this->buffer[$this->bytesRead] & 0xff); + $this->bytesRead++; + $numBits -= 8; + } + + // Finally read a partial byte + if($numBits > 0){ + $bitsToNotRead = 8 - $numBits; + $mask = (0xff >> $bitsToNotRead) << $bitsToNotRead; + $result = ($result << $numBits) | (($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead); + $this->bitsRead += $numBits; + } + } + + return $result; + } + } From c06e3f1cd6b7757c424087f6043e800460cbc1b4 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 23 Jan 2021 01:26:11 +0100 Subject: [PATCH 53/78] :octocat: make datamode field static --- src/Data/AlphaNum.php | 6 +++--- src/Data/Byte.php | 6 +++--- src/Data/ECI.php | 4 ++-- src/Data/Kanji.php | 6 +++--- src/Data/Number.php | 6 +++--- src/Data/QRDataModeAbstract.php | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index 9852a54e3..71ace3291 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -38,7 +38,7 @@ final class AlphaNum extends QRDataModeAbstract{ '+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44, ]; - protected int $datamode = Mode::DATA_ALPHANUM; + protected static int $datamode = Mode::DATA_ALPHANUM; /** * @inheritdoc @@ -68,8 +68,8 @@ final class AlphaNum extends QRDataModeAbstract{ $len = $this->getCharCount(); $bitBuffer - ->put($this->datamode, 4) - ->put($len, Mode::getLengthBitsForVersion($this->datamode, $versionNumber)) + ->put($this::$datamode, 4) + ->put($len, Mode::getLengthBitsForVersion($this::$datamode, $versionNumber)) ; // encode 2 characters in 11 bits diff --git a/src/Data/Byte.php b/src/Data/Byte.php index 50a172bff..3043b1713 100644 --- a/src/Data/Byte.php +++ b/src/Data/Byte.php @@ -24,7 +24,7 @@ use function ord; */ final class Byte extends QRDataModeAbstract{ - protected int $datamode = Mode::DATA_BYTE; + protected static int $datamode = Mode::DATA_BYTE; /** * @inheritdoc @@ -47,8 +47,8 @@ final class Byte extends QRDataModeAbstract{ $len = $this->getCharCount(); $bitBuffer - ->put($this->datamode, 4) - ->put($len, Mode::getLengthBitsForVersion($this->datamode, $versionNumber)) + ->put($this::$datamode, 4) + ->put($len, Mode::getLengthBitsForVersion($this::$datamode, $versionNumber)) ; $i = 0; diff --git a/src/Data/ECI.php b/src/Data/ECI.php index 20af7aecd..9271aba38 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -21,7 +21,7 @@ use chillerlan\QRCode\Common\{BitBuffer, Mode}; */ final class ECI extends QRDataModeAbstract{ - protected int $datamode = Mode::DATA_ECI; + protected static int $datamode = Mode::DATA_ECI; /** * The current ECI encoding id @@ -58,7 +58,7 @@ final class ECI extends QRDataModeAbstract{ */ public function write(BitBuffer $bitBuffer, int $versionNumber):void{ $bitBuffer - ->put($this->datamode, 4) + ->put($this::$datamode, 4) ->put($this->encoding, 8) ; } diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index 69972592a..191d98535 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -24,7 +24,7 @@ use function mb_convert_encoding, mb_detect_encoding, mb_strlen, ord, sprintf, s */ final class Kanji extends QRDataModeAbstract{ - protected int $datamode = Mode::DATA_KANJI; + protected static int $datamode = Mode::DATA_KANJI; public function __construct(string $data){ parent::__construct($data); @@ -75,8 +75,8 @@ final class Kanji extends QRDataModeAbstract{ public function write(BitBuffer $bitBuffer, int $versionNumber):void{ $bitBuffer - ->put($this->datamode, 4) - ->put($this->getCharCount(), Mode::getLengthBitsForVersion($this->datamode, $versionNumber)) + ->put($this::$datamode, 4) + ->put($this->getCharCount(), Mode::getLengthBitsForVersion($this::$datamode, $versionNumber)) ; $len = strlen($this->data); diff --git a/src/Data/Number.php b/src/Data/Number.php index b627dbded..7260e4bcf 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -31,7 +31,7 @@ final class Number extends QRDataModeAbstract{ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, ]; - protected int $datamode = Mode::DATA_NUMBER; + protected static int $datamode = Mode::DATA_NUMBER; /** * @inheritdoc @@ -61,8 +61,8 @@ final class Number extends QRDataModeAbstract{ $len = $this->getCharCount(); $bitBuffer - ->put($this->datamode, 4) - ->put($len, Mode::getLengthBitsForVersion($this->datamode, $versionNumber)) + ->put($this::$datamode, 4) + ->put($len, Mode::getLengthBitsForVersion($this::$datamode, $versionNumber)) ; $i = 0; diff --git a/src/Data/QRDataModeAbstract.php b/src/Data/QRDataModeAbstract.php index 062707d7d..7dc4eb6c3 100644 --- a/src/Data/QRDataModeAbstract.php +++ b/src/Data/QRDataModeAbstract.php @@ -19,7 +19,7 @@ abstract class QRDataModeAbstract implements QRDataModeInterface{ /** * the current data mode: Num, Alphanum, Kanji, Byte */ - protected int $datamode; + protected static int $datamode; /** * The data to write @@ -44,7 +44,7 @@ abstract class QRDataModeAbstract implements QRDataModeInterface{ * @inheritDoc */ public function getDataMode():int{ - return $this->datamode; + return $this::$datamode; } } From 22f1672fac3394b6e39c20efef896704b8f17315 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 23 Jan 2021 04:10:37 +0100 Subject: [PATCH 54/78] :octocat: +data mode decodeSegment() (WIP) --- src/Data/AlphaNum.php | 47 +++++++++++++++++++- src/Data/Byte.php | 21 +++++++++ src/Data/ECI.php | 54 ++++++++++++++++++----- src/Data/Kanji.php | 45 ++++++++++++++++--- src/Data/Number.php | 75 +++++++++++++++++++++++++++++++- src/Data/QRDataModeInterface.php | 5 +++ 6 files changed, 228 insertions(+), 19 deletions(-) diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index 71ace3291..f3ff54dab 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -14,7 +14,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\Common\{BitBuffer, Mode}; -use function ceil, ord, sprintf, str_split; +use function array_flip, ceil, ord, sprintf, str_split; /** * Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / : @@ -98,4 +98,49 @@ final class AlphaNum extends QRDataModeAbstract{ return self::CHAR_MAP_ALPHANUM[$chr]; } + /** + * @inheritdoc + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $length = $bitBuffer->read(Mode::getLengthBitsForVersion(self::$datamode, $versionNumber)); + $charmap = array_flip(self::CHAR_MAP_ALPHANUM); + + // @todo + $toAlphaNumericChar = function(int $ord) use ($charmap):string{ + + if(isset($charmap[$ord])){ + return $charmap[$ord]; + } + + throw new QRCodeDataException('invalid character value: '.$ord); + }; + + $result = ''; + // Read two characters at a time + while($length > 1){ + + if($bitBuffer->available() < 11){ + throw new QRCodeDataException('not enough bits available'); + } + + $nextTwoCharsBits = $bitBuffer->read(11); + $result .= $toAlphaNumericChar($nextTwoCharsBits / 45); + $result .= $toAlphaNumericChar($nextTwoCharsBits % 45); + $length -= 2; + } + + if($length === 1){ + // special case: one character left + if($bitBuffer->available() < 6){ + throw new QRCodeDataException('not enough bits available'); + } + + $result .= $toAlphaNumericChar($bitBuffer->read(6)); + } + + return $result; + } + } diff --git a/src/Data/Byte.php b/src/Data/Byte.php index 3043b1713..229952d0a 100644 --- a/src/Data/Byte.php +++ b/src/Data/Byte.php @@ -60,4 +60,25 @@ final class Byte extends QRDataModeAbstract{ } + /** + * @inheritdoc + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $length = $bitBuffer->read(Mode::getLengthBitsForVersion(self::$datamode, $versionNumber)); + + if($bitBuffer->available() < 8 * $length){ + throw new QRCodeDataException('not enough bits available'); + } + + $readBytes = ''; + + for($i = 0; $i < $length; $i++){ + $readBytes .= \chr($bitBuffer->read(8)); + } + + return $readBytes; + } + } diff --git a/src/Data/ECI.php b/src/Data/ECI.php index 9271aba38..a3e08130b 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -43,17 +43,7 @@ final class ECI extends QRDataModeAbstract{ return 8; } - /** - * Unused, but required as per interface - * - * @inheritDoc - * @codeCoverageIgnore - */ - public static function validateString(string $string):bool{ - return true; - } - - /** + /** * @inheritDoc */ public function write(BitBuffer $bitBuffer, int $versionNumber):void{ @@ -63,4 +53,46 @@ final class ECI extends QRDataModeAbstract{ ; } + /** + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function parseValue(BitBuffer $bitBuffer):int{ + $firstByte = $bitBuffer->read(8); + + if(($firstByte & 0x80) === 0){ + // just one byte + return $firstByte & 0x7f; + } + + if(($firstByte & 0xc0) === 0x80){ + // two bytes + $secondByte = $bitBuffer->read(8); + + return (($firstByte & 0x3f) << 8) | $secondByte; + } + + if(($firstByte & 0xe0) === 0xC0){ + // three bytes + $secondThirdBytes = $bitBuffer->read(16); + + return (($firstByte & 0x1f) << 16) | $secondThirdBytes; + } + + throw new QRCodeDataException('error decoding ECI value'); + } + + /** + * @codeCoverageIgnore Unused, but required as per interface + */ + public static function validateString(string $string):bool{ + return true; + } + + /** + * @codeCoverageIgnore Unused, but required as per interface + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + return ''; + } + } diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index 191d98535..856b54779 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -14,7 +14,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\Common\{BitBuffer, Mode}; -use function mb_convert_encoding, mb_detect_encoding, mb_strlen, ord, sprintf, strlen; +use function chr, implode, mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, mb_strlen, ord, sprintf, strlen; /** * Kanji mode: double-byte characters from the Shift JIS character set @@ -57,7 +57,7 @@ final class Kanji extends QRDataModeAbstract{ while($i + 1 < $len){ $c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1])); - if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){ + if(!($c >= 0x8140 && $c <= 0x9ffc) && !($c >= 0xe040 && $c <= 0xebbf)){ return false; } @@ -84,17 +84,17 @@ final class Kanji extends QRDataModeAbstract{ for($i = 0; $i + 1 < $len; $i += 2){ $c = ((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[$i + 1])); - if($c >= 0x8140 && $c <= 0x9FFC){ + if($c >= 0x8140 && $c <= 0x9ffC){ $c -= 0x8140; } - elseif($c >= 0xE040 && $c <= 0xEBBF){ - $c -= 0xC140; + elseif($c >= 0xe040 && $c <= 0xebbf){ + $c -= 0xc140; } else{ throw new QRCodeDataException(sprintf('illegal char at %d [%d]', $i + 1, $c)); } - $bitBuffer->put(((($c >> 8) & 0xff) * 0xC0) + ($c & 0xff), 13); + $bitBuffer->put(((($c >> 8) & 0xff) * 0xc0) + ($c & 0xff), 13); } if($i < $len){ @@ -103,4 +103,37 @@ final class Kanji extends QRDataModeAbstract{ } + /** + * @inheritdoc + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $length = $bitBuffer->read(Mode::getLengthBitsForVersion(self::$datamode, $versionNumber)); + + if($bitBuffer->available() < $length * 13){ + throw new QRCodeDataException('not enough bits available'); + } + + $buffer = []; + $offset = 0; + + while($length > 0){ + // Each 13 bits encodes a 2-byte character + $twoBytes = $bitBuffer->read(13); + $assembledTwoBytes = (($twoBytes / 0x0c0) << 8) | ($twoBytes % 0x0c0); + + $assembledTwoBytes += ($assembledTwoBytes < 0x01f00) + ? 0x08140 // In the 0x8140 to 0x9FFC range + : 0x0c140; // In the 0xE040 to 0xEBBF range + + $buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8)); + $buffer[$offset + 1] = chr(0xff & $assembledTwoBytes); + $offset += 2; + $length--; + } + + return mb_convert_encoding(implode($buffer), mb_internal_encoding(), 'SJIS'); + } + } diff --git a/src/Data/Number.php b/src/Data/Number.php index 7260e4bcf..01ae36ecf 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -14,7 +14,7 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\Common\{BitBuffer, Mode}; -use function ceil, ord, sprintf, str_split, substr; +use function array_flip, ceil, ord, sprintf, str_split, substr; /** * Numeric mode: decimal digits 0 to 9 @@ -110,4 +110,77 @@ final class Number extends QRDataModeAbstract{ return $num; } + /** + * @inheritdoc + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $length = $bitBuffer->read(Mode::getLengthBitsForVersion(self::$datamode, $versionNumber)); + $charmap = array_flip(self::CHAR_MAP_NUMBER); + + // @todo + $toNumericChar = function(int $ord) use ($charmap):string{ + + if(isset($charmap[$ord])){ + return $charmap[$ord]; + } + + throw new QRCodeDataException('invalid character value: '.$ord); + }; + + $result = ''; + // Read three digits at a time + while($length >= 3){ + // Each 10 bits encodes three digits + if($bitBuffer->available() < 10){ + throw new QRCodeDataException('not enough bits available'); + } + + $threeDigitsBits = $bitBuffer->read(10); + + if($threeDigitsBits >= 1000){ + throw new QRCodeDataException('error decoding numeric value'); + } + + $result .= $toNumericChar($threeDigitsBits / 100); + $result .= $toNumericChar(($threeDigitsBits / 10) % 10); + $result .= $toNumericChar($threeDigitsBits % 10); + + $length -= 3; + } + + if($length === 2){ + // Two digits left over to read, encoded in 7 bits + if($bitBuffer->available() < 7){ + throw new QRCodeDataException('not enough bits available'); + } + + $twoDigitsBits = $bitBuffer->read(7); + + if($twoDigitsBits >= 100){ + throw new QRCodeDataException('error decoding numeric value'); + } + + $result .= $toNumericChar($twoDigitsBits / 10); + $result .= $toNumericChar($twoDigitsBits % 10); + } + elseif($length === 1){ + // One digit left over to read + if($bitBuffer->available() < 4){ + throw new QRCodeDataException('not enough bits available'); + } + + $digitBits = $bitBuffer->read(4); + + if($digitBits >= 10){ + throw new QRCodeDataException('error decoding numeric value'); + } + + $result .= $toNumericChar($digitBits); + } + + return $result; + } + } diff --git a/src/Data/QRDataModeInterface.php b/src/Data/QRDataModeInterface.php index cb791c7ef..b69c127e6 100644 --- a/src/Data/QRDataModeInterface.php +++ b/src/Data/QRDataModeInterface.php @@ -42,4 +42,9 @@ interface QRDataModeInterface{ */ public function write(BitBuffer $bitBuffer, int $versionNumber):void; + /** + * reads a segment from the BitBuffer and decodes in the current data mode + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string; + } From 93618e83ff57e80da1a48f02bd818690e6011d61 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 24 Jan 2021 22:52:33 +0100 Subject: [PATCH 55/78] :octocat: removed filesource and package phpdoc tags --- examples/MyCustomOutput.php | 2 -- examples/QRImageWithLogo.php | 2 -- examples/QRImageWithText.php | 2 -- examples/custom_output.php | 2 -- examples/html.php | 2 -- examples/image.php | 2 -- examples/imageWithLogo.php | 2 -- examples/imageWithText.php | 1 - examples/imagick.php | 2 -- examples/svg.php | 2 -- examples/text.php | 2 -- public/qrcode.php | 1 - src/Common/BitBuffer.php | 2 -- src/Common/ECICharset.php | 2 -- src/Common/EccLevel.php | 2 -- src/Common/GF256.php | 2 -- src/Common/GenericGFPoly.php | 2 -- src/Common/MaskPattern.php | 2 -- src/Common/Mode.php | 2 -- src/Common/ReedSolomonEncoder.php | 2 -- src/Common/Version.php | 2 -- src/Data/AlphaNum.php | 2 -- src/Data/Byte.php | 2 -- src/Data/ECI.php | 2 -- src/Data/Kanji.php | 2 -- src/Data/MaskPatternTester.php | 2 -- src/Data/Number.php | 2 -- src/Data/QRCodeDataException.php | 2 -- src/Data/QRData.php | 2 -- src/Data/QRDataModeAbstract.php | 2 -- src/Data/QRDataModeInterface.php | 2 -- src/Data/QRMatrix.php | 2 -- src/Output/QRCodeOutputException.php | 2 -- src/Output/QRFpdf.php | 7 ++----- src/Output/QRImage.php | 2 -- src/Output/QRImagick.php | 2 -- src/Output/QRMarkup.php | 2 -- src/Output/QROutputAbstract.php | 4 ++-- src/Output/QROutputInterface.php | 2 -- src/Output/QRString.php | 2 -- src/QRCode.php | 2 -- src/QRCodeException.php | 2 -- src/QROptions.php | 2 -- src/QROptionsTrait.php | 2 -- tests/Common/BitBufferTest.php | 2 -- tests/Data/AlphaNumTest.php | 2 -- tests/Data/ByteTest.php | 2 -- tests/Data/DatainterfaceTestAbstract.php | 2 -- tests/Data/KanjiTest.php | 2 -- tests/Data/MaskPatternTesterTest.php | 2 -- tests/Data/NumberTest.php | 2 -- tests/Data/QRMatrixTest.php | 2 -- tests/Output/QRFpdfTest.php | 2 -- tests/Output/QRImageTest.php | 2 -- tests/Output/QRImagickTest.php | 2 -- tests/Output/QRMarkupTest.php | 2 -- tests/Output/QROutputTestAbstract.php | 2 -- tests/Output/QRStringTest.php | 2 -- tests/QRCodeTest.php | 2 -- tests/QROptionsTest.php | 2 -- 60 files changed, 4 insertions(+), 121 deletions(-) diff --git a/examples/MyCustomOutput.php b/examples/MyCustomOutput.php index c48dcf912..40f4aae2b 100644 --- a/examples/MyCustomOutput.php +++ b/examples/MyCustomOutput.php @@ -2,9 +2,7 @@ /** * Class MyCustomOutput * - * @filesource MyCustomOutput.php * @created 24.12.2017 - * @package chillerlan\QRCodeExamples * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/examples/QRImageWithLogo.php b/examples/QRImageWithLogo.php index 76f9e40ec..ac2dacadd 100644 --- a/examples/QRImageWithLogo.php +++ b/examples/QRImageWithLogo.php @@ -2,9 +2,7 @@ /** * Class QRImageWithLogo * - * @filesource QRImageWithLogo.php * @created 18.11.2020 - * @package chillerlan\QRCodeExamples * @author smiley * @copyright 2020 smiley * @license MIT diff --git a/examples/QRImageWithText.php b/examples/QRImageWithText.php index e3b2d8c62..b5971bdf1 100644 --- a/examples/QRImageWithText.php +++ b/examples/QRImageWithText.php @@ -6,9 +6,7 @@ * * @link https://github.com/chillerlan/php-qrcode/issues/35 * - * @filesource QRImageWithText.php * @created 22.06.2019 - * @package chillerlan\QRCodeExamples * @author smiley * @copyright 2019 smiley * @license MIT diff --git a/examples/custom_output.php b/examples/custom_output.php index f55f69b41..175b5b86e 100644 --- a/examples/custom_output.php +++ b/examples/custom_output.php @@ -1,7 +1,5 @@ * @copyright 2017 Smiley diff --git a/examples/html.php b/examples/html.php index e7d658961..5ac189136 100644 --- a/examples/html.php +++ b/examples/html.php @@ -1,7 +1,5 @@ * @copyright 2017 Smiley diff --git a/examples/image.php b/examples/image.php index 6d9069f33..bbb0c20a1 100644 --- a/examples/image.php +++ b/examples/image.php @@ -1,7 +1,5 @@ * @copyright 2017 Smiley diff --git a/examples/imageWithLogo.php b/examples/imageWithLogo.php index 622c74bed..73aa887d1 100644 --- a/examples/imageWithLogo.php +++ b/examples/imageWithLogo.php @@ -1,7 +1,5 @@ * @copyright 2020 smiley diff --git a/examples/imageWithText.php b/examples/imageWithText.php index 44175b5b0..0f040f79e 100644 --- a/examples/imageWithText.php +++ b/examples/imageWithText.php @@ -3,7 +3,6 @@ * example for additional text * @link https://github.com/chillerlan/php-qrcode/issues/35 * - * @filesource imageWithText.php * @created 22.06.2019 * @author Smiley * @copyright 2019 Smiley diff --git a/examples/imagick.php b/examples/imagick.php index fb0561f06..ea8af1d57 100644 --- a/examples/imagick.php +++ b/examples/imagick.php @@ -1,7 +1,5 @@ * @copyright 2017 Smiley diff --git a/examples/svg.php b/examples/svg.php index b02040dda..63cb54552 100644 --- a/examples/svg.php +++ b/examples/svg.php @@ -1,7 +1,5 @@ * @copyright 2017 Smiley diff --git a/examples/text.php b/examples/text.php index 89854db6a..bc24ddf28 100644 --- a/examples/text.php +++ b/examples/text.php @@ -1,7 +1,5 @@ * @copyright 2017 Smiley diff --git a/public/qrcode.php b/public/qrcode.php index 1f8427c7e..d4e5deb32 100644 --- a/public/qrcode.php +++ b/public/qrcode.php @@ -1,6 +1,5 @@ * @copyright 2017 Smiley diff --git a/src/Common/BitBuffer.php b/src/Common/BitBuffer.php index 094e363b0..ba1e4759c 100644 --- a/src/Common/BitBuffer.php +++ b/src/Common/BitBuffer.php @@ -2,9 +2,7 @@ /** * Class BitBuffer * - * @filesource BitBuffer.php * @created 25.11.2015 - * @package chillerlan\QRCode\Common * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Common/ECICharset.php b/src/Common/ECICharset.php index 39d6b1afb..9193c0343 100644 --- a/src/Common/ECICharset.php +++ b/src/Common/ECICharset.php @@ -2,9 +2,7 @@ /** * Class ECICharset * - * @filesource ECICharset.php * @created 21.01.2021 - * @package chillerlan\QRCode\Common * @author smiley * @copyright 2021 smiley * @license MIT diff --git a/src/Common/EccLevel.php b/src/Common/EccLevel.php index 917963bd4..d1f77b2fa 100644 --- a/src/Common/EccLevel.php +++ b/src/Common/EccLevel.php @@ -2,9 +2,7 @@ /** * Class EccLevel * - * @filesource EccLevel.php * @created 19.11.2020 - * @package chillerlan\QRCode\Common * @author smiley * @copyright 2020 smiley * @license MIT diff --git a/src/Common/GF256.php b/src/Common/GF256.php index e7f4cc1a2..5ae7a85f9 100644 --- a/src/Common/GF256.php +++ b/src/Common/GF256.php @@ -2,9 +2,7 @@ /** * Class GF256 * - * @filesource GF256.php * @created 16.01.2021 - * @package chillerlan\QRCode\Common * @author ZXing Authors * @author Smiley * @copyright 2021 Smiley diff --git a/src/Common/GenericGFPoly.php b/src/Common/GenericGFPoly.php index 87e8efd8f..52563a454 100644 --- a/src/Common/GenericGFPoly.php +++ b/src/Common/GenericGFPoly.php @@ -2,9 +2,7 @@ /** * Class GenericGFPoly * - * @filesource GenericGFPoly.php * @created 16.01.2021 - * @package chillerlan\QRCode\Common * @author ZXing Authors * @author Smiley * @copyright 2021 Smiley diff --git a/src/Common/MaskPattern.php b/src/Common/MaskPattern.php index e9e628a6a..7e3f77d62 100644 --- a/src/Common/MaskPattern.php +++ b/src/Common/MaskPattern.php @@ -2,9 +2,7 @@ /** * Class MaskPattern * - * @filesource MaskPattern.php * @created 19.01.2021 - * @package chillerlan\QRCode\Common * @author smiley * @copyright 2021 smiley * @license MIT diff --git a/src/Common/Mode.php b/src/Common/Mode.php index fd73f6783..4ac298766 100644 --- a/src/Common/Mode.php +++ b/src/Common/Mode.php @@ -2,9 +2,7 @@ /** * Class Mode * - * @filesource Mode.php * @created 19.11.2020 - * @package chillerlan\QRCode\Common * @author smiley * @copyright 2020 smiley * @license MIT diff --git a/src/Common/ReedSolomonEncoder.php b/src/Common/ReedSolomonEncoder.php index ca8e0e03b..4a65bba13 100644 --- a/src/Common/ReedSolomonEncoder.php +++ b/src/Common/ReedSolomonEncoder.php @@ -2,9 +2,7 @@ /** * Class ReedSolomonEncoder * - * @filesource ReedSolomonEncoder.php * @created 07.01.2021 - * @package chillerlan\QRCode\Common * @author smiley * @copyright 2021 smiley * @license MIT diff --git a/src/Common/Version.php b/src/Common/Version.php index 37c91bb22..3b613587e 100644 --- a/src/Common/Version.php +++ b/src/Common/Version.php @@ -2,9 +2,7 @@ /** * Class Version * - * @filesource Version.php * @created 19.11.2020 - * @package chillerlan\QRCode\Common * @author smiley * @copyright 2020 smiley * @license MIT diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index f3ff54dab..87291d967 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -2,9 +2,7 @@ /** * Class AlphaNum * - * @filesource AlphaNum.php * @created 25.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Data/Byte.php b/src/Data/Byte.php index 229952d0a..06963e21e 100644 --- a/src/Data/Byte.php +++ b/src/Data/Byte.php @@ -2,9 +2,7 @@ /** * Class Byte * - * @filesource Byte.php * @created 25.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Data/ECI.php b/src/Data/ECI.php index a3e08130b..a335039c3 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -2,9 +2,7 @@ /** * Class ECI * - * @filesource ECI.php * @created 20.11.2020 - * @package chillerlan\QRCode\Data * @author smiley * @copyright 2020 smiley * @license MIT diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index 856b54779..e37604cf0 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -2,9 +2,7 @@ /** * Class Kanji * - * @filesource Kanji.php * @created 25.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Data/MaskPatternTester.php b/src/Data/MaskPatternTester.php index d303bc2ef..792920b32 100644 --- a/src/Data/MaskPatternTester.php +++ b/src/Data/MaskPatternTester.php @@ -2,9 +2,7 @@ /** * Class MaskPatternTester * - * @filesource MaskPatternTester.php * @created 22.11.2017 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/src/Data/Number.php b/src/Data/Number.php index 01ae36ecf..d31605826 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -2,9 +2,7 @@ /** * Class Number * - * @filesource Number.php * @created 26.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Data/QRCodeDataException.php b/src/Data/QRCodeDataException.php index 862f57ba0..b260e4942 100644 --- a/src/Data/QRCodeDataException.php +++ b/src/Data/QRCodeDataException.php @@ -2,9 +2,7 @@ /** * Class QRCodeDataException * - * @filesource QRCodeDataException.php * @created 09.12.2015 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Data/QRData.php b/src/Data/QRData.php index bce994c70..cf6c1d274 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -2,9 +2,7 @@ /** * Class QRData * - * @filesource QRData.php * @created 25.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Data/QRDataModeAbstract.php b/src/Data/QRDataModeAbstract.php index 7dc4eb6c3..9f1211162 100644 --- a/src/Data/QRDataModeAbstract.php +++ b/src/Data/QRDataModeAbstract.php @@ -2,9 +2,7 @@ /** * Class QRDataModeAbstract * - * @filesource QRDataModeAbstract.php * @created 19.11.2020 - * @package chillerlan\QRCode\Data * @author smiley * @copyright 2020 smiley * @license MIT diff --git a/src/Data/QRDataModeInterface.php b/src/Data/QRDataModeInterface.php index b69c127e6..a4729ce30 100644 --- a/src/Data/QRDataModeInterface.php +++ b/src/Data/QRDataModeInterface.php @@ -2,9 +2,7 @@ /** * Interface QRDataModeInterface * - * @filesource QRDataModeInterface.php * @created 01.12.2015 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index d44c732ad..11fd3445e 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -2,9 +2,7 @@ /** * Class QRMatrix * - * @filesource QRMatrix.php * @created 15.11.2017 - * @package chillerlan\QRCode\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/src/Output/QRCodeOutputException.php b/src/Output/QRCodeOutputException.php index 639bdd111..62d971877 100644 --- a/src/Output/QRCodeOutputException.php +++ b/src/Output/QRCodeOutputException.php @@ -2,9 +2,7 @@ /** * Class QRCodeOutputException * - * @filesource QRCodeOutputException.php * @created 09.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Output/QRFpdf.php b/src/Output/QRFpdf.php index 1c3b99d4e..3ac544d40 100644 --- a/src/Output/QRFpdf.php +++ b/src/Output/QRFpdf.php @@ -2,14 +2,11 @@ /** * Class QRFpdf * - * https://github.com/chillerlan/php-qrcode/pull/49 - * - * @filesource QRFpdf.php * @created 03.06.2020 - * @package chillerlan\QRCode\Output * @author Maximilian Kresse - * * @license MIT + * + * @see https://github.com/chillerlan/php-qrcode/pull/49 */ namespace chillerlan\QRCode\Output; diff --git a/src/Output/QRImage.php b/src/Output/QRImage.php index 098a7825d..177ad50e5 100644 --- a/src/Output/QRImage.php +++ b/src/Output/QRImage.php @@ -2,9 +2,7 @@ /** * Class QRImage * - * @filesource QRImage.php * @created 05.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Output/QRImagick.php b/src/Output/QRImagick.php index 49516d30e..d93cb758f 100644 --- a/src/Output/QRImagick.php +++ b/src/Output/QRImagick.php @@ -2,9 +2,7 @@ /** * Class QRImagick * - * @filesource QRImagick.php * @created 04.07.2018 - * @package chillerlan\QRCode\Output * @author smiley * @copyright 2018 smiley * @license MIT diff --git a/src/Output/QRMarkup.php b/src/Output/QRMarkup.php index a351c8cf4..bf79bdcdf 100644 --- a/src/Output/QRMarkup.php +++ b/src/Output/QRMarkup.php @@ -2,9 +2,7 @@ /** * Class QRMarkup * - * @filesource QRMarkup.php * @created 17.12.2016 - * @package chillerlan\QRCode\Output * @author Smiley * @copyright 2016 Smiley * @license MIT diff --git a/src/Output/QROutputAbstract.php b/src/Output/QROutputAbstract.php index a115cf722..0c384e6d6 100644 --- a/src/Output/QROutputAbstract.php +++ b/src/Output/QROutputAbstract.php @@ -2,9 +2,7 @@ /** * Class QROutputAbstract * - * @filesource QROutputAbstract.php * @created 09.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley * @copyright 2015 Smiley * @license MIT @@ -117,6 +115,8 @@ abstract class QROutputAbstract implements QROutputInterface{ /** * @inheritDoc + * + * @return mixed */ public function dump(string $file = null){ $file ??= $this->options->cachefile; diff --git a/src/Output/QROutputInterface.php b/src/Output/QROutputInterface.php index 847b2b754..32b1bf6e8 100644 --- a/src/Output/QROutputInterface.php +++ b/src/Output/QROutputInterface.php @@ -2,9 +2,7 @@ /** * Interface QROutputInterface, * - * @filesource QROutputInterface.php * @created 02.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/Output/QRString.php b/src/Output/QRString.php index 3ed5153e1..a5dbcf8d8 100644 --- a/src/Output/QRString.php +++ b/src/Output/QRString.php @@ -2,9 +2,7 @@ /** * Class QRString * - * @filesource QRString.php * @created 05.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/QRCode.php b/src/QRCode.php index 91761221c..1ab3e4844 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -2,9 +2,7 @@ /** * Class QRCode * - * @filesource QRCode.php * @created 26.11.2015 - * @package chillerlan\QRCode * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/QRCodeException.php b/src/QRCodeException.php index 737a0803e..b20836ca2 100644 --- a/src/QRCodeException.php +++ b/src/QRCodeException.php @@ -2,9 +2,7 @@ /** * Class QRCodeException * - * @filesource QRCodeException.php * @created 27.11.2015 - * @package chillerlan\QRCode * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/QROptions.php b/src/QROptions.php index 437eb5b90..ca7fac3d5 100644 --- a/src/QROptions.php +++ b/src/QROptions.php @@ -2,9 +2,7 @@ /** * Class QROptions * - * @filesource QROptions.php * @created 08.12.2015 - * @package chillerlan\QRCode * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/src/QROptionsTrait.php b/src/QROptionsTrait.php index 0bae75f02..7b68f7436 100644 --- a/src/QROptionsTrait.php +++ b/src/QROptionsTrait.php @@ -2,9 +2,7 @@ /** * Trait QROptionsTrait * - * @filesource QROptionsTrait.php * @created 10.03.2018 - * @package chillerlan\QRCode * @author smiley * @copyright 2018 smiley * @license MIT diff --git a/tests/Common/BitBufferTest.php b/tests/Common/BitBufferTest.php index e863c8443..09342f0a5 100644 --- a/tests/Common/BitBufferTest.php +++ b/tests/Common/BitBufferTest.php @@ -2,9 +2,7 @@ /** * Class BitBufferTest * - * @filesource BitBufferTest.php * @created 08.02.2016 - * @package chillerlan\QRCodeTest\Common * @author Smiley * @copyright 2015 Smiley * @license MIT diff --git a/tests/Data/AlphaNumTest.php b/tests/Data/AlphaNumTest.php index 80a6ff7df..ae99a772e 100644 --- a/tests/Data/AlphaNumTest.php +++ b/tests/Data/AlphaNumTest.php @@ -2,9 +2,7 @@ /** * Class AlphaNumTest * - * @filesource AlphaNumTest.php * @created 24.11.2017 - * @package chillerlan\QRCodeTest\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Data/ByteTest.php b/tests/Data/ByteTest.php index dacc523a5..dce159b66 100644 --- a/tests/Data/ByteTest.php +++ b/tests/Data/ByteTest.php @@ -2,9 +2,7 @@ /** * Class ByteTest * - * @filesource ByteTest.php * @created 24.11.2017 - * @package chillerlan\QRCodeTest\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Data/DatainterfaceTestAbstract.php b/tests/Data/DatainterfaceTestAbstract.php index 5fda9946c..a747cba61 100644 --- a/tests/Data/DatainterfaceTestAbstract.php +++ b/tests/Data/DatainterfaceTestAbstract.php @@ -2,9 +2,7 @@ /** * Class DatainterfaceTestAbstract * - * @filesource DatainterfaceTestAbstract.php * @created 24.11.2017 - * @package chillerlan\QRCodeTest\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Data/KanjiTest.php b/tests/Data/KanjiTest.php index a34df88fa..aae2b9b25 100644 --- a/tests/Data/KanjiTest.php +++ b/tests/Data/KanjiTest.php @@ -2,9 +2,7 @@ /** * Class KanjiTest * - * @filesource KanjiTest.php * @created 24.11.2017 - * @package chillerlan\QRCodeTest\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Data/MaskPatternTesterTest.php b/tests/Data/MaskPatternTesterTest.php index 49b1c4924..cf162be38 100644 --- a/tests/Data/MaskPatternTesterTest.php +++ b/tests/Data/MaskPatternTesterTest.php @@ -2,9 +2,7 @@ /** * Class MaskPatternTesterTest * - * @filesource MaskPatternTesterTest.php * @created 24.11.2017 - * @package chillerlan\QRCodeTest\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Data/NumberTest.php b/tests/Data/NumberTest.php index ccbe6f65e..6400c5340 100644 --- a/tests/Data/NumberTest.php +++ b/tests/Data/NumberTest.php @@ -2,9 +2,7 @@ /** * Class NumberTest * - * @filesource NumberTest.php * @created 24.11.2017 - * @package chillerlan\QRCodeTest\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Data/QRMatrixTest.php b/tests/Data/QRMatrixTest.php index 39dd6cedd..bf741df16 100755 --- a/tests/Data/QRMatrixTest.php +++ b/tests/Data/QRMatrixTest.php @@ -2,9 +2,7 @@ /** * Class QRMatrixTest * - * @filesource QRMatrixTest.php * @created 17.11.2017 - * @package chillerlan\QRCodeTest\Data * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Output/QRFpdfTest.php b/tests/Output/QRFpdfTest.php index a3ab0f5b9..c1f65b44e 100644 --- a/tests/Output/QRFpdfTest.php +++ b/tests/Output/QRFpdfTest.php @@ -2,9 +2,7 @@ /** * Class QRFpdfTest * - * @filesource QRFpdfTest.php * @created 03.06.2020 - * @package chillerlan\QRCodeTest\Output * @author smiley * @copyright 2020 smiley * @license MIT diff --git a/tests/Output/QRImageTest.php b/tests/Output/QRImageTest.php index 4da150b83..f7e163683 100644 --- a/tests/Output/QRImageTest.php +++ b/tests/Output/QRImageTest.php @@ -2,9 +2,7 @@ /** * Class QRImageTest * - * @filesource QRImageTest.php * @created 24.12.2017 - * @package chillerlan\QRCodeTest\Output * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Output/QRImagickTest.php b/tests/Output/QRImagickTest.php index aeb5109d5..48e1ab1d7 100644 --- a/tests/Output/QRImagickTest.php +++ b/tests/Output/QRImagickTest.php @@ -2,9 +2,7 @@ /** * Class QRImagickTest * - * @filesource QRImagickTest.php * @created 04.07.2018 - * @package chillerlan\QRCodeTest\Output * @author smiley * @copyright 2018 smiley * @license MIT diff --git a/tests/Output/QRMarkupTest.php b/tests/Output/QRMarkupTest.php index b928c5062..5c9852978 100644 --- a/tests/Output/QRMarkupTest.php +++ b/tests/Output/QRMarkupTest.php @@ -2,9 +2,7 @@ /** * Class QRMarkupTest * - * @filesource QRMarkupTest.php * @created 24.12.2017 - * @package chillerlan\QRCodeTest\Output * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index 21946d334..c3937ae65 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -2,9 +2,7 @@ /** * Class QROutputTestAbstract * - * @filesource QROutputTestAbstract.php * @created 24.12.2017 - * @package chillerlan\QRCodeTest\Output * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/Output/QRStringTest.php b/tests/Output/QRStringTest.php index c40694ec8..91bf11c83 100644 --- a/tests/Output/QRStringTest.php +++ b/tests/Output/QRStringTest.php @@ -2,9 +2,7 @@ /** * Class QRStringTest * - * @filesource QRStringTest.php * @created 24.12.2017 - * @package chillerlan\QRCodeTest\Output * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/QRCodeTest.php b/tests/QRCodeTest.php index cdf984771..21e41f5d3 100755 --- a/tests/QRCodeTest.php +++ b/tests/QRCodeTest.php @@ -2,9 +2,7 @@ /** * Class QRCodeTest * - * @filesource QRCodeTest.php * @created 17.11.2017 - * @package chillerlan\QRCodeTest * @author Smiley * @copyright 2017 Smiley * @license MIT diff --git a/tests/QROptionsTest.php b/tests/QROptionsTest.php index 3a0260623..dcc19e3e6 100644 --- a/tests/QROptionsTest.php +++ b/tests/QROptionsTest.php @@ -2,9 +2,7 @@ /** * Class QROptionsTest * - * @filesource QROptionsTest.php * @created 08.11.2018 - * @package chillerlan\QRCodeTest * @author smiley * @copyright 2018 smiley * @license MIT From 84eb31696ca6dc7f2be9e492fdf2cf3bfe0126c6 Mon Sep 17 00:00:00 2001 From: codemasher Date: Mon, 25 Jan 2021 00:42:26 +0100 Subject: [PATCH 56/78] :sparkles: init from https://github.com/codemasher/php-qrcode-decoder/commit/5798a53268f14eb1d7d70098d23db0a81b25f98b --- composer.json | 5 +- src/Common/FormatInformation.php | 82 +++ src/Common/ReedSolomonDecoder.php | 192 ++++++ src/Common/Version.php | 2 +- src/Common/functions.php | 58 ++ src/Decoder/Binarizer.php | 361 +++++++++++ src/Decoder/BitMatrix.php | 204 +++++++ src/Decoder/BitMatrixParser.php | 333 ++++++++++ src/Decoder/Decoder.php | 336 ++++++++++ src/Decoder/DecoderResult.php | 86 +++ src/Decoder/GDLuminanceSource.php | 71 +++ src/Decoder/IMagickLuminanceSource.php | 53 ++ src/Decoder/LuminanceSource.php | 103 ++++ src/Detector/AlignmentPattern.php | 34 ++ src/Detector/AlignmentPatternFinder.php | 287 +++++++++ src/Detector/Detector.php | 358 +++++++++++ src/Detector/FinderPattern.php | 69 +++ src/Detector/FinderPatternFinder.php | 775 ++++++++++++++++++++++++ src/Detector/GridSampler.php | 171 ++++++ src/Detector/PerspectiveTransform.php | 152 +++++ src/Detector/ResultPoint.php | 61 ++ src/QRCodeReader.php | 88 +++ src/includes.php | 16 + tests/QRCodeReaderTest.php | 121 ++++ tests/qrcodes/alphanum.png | Bin 0 -> 383 bytes tests/qrcodes/byte.png | Bin 0 -> 1759 bytes tests/qrcodes/damaged.png | Bin 0 -> 8646 bytes tests/qrcodes/hello_world.png | Bin 0 -> 3121 bytes tests/qrcodes/hello_world_mirrored.png | Bin 0 -> 12319 bytes tests/qrcodes/kanji.png | Bin 0 -> 383 bytes tests/qrcodes/numeric.png | Bin 0 -> 388 bytes tests/qrcodes/smol.png | Bin 0 -> 389 bytes 32 files changed, 4016 insertions(+), 2 deletions(-) create mode 100644 src/Common/FormatInformation.php create mode 100644 src/Common/ReedSolomonDecoder.php create mode 100644 src/Common/functions.php create mode 100644 src/Decoder/Binarizer.php create mode 100644 src/Decoder/BitMatrix.php create mode 100644 src/Decoder/BitMatrixParser.php create mode 100644 src/Decoder/Decoder.php create mode 100644 src/Decoder/DecoderResult.php create mode 100644 src/Decoder/GDLuminanceSource.php create mode 100644 src/Decoder/IMagickLuminanceSource.php create mode 100644 src/Decoder/LuminanceSource.php create mode 100644 src/Detector/AlignmentPattern.php create mode 100644 src/Detector/AlignmentPatternFinder.php create mode 100644 src/Detector/Detector.php create mode 100644 src/Detector/FinderPattern.php create mode 100644 src/Detector/FinderPatternFinder.php create mode 100644 src/Detector/GridSampler.php create mode 100644 src/Detector/PerspectiveTransform.php create mode 100644 src/Detector/ResultPoint.php create mode 100644 src/QRCodeReader.php create mode 100644 src/includes.php create mode 100644 tests/QRCodeReaderTest.php create mode 100644 tests/qrcodes/alphanum.png create mode 100644 tests/qrcodes/byte.png create mode 100644 tests/qrcodes/damaged.png create mode 100644 tests/qrcodes/hello_world.png create mode 100644 tests/qrcodes/hello_world_mirrored.png create mode 100644 tests/qrcodes/kanji.png create mode 100644 tests/qrcodes/numeric.png create mode 100644 tests/qrcodes/smol.png diff --git a/composer.json b/composer.json index 536bdf56f..311442500 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,10 @@ "autoload": { "psr-4": { "chillerlan\\QRCode\\": "src/" - } + }, + "files": [ + "src/includes.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Common/FormatInformation.php b/src/Common/FormatInformation.php new file mode 100644 index 000000000..4f409fc8f --- /dev/null +++ b/src/Common/FormatInformation.php @@ -0,0 +1,82 @@ + + * @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\ErrorCorrectionLevel + */ +final class FormatInformation{ + + public const 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], + ]; + + private int $errorCorrectionLevel; + private int $dataMask; + + public function __construct(int $formatInfo){ + $this->errorCorrectionLevel = ($formatInfo >> 3) & 0x03; // Bits 3,4 + $this->dataMask = ($formatInfo & 0x07); // Bottom 3 bits + } + + public function getErrorCorrectionLevel():EccLevel{ + return new EccLevel($this->errorCorrectionLevel); + } + + public function getDataMask():MaskPattern{ + return new MaskPattern($this->dataMask); + } + +} + diff --git a/src/Common/ReedSolomonDecoder.php b/src/Common/ReedSolomonDecoder.php new file mode 100644 index 000000000..943973860 --- /dev/null +++ b/src/Common/ReedSolomonDecoder.php @@ -0,0 +1,192 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use RuntimeException; +use function array_fill, count; + +/** + *

Implements Reed-Solomon decoding, as the name implies.

+ * + *

The algorithm will not be explained here, but the following references were helpful + * in creating this implementation:

+ * + * + * + *

Much credit is due to William Rucklidge since portions of this code are an indirect + * port of his C++ Reed-Solomon implementation.

+ * + * @author Sean Owen + * @author William Rucklidge + * @author sanfordsquires + */ +final class ReedSolomonDecoder{ + + /** + *

Decodes given set of received codewords, which include both data and error-correction + * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place, + * in the input.

+ * + * @param array $received data and error-correction codewords + * @param int $numEccCodewords number of error-correction codewords available + * + * @return int[] + * @throws \RuntimeException if decoding fails for any reason + */ + public function decode(array $received, int $numEccCodewords):array{ + $poly = new GenericGFPoly($received); + $syndromeCoefficients = []; + $noError = true; + + for($i = 0, $j = $numEccCodewords - 1; $i < $numEccCodewords; $i++, $j--){ + $eval = $poly->evaluateAt(GF256::exp($i)); + $syndromeCoefficients[$j] = $eval; + + if($eval !== 0){ + $noError = false; + } + } + + if($noError){ + return $received; + } + + [$sigma, $omega] = $this->runEuclideanAlgorithm( + GF256::buildMonomial($numEccCodewords, 1), + new GenericGFPoly($syndromeCoefficients), + $numEccCodewords + ); + + $errorLocations = $this->findErrorLocations($sigma); + $errorMagnitudes = $this->findErrorMagnitudes($omega, $errorLocations); + $errorLocationsCount = count($errorLocations); + $receivedCount = count($received); + + for($i = 0; $i < $errorLocationsCount; $i++){ + $position = $receivedCount - 1 - GF256::log($errorLocations[$i]); + + if($position < 0){ + throw new RuntimeException('Bad error location'); + } + + $received[$position] ^= $errorMagnitudes[$i]; + } + + return $received; + } + + /** + * @return \chillerlan\QRCode\Common\GenericGFPoly[] [sigma, omega] + * @throws \RuntimeException + */ + private function runEuclideanAlgorithm(GenericGFPoly $a, GenericGFPoly $b, int $R):array{ + // Assume a's degree is >= b's + if($a->getDegree() < $b->getDegree()){ + $temp = $a; + $a = $b; + $b = $temp; + } + + $rLast = $a; + $r = $b; + $tLast = new GenericGFPoly([0]); + $t = new GenericGFPoly([1]); + + // Run Euclidean algorithm until r's degree is less than R/2 + while($r->getDegree() >= $R / 2){ + $rLastLast = $rLast; + $tLastLast = $tLast; + $rLast = $r; + $tLast = $t; + + // Divide rLastLast by rLast, with quotient in q and remainder in r + [$q, $r] = $rLastLast->divide($rLast); + + $t = $q->multiply($tLast)->addOrSubtract($tLastLast); + + if($r->getDegree() >= $rLast->getDegree()){ + throw new RuntimeException('Division algorithm failed to reduce polynomial?'); + } + } + + $sigmaTildeAtZero = $t->getCoefficient(0); + + if($sigmaTildeAtZero === 0){ + throw new RuntimeException('sigmaTilde(0) was zero'); + } + + $inverse = GF256::inverse($sigmaTildeAtZero); + + return [$t->multiplyInt($inverse), $r->multiplyInt($inverse)]; + } + + /** + * @throws \RuntimeException + */ + private function findErrorLocations(GenericGFPoly $errorLocator):array{ + // This is a direct application of Chien's search + $numErrors = $errorLocator->getDegree(); + + if($numErrors === 1){ // shortcut + return [$errorLocator->getCoefficient(1)]; + } + + $result = array_fill(0, $numErrors, 0); + $e = 0; + + for($i = 1; $i < 256 && $e < $numErrors; $i++){ + if($errorLocator->evaluateAt($i) === 0){ + $result[$e] = GF256::inverse($i); + $e++; + } + } + + if($e !== $numErrors){ + throw new RuntimeException('Error locator degree does not match number of roots'); + } + + return $result; + } + + private function findErrorMagnitudes(GenericGFPoly $errorEvaluator, array $errorLocations):array{ + // This is directly applying Forney's Formula + $s = count($errorLocations); + $result = []; + + for($i = 0; $i < $s; $i++){ + $xiInverse = GF256::inverse($errorLocations[$i]); + $denominator = 1; + + for($j = 0; $j < $s; $j++){ + if($i !== $j){ +# $denominator = GF256::multiply($denominator, GF256::addOrSubtract(1, GF256::multiply($errorLocations[$j], $xiInverse))); + // Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug. + // Below is a funny-looking workaround from Steven Parkes + $term = GF256::multiply($errorLocations[$j], $xiInverse); + $denominator = GF256::multiply($denominator, (($term & 0x1) === 0 ? $term | 1 : $term & ~1)); + } + } + + $result[$i] = GF256::multiply($errorEvaluator->evaluateAt($xiInverse), GF256::inverse($denominator)); + } + + return $result; + } + +} diff --git a/src/Common/Version.php b/src/Common/Version.php index 3b613587e..1c0ba45d4 100644 --- a/src/Common/Version.php +++ b/src/Common/Version.php @@ -306,7 +306,7 @@ final class Version{ * the maximum character count for the given $mode and $eccLevel */ public function getMaxLengthForMode(int $mode, EccLevel $eccLevel):?int{ - return self::MAX_LENGTH[$this->version][$mode][$eccLevel->getOrdinal()] ?? null; + return self::MAX_LENGTH[$this->version][Mode::DATA_MODES[$mode]][$eccLevel->getOrdinal()] ?? null; } /** diff --git a/src/Common/functions.php b/src/Common/functions.php new file mode 100644 index 000000000..ebfa03094 --- /dev/null +++ b/src/Common/functions.php @@ -0,0 +1,58 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use function array_slice, array_splice, sqrt; +use const PHP_INT_SIZE; + +const QRCODE_DECODER_INCLUDES = true; + +function arraycopy(array $srcArray, int $srcPos, array $destArray, int $destPos, int $length):array{ + array_splice($destArray, $destPos, $length, array_slice($srcArray, $srcPos, $length)); + + return $destArray; +} + +function uRShift(int $a, int $b):int{ + + if($b === 0){ + return $a; + } + + return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)); +} + +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[uRShift($a, $i) & 0x0F]; + } + + return $count; +} + +function squaredDistance(float $aX, float $aY, float $bX, float $bY):float{ + $xDiff = $aX - $bX; + $yDiff = $aY - $bY; + + return $xDiff * $xDiff + $yDiff * $yDiff; +} + +function distance(float $aX, float $aY, float $bX, float $bY):float{ + return sqrt(squaredDistance($aX, $aY, $bX, $bY)); +} + diff --git a/src/Decoder/Binarizer.php b/src/Decoder/Binarizer.php new file mode 100644 index 000000000..d3c7fdaf3 --- /dev/null +++ b/src/Decoder/Binarizer.php @@ -0,0 +1,361 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use RuntimeException; +use function array_fill, count, max; + +/** + * This class implements a local thresholding algorithm, which while slower than the + * GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for + * high frequency images of barcodes with black data on white backgrounds. For this application, + * it does a much better job than a global blackpoint with severe shadows and gradients. + * However it tends to produce artifacts on lower frequency images and is therefore not + * a good general purpose binarizer for uses outside ZXing. + * + * This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers, + * and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already + * inherently local, and only fails for horizontal gradients. We can revisit that problem later, + * but for now it was not a win to use local blocks for 1D. + * + * This Binarizer is the default for the unit tests and the recommended class for library users. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class Binarizer{ + + // This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels. + // So this is the smallest dimension in each axis we can accept. + private const BLOCK_SIZE_POWER = 3; + private const BLOCK_SIZE = 8; // ...0100...00 + private const BLOCK_SIZE_MASK = 7; // ...0011...11 + private const MINIMUM_DIMENSION = 40; + private const MIN_DYNAMIC_RANGE = 24; + +# private const LUMINANCE_BITS = 5; + private const LUMINANCE_SHIFT = 3; + private const LUMINANCE_BUCKETS = 32; + + private LuminanceSource $source; + + public function __construct(LuminanceSource $source){ + $this->source = $source; + } + + /** + * @throws \RuntimeException + */ + private function estimateBlackPoint(array $buckets):int{ + // Find the tallest peak in the histogram. + $numBuckets = count($buckets); + $maxBucketCount = 0; + $firstPeak = 0; + $firstPeakSize = 0; + + for($x = 0; $x < $numBuckets; $x++){ + + if($buckets[$x] > $firstPeakSize){ + $firstPeak = $x; + $firstPeakSize = $buckets[$x]; + } + + if($buckets[$x] > $maxBucketCount){ + $maxBucketCount = $buckets[$x]; + } + } + + // Find the second-tallest peak which is somewhat far from the tallest peak. + $secondPeak = 0; + $secondPeakScore = 0; + + for($x = 0; $x < $numBuckets; $x++){ + $distanceToBiggest = $x - $firstPeak; + // Encourage more distant second peaks by multiplying by square of distance. + $score = $buckets[$x] * $distanceToBiggest * $distanceToBiggest; + + if($score > $secondPeakScore){ + $secondPeak = $x; + $secondPeakScore = $score; + } + } + + // Make sure firstPeak corresponds to the black peak. + if($firstPeak > $secondPeak){ + $temp = $firstPeak; + $firstPeak = $secondPeak; + $secondPeak = $temp; + } + + // If there is too little contrast in the image to pick a meaningful black point, throw rather + // than waste time trying to decode the image, and risk false positives. + if($secondPeak - $firstPeak <= $numBuckets / 16){ + throw new RuntimeException('no meaningful dark point found'); + } + + // Find a valley between them that is low and closer to the white peak. + $bestValley = $secondPeak - 1; + $bestValleyScore = -1; + + for($x = $secondPeak - 1; $x > $firstPeak; $x--){ + $fromFirst = $x - $firstPeak; + $score = $fromFirst * $fromFirst * ($secondPeak - $x) * ($maxBucketCount - $buckets[$x]); + + if($score > $bestValleyScore){ + $bestValley = $x; + $bestValleyScore = $score; + } + } + + return $bestValley << self::LUMINANCE_SHIFT; + } + + /** + * Calculates the final BitMatrix once for all requests. This could be called once from the + * constructor instead, but there are some advantages to doing it lazily, such as making + * profiling easier, and not doing heavy lifting when callers don't expect it. + * + * Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive + * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or + * may not apply sharpening. Therefore, a row from this matrix may not be identical to one + * fetched using getBlackRow(), so don't mix and match between them. + * + * @return \chillerlan\QRCode\Decoder\BitMatrix The 2D array of bits for the image (true means black). + */ + public function getBlackMatrix():BitMatrix{ + $width = $this->source->getWidth(); + $height = $this->source->getHeight(); + + if($width >= self::MINIMUM_DIMENSION && $height >= self::MINIMUM_DIMENSION){ + $subWidth = $width >> self::BLOCK_SIZE_POWER; + + if(($width & self::BLOCK_SIZE_MASK) !== 0){ + $subWidth++; + } + + $subHeight = $height >> self::BLOCK_SIZE_POWER; + + if(($height & self::BLOCK_SIZE_MASK) !== 0){ + $subHeight++; + } + + return $this->calculateThresholdForBlock($subWidth, $subHeight, $width, $height); + } + + // If the image is too small, fall back to the global histogram approach. + return $this->getHistogramBlackMatrix($width, $height); + } + + public 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. + $buckets = array_fill(0, self::LUMINANCE_BUCKETS, 0); + + for($y = 1; $y < 5; $y++){ + $row = (int)($height * $y / 5); + $localLuminances = $this->source->getRow($row); + $right = (int)(($width * 4) / 5); + + for($x = (int)($width / 5); $x < $right; $x++){ + $pixel = $localLuminances[(int)$x] & 0xff; + $buckets[$pixel >> self::LUMINANCE_SHIFT]++; + } + } + + $blackPoint = $this->estimateBlackPoint($buckets); + + // We delay reading the entire image luminance until the black point estimation succeeds. + // 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(); + + for($y = 0; $y < $height; $y++){ + $offset = $y * $width; + + for($x = 0; $x < $width; $x++){ + $pixel = (int)($localLuminances[$offset + $x] & 0xff); + + if($pixel < $blackPoint){ + $matrix->set($x, $y); + } + } + } + + return $matrix; + } + + /** + * Calculates a single black point for each block of pixels and saves it away. + * See the following thread for a discussion of this algorithm: + * + * @see http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0 + */ + private function calculateBlackPoints(array $luminances, int $subWidth, int $subHeight, int $width, int $height):array{ + $blackPoints = array_fill(0, $subHeight, 0); + + foreach($blackPoints as $key => $point){ + $blackPoints[$key] = array_fill(0, $subWidth, 0); + } + + for($y = 0; $y < $subHeight; $y++){ + $yoffset = ($y << self::BLOCK_SIZE_POWER); + $maxYOffset = $height - self::BLOCK_SIZE; + + if($yoffset > $maxYOffset){ + $yoffset = $maxYOffset; + } + + for($x = 0; $x < $subWidth; $x++){ + $xoffset = ($x << self::BLOCK_SIZE_POWER); + $maxXOffset = $width - self::BLOCK_SIZE; + + if($xoffset > $maxXOffset){ + $xoffset = $maxXOffset; + } + + $sum = 0; + $min = 255; + $max = 0; + + for($yy = 0, $offset = $yoffset * $width + $xoffset; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){ + + for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){ + $pixel = (int)($luminances[(int)($offset + $xx)]) & 0xff; + $sum += $pixel; + // still looking for good contrast + if($pixel < $min){ + $min = $pixel; + } + + if($pixel > $max){ + $max = $pixel; + } + } + + // short-circuit min/max tests once dynamic range is met + if($max - $min > self::MIN_DYNAMIC_RANGE){ + // finish the rest of the rows quickly + for($yy++, $offset += $width; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){ + for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){ + $sum += $luminances[$offset + $xx] & 0xff; + } + } + } + } + + // The default estimate is the average of the values in the block. + $average = $sum >> (self::BLOCK_SIZE_POWER * 2); + + if($max - $min <= self::MIN_DYNAMIC_RANGE){ + // If variation within the block is low, assume this is a block with only light or only + // dark pixels. In that case we do not want to use the average, as it would divide this + // low contrast area into black and white pixels, essentially creating data out of noise. + // + // The default assumption is that the block is light/background. Since no estimate for + // the level of dark pixels exists locally, use half the min for the block. + $average = (int)($min / 2); + + if($y > 0 && $x > 0){ + // Correct the "white background" assumption for blocks that have neighbors by comparing + // the pixels in this block to the previously calculated black points. This is based on + // the fact that dark barcode symbology is always surrounded by some amount of light + // background for which reasonable black point estimates were made. The bp estimated at + // the boundaries is used for the interior. + + // The (min < bp) is arbitrary but works better than other heuristics that were tried. + $averageNeighborBlackPoint = (int)(($blackPoints[$y - 1][$x] + (2 * $blackPoints[$y][$x - 1]) + $blackPoints[$y - 1][$x - 1]) / 4); + + if($min < $averageNeighborBlackPoint){ + $average = $averageNeighborBlackPoint; + } + } + } + + $blackPoints[$y][$x] = (int)($average); + } + } + + return $blackPoints; + } + + /** + * For each block in the image, calculate the average black point using a 5x5 grid + * of the blocks around it. Also handles the corner cases (fractional blocks are computed based + * on the last pixels in the row/column which are also used in the previous block). + */ + private function calculateThresholdForBlock( + int $subWidth, + int $subHeight, + int $width, + int $height + ):BitMatrix{ + $matrix = new BitMatrix(max($width, $height)); + $luminances = $this->source->getMatrix(); + $blackPoints = $this->calculateBlackPoints($luminances, $subWidth, $subHeight, $width, $height); + + for($y = 0; $y < $subHeight; $y++){ + $yoffset = ($y << self::BLOCK_SIZE_POWER); + $maxYOffset = $height - self::BLOCK_SIZE; + + if($yoffset > $maxYOffset){ + $yoffset = $maxYOffset; + } + + for($x = 0; $x < $subWidth; $x++){ + $xoffset = ($x << self::BLOCK_SIZE_POWER); + $maxXOffset = $width - self::BLOCK_SIZE; + + if($xoffset > $maxXOffset){ + $xoffset = $maxXOffset; + } + + $left = $this->cap($x, 2, $subWidth - 3); + $top = $this->cap($y, 2, $subHeight - 3); + $sum = 0; + + for($z = -2; $z <= 2; $z++){ + $blackRow = $blackPoints[$top + $z]; + $sum += $blackRow[$left - 2] + $blackRow[$left - 1] + $blackRow[$left] + $blackRow[$left + 1] + $blackRow[$left + 2]; + } + + $average = (int)($sum / 25); + + // Applies a single threshold to a block of pixels. + 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(($luminances[$o + $i] & 0xff) <= $average){ + $matrix->set($xoffset + $i, $yoffset + $j); + } + } + } + } + } + + return $matrix; + } + + private function cap(int $value, int $min, int $max):int{ + + if($value < $min){ + return $min; + } + + if($value > $max){ + return $max; + } + + return $value; + } + +} diff --git a/src/Decoder/BitMatrix.php b/src/Decoder/BitMatrix.php new file mode 100644 index 000000000..a8d41eefa --- /dev/null +++ b/src/Decoder/BitMatrix.php @@ -0,0 +1,204 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use chillerlan\QRCode\Common\{MaskPattern, Version}; +use InvalidArgumentException; +use function chillerlan\QRCode\Common\uRShift; +use function array_fill, count; + +final class BitMatrix{ + + private int $dimension; + private int $rowSize; + private array $bits; + + 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):void{ + $offset = (int)($y * $this->rowSize + ($x / 0x20)); + + $this->bits[$offset] ??= 0; + $this->bits[$offset] |= ($this->bits[$offset] |= 1 << ($x & 0x1f)); + } + + /** + *

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):void{ + $offset = $y * $this->rowSize + (int)($x / 0x20); + + $this->bits[$offset] = ($this->bits[$offset] ^ (1 << ($x & 0x1f))); + } + + /** + *

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 \InvalidArgumentException + */ + public function setRegion(int $left, int $top, int $width, int $height):void{ + + if($top < 0 || $left < 0){ + throw new InvalidArgumentException('Left and top must be nonnegative'); + } + + if($height < 1 || $width < 1){ + throw new InvalidArgumentException('Height and width must be at least 1'); + } + + $right = $left + $width; + $bottom = $top + $height; + + if($bottom > $this->dimension || $right > $this->dimension){ + throw new InvalidArgumentException('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 int The dimension (width/height) of the matrix + */ + public function getDimension():int{ + return $this->dimension; + } + + /** + *

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 (uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0; + } + + /** + * See ISO 18004:2006 Annex E + */ + public function buildFunctionPattern(Version $version):BitMatrix{ + $dimension = $version->getDimension(); + // @todo + $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 = $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($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():void{ + + 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); + } + } + } + + } + + /** + *

Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations + * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix, + * including areas used for finder patterns, timing patterns, etc. These areas should be unused + * after the point they are unmasked anyway.

+ * + *

Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position + * and j is row position. In fact, as the text says, i is row position and j is column position.

+ * + *

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(); + + for($y = 0; $y < $dimension; $y++){ + for($x = 0; $x < $dimension; $x++){ + if($mask($x, $y) === 0){ + $this->flip($x, $y); + } + } + } + + } + +} diff --git a/src/Decoder/BitMatrixParser.php b/src/Decoder/BitMatrixParser.php new file mode 100644 index 000000000..190b7ff82 --- /dev/null +++ b/src/Decoder/BitMatrixParser.php @@ -0,0 +1,333 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use RuntimeException; +use chillerlan\QRCode\Common\{Version, FormatInformation}; +use function chillerlan\QRCode\Common\numBitsDiffering; +use const PHP_INT_MAX; + +/** + * @author Sean Owen + */ +final class BitMatrixParser{ + + private BitMatrix $bitMatrix; + private ?Version $parsedVersion = null; + private ?FormatInformation $parsedFormatInfo = null; + private bool $mirror = false; + + /** + * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix + * + * @throws \RuntimeException if dimension is not >= 21 and 1 mod 4 + */ + public function __construct(BitMatrix $bitMatrix){ + $dimension = $bitMatrix->getDimension(); + + if($dimension < 21 || ($dimension % 4) !== 1){ + throw new RuntimeException('dimension is not >= 21, dimension mod 4 not 1'); + } + + $this->bitMatrix = $bitMatrix; + } + + /** + * Prepare the parser for a mirrored operation. + * This flag has effect only on the {@link #readFormatInformation()} and the + * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the + * {@link #mirror()} method should be called. + * + * @param bool mirror Whether to read version and format information mirrored. + */ + public function setMirror(bool $mirror):void{ + $this->parsedVersion = null; + $this->parsedFormatInfo = null; + $this->mirror = $mirror; + } + + /** + * Mirror the bit matrix in order to attempt a second reading. + */ + public function mirror():void{ + $this->bitMatrix->mirror(); + } + + private function copyBit(int $i, int $j, int $versionBits):int{ + + $bit = $this->mirror + ? $this->bitMatrix->get($j, $i) + : $this->bitMatrix->get($i, $j); + + return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1; + } + + /** + *

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

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

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

+ * + * @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info + * @throws \RuntimeException if both format information locations cannot be parsed as + * the valid encoding of format information + */ + public function readFormatInformation():FormatInformation{ + + if($this->parsedFormatInfo !== null){ + return $this->parsedFormatInfo; + } + + // Read top-left format info bits + $formatInfoBits1 = 0; + + for($i = 0; $i < 6; $i++){ + $formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1); + } + + // .. and skip a bit in the timing pattern ... + $formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1); + // .. and skip a bit in the timing pattern ... + for($j = 5; $j >= 0; $j--){ + $formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1); + } + + // Read the top-right/bottom-left pattern too + $dimension = $this->bitMatrix->getDimension(); + $formatInfoBits2 = 0; + $jMin = $dimension - 7; + + for($j = $dimension - 1; $j >= $jMin; $j--){ + $formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2); + } + + for($i = $dimension - 8; $i < $dimension; $i++){ + $formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2); + } + + $this->parsedFormatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2); + + if($this->parsedFormatInfo !== null){ + return $this->parsedFormatInfo; + } + + // Should return null, but, some QR codes apparently do not mask this info. + // Try again by actually masking the pattern first. + $this->parsedFormatInfo = $this->doDecodeFormatInformation( + $formatInfoBits1 ^ FormatInformation::MASK_QR, + $formatInfoBits2 ^ FormatInformation::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 information about the format it specifies, or {@code 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 = numBitsDiffering($maskedFormatInfo1, $dataBits); + + if($bitsDifference < $bestDifference){ + $bestFormatInfo = $maskedBits; + $bestDifference = $bitsDifference; + } + + if($maskedFormatInfo1 !== $maskedFormatInfo2){ + // also try the other option + $bitsDifference = numBitsDiffering($maskedFormatInfo2, $dataBits); + + if($bitsDifference < $bestDifference){ + $bestFormatInfo = $maskedBits; + $bestDifference = $bitsDifference; + } + } + } + // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match + if($bestDifference <= 3){ + return new FormatInformation($bestFormatInfo); + } + + return null; + } + + /** + *

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

+ * + * @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version + * @throws \RuntimeException if both version information locations cannot be parsed as + * the valid encoding of version information + */ + public function readVersion():Version{ + + if($this->parsedVersion !== null){ + return $this->parsedVersion; + } + + $dimension = $this->bitMatrix->getDimension(); + $provisionalVersion = ($dimension - 17) / 4; + + if($provisionalVersion <= 6){ + return new Version($provisionalVersion); + } + + // Read top-right version info: 3 wide by 6 tall + $versionBits = 0; + $ijMin = $dimension - 11; + + for($j = 5; $j >= 0; $j--){ + for($i = $dimension - 9; $i >= $ijMin; $i--){ + $versionBits = $this->copyBit($i, $j, $versionBits); + } + } + + $this->parsedVersion = $this->decodeVersionInformation($versionBits); + + if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){ + return $this->parsedVersion; + } + + // Hmm, failed. Try bottom left: 6 wide by 3 tall + $versionBits = 0; + + for($i = 5; $i >= 0; $i--){ + for($j = $dimension - 9; $j >= $ijMin; $j--){ + $versionBits = $this->copyBit($i, $j, $versionBits); + } + } + + $this->parsedVersion = $this->decodeVersionInformation($versionBits); + + if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){ + return $this->parsedVersion; + } + + throw new RuntimeException('failed to read version'); + } + + 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 + $bitsDifference = 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; + } + +} diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php new file mode 100644 index 000000000..f50f43ef1 --- /dev/null +++ b/src/Decoder/Decoder.php @@ -0,0 +1,336 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use Exception, InvalidArgumentException, RuntimeException; +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, ECICharset, 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; + +/** + *

The main class which implements QR Code decoding -- as opposed to locating and extracting + * the QR Code from an image.

+ * + * @author Sean Owen + */ +final class Decoder{ + +# private const GB2312_SUBSET = 1; + + /** + *

Decodes a QR Code represented as a {@link \chillerlan\QRCode\Decoder\BitMatrix}. + * A 1 or "true" is taken to mean a black module.

+ * + * @param \chillerlan\QRCode\Decoder\LuminanceSource $source + * + * @return \chillerlan\QRCode\Decoder\DecoderResult text and bytes encoded within the QR Code + * @throws \Exception if the QR Code cannot be decoded + */ + public function decode(LuminanceSource $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); + } + catch(Exception $e){ + // Throw the exception from the original reading + if($fe instanceof Exception){ + throw $fe; + } + + throw $e; + } + + } + + /** + * @param \chillerlan\QRCode\Decoder\BitMatrixParser $parser + * + * @return \chillerlan\QRCode\Decoder\DecoderResult + */ + private function decodeParser(BitMatrixParser $parser):DecoderResult{ + $version = $parser->readVersion(); + $eccLevel = $parser->readFormatInformation()->getErrorCorrectionLevel(); + + // Read raw codewords + $rawCodewords = $parser->readCodewords(); + // Separate into data blocks + $dataBlocks = $this->getDataBlocks($rawCodewords, $version, $eccLevel); + + $resultBytes = []; + $resultOffset = 0; + + // Error-correct and copy data blocks together into a stream of bytes + foreach($dataBlocks as $dataBlock){ + [$numDataCodewords, $codewordBytes] = $dataBlock; + + $corrected = $this->correctErrors($codewordBytes, $numDataCodewords); + + for($i = 0; $i < $numDataCodewords; $i++){ + $resultBytes[$resultOffset++] = $corrected[$i]; + } + } + + // Decode the contents of that stream of bytes + return $this->decodeBitStream($resultBytes, $version, $eccLevel); + } + + /** + *

When QR Codes use multiple data blocks, they are actually interleaved. + * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This + * method will separate the data into original blocks.

+ * + * @param array $rawCodewords bytes as read directly from the QR Code + * @param \chillerlan\QRCode\Common\Version $version version of the QR Code + * @param \chillerlan\QRCode\Common\EccLevel $eccLevel error-correction level of the QR Code + * + * @return array DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code + * @throws \InvalidArgumentException + */ + private function getDataBlocks(array $rawCodewords, Version $version, EccLevel $eccLevel):array{ + + if(count($rawCodewords) !== $version->getTotalCodewords()){ + throw new InvalidArgumentException('$rawCodewords differ from total codewords for version'); + } + + // Figure out the number and size of data blocks used by this version and + // error correction level + [$numEccCodewords, $eccBlocks] = $version->getRSBlocks($eccLevel); + + // Now establish DataBlocks of the appropriate size and number of data codewords + $result = [];//new DataBlock[$totalBlocks]; + $numResultBlocks = 0; + + foreach($eccBlocks as $blockData){ + [$numEccBlocks, $eccPerBlock] = $blockData; + + for($i = 0; $i < $numEccBlocks; $i++, $numResultBlocks++){ + $result[$numResultBlocks] = [$eccPerBlock, array_fill(0, $numEccCodewords + $eccPerBlock, 0)]; + } + } + + // All blocks have the same amount of data, except that the last n + // (where n may be 0) have 1 more byte. Figure out where these start. + $shorterBlocksTotalCodewords = count($result[0][1]); + $longerBlocksStartAt = count($result) - 1; + + while($longerBlocksStartAt >= 0){ + $numCodewords = count($result[$longerBlocksStartAt][1]); + + if($numCodewords == $shorterBlocksTotalCodewords){ + break; + } + + $longerBlocksStartAt--; + } + + $longerBlocksStartAt++; + + $shorterBlocksNumDataCodewords = $shorterBlocksTotalCodewords - $numEccCodewords; + // The last elements of result may be 1 element longer; + // first fill out as many elements as all of them have + $rawCodewordsOffset = 0; + + for($i = 0; $i < $shorterBlocksNumDataCodewords; $i++){ + for($j = 0; $j < $numResultBlocks; $j++){ + $result[$j][1][$i] = $rawCodewords[$rawCodewordsOffset++]; + } + } + + // Fill out the last data block in the longer ones + for($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++){ + $result[$j][1][$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++]; + } + + // Now add in error correction blocks + $max = count($result[0][1]); + + for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){ + for($j = 0; $j < $numResultBlocks; $j++){ + $iOffset = $j < $longerBlocksStartAt ? $i : $i + 1; + $result[$j][1][$iOffset] = $rawCodewords[$rawCodewordsOffset++]; + } + } + + return $result; + } + + /** + *

Given data and error-correction codewords received, possibly corrupted by errors, attempts to + * correct the errors in-place using Reed-Solomon error correction.

+ */ + private function correctErrors(array $codewordBytes, int $numDataCodewords):array{ + // First read into an array of ints + $codewordsInts = []; + + foreach($codewordBytes as $i => $codewordByte){ + $codewordsInts[$i] = $codewordByte & 0xFF; + } + + $decoded = (new ReedSolomonDecoder)->decode($codewordsInts, (count($codewordBytes) - $numDataCodewords)); + + // Copy back into array of bytes -- only need to worry about the bytes that were data + // We don't care about errors in the error-correction codewords + for($i = 0; $i < $numDataCodewords; $i++){ + $codewordBytes[$i] = $decoded[$i]; + } + + return $codewordBytes; + } + + /** + * @throws \RuntimeException + */ + private function decodeBitStream(array $bytes, Version $version, EccLevel $ecLevel):DecoderResult{ + $bits = new BitBuffer($bytes); + $symbolSequence = -1; + $parityData = -1; + $versionNumber = $version->getVersionNumber(); + + $result = ''; + $eciCharset = null; +# $fc1InEffect = false; + + // While still another segment to read... + while($bits->available() >= 4){ + $datamode = $bits->read(4); // mode is encoded by 4 bits + + // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here + if($datamode === Mode::DATA_TERMINATOR){ + break; + } + + if($datamode === Mode::DATA_ECI){ + // Count doesn't apply to ECI + $value = ECI::parseValue($bits); + $eciCharset = new ECICharset($value); + } + /** @noinspection PhpStatementHasEmptyBodyInspection */ + elseif($datamode === Mode::DATA_FNC1_FIRST || $datamode === Mode::DATA_FNC1_SECOND){ + // We do little with FNC1 except alter the parsed result a bit according to the spec +# $fc1InEffect = true; + } + elseif($datamode === Mode::DATA_STRCTURED_APPEND){ + if($bits->available() < 16){ + throw new RuntimeException('structured append: not enough bits left'); + } + // sequence number and parity is added later to the result metadata + // Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue + $symbolSequence = $bits->read(8); + $parityData = $bits->read(8); + } + else{ + // First handle Hanzi mode which does not start with character count +/* if($datamode === Mode::DATA_HANZI){ + //chinese mode contains a sub set indicator right after mode indicator + $subset = $bits->read(4); + $length = $bits->read(Mode::getLengthBitsForVersion($datamode, $versionNumber)); + if($subset === self::GB2312_SUBSET){ + $result .= $this->decodeHanziSegment($bits, $length); + } + }*/ +# else{ + // "Normal" QR code modes: + if($datamode === Mode::DATA_NUMBER){ + $result .= Number::decodeSegment($bits, $versionNumber); + } + elseif($datamode === Mode::DATA_ALPHANUM){ + $str = AlphaNum::decodeSegment($bits, $versionNumber); + + // See section 6.4.8.1, 6.4.8.2 +/* if($fc1InEffect){ + $start = \strlen($str); + // We need to massage the result a bit if in an FNC1 mode: + for($i = $start; $i < $start; $i++){ + if($str[$i] === '%'){ + if($i < $start - 1 && $str[$i + 1] === '%'){ + // %% is rendered as % + $str = \substr_replace($str, '', $i + 1, 1);//deleteCharAt(i + 1); + } +# else{ + // In alpha mode, % should be converted to FNC1 separator 0x1D @todo +# $str = setCharAt($i, \chr(0x1D)); // ??? +# } + } + } + } +*/ + $result .= $str; + } + elseif($datamode === Mode::DATA_BYTE){ + $str = Byte::decodeSegment($bits, $versionNumber); + + if($eciCharset !== null){ + $encoding = $eciCharset->getName(); + + if($encoding === null){ + // The spec isn't clear on this mode; see + // section 6.4.5: t does not say which encoding to assuming + // upon decoding. I have seen ISO-8859-1 used as well as + // Shift_JIS -- without anything like an ECI designator to + // give a hint. + $encoding = mb_detect_encoding($str, ['ISO-8859-1', 'SJIS', 'UTF-8']); + } + + $eciCharset = null; + $str = mb_convert_encoding($str, $encoding); + } + + $result .= $str; + } + elseif($datamode === Mode::DATA_KANJI){ + $result .= Kanji::decodeSegment($bits, $versionNumber); + } + else{ + throw new RuntimeException('invalid data mode'); + } +# } + } + } + + return new DecoderResult($bytes, $result, $version, $ecLevel, $symbolSequence, $parityData); + } + +} diff --git a/src/Decoder/DecoderResult.php b/src/Decoder/DecoderResult.php new file mode 100644 index 000000000..fb61802e1 --- /dev/null +++ b/src/Decoder/DecoderResult.php @@ -0,0 +1,86 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use chillerlan\QRCode\Common\{EccLevel, Version}; + +/** + *

Encapsulates the result of decoding a matrix of bits. This typically + * applies to 2D barcode formats. For now it contains the raw bytes obtained, + * as well as a String interpretation of those bytes, if applicable.

+ * + * @author Sean Owen + */ +final class DecoderResult{ + + private array $rawBytes; + private string $text; + private Version $version; + private EccLevel $eccLevel; + private int $structuredAppendParity; + private int $structuredAppendSequenceNumber; + + public function __construct( + array $rawBytes, + string $text, + Version $version, + EccLevel $eccLevel, + int $saSequence = -1, + int $saParity = -1 + ){ + $this->rawBytes = $rawBytes; + $this->text = $text; + $this->version = $version; + $this->eccLevel = $eccLevel; + $this->structuredAppendParity = $saParity; + $this->structuredAppendSequenceNumber = $saSequence; + } + + /** + * @return int[] raw bytes encoded by the barcode, if applicable, otherwise {@code null} + */ + public function getRawBytes():array{ + return $this->rawBytes; + } + + /** + * @return string raw text encoded by the barcode + */ + public function getText():string{ + return $this->text; + } + + public function __toString():string{ + return $this->text; + } + + public function getVersion():Version{ + return $this->version; + } + + public function getEccLevel():EccLevel{ + return $this->eccLevel; + } + + public function hasStructuredAppend():bool{ + return $this->structuredAppendParity >= 0 && $this->structuredAppendSequenceNumber >= 0; + } + + public function getStructuredAppendParity():int{ + return $this->structuredAppendParity; + } + + public function getStructuredAppendSequenceNumber():int{ + return $this->structuredAppendSequenceNumber; + } + +} diff --git a/src/Decoder/GDLuminanceSource.php b/src/Decoder/GDLuminanceSource.php new file mode 100644 index 000000000..710887aae --- /dev/null +++ b/src/Decoder/GDLuminanceSource.php @@ -0,0 +1,71 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Decoder; + +use InvalidArgumentException; +use function get_resource_type, imagecolorat, imagecolorsforindex, imagesx, imagesy, is_resource; +use const PHP_MAJOR_VERSION; + +/** + * This class is used to help decode images from files which arrive as GD Resource + * It does not support rotation. + */ +final class GDLuminanceSource extends LuminanceSource{ + + /** + * @var resource|\GdImage + * @phan-suppress PhanUndeclaredTypeProperty + */ + private $gdImage; + + /** + * GDLuminanceSource constructor. + * + * @param resource|\GdImage $gdImage + * @phan-suppress PhanUndeclaredTypeParameter + * + * @throws \InvalidArgumentException + */ + public function __construct($gdImage){ + + /** + * @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection + * @phan-suppress PhanUndeclaredClassInstanceof + */ + if( + (PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage) + || (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd')) + ){ + throw new InvalidArgumentException('Invalid GD image source.'); + } + + parent::__construct(imagesx($gdImage), imagesy($gdImage)); + + $this->gdImage = $gdImage; + + $this->setLuminancePixels(); + } + + private function setLuminancePixels():void{ + for($j = 0; $j < $this->height; $j++){ + for($i = 0; $i < $this->width; $i++){ + $argb = imagecolorat($this->gdImage, $i, $j); + $pixel = imagecolorsforindex($this->gdImage, $argb); + + $this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']); + } + } + } + +} diff --git a/src/Decoder/IMagickLuminanceSource.php b/src/Decoder/IMagickLuminanceSource.php new file mode 100644 index 000000000..c2f71231c --- /dev/null +++ b/src/Decoder/IMagickLuminanceSource.php @@ -0,0 +1,53 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Decoder; + +use Imagick, InvalidArgumentException; +use function count; + +/** + * This class is used to help decode images from files which arrive as Imagick Resource + * It does not support rotation. + */ +final class IMagickLuminanceSource extends LuminanceSource{ + + private Imagick $imagick; + + /** + * IMagickLuminanceSource constructor. + * + * @param \Imagick $imagick + * + * @throws \InvalidArgumentException + */ + public function __construct(Imagick $imagick){ + parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight()); + + $this->imagick = $imagick; + + $this->setLuminancePixels(); + } + + private function setLuminancePixels():void{ + $this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY); + $pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR); + + $countPixels = count($pixels); + + for($i = 0; $i < $countPixels; $i += 3){ + $this->setLuminancePixel($pixels[$i] & 0xff, $pixels[$i + 1] & 0xff, $pixels[$i + 2] & 0xff); + } + } + +} diff --git a/src/Decoder/LuminanceSource.php b/src/Decoder/LuminanceSource.php new file mode 100644 index 000000000..fba88ab9e --- /dev/null +++ b/src/Decoder/LuminanceSource.php @@ -0,0 +1,103 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use InvalidArgumentException; +use function chillerlan\QRCode\Common\arraycopy; + +/** + * The purpose of this class hierarchy is to abstract different bitmap implementations across + * platforms into a standard interface for requesting greyscale luminance values. The interface + * only provides immutable methods; therefore crop and rotation create copies. This is to ensure + * that one Reader does not modify the original luminance source and leave it in an unknown state + * for other Readers in the chain. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +abstract class LuminanceSource{ + + protected array $luminances; + protected int $width; + protected int $height; + + public function __construct(int $width, int $height){ + $this->width = $width; + $this->height = $height; + // In order to measure pure decoding speed, we convert the entire image to a greyscale array + // up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. + $this->luminances = []; + // @todo: grayscale? + //$this->luminances = $this->grayScaleToBitmap($this->grayscale()); + } + + /** + * Fetches luminance data for the underlying bitmap. Values should be fetched using: + * {@code int luminance = array[y * width + x] & 0xff} + * + * @return array A row-major 2D array of luminance values. Do not use result.length as it may be + * larger than width * height bytes on some platforms. Do not modify the contents + * of the result. + */ + public function getMatrix():array{ + return $this->luminances; + } + + /** + * @return int The width of the bitmap. + */ + public function getWidth():int{ + return $this->width; + } + + /** + * @return int The height of the bitmap. + */ + public function getHeight():int{ + return $this->height; + } + + /** + * Fetches one row of luminance data from the underlying platform's bitmap. Values range from + * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have + * to bitwise and with 0xff for each value. It is preferable for implementations of this method + * to only fetch this row rather than the whole image, since no 2D Readers may be installed and + * getMatrix() may never be called. + * + * @param int $y The row to fetch, which must be in [0,getHeight()) + * + * @return array An array containing the luminance data. + */ + public function getRow(int $y):array{ + + if($y < 0 || $y >= $this->getHeight()){ + throw new InvalidArgumentException('Requested row is outside the image: '.$y); + } + + return arraycopy($this->luminances, $y * $this->width, [], 0, $this->width); + } + + /** + * @param int $r + * @param int $g + * @param int $b + * + * @return void + */ + protected function setLuminancePixel(int $r, int $g, int $b):void{ + $this->luminances[] = $r === $g && $g === $b + // Image is already greyscale, so pick any channel. + ? $r // (($r + 128) % 256) - 128; + // Calculate luminance cheaply, favoring green. + : ($r + 2 * $g + $b) / 4; // (((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; + } + +} diff --git a/src/Detector/AlignmentPattern.php b/src/Detector/AlignmentPattern.php new file mode 100644 index 000000000..1a8ad016a --- /dev/null +++ b/src/Detector/AlignmentPattern.php @@ -0,0 +1,34 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +/** + *

Encapsulates an alignment pattern, which are the smaller square patterns found in + * all but the simplest QR Codes.

+ * + * @author Sean Owen + */ +final class AlignmentPattern extends ResultPoint{ + + /** + * Combines this object's current estimate of a finder pattern position and module size + * with a new estimate. It returns a new {@code FinderPattern} containing an average of the two. + */ + public function combineEstimate(float $i, float $j, float $newModuleSize):AlignmentPattern{ + return new self( + ($this->x + $j) / 2.0, + ($this->y + $i) / 2.0, + ($this->estimatedModuleSize + $newModuleSize) / 2.0 + ); + } + +} diff --git a/src/Detector/AlignmentPatternFinder.php b/src/Detector/AlignmentPatternFinder.php new file mode 100644 index 000000000..fcbb26296 --- /dev/null +++ b/src/Detector/AlignmentPatternFinder.php @@ -0,0 +1,287 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use chillerlan\QRCode\Decoder\BitMatrix; +use function abs, count; + + +/** + *

This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder + * patterns but are smaller and appear at regular intervals throughout the image.

+ * + *

At the moment this only looks for the bottom-right alignment pattern.

+ * + *

This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied, + * pasted and stripped down here for maximum performance but does unfortunately duplicate + * some code.

+ * + *

This class is thread-safe but not reentrant. Each thread must allocate its own object.

+ * + * @author Sean Owen + */ +final class AlignmentPatternFinder{ + + private BitMatrix $bitMatrix; + private float $moduleSize; + /** @var \chillerlan\QRCode\Detector\AlignmentPattern[] */ + private array $possibleCenters; + private array $crossCheckStateCount; + + /** + *

Creates a finder that will look in a portion of the whole image.

+ * + * @param \chillerlan\QRCode\Decoder\BitMatrix $image image to search + * @param float $moduleSize estimated module size so far + */ + public function __construct(BitMatrix $image, float $moduleSize){ + $this->bitMatrix = $image; + $this->moduleSize = $moduleSize; + $this->possibleCenters = []; + $this->crossCheckStateCount = []; + } + + /** + *

This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since + * it's pretty performance-critical and so is written to be fast foremost.

+ * + * @param int $startX left column from which to start searching + * @param int $startY top row from which to start searching + * @param int $width width of region to search + * @param int $height height of region to search + * + * @return \chillerlan\QRCode\Detector\AlignmentPattern|null + */ + public function find(int $startX, int $startY, int $width, int $height):?AlignmentPattern{ + $maxJ = $startX + $width; + $middleI = $startY + ($height / 2); + $stateCount = []; + + // We are looking for black/white/black modules in 1:1:1 ratio; + // this tracks the number of black/white/black modules seen so far + for($iGen = 0; $iGen < $height; $iGen++){ + // Search from middle outwards + $i = (int)($middleI + (($iGen & 0x01) === 0 ? ($iGen + 1) / 2 : -(($iGen + 1) / 2))); + $stateCount[0] = 0; + $stateCount[1] = 0; + $stateCount[2] = 0; + $j = $startX; + // 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)){ + $j++; + } + + $currentState = 0; + + while($j < $maxJ){ + + if($this->bitMatrix->get($j, $i)){ + // Black pixel + if($currentState === 1){ // Counting black pixels + $stateCount[$currentState]++; + } + // Counting white pixels + else{ + // A winner? + if($currentState === 2){ + // Yes + if($this->foundPatternCross($stateCount)){ + $confirmed = $this->handlePossibleCenter($stateCount, $i, $j); + + if($confirmed !== null){ + return $confirmed; + } + } + + $stateCount[0] = $stateCount[2]; + $stateCount[1] = 1; + $stateCount[2] = 0; + $currentState = 1; + } + else{ + $stateCount[++$currentState]++; + } + } + } + // White pixel + else{ + // Counting black pixels + if($currentState === 1){ + $currentState++; + } + + $stateCount[$currentState]++; + } + + $j++; + } + + if($this->foundPatternCross($stateCount)){ + $confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ); + + if($confirmed !== null){ + return $confirmed; + } + } + + } + + // Hmm, nothing we saw was observed and confirmed twice. If we had + // any guess at all, return it. + if(count($this->possibleCenters)){ + return $this->possibleCenters[0]; + } + + return null; + } + + /** + * @param int[] $stateCount count of black/white/black pixels just read + * + * @return bool true if the proportions of the counts is close enough to the 1/1/1 ratios + * used by alignment patterns to be considered a match + */ + private function foundPatternCross(array $stateCount):bool{ + $moduleSize = $this->moduleSize; + $maxVariance = $moduleSize / 2.0; + + for($i = 0; $i < 3; $i++){ + if(abs($moduleSize - $stateCount[$i]) >= $maxVariance){ + return false; + } + } + + return true; + } + + /** + *

This is called when a horizontal scan finds a possible alignment pattern. It will + * cross check with a vertical scan, and if successful, will see if this pattern had been + * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have + * found the alignment pattern.

+ * + * @param int[] $stateCount reading state module counts from horizontal scan + * @param int $i row where alignment pattern may be found + * @param int $j end of possible alignment pattern in row + * + * @return \chillerlan\QRCode\Detector\AlignmentPattern|null if we have found the same pattern twice, or null if not + */ + private function handlePossibleCenter(array $stateCount, int $i, int $j):?AlignmentPattern{ + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2]; + $centerJ = $this->centerFromEnd($stateCount, $j); + $centerI = $this->crossCheckVertical($i, (int)$centerJ, 2 * $stateCount[1], $stateCountTotal); + + if($centerI !== null){ + $estimatedModuleSize = (float)($stateCount[0] + $stateCount[1] + $stateCount[2]) / 3.0; + + foreach($this->possibleCenters as $center){ + // Look for about the same center and module size: + if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){ + return $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize); + } + } + + // Hadn't found this before; save it + $point = new AlignmentPattern($centerJ, $centerI, $estimatedModuleSize); + $this->possibleCenters[] = $point; + } + + return null; + } + + /** + * Given a count of black/white/black pixels just seen and an end position, + * figures the location of the center of this black/white/black run. + * + * @param int[] $stateCount + * @param int $end + * + * @return float + */ + private function centerFromEnd(array $stateCount, int $end):float{ + return (float)(($end - $stateCount[2]) - $stateCount[1] / 2.0); + } + + /** + *

After a horizontal scan finds a potential alignment pattern, this method + * "cross-checks" by scanning down vertically through the center of the possible + * alignment pattern to see if the same proportion is detected.

+ * + * @param int $startI row where an alignment pattern was detected + * @param int $centerJ center of the section that appears to cross an alignment pattern + * @param int $maxCount maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @param int $originalStateCountTotal + * + * @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(); + $stateCount = $this->crossCheckStateCount; + $stateCount[0] = 0; + $stateCount[1] = 0; + $stateCount[2] = 0; + + // Start counting up from center + $i = $startI; + while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){ + $stateCount[1]++; + $i--; + } + // If already too many modules in this state or ran off the edge: + if($i < 0 || $stateCount[1] > $maxCount){ + return null; + } + + while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){ + $stateCount[0]++; + $i--; + } + + if($stateCount[0] > $maxCount){ + return null; + } + + // Now also count down from center + $i = $startI + 1; + while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){ + $stateCount[1]++; + $i++; + } + + if($i == $maxI || $stateCount[1] > $maxCount){ + return null; + } + + while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[2] <= $maxCount){ + $stateCount[2]++; + $i++; + } + + if($stateCount[2] > $maxCount){ + return null; + } + + if(5 * abs(($stateCount[0] + $stateCount[1] + $stateCount[2]) - $originalStateCountTotal) >= 2 * $originalStateCountTotal){ + return null; + } + + if(!$this->foundPatternCross($stateCount)){ + return null; + } + + return $this->centerFromEnd($stateCount, $i); + } + +} diff --git a/src/Detector/Detector.php b/src/Detector/Detector.php new file mode 100644 index 000000000..ae28805b4 --- /dev/null +++ b/src/Detector/Detector.php @@ -0,0 +1,358 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use RuntimeException; +use chillerlan\QRCode\Common\Version; +use chillerlan\QRCode\Decoder\BitMatrix; +use function abs, is_nan, max, min, round; +use function chillerlan\QRCode\Common\distance; +use const NAN; + +/** + *

Encapsulates logic that can detect a QR Code in an image, even if the QR Code + * is rotated or skewed, or partially obscured.

+ * + * @author Sean Owen + */ +final class Detector{ + + private BitMatrix $bitMatrix; + + /** + * Detector constructor. + */ + public function __construct(BitMatrix $image){ + $this->bitMatrix = $image; + } + + /** + *

Detects a QR Code in an image.

+ */ + public function detect():BitMatrix{ + [$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->bitMatrix))->find(); + + $moduleSize = (float)$this->calculateModuleSize($topLeft, $topRight, $bottomLeft); + $dimension = $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize); + $provisionalVersion = new Version((int)(($dimension - 17) / 4)); + $alignmentPattern = null; + + // Anything above version 1 has an alignment pattern + if(!empty($provisionalVersion->getAlignmentPattern())){ + // Guess where a "bottom right" finder pattern would have been + $bottomRightX = $topRight->getX() - $topLeft->getX() + $bottomLeft->getX(); + $bottomRightY = $topRight->getY() - $topLeft->getY() + $bottomLeft->getY(); + + // Estimate that alignment pattern is closer by 3 modules + // from "bottom right" to known top left location + $correctionToTopLeft = 1.0 - 3.0 / (float)($provisionalVersion->getDimension() - 7); + $estAlignmentX = (int)($topLeft->getX() + $correctionToTopLeft * ($bottomRightX - $topLeft->getX())); + $estAlignmentY = (int)($topLeft->getY() + $correctionToTopLeft * ($bottomRightY - $topLeft->getY())); + + // Kind of arbitrary -- expand search radius before giving up + for($i = 4; $i <= 16; $i <<= 1){//?????????? + $alignmentPattern = $this->findAlignmentInRegion($moduleSize, $estAlignmentX, $estAlignmentY, (float)$i); + + if($alignmentPattern !== null){ + break; + } + } + // If we didn't find alignment pattern... well try anyway without it + } + + $transform = $this->createTransform($topLeft, $topRight, $bottomLeft, $dimension, $alignmentPattern); + + return (new GridSampler)->sampleGrid($this->bitMatrix, $dimension, $transform); + } + + /** + *

Computes an average estimated module size based on estimated derived from the positions + * of the three finder patterns.

+ * + * @throws \RuntimeException + */ + private function calculateModuleSize(FinderPattern $topLeft, FinderPattern $topRight, FinderPattern $bottomLeft):float{ + // Take the average + $moduleSize = ( + $this->calculateModuleSizeOneWay($topLeft, $topRight) + + $this->calculateModuleSizeOneWay($topLeft, $bottomLeft) + ) / 2.0; + + if($moduleSize < 1.0){ + throw new RuntimeException('module size < 1.0'); + } + + return $moduleSize; + } + + /** + *

Estimates module size based on two finder patterns -- it uses + * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the + * width of each, measuring along the axis between their centers.

+ */ + private function calculateModuleSizeOneWay(FinderPattern $pattern, FinderPattern $otherPattern):float{ + + $moduleSizeEst1 = $this->sizeOfBlackWhiteBlackRunBothWays( + $pattern->getX(), + $pattern->getY(), + $otherPattern->getX(), + $otherPattern->getY() + ); + + $moduleSizeEst2 = $this->sizeOfBlackWhiteBlackRunBothWays( + $otherPattern->getX(), + $otherPattern->getY(), + $pattern->getX(), + $pattern->getY() + ); + + if(is_nan($moduleSizeEst1)){ + return $moduleSizeEst2 / 7.0; + } + + if(is_nan($moduleSizeEst2)){ + return $moduleSizeEst1 / 7.0; + } + // Average them, and divide by 7 since we've counted the width of 3 black modules, + // and 1 white and 1 black module on either side. Ergo, divide sum by 14. + return ($moduleSizeEst1 + $moduleSizeEst2) / 14.0; + } + + /** + * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of + * a finder pattern by looking for a black-white-black run from the center in the direction + * of another po$(another finder pattern center), and in the opposite direction too.

+ */ + 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(); + // Now count other way -- don't run off image though of course + $scale = 1.0; + $otherToX = $fromX - ($toX - $fromX); + + if($otherToX < 0){ + $scale = $fromX / ($fromX - $otherToX); + $otherToX = 0; + } + elseif($otherToX >= $dimension){ + $scale = ($dimension - 1 - $fromX) / ($otherToX - $fromX); + $otherToX = $dimension - 1; + } + + $otherToY = (int)($fromY - ($toY - $fromY) * $scale); + $scale = 1.0; + + if($otherToY < 0){ + $scale = $fromY / ($fromY - $otherToY); + $otherToY = 0; + } + elseif($otherToY >= $dimension){ + $scale = ($dimension - 1 - $fromY) / ($otherToY - $fromY); + $otherToY = $dimension - 1; + } + + $otherToX = (int)($fromX + ($otherToX - $fromX) * $scale); + $result += $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$otherToX, (int)$otherToY); + + // Middle pixel is double-counted this way; subtract 1 + return $result - 1.0; + } + + /** + *

This method traces a line from a po$in the image, in the direction towards another point. + * It begins in a black region, and keeps going until it finds white, then black, then white again. + * It reports the distance from the start to this point.

+ * + *

This is used when figuring out how wide a finder pattern is, when the finder pattern + * may be skewed or rotated.

+ */ + private function sizeOfBlackWhiteBlackRun(int $fromX, int $fromY, int $toX, int $toY):float{ + // Mild variant of Bresenham's algorithm; + // see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm + $steep = abs($toY - $fromY) > abs($toX - $fromX); + + if($steep){ + $temp = $fromX; + $fromX = $fromY; + $fromY = $temp; + $temp = $toX; + $toX = $toY; + $toY = $temp; + } + + $dx = abs($toX - $fromX); + $dy = abs($toY - $fromY); + $error = -$dx / 2; + $xstep = $fromX < $toX ? 1 : -1; + $ystep = $fromY < $toY ? 1 : -1; + + // In black pixels, looking for white, first or second time. + $state = 0; + // Loop up until x == toX, but not beyond + $xLimit = $toX + $xstep; + + for($x = $fromX, $y = $fromY; $x !== $xLimit; $x += $xstep){ + $realX = $steep ? $y : $x; + $realY = $steep ? $x : $y; + + // 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 === 2){ + return distance($x, $y, $fromX, $fromY); + } + + $state++; + } + + $error += $dy; + + if($error > 0){ + + if($y === $toY){ + break; + } + + $y += $ystep; + $error -= $dx; + } + } + + // Found black-white-black; give the benefit of the doubt that the next pixel outside the image + // is "white" so this last po$at (toX+xStep,toY) is the right ending. This is really a + // small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this. + if($state === 2){ + return distance($toX + $xstep, $toY, $fromX, $fromY); + } + + // else we didn't find even black-white-black; no estimate is really possible + return NAN; + } + + /** + *

Computes the dimension (number of modules on a size) of the QR Code based on the position + * of the finder patterns and estimated module size.

+ * + * @throws \RuntimeException + */ + private function computeDimension( + FinderPattern $topLeft, + FinderPattern $topRight, + FinderPattern $bottomLeft, + float $moduleSize + ):int{ + $tltrCentersDimension = (int)round($topLeft->distance($topRight) / $moduleSize); + $tlblCentersDimension = (int)round($topLeft->distance($bottomLeft) / $moduleSize); + $dimension = (int)((($tltrCentersDimension + $tlblCentersDimension) / 2) + 7); + + switch($dimension % 4){ + case 0: + $dimension++; + break; + // 1? do nothing + case 2: + $dimension--; + break; + case 3: + throw new RuntimeException('estimated dimension: '.$dimension); + } + + if($dimension % 4 !== 1){ + throw new RuntimeException('dimension mod 4 is not 1'); + } + + return $dimension; + } + + /** + *

Attempts to locate an alignment pattern in a limited region of the image, which is + * guessed to contain it.

+ * + * @param float $overallEstModuleSize estimated module size so far + * @param int $estAlignmentX x coordinate of center of area probably containing alignment pattern + * @param int $estAlignmentY y coordinate of above + * @param float $allowanceFactor number of pixels in all directions to search from the center + * + * @return \chillerlan\QRCode\Detector\AlignmentPattern|null if found, or null otherwise + */ + private function findAlignmentInRegion( + float $overallEstModuleSize, + int $estAlignmentX, + int $estAlignmentY, + float $allowanceFactor + ):?AlignmentPattern{ + // Look for an alignment pattern (3 modules in size) around where it should be + $dimension = $this->bitMatrix->getDimension(); + $allowance = (int)($allowanceFactor * $overallEstModuleSize); + $alignmentAreaLeftX = max(0, $estAlignmentX - $allowance); + $alignmentAreaRightX = min($dimension - 1, $estAlignmentX + $allowance); + + if($alignmentAreaRightX - $alignmentAreaLeftX < $overallEstModuleSize * 3){ + return null; + } + + $alignmentAreaTopY = max(0, $estAlignmentY - $allowance); + $alignmentAreaBottomY = min($dimension - 1, $estAlignmentY + $allowance); + + if($alignmentAreaBottomY - $alignmentAreaTopY < $overallEstModuleSize * 3){ + return null; + } + + return (new AlignmentPatternFinder($this->bitMatrix, $overallEstModuleSize))->find( + $alignmentAreaLeftX, + $alignmentAreaTopY, + $alignmentAreaRightX - $alignmentAreaLeftX, + $alignmentAreaBottomY - $alignmentAreaTopY, + ); + } + + /** + * + */ + private function createTransform( + FinderPattern $topLeft, + FinderPattern $topRight, + FinderPattern $bottomLeft, + int $dimension, + AlignmentPattern $alignmentPattern = null + ):PerspectiveTransform{ + $dimMinusThree = (float)$dimension - 3.5; + + if($alignmentPattern instanceof AlignmentPattern){ + $bottomRightX = $alignmentPattern->getX(); + $bottomRightY = $alignmentPattern->getY(); + $sourceBottomRightX = $dimMinusThree - 3.0; + $sourceBottomRightY = $sourceBottomRightX; + } + else{ + // Don't have an alignment pattern, just make up the bottom-right point + $bottomRightX = ($topRight->getX() - $topLeft->getX()) + $bottomLeft->getX(); + $bottomRightY = ($topRight->getY() - $topLeft->getY()) + $bottomLeft->getY(); + $sourceBottomRightX = $dimMinusThree; + $sourceBottomRightY = $dimMinusThree; + } + + return PerspectiveTransform::quadrilateralToQuadrilateral( + 3.5, 3.5, + $dimMinusThree, 3.5, + $sourceBottomRightX, $sourceBottomRightY, + 3.5, $dimMinusThree, + $topLeft->getX(), $topLeft->getY(), + $topRight->getX(), $topRight->getY(), + $bottomRightX, $bottomRightY, + $bottomLeft->getX(), $bottomLeft->getY() + ); + } + +} diff --git a/src/Detector/FinderPattern.php b/src/Detector/FinderPattern.php new file mode 100644 index 000000000..e522d6cfd --- /dev/null +++ b/src/Detector/FinderPattern.php @@ -0,0 +1,69 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use function chillerlan\QRCode\Common\{distance, squaredDistance}; + +/** + *

Encapsulates a finder pattern, which are the three square patterns found in + * the corners of QR Codes. It also encapsulates a count of similar finder patterns, + * as a convenience to the finder's bookkeeping.

+ * + * @author Sean Owen + */ +final class FinderPattern extends ResultPoint{ + + private int $count; + + public function __construct(float $posX, float $posY, float $estimatedModuleSize, int $count = 1){ + parent::__construct($posX, $posY, $estimatedModuleSize); + + $this->count = $count; + } + + public function getCount():int{ + return $this->count; + } + + /** + * @param \chillerlan\QRCode\Detector\FinderPattern $b second pattern + * + * @return float distance between two points + */ + public function distance(FinderPattern $b):float{ + return distance($this->getX(), $this->getY(), $b->getX(), $b->getY()); + } + + /** + * Get square of distance between a and b. + */ + public function squaredDistance(FinderPattern $b):float{ + return squaredDistance($this->getX(), $this->getY(), $b->getX(), $b->getY()); + } + + /** + * Combines this object's current estimate of a finder pattern position and module size + * with a new estimate. It returns a new {@code FinderPattern} containing a weighted average + * based on count. + */ + public function combineEstimate(float $i, float $j, float $newModuleSize):FinderPattern{ + $combinedCount = $this->count + 1; + + return new self( + ($this->count * $this->x + $j) / $combinedCount, + ($this->count * $this->y + $i) / $combinedCount, + ($this->count * $this->estimatedModuleSize + $newModuleSize) / $combinedCount, + $combinedCount + ); + } + +} diff --git a/src/Detector/FinderPatternFinder.php b/src/Detector/FinderPatternFinder.php new file mode 100644 index 000000000..374688931 --- /dev/null +++ b/src/Detector/FinderPatternFinder.php @@ -0,0 +1,775 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + * + * @phan-file-suppress PhanTypePossiblyInvalidDimOffset + */ + +namespace chillerlan\QRCode\Detector; + +use RuntimeException; +use chillerlan\QRCode\Decoder\BitMatrix; +use function abs, count, usort; +use const PHP_FLOAT_MAX; + +/** + *

This class attempts to find finder patterns in a QR Code. Finder patterns are the square + * markers at three corners of a QR Code.

+ * + *

This class is thread-safe but not reentrant. Each thread must allocate its own object. + * + * @author Sean Owen + */ +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; + /** @var \chillerlan\QRCode\Detector\FinderPattern[] */ + private array $possibleCenters; + private bool $hasSkipped = false; + /** @var int[] */ + private array $crossCheckStateCount; + + /** + *

Creates a finder that will search the image for three finder patterns.

+ * + * @param BitMatrix $bitMatrix image to search + */ + public function __construct(BitMatrix $bitMatrix){ + $this->bitMatrix = $bitMatrix; + $this->possibleCenters = []; + $this->crossCheckStateCount = $this->getCrossCheckStateCount(); + } + + /** + * @return \chillerlan\QRCode\Detector\FinderPattern[] + */ + public function find():array{ + $dimension = $this->bitMatrix->getDimension(); + + // 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 + // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the + // image, and then account for the center being 3 modules in size. This gives the smallest + // number of pixels the center could be, so skip this often. + $iSkip = (int)((3 * $dimension) / (4 * self::MAX_MODULES)); + + if($iSkip < self::MIN_SKIP){ + $iSkip = self::MIN_SKIP; + } + + $done = false; + + for($i = $iSkip - 1; $i < $dimension && !$done; $i += $iSkip){ + // Get a row of black/white values + $stateCount = $this->getCrossCheckStateCount(); + $currentState = 0; + + for($j = 0; $j < $dimension; $j++){ + + // Black pixel + if($this->bitMatrix->get($j, $i)){ + // Counting white pixels + if(($currentState & 1) === 1){ + $currentState++; + } + + $stateCount[$currentState]++; + } + // White pixel + else{ + // Counting black pixels + if(($currentState & 1) === 0){ + // A winner? + if($currentState === 4){ + // Yes + if($this->foundPatternCross($stateCount)){ + $confirmed = $this->handlePossibleCenter($stateCount, $i, $j); + + if($confirmed){ + // Start examining every other line. Checking each line turned out to be too + // expensive and didn't improve performance. + $iSkip = 3; + + if($this->hasSkipped){ + $done = $this->haveMultiplyConfirmedCenters(); + } + else{ + $rowSkip = $this->findRowSkip(); + + if($rowSkip > $stateCount[2]){ + // Skip rows between row of lower confirmed center + // and top of presumed third confirmed center + // but back up a bit to get a full chance of detecting + // it, entire width of center of finder pattern + + // Skip by rowSkip, but back off by $stateCount[2] (size of last center + // of pattern we saw) to be conservative, and also back off by iSkip which + // is about to be re-added + $i += $rowSkip - $stateCount[2] - $iSkip; + $j = $dimension - 1; + } + } + } + else{ + $stateCount = $this->doShiftCounts2($stateCount); + $currentState = 3; + + continue; + } + // Clear state to start looking again + $currentState = 0; + $stateCount = $this->getCrossCheckStateCount(); + } + // No, shift counts back by two + else{ + $stateCount = $this->doShiftCounts2($stateCount); + $currentState = 3; + } + } + else{ + $stateCount[++$currentState]++; + } + } + // Counting white pixels + else{ + $stateCount[$currentState]++; + } + } + } + + if($this->foundPatternCross($stateCount)){ + $confirmed = $this->handlePossibleCenter($stateCount, $i, $dimension); + + if($confirmed){ + $iSkip = $stateCount[0]; + + if($this->hasSkipped){ + // Found a third one + $done = $this->haveMultiplyConfirmedCenters(); + } + } + } + } + + return $this->orderBestPatterns($this->selectBestPatterns()); + } + + /** + * @return int[] + */ + private function getCrossCheckStateCount():array{ + return [0, 0, 0, 0, 0]; + } + + /** + * @param int[] $stateCount + * + * @return int[] + */ + private function doShiftCounts2(array $stateCount):array{ + $stateCount[0] = $stateCount[2]; + $stateCount[1] = $stateCount[3]; + $stateCount[2] = $stateCount[4]; + $stateCount[3] = 1; + $stateCount[4] = 0; + + return $stateCount; + } + + /** + * Given a count of black/white/black/white/black pixels just seen and an end position, + * figures the location of the center of this run. + * + * @param int[] $stateCount + * + * @return float + */ + private function centerFromEnd(array $stateCount, int $end):float{ + return (float)(($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2.0); + } + + /** + * @param int[] $stateCount + * + * @return bool + */ + private function foundPatternCross(array $stateCount):bool{ + // Allow less than 50% variance from 1-1-3-1-1 proportions + return $this->foundPatternVariance($stateCount, 2.0); + } + + /** + * @param int[] $stateCount + * + * @return bool + */ + private function foundPatternDiagonal(array $stateCount):bool{ + // Allow less than 75% variance from 1-1-3-1-1 proportions + return $this->foundPatternVariance($stateCount, 1.333); + } + + /** + * @param int[] $stateCount count of black/white/black/white/black pixels just read + * + * @return bool true if the proportions of the counts is close enough to the 1/1/3/1/1 ratios + * used by finder patterns to be considered a match + */ + private function foundPatternVariance(array $stateCount, float $variance):bool{ + $totalModuleSize = 0; + + for($i = 0; $i < 5; $i++){ + $count = $stateCount[$i]; + + if($count === 0){ + return false; + } + + $totalModuleSize += $count; + } + + if($totalModuleSize < 7){ + return false; + } + + $moduleSize = $totalModuleSize / 7.0; + $maxVariance = $moduleSize / $variance; + + return + abs($moduleSize - $stateCount[0]) < $maxVariance + && abs($moduleSize - $stateCount[1]) < $maxVariance + && abs(3.0 * $moduleSize - $stateCount[2]) < 3 * $maxVariance + && abs($moduleSize - $stateCount[3]) < $maxVariance + && abs($moduleSize - $stateCount[4]) < $maxVariance; + } + + /** + * After a vertical and horizontal scan finds a potential finder pattern, this method + * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible + * finder pattern to see if the same proportion is detected. + * + * @param $centerI ; row where a finder pattern was detected + * @param $centerJ ; center of the section that appears to cross a finder pattern + * + * @return bool true if proportions are withing expected limits + */ + private function crossCheckDiagonal(int $centerI, int $centerJ):bool{ + $stateCount = $this->getCrossCheckStateCount(); + + // Start counting up, left from center finding black center mass + $i = 0; + + while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){ + $stateCount[2]++; + $i++; + } + + if($stateCount[2] === 0){ + return false; + } + + // Continue up, left finding white space + while($centerI >= $i && $centerJ >= $i && !$this->bitMatrix->get($centerJ - $i, $centerI - $i)){ + $stateCount[1]++; + $i++; + } + + if($stateCount[1] === 0){ + return false; + } + + // Continue up, left finding black border + while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){ + $stateCount[0]++; + $i++; + } + + if($stateCount[0] === 0){ + return false; + } + + $dimension = $this->bitMatrix->getDimension(); + + // Now also count down, right from center + $i = 1; + while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){ + $stateCount[2]++; + $i++; + } + + while($centerI + $i < $dimension && $centerJ + $i < $dimension && !$this->bitMatrix->get($centerJ + $i, $centerI + $i)){ + $stateCount[3]++; + $i++; + } + + if($stateCount[3] === 0){ + return false; + } + + while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){ + $stateCount[4]++; + $i++; + } + + if($stateCount[4] === 0){ + return false; + } + + return $this->foundPatternDiagonal($stateCount); + } + + /** + *

After a horizontal scan finds a potential finder pattern, this method + * "cross-checks" by scanning down vertically through the center of the possible + * finder pattern to see if the same proportion is detected.

+ * + * @param int $startI ; row where a finder pattern was detected + * @param int $centerJ ; center of the section that appears to cross a finder pattern + * @param int $maxCount ; maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @param int $originalStateCountTotal + * + * @return float|null vertical center of finder pattern, or null if not found + */ + private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{ + $maxI = $this->bitMatrix->getDimension(); + $stateCount = $this->getCrossCheckStateCount(); + + // Start counting up from center + $i = $startI; + while($i >= 0 && $this->bitMatrix->get($centerJ, $i)){ + $stateCount[2]++; + $i--; + } + + if($i < 0){ + return null; + } + + while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){ + $stateCount[1]++; + $i--; + } + + // If already too many modules in this state or ran off the edge: + if($i < 0 || $stateCount[1] > $maxCount){ + return null; + } + + while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){ + $stateCount[0]++; + $i--; + } + + if($stateCount[0] > $maxCount){ + return null; + } + + // Now also count down from center + $i = $startI + 1; + while($i < $maxI && $this->bitMatrix->get($centerJ, $i)){ + $stateCount[2]++; + $i++; + } + + if($i === $maxI){ + return null; + } + + while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[3] < $maxCount){ + $stateCount[3]++; + $i++; + } + + if($i === $maxI || $stateCount[3] >= $maxCount){ + return null; + } + + while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[4] < $maxCount){ + $stateCount[4]++; + $i++; + } + + if($stateCount[4] >= $maxCount){ + return null; + } + + // If we found a finder-pattern-like section, but its size is more than 40% different than + // the original, assume it's a false positive + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]; + + if(5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal){ + return null; + } + + if(!$this->foundPatternCross($stateCount)){ + return null; + } + + return $this->centerFromEnd($stateCount, $i); + } + + /** + *

Like {@link #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.

+ */ + private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{ + $maxJ = $this->bitMatrix->getDimension(); + $stateCount = $this->getCrossCheckStateCount(); + + $j = $startJ; + while($j >= 0 && $this->bitMatrix->get($j, $centerI)){ + $stateCount[2]++; + $j--; + } + + if($j < 0){ + return null; + } + + while($j >= 0 && !$this->bitMatrix->get($j, $centerI) && $stateCount[1] <= $maxCount){ + $stateCount[1]++; + $j--; + } + + if($j < 0 || $stateCount[1] > $maxCount){ + return null; + } + + while($j >= 0 && $this->bitMatrix->get($j, $centerI) && $stateCount[0] <= $maxCount){ + $stateCount[0]++; + $j--; + } + + if($stateCount[0] > $maxCount){ + return null; + } + + $j = $startJ + 1; + while($j < $maxJ && $this->bitMatrix->get($j, $centerI)){ + $stateCount[2]++; + $j++; + } + + if($j === $maxJ){ + return null; + } + + while($j < $maxJ && !$this->bitMatrix->get($j, $centerI) && $stateCount[3] < $maxCount){ + $stateCount[3]++; + $j++; + } + + if($j === $maxJ || $stateCount[3] >= $maxCount){ + return null; + } + + while($j < $maxJ && $this->bitMatrix->get($j, $centerI) && $stateCount[4] < $maxCount){ + $stateCount[4]++; + $j++; + } + + if($stateCount[4] >= $maxCount){ + return null; + } + + // If we found a finder-pattern-like section, but its size is significantly different than + // the original, assume it's a false positive + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]; + + if(5 * abs($stateCountTotal - $originalStateCountTotal) >= $originalStateCountTotal){ + return null; + } + + if(!$this->foundPatternCross($stateCount)){ + return null; + } + + return $this->centerFromEnd($stateCount, $j); + } + + /** + *

This is called when a horizontal scan finds a possible alignment pattern. It will + * cross check with a vertical scan, and if successful, will, ah, cross-cross-check + * with another horizontal scan. This is needed primarily to locate the real horizontal + * center of the pattern in cases of extreme skew. + * And then we cross-cross-cross check with another diagonal scan.

+ * + *

If that succeeds the finder pattern location is added to a list that tracks + * the number of times each location has been nearly-matched as a finder pattern. + * Each additional find is more evidence that the location is in fact a finder + * pattern center + * + * @param int[] $stateCount reading state module counts from horizontal scan + * @param int $i row where finder pattern may be found + * @param int $j end of possible finder pattern in row + * + * @return bool if a finder pattern candidate was found this time + */ + private function handlePossibleCenter(array $stateCount, int $i, int $j):bool{ + $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]; + $centerJ = $this->centerFromEnd($stateCount, $j); + $centerI = $this->crossCheckVertical($i, (int)$centerJ, $stateCount[2], $stateCountTotal); + + if($centerI !== null){ + // Re-cross check + $centerJ = $this->crossCheckHorizontal((int)$centerJ, (int)$centerI, $stateCount[2], $stateCountTotal); + if($centerJ !== null && ($this->crossCheckDiagonal((int)$centerI, (int)$centerJ))){ + $estimatedModuleSize = $stateCountTotal / 7.0; + $found = false; + + for($index = 0; $index < count($this->possibleCenters); $index++){ + $center = $this->possibleCenters[$index]; + // Look for about the same center and module size: + if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){ + $this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize); + $found = true; + break; + } + } + + if(!$found){ + $point = new FinderPattern($centerJ, $centerI, $estimatedModuleSize); + $this->possibleCenters[] = $point; + } + + return true; + } + } + + return false; + } + + /** + * @return int number of rows we could safely skip during scanning, based on the first + * two finder patterns that have been located. In some cases their position will + * allow us to infer that the third pattern must lie below a certain point farther + * down in the image. + */ + private function findRowSkip():int{ + $max = count($this->possibleCenters); + + if($max <= 1){ + return 0; + } + + $firstConfirmedCenter = null; + + foreach($this->possibleCenters as $center){ + + if($center->getCount() >= self::CENTER_QUORUM){ + + if($firstConfirmedCenter === null){ + $firstConfirmedCenter = $center; + } + else{ + // We have two confirmed centers + // How far down can we skip before resuming looking for the next + // pattern? In the worst case, only the difference between the + // difference in the x / y coordinates of the two centers. + // This is the case where you find top left last. + $this->hasSkipped = true; + + return (int)((abs($firstConfirmedCenter->getX() - $center->getX()) - + abs($firstConfirmedCenter->getY() - $center->getY())) / 2); + } + } + } + + return 0; + } + + /** + * @return bool true if we have found at least 3 finder patterns that have been detected + * at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the + * candidates is "pretty similar" + */ + private function haveMultiplyConfirmedCenters():bool{ + $confirmedCount = 0; + $totalModuleSize = 0.0; + $max = count($this->possibleCenters); + + foreach($this->possibleCenters as $pattern){ + if($pattern->getCount() >= self::CENTER_QUORUM){ + $confirmedCount++; + $totalModuleSize += $pattern->getEstimatedModuleSize(); + } + } + + if($confirmedCount < 3){ + return false; + } + // OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive" + // and that we need to keep looking. We detect this by asking if the estimated module sizes + // vary too much. We arbitrarily say that when the total deviation from average exceeds + // 5% of the total module size estimates, it's too much. + $average = $totalModuleSize / (float)$max; + $totalDeviation = 0.0; + + foreach($this->possibleCenters as $pattern){ + $totalDeviation += abs($pattern->getEstimatedModuleSize() - $average); + } + + return $totalDeviation <= 0.05 * $totalModuleSize; + } + + /** + * @return \chillerlan\QRCode\Detector\FinderPattern[] the 3 best {@link FinderPattern}s from our list of candidates. The "best" are + * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module + * size differs from the average among those patterns the least + * @throws \RuntimeException if 3 such finder patterns do not exist + */ + private function selectBestPatterns():array{ + $startSize = count($this->possibleCenters); + + if($startSize < 3){ + throw new RuntimeException('could not find enough finder patterns'); + } + + usort( + $this->possibleCenters, + fn(FinderPattern $a, FinderPattern $b) => $a->getEstimatedModuleSize() <=> $b->getEstimatedModuleSize() + ); + + $distortion = PHP_FLOAT_MAX; + $bestPatterns = []; + + for($i = 0; $i < $startSize - 2; $i++){ + $fpi = $this->possibleCenters[$i]; + $minModuleSize = $fpi->getEstimatedModuleSize(); + + for($j = $i + 1; $j < $startSize - 1; $j++){ + $fpj = $this->possibleCenters[$j]; + $squares0 = $fpi->squaredDistance($fpj); + + for($k = $j + 1; $k < $startSize; $k++){ + $fpk = $this->possibleCenters[$k]; + $maxModuleSize = $fpk->getEstimatedModuleSize(); + + // module size is not similar + if($maxModuleSize > $minModuleSize * 1.4){ + continue; + } + + $a = $squares0; + $b = $fpj->squaredDistance($fpk); + $c = $fpi->squaredDistance($fpk); + + // sorts ascending - inlined + if($a < $b){ + if($b > $c){ + if($a < $c){ + $temp = $b; + $b = $c; + $c = $temp; + } + else{ + $temp = $a; + $a = $c; + $c = $b; + $b = $temp; + } + } + } + else{ + if($b < $c){ + if($a < $c){ + $temp = $a; + $a = $b; + $b = $temp; + } + else{ + $temp = $a; + $a = $b; + $b = $c; + $c = $temp; + } + } + else{ + $temp = $a; + $a = $c; + $c = $temp; + } + } + + // a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle). + // Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0, + // we need to check both two equal sides separately. + // The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity + // from isosceles right triangle. + $d = abs($c - 2 * $b) + abs($c - 2 * $a); + + if($d < $distortion){ + $distortion = $d; + $bestPatterns = [$fpi, $fpj, $fpk]; + } + } + } + } + + if($distortion === PHP_FLOAT_MAX){ + throw new RuntimeException('finder patterns may be too distorted'); + } + + return $bestPatterns; + } + + /** + * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC + * and BC is less than AC, and the angle between BC and BA is less than 180 degrees. + * + * @param \chillerlan\QRCode\Detector\FinderPattern[] $patterns array of three FinderPattern to order + * + * @return \chillerlan\QRCode\Detector\FinderPattern[] + */ + private function orderBestPatterns(array $patterns):array{ + + // Find distances between pattern centers + $zeroOneDistance = $patterns[0]->distance($patterns[1]); + $oneTwoDistance = $patterns[1]->distance($patterns[2]); + $zeroTwoDistance = $patterns[0]->distance($patterns[2]); + + // Assume one closest to other two is B; A and C will just be guesses at first + if($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance){ + [$pointB, $pointA, $pointC] = $patterns; + } + elseif($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance){ + [$pointA, $pointB, $pointC] = $patterns; + } + else{ + [$pointA, $pointC, $pointB] = $patterns; + } + + // Use cross product to figure out whether A and C are correct or flipped. + // This asks whether BC x BA has a positive z component, which is the arrangement + // we want for A, B, C. If it's negative, then we've got it flipped around and + // should swap A and C. + if($this->crossProductZ($pointA, $pointB, $pointC) < 0.0){ + $temp = $pointA; + $pointA = $pointC; + $pointC = $temp; + } + + return [$pointA, $pointB, $pointC]; + } + + /** + * Returns the z component of the cross product between vectors BC and BA. + */ + private function crossProductZ(FinderPattern $pointA, FinderPattern $pointB, FinderPattern $pointC):float{ + $bX = $pointB->getX(); + $bY = $pointB->getY(); + + return (($pointC->getX() - $bX) * ($pointA->getY() - $bY)) - (($pointC->getY() - $bY) * ($pointA->getX() - $bX)); + } + +} diff --git a/src/Detector/GridSampler.php b/src/Detector/GridSampler.php new file mode 100644 index 000000000..7f03c9436 --- /dev/null +++ b/src/Detector/GridSampler.php @@ -0,0 +1,171 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use Exception, RuntimeException; +use chillerlan\QRCode\Decoder\BitMatrix; +use function array_fill, count, sprintf; + +/** + * Implementations of this class can, given locations of finder patterns for a QR code in an + * image, sample the right points in the image to reconstruct the QR code, accounting for + * perspective distortion. It is abstracted since it is relatively expensive and should be allowed + * to take advantage of platform-specific optimized implementations, like Sun's Java Advanced + * Imaging library, but which may not be available in other environments such as J2ME, and vice + * versa. + * + * The implementation used can be controlled by calling {@link #setGridSampler(GridSampler)} + * with an instance of a class which implements this interface. + * + * @author Sean Owen + */ +final class GridSampler{ + + /** + *

Checks a set of points that have been transformed to sample points on an image against + * the image's dimensions to see if the point are even within the image.

+ * + *

This method will actually "nudge" the endpoints back onto the image if they are found to be + * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder + * patterns in an image where the QR Code runs all the way to the image border.

+ * + *

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 + * + * @throws \RuntimeException if an endpoint is lies outside the image boundaries + */ + private function checkAndNudgePoints(BitMatrix $bitMatrix, array $points):void{ + $dimension = $bitMatrix->getDimension(); + $nudged = true; + $max = count($points); + + // Check and nudge points from start until we see some that are OK: + for($offset = 0; $offset < $max && $nudged; $offset += 2){ + $x = (int)$points[$offset]; + $y = (int)$points[$offset + 1]; + + if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){ + throw new RuntimeException(sprintf('checkAndNudgePoints 1, x: %s, y: %s, d: %s', $x, $y, $dimension)); + } + + $nudged = false; + + if($x === -1){ + $points[$offset] = 0.0; + $nudged = true; + } + elseif($x === $dimension){ + $points[$offset] = $dimension - 1; + $nudged = true; + } + if($y === -1){ + $points[$offset + 1] = 0.0; + $nudged = true; + } + elseif($y === $dimension){ + $points[$offset + 1] = $dimension - 1; + $nudged = true; + } + } + // Check and nudge points from end: + $nudged = true; + + for($offset = count($points) - 2; $offset >= 0 && $nudged; $offset -= 2){ + $x = (int)$points[$offset]; + $y = (int)$points[$offset + 1]; + + if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){ + throw new RuntimeException(sprintf('checkAndNudgePoints 2, x: %s, y: %s, d: %s', $x, $y, $dimension)); + } + + $nudged = false; + + if($x === -1){ + $points[$offset] = 0.0; + $nudged = true; + } + elseif($x === $dimension){ + $points[$offset] = $dimension - 1; + $nudged = true; + } + if($y === -1){ + $points[$offset + 1] = 0.0; + $nudged = true; + } + elseif($y === $dimension){ + $points[$offset + 1] = $dimension - 1; + $nudged = true; + } + } + } + + /** + * Samples an image for a rectangular matrix of bits of the given dimension. The sampling + * transformation is determined by the coordinates of 4 points, in the original and transformed + * image space. + * + * @return \chillerlan\QRCode\Decoder\BitMatrix representing a grid of points sampled from the image within a region + * defined by the "from" parameters + * @throws \RuntimeException 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{ + + if($dimension <= 0){ + throw new RuntimeException('invalid matrix size'); + } + + $bits = new BitMatrix($dimension); + $points = array_fill(0, 2 * $dimension, 0.0); + + for($y = 0; $y < $dimension; $y++){ + $max = count($points); + $iValue = (float)$y + 0.5; + + for($x = 0; $x < $max; $x += 2){ + $points[$x] = (float)($x / 2) + 0.5; + $points[$x + 1] = $iValue; + } + + $transform->transformPoints($points); + // Quick check to see if points transformed to something inside the image; + // sufficient to check the endpoints + $this->checkAndNudgePoints($image, $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); + } + } + } + catch(Exception $aioobe){//ArrayIndexOutOfBoundsException + // This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting + // transform gets "twisted" such that it maps a straight line of points to a set of points + // whose endpoints are in bounds, but others are not. There is probably some mathematical + // way to detect this about the transformation that I don't know yet. + // This results in an ugly runtime exception despite our clever checks above -- can't have + // that. We could check each point's coordinates but that feels duplicative. We settle for + // catching and wrapping ArrayIndexOutOfBoundsException. + throw new RuntimeException('ArrayIndexOutOfBoundsException'); + } + + } + + return $bits; + } + +} diff --git a/src/Detector/PerspectiveTransform.php b/src/Detector/PerspectiveTransform.php new file mode 100644 index 000000000..cabce5911 --- /dev/null +++ b/src/Detector/PerspectiveTransform.php @@ -0,0 +1,152 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use function count; + +/** + *

This class implements a perspective transform in two dimensions. Given four source and four + * destination points, it will compute the transformation implied between them. The code is based + * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.

+ * + * @author Sean Owen + */ +final class PerspectiveTransform{ + + private float $a11; + private float $a12; + private float $a13; + private float $a21; + private float $a22; + private float $a23; + private float $a31; + private float $a32; + private float $a33; + + private function __construct( + float $a11, float $a21, float $a31, + float $a12, float $a22, float $a32, + float $a13, float $a23, float $a33 + ){ + $this->a11 = $a11; + $this->a12 = $a12; + $this->a13 = $a13; + $this->a21 = $a21; + $this->a22 = $a22; + $this->a23 = $a23; + $this->a31 = $a31; + $this->a32 = $a32; + $this->a33 = $a33; + } + + public static function quadrilateralToQuadrilateral( + float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3, + float $x0p, float $y0p, float $x1p, float $y1p, float $x2p, float $y2p, float $x3p, float $y3p + ):PerspectiveTransform{ + + $qToS = self::quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3); + $sToQ = self::squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p); + + return $sToQ->times($qToS); + } + + public static function quadrilateralToSquare( + float $x0, float $y0, float $x1, float $y1, + float $x2, float $y2, float $x3, float $y3 + ):PerspectiveTransform{ + // Here, the adjoint serves as the inverse: + return self::squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)->buildAdjoint(); + } + + public function buildAdjoint():PerspectiveTransform{ + // Adjoint is the transpose of the cofactor matrix: + return new self( + $this->a22 * $this->a33 - $this->a23 * $this->a32, + $this->a23 * $this->a31 - $this->a21 * $this->a33, + $this->a21 * $this->a32 - $this->a22 * $this->a31, + $this->a13 * $this->a32 - $this->a12 * $this->a33, + $this->a11 * $this->a33 - $this->a13 * $this->a31, + $this->a12 * $this->a31 - $this->a11 * $this->a32, + $this->a12 * $this->a23 - $this->a13 * $this->a22, + $this->a13 * $this->a21 - $this->a11 * $this->a23, + $this->a11 * $this->a22 - $this->a12 * $this->a21 + ); + } + + public static function squareToQuadrilateral( + float $x0, float $y0, float $x1, float $y1, + float $x2, float $y2, float $x3, float $y3 + ):PerspectiveTransform{ + $dx3 = $x0 - $x1 + $x2 - $x3; + $dy3 = $y0 - $y1 + $y2 - $y3; + + if($dx3 === 0.0 && $dy3 === 0.0){ + // Affine + return new self($x1 - $x0, $x2 - $x1, $x0, $y1 - $y0, $y2 - $y1, $y0, 0.0, 0.0, 1.0); + } + else{ + $dx1 = $x1 - $x2; + $dx2 = $x3 - $x2; + $dy1 = $y1 - $y2; + $dy2 = $y3 - $y2; + $denominator = $dx1 * $dy2 - $dx2 * $dy1; + $a13 = ($dx3 * $dy2 - $dx2 * $dy3) / $denominator; + $a23 = ($dx1 * $dy3 - $dx3 * $dy1) / $denominator; + + return new self( + $x1 - $x0 + $a13 * $x1, $x3 - $x0 + $a23 * $x3, $x0, + $y1 - $y0 + $a13 * $y1, $y3 - $y0 + $a23 * $y3, $y0, + $a13, $a23, 1.0 + ); + } + } + + public function times(PerspectiveTransform $other):PerspectiveTransform{ + return new self( + $this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13, + $this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23, + $this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33, + $this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13, + $this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23, + $this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33, + $this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13, + $this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23, + $this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33 + ); + } + + public function transformPoints(array &$xValues, array &$yValues = null):void{ + $max = count($xValues); + + if($yValues !== null){ + + for($i = 0; $i < $max; $i++){ + $x = $xValues[$i]; + $y = $yValues[$i]; + $denominator = $this->a13 * $x + $this->a23 * $y + $this->a33; + $xValues[$i] = ($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator; + $yValues[$i] = ($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator; + } + + return; + } + + for($i = 0; $i < $max; $i += 2){ + $x = $xValues[$i]; + $y = $xValues[$i + 1]; + $denominator = $this->a13 * $x + $this->a23 * $y + $this->a33; + $xValues[$i] = ($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator; + $xValues[$i + 1] = ($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator; + } + } + +} diff --git a/src/Detector/ResultPoint.php b/src/Detector/ResultPoint.php new file mode 100644 index 000000000..f57dbaf25 --- /dev/null +++ b/src/Detector/ResultPoint.php @@ -0,0 +1,61 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use function abs; + +/** + *

Encapsulates a point of interest in an image containing a barcode. Typically, this + * would be the location of a finder pattern or the corner of the barcode, for example.

+ * + * @author Sean Owen + */ +abstract class ResultPoint{ + + protected float $x; + protected float $y; + protected float $estimatedModuleSize; + + public function __construct(float $x, float $y, float $estimatedModuleSize){ + $this->x = $x; + $this->y = $y; + $this->estimatedModuleSize = $estimatedModuleSize; + } + + public function getX():float{ + return (float)$this->x; + } + + public function getY():float{ + return (float)$this->y; + } + + public function getEstimatedModuleSize():float{ + return $this->estimatedModuleSize; + } + + /** + *

Determines if this finder pattern "about equals" a finder pattern at the stated + * position and size -- meaning, it is at nearly the same center with nearly the same size.

+ */ + public function aboutEquals(float $moduleSize, float $i, float $j):bool{ + + if(abs($i - $this->y) <= $moduleSize && abs($j - $this->x) <= $moduleSize){ + $moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize); + + return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize; + } + + return false; + } + +} diff --git a/src/QRCodeReader.php b/src/QRCodeReader.php new file mode 100644 index 000000000..92e53185f --- /dev/null +++ b/src/QRCodeReader.php @@ -0,0 +1,88 @@ + + * @copyright 2021 Smiley + * @license Apache-2.0 + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode; + +use Imagick, InvalidArgumentException; +use chillerlan\QRCode\Decoder\{Decoder, DecoderResult, GDLuminanceSource, IMagickLuminanceSource}; +use function extension_loaded, file_exists, file_get_contents, imagecreatefromstring, is_file, is_readable; + +final class QRCodeReader{ + + private bool $useImagickIfAvailable; + + public function __construct(bool $useImagickIfAvailable = true){ + $this->useImagickIfAvailable = $useImagickIfAvailable && extension_loaded('imagick'); + } + + /** + * @param \Imagick|\GdImage|resource $im + * + * @return \chillerlan\QRCode\Decoder\DecoderResult + * @phan-suppress PhanUndeclaredTypeParameter (GdImage) + */ + protected function decode($im):DecoderResult{ + + $source = $this->useImagickIfAvailable + ? new IMagickLuminanceSource($im) + : new GDLuminanceSource($im); + + return (new Decoder)->decode($source); + } + + /** + * @param string $imgFilePath + * + * @return \chillerlan\QRCode\Decoder\DecoderResult + */ + public function readFile(string $imgFilePath):DecoderResult{ + + if(!file_exists($imgFilePath) || !is_file($imgFilePath) || !is_readable($imgFilePath)){ + throw new InvalidArgumentException('invalid file: '.$imgFilePath); + } + + $im = $this->useImagickIfAvailable + ? new Imagick($imgFilePath) + : imagecreatefromstring(file_get_contents($imgFilePath)); + + return $this->decode($im); + } + + /** + * @param string $imgBlob + * + * @return \chillerlan\QRCode\Decoder\DecoderResult + */ + public function readBlob(string $imgBlob):DecoderResult{ + + if($this->useImagickIfAvailable){ + $im = new Imagick; + $im->readImageBlob($imgBlob); + } + else{ + $im = imagecreatefromstring($imgBlob); + } + + return $this->decode($im); + } + + /** + * @param \Imagick|\GdImage|resource $imgSource + * + * @return \chillerlan\QRCode\Decoder\DecoderResult + */ + public function readResource($imgSource):DecoderResult{ + return $this->decode($imgSource); + } + +} diff --git a/src/includes.php b/src/includes.php new file mode 100644 index 000000000..0e326fcaa --- /dev/null +++ b/src/includes.php @@ -0,0 +1,16 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode; + +// @codeCoverageIgnoreStart +if(!\defined('QRCODE_DECODER_INCLUDES')){ + require_once __DIR__.'/Common/functions.php'; +} + +// @codeCoverageIgnoreEnd diff --git a/tests/QRCodeReaderTest.php b/tests/QRCodeReaderTest.php new file mode 100644 index 000000000..2fcf06158 --- /dev/null +++ b/tests/QRCodeReaderTest.php @@ -0,0 +1,121 @@ + + * @copyright 2021 Smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCodeTest; + +use Exception; +use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; +use chillerlan\QRCode\{QRCode, QROptions, QRCodeReader}; +use PHPUnit\Framework\TestCase; +use function extension_loaded, range, str_repeat, substr; + +/** + * Tests the QR Code reader + */ +class QRCodeReaderTest extends TestCase{ + + // https://www.bobrosslipsum.com/ + protected const loremipsum = 'Just let this happen. We just let this flow right out of our minds. ' + .'Anyone can paint. We touch the canvas, the canvas takes what it wants. From all of us here, ' + .'I want to wish you happy painting and God bless, my friends. A tree cannot be straight if it has a crooked trunk. ' + .'You have to make almighty decisions when you\'re the creator. I guess that would be considered a UFO. ' + .'A big cotton ball in the sky. I\'m gonna add just a tiny little amount of Prussian Blue. ' + .'They say everything looks better with odd numbers of things. But sometimes I put even numbers—just ' + .'to upset the critics. We\'ll lay all these little funky little things in there. '; + + public function qrCodeProvider():array{ + return [ + 'helloworld' => ['hello_world.png', 'Hello world!'], + // covers mirroring + 'mirrored' => ['hello_world_mirrored.png', 'Hello world!'], + // data modes + 'byte' => ['byte.png', 'https://smiley.codes/qrcode/'], + 'numeric' => ['numeric.png', '123456789012345678901234567890'], + 'alphanum' => ['alphanum.png', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:'], + 'kanji' => ['kanji.png', '茗荷茗荷茗荷茗荷'], + // covers most of ReedSolomonDecoder + 'damaged' => ['damaged.png', 'https://smiley.codes/qrcode/'], + // covers Binarizer::getHistogramBlackMatrix() + 'smol' => ['smol.png', 'https://smiley.codes/qrcode/'], + ]; + } + + /** + * @dataProvider qrCodeProvider + */ + public function testReaderGD(string $img, string $expected):void{ + $reader = new QRCodeReader(false); + + self::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img)); + } + + /** + * @dataProvider qrCodeProvider + */ + public function testReaderImagick(string $img, string $expected):void{ + + if(!extension_loaded('imagick')){ + self::markTestSkipped('imagick not installed'); + } + + $reader = new QRCodeReader(true); + + self::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img)); + } + + public function dataTestProvider():array{ + $data = []; + $str = str_repeat(self::loremipsum, 5); + + foreach(range(1, 40) as $v){ + $version = new Version($v); + + foreach(EccLevel::MODES as $ecc => $_){ + $eccLevel = new EccLevel($ecc); + + $data['version: '.$version->getVersionNumber().$eccLevel->__toString()] = [ + $version, + $eccLevel, + substr($str, 0, $version->getMaxLengthForMode(Mode::DATA_BYTE, $eccLevel)) + ]; + } + } + + return $data; + } + + /** + * @dataProvider dataTestProvider + */ + public function testReadData(Version $version, EccLevel $ecc, string $expected):void{ + $options = new QROptions; +# $options->imageTransparent = false; + $options->eccLevel = $ecc->getLevel(); + $options->version = $version->getVersionNumber(); + $options->imageBase64 = false; + $options->scale = 1; // what's interesting is that a smaller scale seems to produce less errors??? + + $imagedata = (new QRCode($options))->render($expected); + + try{ + $result = (new QRCodeReader(true))->readBlob($imagedata); + } + catch(Exception $e){ + self::markTestSkipped($version->getVersionNumber().$ecc->__toString().': '.$e->getMessage()); + } + + self::assertSame($expected, $result->getText()); + self::assertSame($version->getVersionNumber(), $result->getVersion()->getVersionNumber()); + self::assertSame($ecc->getLevel(), $result->getEccLevel()->getLevel()); + } + +} diff --git a/tests/qrcodes/alphanum.png b/tests/qrcodes/alphanum.png new file mode 100644 index 0000000000000000000000000000000000000000..fdcf4f289da99534c5e8f7b7e6b971921ca01cf6 GIT binary patch literal 383 zcmV-_0f7FAP)4R?8B?FbE7z|NoccAn&3I>Ms}6M~ zP#{DsiY?}Wo;JIrFqYO;=A-|{3t8x59_YUE2z{p zJy>is)F(?L=Ni~hp}kGp(36yntdjB=L5t-HE>d=aL7v0Nm zS)1G4h&2OI56$clUSyBi$!^F$^K#j{bYIhuN+=XUKP7#Vx0!3lP$mn$WG9*APZk*d zE9CL6uHCy!2Uo1AHnW&G=-l(}x5^FivSXe7dNf8wyx+n3s0vJx^VdnU+t^Kng;VLT zKd^SjeoqPYnDh5(DH%)aqcxDjoX{6kuKc2S=yojnik?*}tKjq1pfNY;nfCH)39CeQ z`9g%OIqVjcz1_4o(PT%T76dOQD3@*9La@B}tk%YKYX#ot%>fO4nrHv%vIHZNwb4Z_ zG;(&FiecUK$U4<(ESe-3D1;Ajr6(w#P8nYxc?--d3ICdp~i|md;2>D zNf!UQSl_+z#J~^?xMw;3NA7@<9)5mo1!+LOri|8GSJCCyECCFoDNBtNpAa&)8*s-b z7$zFq(Qq6c#l#&?*Ma%Y2y&ms%%J|yIY?KG4=n{VAdAW<<2aUfE$#P#a>fJ_mYK7% zBsZN1h-FCX5F#o((Io$E%n>FFAta1pL|<*8Xdg=`R19CTDy?iS0bqta;tqLmUwDdC z(M51X_g*}qV+?;Rj+HR^18Q|ja@Nv%Z>Hl*yBKLP^%?eOi%bChA1*M)bW2F^0JECu z=5_8%X;s?*?E&wu7r+!Dc`3ahm|@ zQs~~Z8Gi2+=~ak-_v{(V!~1TCFUVFPt2~0_f=7}|>jrDNBkHhY*Ds@_B@yE)3yX#3r z)e&9Jk+|*@VUCnGeS94IYGx7)^Q{%+9`p6wnf=;|NX+SKS!;&tT5i>sY?&O{g>nCd zJgjnspEQVzX$z7x1y}(5wsem$zo%^XqZ`w@%sw=c(yE20!O>UgfNJs!dQmuj!#N%5 z({es>FiNUl_;Hd$(T4egw!Yq2JY0LvoDZIq&N+x0NrH`9@>~s}6$(5#S*>Txh_*fJ zHx$~Dai&S~nsk`EUSorC*bh)Flch{6sE=?r`QpLP-c?ZpZqKfT)nvOF_ZAqw5&6W< z1k21a1f?CqW6n`TVyExpUP@W-#=!)xUe4s@jG>5cD%Ml$f}B$|{doEcbIIWLAEs2t^$SEg9Et+;$R*bJb5))`&;UT{h| zPCUS+H-R#duW06wm?-tp%o3^3*2^wJl)-~>pmPlxu243d|q?Jg`Uw|T<*EplA zQn_s*J~%BlY|}u3QM;w<&Xe+HOS1)6Y^K5ou^4GD4V4e04m}+4NYiXg6tXR{D|7dp zow|8mi*U3!FTFzTqUjUe;m`~o%Ba7OMTQKd#lCB&+;W8jJR|@B*J?VqXvQR-9lrlM zqVO^o+4r|H_J@Crv>Cy%T|zye-yr>p!7n+Q`eFMb)Z$%l4K6bt*4>j^kHOYl!wuw> zFr+b0S%(Q~*;r)zInVYS^|CeM=#sq+e_9StMJH8!rlvii({o_a+PYl94RL(UqgLjg zo%Oyb9@YiK6zW5<1iOvQ@mF|A)4S)3Vn&7@)ifc-h;aB7swiVc zU8-3|9L37_JwdlVQuZ?{bKt>ktOIWADv)bwFY3Ef&L_V5t@}NFaW!Di?FhDD;vq5C zPyrYLG^wZ)prQNgZ+$D(v~vSriCf5N+NzGvQa78x#;G9arEF9vwlx;3yp!FEj4NIH zT^J02^h;xb$y@xTCCuac0?UJJ)0wIt$scUhk~V>TeL=`ce%C3fdoP2a1jLEOZ{>); z8}B+9i6g%iFlQcgM3RZziH4(DUEM><>w*6}lANc*NMR89b~a)jVd0eWzeqY0=MLEJ mfh0<{3#iM##>PKGWL^KXm$ua&n7w*%fdYJjyvbgtFZ~5)FM?D6 literal 0 HcmV?d00001 diff --git a/tests/qrcodes/damaged.png b/tests/qrcodes/damaged.png new file mode 100644 index 0000000000000000000000000000000000000000..feeb7443efeee44979a090d86a7e49aaa7c4ee39 GIT binary patch literal 8646 zcmZvCcQjm4*S-itMrWc$v`i2Y(FvkNi54w-?=5<7A&gFRK|~LNsG~&2=)y!~i0Dx! zdh|}T@8tb{>s#+1zq>GZnRCy*ckg}nex7HaSWWe(WKc#Z9v&WG^j?~=HgPRA&lJ3>gNS(OB) zK=HU)=3xs=A&eP%kZz1s7-sf-5f6(>nR2w0@SrwUhCecc8&cnkJrjpVGm&3K*93+d z`REM5?-~Zq$qqdsE60@`2Mrw03N{!$2HnPtd;$Ob|9=gxDK6vl+Z%u&EQ8FJmY{J+Orc(vKPtb9KBci^ z@v%$LvAn6y<-u0jH0{@o??(93|FILWPL_Sb3}H7fhR!Xga$12Xc$#CsHEx?~TJQ^b z$G7JBmK}ddJES3HgtJKHy7(OBvZ1I4t}D_(=#RS?OhHRmm^tYzxXr22hzq7t@`G8d zJVaov)@Lbh^zYJM;ieKUEP7Dy;=8+BSx(Fu1k>Ul3jO3 z&R}V&V}?qJS-S2z8bm;0(A%ch^P?EWM+C?Pi zwPG3ToIf))?57L#oxp1>}(U>J_4(}$9*1-jYjQ% zM{BxpwWCgv83NgApQ}Z41{I67s`OM#v0`1ZI4Q^H}*V7tv*L6icuO@jbYL_Un^Qyj)uoNiR6fJ-Bh?o8xIVSIgp`p z_t^-r(!f&qpqF@VD!fGdp02J()Rc3Rx(W#zb;}q^()1u|0(tb&tY(2ph@Mojn2NJ_ z(m9F-a_RVX%vnTNw@QaQMo2K5lpSG*bZnl^8RX#C9T7^V2+0{)BQTuQ+kBp9i<}Gu zMjZDs6=i%~#AU?uMz2yKgx>kq#Pgc*7mY^XSzyr9V5v0B`;j%Px)1B0XI$Nc7=h*V zD%p5C2*F9O6_r9tKod)V@8O?Otkjvar{~@*I!G#?LK&(B!}WIGy#B!bRac%cBblx) z?lN=0RzZuB$oa&va+E!joF!ZQEYZHE{qpbO`gq@$FCjY!mX1tng<;p`=}li1#f~gC z7;9RT*Ygpf3F&7e7DuZ+5uGoD7zgb}Ecmk2I3n_a19Wvo>?FF8gWEydkY$thkc-Xc zIgy;f&(#vS>IAXCwRXrbGhiQu-J=c~D!dO-Dvum~IyE|r+)6O49DUMyFRp0Nu3I?{ zW>jbJ>sQ6*_XpOdt(#}x^JY+f*fsB|P2Z$GYvklR^|C=bFEOT6cxM*w&c<^IY`-2A zh7^K;D-YVyWG4bEi{w0Jrm&rYDGd9dO?WFSzUn!J?y70QlKKRNgkaFfU#`tOAcI}o z3O&2x`sGW{Z90w%DzB(;WK3XDin^2GZ-<;T;ng=c&mUNNVxU>2M-wYwsA+Mu@h~p{ zJ)6d<0l~kP)OTl~zSk;DZY%`4x;`ZIOjnn$bcRzfTP+nnjM&AKrL~(&v`m-a3>KI2=xf74PbH>=_v^@oJ>nQ?yyM`1J-Ff=;VCRuep< zDq1dsnsGweTB9niS0E;w8vmZ`E+k3)b@1`YC`Ss@d!V)+7=|LD4p zuC5k6tDqepJt^dRIwSFI@NsvvKkpF}xqncMkogXFJGX4EcV(ujz9;YNNYT&N*cXkA zduCO7BoI>g9TcG}+q?HL-UOx=hXXuqms;DYGD#4CV5#3!el`M)#8;Qo)vX-m9_1H) z239(4JfUpKq;r!|l^E~6Mn&5XPK^~6PL-qe)0_1?ci8&i)7z2*EBhMxQoW7_T(D%1 z4v+G6@d|v+R5q`8ll%2Ayu~|S74m78>ljtNfh$YpW7iME6TfA={E93oDJj#)|7=pd zw6rA8uEm|f_!RR{gm)J341m>-b7u{Ww}#>1D#{>i-|*yP!Lo_(r6z?CK_E7Uvy^CG0V)So ztzlkAyV-L;Tz^x0|4P%d)~+Kklq5VEV?!XzMH`Q?`LTVBunA`+B_q2n9rCM%JUxMR zQYOo=T!%oGFI!Eo@+L=>UgcftSOXpradF02`CzI18P^r0CryD(_$R1`c<(kgqpmeo zQZDfT*Bu?s%Jc9c7K^1IBST*tO`Fx|Yo!OG6A{#pj7`UNbrrbch&yZqCY?q8f?Syk zj=a3f^L^~qUS_*Nm0s!aRfE@S#p^z>uSA7yPorF}kD4jxg;|bQo6->AK+33f%22Vj zu)srm^jonGPdSSWED2n0f+WZ@0B-fhHmPsHUCednb7O6-`@)ykXUE&v!9jk^C1>}% z&%8NZg`djJ8{slK)2p1$#(7Qg`lb11=)gnN+y!q1ez^|x$7QfoQlDxn0hx7)wg+ma z)56`~-`~at&P09|ER{28Cy@O;S3*=+_|Q{bm(IUn zvG4i4Q=8gTZ|B^ailss0$^-=k2L=Z0kdHvZPQ#p@1}JE8@nmIXi3OkR>@NKCG;^$c zc&7zwdsMwXvovVsdl09e^)2UX)C=wSYp;(Ko*;S>jXak&wpAIU|O3E|M}2C|k0 zbcY~c!me(NVzC?gaID2^g*}D%QzSw|E8Vl@9d^aCNV{C8(J0b9%s1zVoSzS}!a+qG zjNct&lZN3nud5${pB%Z5ytBPL$UW$d+fh-@qNt$`u!` z80{_o%uzu(b!wo^9U0N&t1k|{clhvQvkBfjEg@0RgISvfkb)GnR7$i(avo$3w3w)m z2q~x97=pE2QSrn{vvM@~0#q)Xd%TsF^WZ9&f^6vZKc2}%pyDZ~#@N&XQ$!7n^dqgv3BfaRT zC7Y(i@loSJKy}bmu0JwfJD(RqhF0xyyJVMWx%H6Fy`x6l9)wbnv+3402Hku}! zJKRo_rgCQk?_v6n;m)LV9^PpwZE|}qs;z8fn;)VL^}%i}_iMm(RGr)5a)UWYS<-%D zOb&fT$F~AG8%nKu%6SHM)`$J%#v;||5-6!iLT;UNGnML)yRlprR3q|8;#eiA#9WY* zLm}5HCymix)ICc4GN-8Tp$^|#e(1OnOd8$&xtcCsA(_-rPpAYM&ZGcl#oIsI>TL^2e0g>Xr^+$3L__zcdYRkakb1EXow$SDcY?hoois7j6?mU9r@656^$((UL4n&8^bgX$>IV^L8-r0AK_E`Fw z5gQu|5^c{*l%P)KXpAT*U`QLj>adS=hO9s08k5Ki!^(H^=h0#I-c<4_^u_PFpj?r= z#*4{Rgu5#EVXp?380qPEPJ_v#7ZlzXe?#8x?nDsH`7gY73q6|#G{vd$Ns(Oa^-m65 zcU}7ZhjB_oOh!B9hmh*Ww^MvkxywTxluhOl-=E6gT#Uw@jrv%G<-?VqCNjk;79$V{ zvzp^x`@~kp{qWBx@}|EgMltzA&LaD#!R0y?dX=l%Q4GaaR4soFP9b%4A6}V91X5vR z7}za1|85V*&q>8W>J5g{>7&+%dI1#qx>K&>WNT{+@;0aRDW?2+nWCa%a`rT%ZkGKt zRuIt%>af)5A`NA~Rb2;L@qxH);p*-tv$z!dY{c#z211dhf_>&U2`brq(H8Vk9m6-> zg}Q@()9dQqWNeI#j0D9*?Tu2|$io@a&Xr|yRv2`q#mhb*U=q}HN+nyI`+F-Zlu5Yf zd-tiG8%$mVd7l#D%2`set2McnU4KdMMkE6&<^kzam4Mr;>XEPC zJC*}LX~6SFr9`C!|0+q~q~MN7e~(q&+S=N~wv*q(#ZSR)OfmhELBgy9dl~oj&4MCzjw7dCN zkX}bYYk6g*tgLK%7A+wm5r2W1={h>RajY)?6UF(b=qFL%5}2QZlk=riK3AO1xud>Z zhj-^Wt9-}j1rYHjlB5)+S1nUuq1_Js#Ap)qE|t%kUW)xAwziWkes}T3ix(6uQo&aj z$J=x5Eb+Z&zp?f~P50e@_rwBTJdpYD`1p9Cp~&Ntj&ekh84s=SGsGPQ49Q-12Pr!P zd|XI^j{jaT&&Nt`+ITL`ti$|RXu~>~~&rAUa>86n07|yd3^4hR8!WY&zUXhWRP+iZe5rIF$dvU)41yhx71UuJF! z_n(`-){zbX?Dkr1qmO6XFAr;;CNUpwk@H~#6Xl}GUv3A@-C|(@$qH~z*yx9t?#42x zomBo)_h_E$CcLDX1Q-h}7g9sQ0r%x5G!ifiE?E5}Pl6$GQXV?EehAoHQXj(d!w03y z%j5R^+}xQch=JU8&x4>2>GgA0CQ1LD%=Yu8i3xfqY(pnKA^}{?D3?4Kj!d$l7f)wx z+C2@PXK1^AU6IVa>+hFM6W(aYFkoB22`@N!Gb;h-hd~dIk0qEAmEhs8nKr2#s()c- zQp+}RS1LrJmGP4cS0V1m$!|-ZKoig|2SxdfFljn?;m9X!87fQr0q`%B^lu04o zFa68&66)gVSSq3+90@8$eO`@&CaEvwxr-1ZdGsA7@~6eafZAwsMclY!%&3hyyKZ(3 zaMjV#Q3D>mD!uaXh3-ENw1vXRpdQ?RHdt`=vhS2N|N7?!D&>?UVFZuv-8H@(g58r}G;CTibMXa4V;NDAYID_UtA=_)A^_6aM)eve=*Uyi7f^ z+wz;$+StqGzz)|Qa4aq^mZ)a7n$?s#0;-h?UF2b9mOs8_bLhVLQi?^pv_bKD9jD%C z(KY|>UHzJIASp>Q=jY_)+6o_6(}| z)4wB23Es5w*eY)mn%BFq3uG|$vR(dcwI2eCIyWsl3P!_NQ|fEzR|vv^dX+g4iQC8e zAlnYB+#2n7g8aO)>`{sKLlCK#f4yyO@8)Cappl*}Y8MTftJapVcYy#ubBn3W5843; z0G4W%o~Jw0)B_r&(>%)y%a?zdLem`0c0qH2xf9{eEyc-1)e7cprz0*1Vq{0TH) ziQjre`>JZI6_nZtWCPYZR!s*bM9pmN`s0Un=%;){&A;rbA$4k;`td_sk{Qsc`1p9r z0-)w-M-4Z~zr86_|5BE+php8_ah5c6tbMk0}xMxb)D}R=%l^A;J*of+NB622x@r$^&oL|QTG@*tZglK7LOUueK z-@MT-hZ$k`-M+1Pi!+h01~~9u6V|ax)j^wKmpp+>Dl02nOsZ>Y+}z!VwvV?lFB85Y zGr^1)o4xplZCUyG8f7DmMuW~GK-f{zHRYv)*C=g`#K={I>R0Nf?!u{WRE~nz0ja%p zdhZ0qKfBQr|Ep>1d$lC+?Kc~NX1BSJoq5?p%~yfLPK}`aINV5~M$kn4r5+6WAw+i3 z%*Ak*`i6WtaQm8fpuJrGtU|Ff)(p~OvIXd`MxLY=tmI_@U$6j|{`7QqKLPNvf)?w7 zp`oF2*$Ci)6LxCw=`@Qx7X|>Oe0_bLoVfF`2;&0AsP8Q^uIb&%I>dsn5>t(;AQ*^$ z)T#gT=g&zBsP+(e=Z>7a>;66;tzuReEw9 zw2zuV>0euOOv8ACYI;l5btC%BZsO8*#Ppb7;)Yn$L2LWPrrT}ur#!T2|KwPT^rGxB z%>!!{2JyEJpYHP*a%aT#Ez}t}J36k}OP@`i?ynS*|8do6P~EyY4g6E;IP4<tOuP z9ndeerc%QW^|FyQ?Vhc8iIttLxTSmGPNN3;+H`B+ulxZ{LEp(V2sG z!vzo(w&A3dcgdzeZHlSjpT;NaI1OfYjMpx2sI47BP2zt;>eBi*Z zitg_RHR(9>KfXNRkWUmM;?7l<4nCp0kIu43dv-K0qK~St2dFkiXmWY)En;aS)t96daXd9RHjEh9H@$oMn0yYJM*2te*J1gXFCHyJMsw*XccoOUqpx6 z;=5Be7=BKCNU;_dRw`W==f39teTccWzJBlWYpDL+uWKGD%X7P=s9>oJ% z*;lbGd8@8Za7Kx6|LRB*qlg zL~7Q~GvJ-TUet5sv4gb{xEGhI1Sf@budc3w5z)CB(_^Z`sGa!D$^EeR?K7@@m+S@t zcx}=NwA!|-Lp=%j)HXn^FB@TiM*4c4@#g19+yEVcSo!mi(9qEKx$WrwXn8v>Udsla z2Q-f6v|-&kx}zy6RLZF*Ap+PuI)?1Qi;D{!XlXzR5wfs!hrwG$N(BiDxcZkpeD9EU z$d>y0ALHY6q>$tH6ZrvSgsG8F3R6;2WLPQ5qqTU-6g-IWBMu+sruPr|++GtR&HieK zWC=OJflAXpV5y4Vd*wD*;z%Wh-O&_y&X}^nCZ? z?AW~5nQH&zv0241eXKkXgCO~&Bhilcg`m$GM&sYm{CMXJ6->h{K4p2CuEuft23QKw z+1V**|DBYA;?5z(LPVk}<9lK&;c?kpYt!Qt-vC!t zSXh!2i$mJ;h3e9i3I?f@EgI4i5*Vbo<2MSzd)APTw z0iX4!!X0$&IfOjBwk;5UAy!uQ?Xp{)ft|hm_OBLXQ`48R_<$o)P=H1&^#MGIo4VVj zJft-4(~}y*Th2gpHPVoL^ZxGy#bU0w?Y6V6D&yuX(Dt$C6B6*2*=Vm6({# zF`6!0=x2Jwq3k-|2Z))~=Z(P}QB-|kWl zU)j7ZwaotIm9r?$;M8zhHO*|8-f?zayz`%myB&IUcIVC=1Bxmxs4xevNqZ`R`hu9; zfsp*qs+eURPxRo;edjwNC*3T}AbVsE6jCPI!N2OsJca}?JZLRkv(4&DY;CWo5Xm76 zf@dj$cLmKVY~Ev3YwNo~yEzbDZ=|A&Si(h+8{Hk<(i(LUc^&8vr zq+oSaMFl7v`}_NVH02Ch9Xp}TILU`e1&tO-Lh}p$f~as0DzV2 z1RksoZ)|LU+Aoz~qr*-7pb%O6@%q_~Iw5j0b(ozI53Lr=xJs{hxPQqLQ0=+PlRoLT zhVj-CX-mKAbGx9UZ?8e)WzzX*L__-J`N=NmMtofo;LfPE{9Ii;Gw#&*wLZZx0Cf6t zZDM5jo2f{;N=r*!=h_C1441Q6iNmYcfxH_Ka0*iT$jFGn%?HHz2bU9~F-Yo`8*lj{ zrSRVVjeY#Fh>P*{mtZMrAj9tVv1~`P0SG|Ho?XP1|MF-OcpUIG&>I0bt(6xPnO%RS z`y{r$=H@IBG-*9+D`_hi*6q|#0^Bi5lev|lUGggumcxN+oR-vQDy&{_%b0huYFyZtsjHZoT3&7
whdBsYR^A^M-ZUEFd0_R(VW== zG=xbi*x%b5hIr|&*Cm#cmGCFyeQNLR_amT@*I%Eor0N`Z^6Q)ePQtAA;0FV-(edJ*!?NL1fS`f$qtwy$s?qo%u6OU97=Ox)LP5vkYH4M&OTeEQ25sk# ze|?%2>8sPN8B}i2dal?ddOErFITd7ogF#JA?N{4N)O|PU{e*xq8c#l~h?nYw&%e$+ z6A>Rh*aZL@`2V+|;osSRZ)o`6{{H`c{onf?tTX4Y@Kn=L$mr$m5U@W3Pgy}-zWRwp G`2PS;r}*6f literal 0 HcmV?d00001 diff --git a/tests/qrcodes/hello_world.png b/tests/qrcodes/hello_world.png new file mode 100644 index 0000000000000000000000000000000000000000..3578ea0cc1bd88805bad82da46582cb3af13c163 GIT binary patch literal 3121 zcmY*c4P27v76-}KFEBH#gqqZ~i{@^+q0+1n%DXUII5q8VCe&JH%g^Z~l@BAE+A8|E z(>@edDn=%*+tL(;EX=vgLY*s96E#IviTDACa36HLADkcW^YEPaJ@0wW`JexDCk4(FO zhR}D?=xTe2@0&1n#&8K};_Fe<>w#sua%_!`05l!x*zquQK59~@(}~KJl3&V3W7q{Z zjcvZt@qw+?x>FXUO`c~;5TmUFQXP*lUzF)f&S*?3BJVNo^0^(4;`wQFo+()fAK4kv z|2!1f8km1*_?gzd>?O(JYQMZqGIXP9(n(^>JPRI{p>CtDxgSfrpq=qS$ zn3`{z`-XyK`W_;6XSf}82*bZ<>C@F3%IB9gwR0c`&Y2Wx?pn(XH&o!?|2mj=0#O0= zU{_5h>n@NIq}Ds^cA=_?e!~N=@hm0tiUsNFT#SiLX0*!MX~r^bpvB#DwO>G8znssx zBA5ytN;DIwn;1t@8iCcoVD*KTUQ;|jMc(x)3$fjt$X4P{*BU&C`Od|Tm4YuS%@aMA zxmD`SS#!YvGEEQt%H7&0#E=!eqcQXD2cdgc&!v+CxP_wWyoKMFC>znmltX-=Iwq0R zEl45$<2h4SvseFXiLn>=r@TY|2Hopl+l%{_Xdjc)Dk-5uku1gR!wr@PUnk}U;KUO6 zy==C&(||5wi$PM7c_)bJjg+;)&g5eHXbejc@XET}fg6DrV9}F9uoH#=p;%Hk70DXh z!-2e)QeO)B9(K6L68IjLqFgWzl0cvyBdef1({xHUc-;z z_PVdgd+4cZkrAW>8Sx&!?vM8&#Kyv~;O>=i)A9Uom*!)tm_dx@n@uWA%Ho>)aN5WI z3`{sms7hFP{Us0pCjc`LgiDluA0hsD^qq;%1`BUCO%$GzS$$cIS0evU$#gu77=u>C z5$`OTz#arL`~(}~ob)IIQ9%s9!ntfwk0$Gan!sM79 z8IdGd(hqqPal0gD@A?0$0VygXiCi2vA87;|b!l(CKJ*ARd3f5qboi(JA!`$BCVknr z$WqE*{gyaeEOr*{&BVMTm6F0EvE?&Qx>{L~*(Ymiy!!FGKCr9!+5`*y_MV9Hv!Kf9Qn1 zYqjLzqc?CTj?_#Piij5{McM{kSO-2Qi#@m}n0?>|86%*ueb*n=T$u4yd?n9d4=R!& zL*pz=d>`Y2lJ^a%4S79=cx$xN{Hx(Twri!%i#&8a6{SbInn`)}R|3g~PJN%Dd~`W! zt84=7!b&wCVS2d9yraC|Kd*AKHA^Y!H-K!YKdja5Wxd zy#k7jB0cg$-ped`Jbwaenp+-1yr{|yre7HVtFc*STe%+|4ibvAK>}6wfj0#|;eX8o z#{tki#=UcUy8NWirkr&fWcqU2s&1N2(4xsH^ccXcD{kq=jaW2@%cXSvH?i}3EDw{R z+-vs*AK|A}wT=4#+LNQ$R%#jQlxh=5Y&qH+yaXCS;m_yr)u(~;&jK}$x{ zMGL41ASE)z&SaOtj0g4-EENE79zaekn2a&|TP@X6?k)&PCQ3(iC)cn}WjvSGn?;2@$0%9?823tk(H#=J;YWuVy7@2 z_QIf`O}oW#qJkA7(+S!T;C4jmdwwWt&?|4!2tEhY)FJ*a?m>wu?tTH>iG`fUdijC8 zgXv^k?v%rQw>6k>QxrXQ>;6BD^x_6#H{l|?T_z=*ru7aU?9`|74s(@=3KSX`l}>%s z?mGe~;f)S#6?F~rA@O2;{CS6uyA*LVJ>(6#$n1w!MxYP64M5MKXWxZr!O>gOX(MLO zom5WsmC)OOx_ACL!PFJ~Y zlt$O766OQrOf=13*oD?2Toh>}nJ5i(Nq)tmS_+bI0aR3YEGl{)4N6ip-OJP#H33I- z4ibIo-VuB>XcRcktq*jdJb*{Av6ELR)jKBcunDd2-jzduETxz-W`ns?7Z$9r0$IG$ z-p;R!4j2jP8d%ACAin>`NbB&OUvM&!KfikTWd~(uEz3hW`}N{E>G@%PKlcsO*s`Lj zoeoJi%c49=XvRvjwgb2Bw=*Ier-9cJc-?=Nkr8HoHB^?0;zC}xe5HB^N!^p8UqJ|5 z+8Jn?U6dl;#T(rcnVYv%y{sB8-5mU$3x_;h&ta;(lh?RVCF-nG(!s!I3J^QZAZVz`;SR=ciO6`TU&o-adxf&}s4} zb?=@2Cwvn`F)a%4mi)3&qj~&`AH>hn=mrAyD_J z+?clkab9R3zRduAP5nckiGyIoXC}YYIGXH+KQJGytM`P1zXGt(zwZvI-o`loZ#g?C AivR!s literal 0 HcmV?d00001 diff --git a/tests/qrcodes/hello_world_mirrored.png b/tests/qrcodes/hello_world_mirrored.png new file mode 100644 index 0000000000000000000000000000000000000000..d871f1dbf8da31bf339e3dd4fa710270807aa881 GIT binary patch literal 12319 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 C@0E}M literal 0 HcmV?d00001 diff --git a/tests/qrcodes/kanji.png b/tests/qrcodes/kanji.png new file mode 100644 index 0000000000000000000000000000000000000000..1ac7990b2661796ec8a46d5a26eaa7e01edd069d GIT binary patch literal 383 zcmV-_0f7FAP)t%ynzs9>4UA)WV^@_)oDn@tg`eyC< z>riJ<>1zGSTX_g++?$Khb#yh25b_@EcZgSZF}e@VfY%((9#NCM+JiHA{nG2cUT-c& zH#G0sbAqf|$x^$%d+oXd!K5R*j4pJ&=UF%{JF*?27~RchWJsSwPINT&qD%6g%^;4; z*V|5HDQ9H1`$kBgN>}p)dq(qiyN-0%<_i0+wf%50x|?T)h#KxDw#~c0Q8BtV-Avtd dGj-F=)E5Rv_Gf{2&pH4A002ovPDHLkV1lwswUqz> literal 0 HcmV?d00001 diff --git a/tests/qrcodes/numeric.png b/tests/qrcodes/numeric.png new file mode 100644 index 0000000000000000000000000000000000000000..24b2b88ce2870e362548c353c739684323a932a5 GIT binary patch literal 388 zcmV-~0ek+5P)4RzVKJFbr!E|NnA0Op09FNegHk$N_a*6UTGhF(YE! z9FMyJ{s`lV7jsVC5fO7v{KZF2mdBuHnV+SzTvoppHmk(YLiWx{R>s?UB%vg+jZ=(> zP%*3$8s=STcc`ru-)5BG5rb+L%q#>>CL6QJ^lX>V?Z5Ame(Z)C9@HrcLeTGyghi3X!WrN&8oI+Hq|Z;`MH&3fvz=h1vctQ$t{aF5X(rCP`yGit-r i{}$iGzx?0*1;z_Nk77}(p>JXU00004R?7~lwLsD2>-OagWmt)Sg( z?KtklXb$%3ct~2dwwtWlc*~H;1CXGfC0(cjBNOu~$7P{_kSyQ*EXjwR+?lJ z7 Date: Mon, 25 Jan 2021 01:09:32 +0100 Subject: [PATCH 57/78] :fire_engine: phan happy --- src/Decoder/BitMatrixParser.php | 12 +++++++++--- src/Decoder/Decoder.php | 2 ++ src/Decoder/GDLuminanceSource.php | 8 +++----- src/Decoder/IMagickLuminanceSource.php | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Decoder/BitMatrixParser.php b/src/Decoder/BitMatrixParser.php index 190b7ff82..f56165cca 100644 --- a/src/Decoder/BitMatrixParser.php +++ b/src/Decoder/BitMatrixParser.php @@ -47,7 +47,7 @@ final class BitMatrixParser{ * {@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. + * @param bool $mirror Whether to read version and format information mirrored. */ public function setMirror(bool $mirror):void{ $this->parsedVersion = null; @@ -203,8 +203,8 @@ final class BitMatrixParser{ * @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time * to establish best match * - * @return \chillerlan\QRCode\Common\FormatInformation information about the format it specifies, or {@code null} - * if doesn't seem to match any known pattern + * @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 @@ -298,6 +298,11 @@ final class BitMatrixParser{ 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; @@ -313,6 +318,7 @@ final class BitMatrixParser{ // 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 = numBitsDiffering($versionBits, $targetVersionPattern); if($bitsDifference < $bestDifference){ diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php index f50f43ef1..d38f33818 100644 --- a/src/Decoder/Decoder.php +++ b/src/Decoder/Decoder.php @@ -153,6 +153,7 @@ final class Decoder{ // All blocks have the same amount of data, except that the last n // (where n may be 0) have 1 more byte. Figure out where these start. + /** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */ $shorterBlocksTotalCodewords = count($result[0][1]); $longerBlocksStartAt = count($result) - 1; @@ -185,6 +186,7 @@ final class Decoder{ } // Now add in error correction blocks + /** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */ $max = count($result[0][1]); for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){ diff --git a/src/Decoder/GDLuminanceSource.php b/src/Decoder/GDLuminanceSource.php index 710887aae..787d3cb35 100644 --- a/src/Decoder/GDLuminanceSource.php +++ b/src/Decoder/GDLuminanceSource.php @@ -8,7 +8,7 @@ * @copyright 2021 Smiley * @license Apache-2.0 * - * @noinspection PhpComposerExtensionStubsInspection + * @noinspection PhpComposerExtensionStubsInspection */ namespace chillerlan\QRCode\Decoder; @@ -39,11 +39,9 @@ final class GDLuminanceSource extends LuminanceSource{ */ public function __construct($gdImage){ - /** - * @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection - * @phan-suppress PhanUndeclaredClassInstanceof - */ + /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection, PhpFullyQualifiedNameUsageInspection */ if( + /** @phan-suppress-next-line PhanUndeclaredClassInstanceof */ (PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage) || (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd')) ){ diff --git a/src/Decoder/IMagickLuminanceSource.php b/src/Decoder/IMagickLuminanceSource.php index c2f71231c..15efb6448 100644 --- a/src/Decoder/IMagickLuminanceSource.php +++ b/src/Decoder/IMagickLuminanceSource.php @@ -13,7 +13,7 @@ namespace chillerlan\QRCode\Decoder; -use Imagick, InvalidArgumentException; +use Imagick; use function count; /** From 6b9eea75e6cc2add7168e41040571648c2d47e3c Mon Sep 17 00:00:00 2001 From: codemasher Date: Mon, 25 Jan 2021 01:15:59 +0100 Subject: [PATCH 58/78] :fire_engine: AHHHHHHH --- src/QRCodeReader.php | 1 + tests/QRCodeReaderTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/QRCodeReader.php b/src/QRCodeReader.php index 92e53185f..f0c24d64b 100644 --- a/src/QRCodeReader.php +++ b/src/QRCodeReader.php @@ -78,6 +78,7 @@ final class QRCodeReader{ /** * @param \Imagick|\GdImage|resource $imgSource + * @phan-suppress PhanUndeclaredTypeParameter * * @return \chillerlan\QRCode\Decoder\DecoderResult */ diff --git a/tests/QRCodeReaderTest.php b/tests/QRCodeReaderTest.php index 2fcf06158..c0524da8a 100644 --- a/tests/QRCodeReaderTest.php +++ b/tests/QRCodeReaderTest.php @@ -85,6 +85,7 @@ class QRCodeReaderTest extends TestCase{ $data['version: '.$version->getVersionNumber().$eccLevel->__toString()] = [ $version, $eccLevel, + /** @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal */ substr($str, 0, $version->getMaxLengthForMode(Mode::DATA_BYTE, $eccLevel)) ]; } From 002ea1a64b61ee5e04f314134e804aa1b0d3d57d Mon Sep 17 00:00:00 2001 From: codemasher Date: Mon, 25 Jan 2021 01:25:40 +0100 Subject: [PATCH 59/78] :octocat: i hate this less --- .phan/stubs/misc.php | 11 +++++++++++ src/Decoder/GDLuminanceSource.php | 5 +---- src/Output/QRImage.php | 2 -- src/QRCodeReader.php | 2 -- tests/Output/QRImageTest.php | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 .phan/stubs/misc.php diff --git a/.phan/stubs/misc.php b/.phan/stubs/misc.php new file mode 100644 index 000000000..344626701 --- /dev/null +++ b/.phan/stubs/misc.php @@ -0,0 +1,11 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +class GdImage{} diff --git a/src/Decoder/GDLuminanceSource.php b/src/Decoder/GDLuminanceSource.php index 787d3cb35..e0a787c4c 100644 --- a/src/Decoder/GDLuminanceSource.php +++ b/src/Decoder/GDLuminanceSource.php @@ -25,7 +25,6 @@ final class GDLuminanceSource extends LuminanceSource{ /** * @var resource|\GdImage - * @phan-suppress PhanUndeclaredTypeProperty */ private $gdImage; @@ -33,15 +32,13 @@ final class GDLuminanceSource extends LuminanceSource{ * GDLuminanceSource constructor. * * @param resource|\GdImage $gdImage - * @phan-suppress PhanUndeclaredTypeParameter * * @throws \InvalidArgumentException */ public function __construct($gdImage){ - /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection, PhpFullyQualifiedNameUsageInspection */ + /** @noinspection PhpFullyQualifiedNameUsageInspection */ if( - /** @phan-suppress-next-line PhanUndeclaredClassInstanceof */ (PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage) || (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd')) ){ diff --git a/src/Output/QRImage.php b/src/Output/QRImage.php index 177ad50e5..391235dcf 100644 --- a/src/Output/QRImage.php +++ b/src/Output/QRImage.php @@ -45,8 +45,6 @@ class QRImage extends QROutputAbstract{ * * @see imagecreatetruecolor() * @var resource|\GdImage - * - * @phan-suppress PhanUndeclaredTypeProperty */ protected $image; diff --git a/src/QRCodeReader.php b/src/QRCodeReader.php index f0c24d64b..6a88b9d0a 100644 --- a/src/QRCodeReader.php +++ b/src/QRCodeReader.php @@ -29,7 +29,6 @@ final class QRCodeReader{ * @param \Imagick|\GdImage|resource $im * * @return \chillerlan\QRCode\Decoder\DecoderResult - * @phan-suppress PhanUndeclaredTypeParameter (GdImage) */ protected function decode($im):DecoderResult{ @@ -78,7 +77,6 @@ final class QRCodeReader{ /** * @param \Imagick|\GdImage|resource $imgSource - * @phan-suppress PhanUndeclaredTypeParameter * * @return \chillerlan\QRCode\Decoder\DecoderResult */ diff --git a/tests/Output/QRImageTest.php b/tests/Output/QRImageTest.php index f7e163683..ff6e1bb36 100644 --- a/tests/Output/QRImageTest.php +++ b/tests/Output/QRImageTest.php @@ -70,7 +70,7 @@ class QRImageTest extends QROutputTestAbstract{ } /** - * @phan-suppress PhanUndeclaredClassReference + * */ public function testOutputGetResource():void{ $this->options->returnResource = true; @@ -78,7 +78,7 @@ class QRImageTest extends QROutputTestAbstract{ $actual = $this->outputInterface->dump(); - /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */ + /** @noinspection PhpFullyQualifiedNameUsageInspection */ \PHP_MAJOR_VERSION >= 8 ? $this::assertInstanceOf(\GdImage::class, $actual) : $this::assertIsResource($actual); From b3dfd0f5a2aae07d8a894b9bd48924fc974f916c Mon Sep 17 00:00:00 2001 From: codemasher Date: Fri, 4 Jun 2021 06:57:47 +0200 Subject: [PATCH 60/78] :octocat: ECI::parseValue(): return ECICharset object --- src/Data/ECI.php | 14 +++++--------- src/Decoder/Decoder.php | 5 ++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Data/ECI.php b/src/Data/ECI.php index a335039c3..86ebf7fdb 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -10,7 +10,7 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{BitBuffer, Mode}; +use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode}; /** * Adds an ECI Designator @@ -54,26 +54,22 @@ final class ECI extends QRDataModeAbstract{ /** * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - public static function parseValue(BitBuffer $bitBuffer):int{ + public static function parseValue(BitBuffer $bitBuffer):ECICharset{ $firstByte = $bitBuffer->read(8); if(($firstByte & 0x80) === 0){ // just one byte - return $firstByte & 0x7f; + return new ECICharset($firstByte & 0x7f); } if(($firstByte & 0xc0) === 0x80){ // two bytes - $secondByte = $bitBuffer->read(8); - - return (($firstByte & 0x3f) << 8) | $secondByte; + return new ECICharset((($firstByte & 0x3f) << 8) | $bitBuffer->read(8)); } if(($firstByte & 0xe0) === 0xC0){ // three bytes - $secondThirdBytes = $bitBuffer->read(16); - - return (($firstByte & 0x1f) << 16) | $secondThirdBytes; + return new ECICharset((($firstByte & 0x1f) << 16) | $bitBuffer->read(16)); } throw new QRCodeDataException('error decoding ECI value'); diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php index d38f33818..1a31e724b 100644 --- a/src/Decoder/Decoder.php +++ b/src/Decoder/Decoder.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCode\Decoder; use Exception, InvalidArgumentException, RuntimeException; -use chillerlan\QRCode\Common\{BitBuffer, EccLevel, ECICharset, Mode, ReedSolomonDecoder, Version}; +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, 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; @@ -246,8 +246,7 @@ final class Decoder{ if($datamode === Mode::DATA_ECI){ // Count doesn't apply to ECI - $value = ECI::parseValue($bits); - $eciCharset = new ECICharset($value); + $eciCharset = ECI::parseValue($bits); } /** @noinspection PhpStatementHasEmptyBodyInspection */ elseif($datamode === Mode::DATA_FNC1_FIRST || $datamode === Mode::DATA_FNC1_SECOND){ From d60a4e0f55f6aed534d18193bfd8d0c66a4d9641 Mon Sep 17 00:00:00 2001 From: codemasher Date: Fri, 4 Jun 2021 14:27:00 +0200 Subject: [PATCH 61/78] :octocat: de-static PerspectiveTransform --- src/Detector/Detector.php | 2 +- src/Detector/PerspectiveTransform.php | 61 ++++++++++++++------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/Detector/Detector.php b/src/Detector/Detector.php index ae28805b4..810a98170 100644 --- a/src/Detector/Detector.php +++ b/src/Detector/Detector.php @@ -343,7 +343,7 @@ final class Detector{ $sourceBottomRightY = $dimMinusThree; } - return PerspectiveTransform::quadrilateralToQuadrilateral( + return (new PerspectiveTransform)->quadrilateralToQuadrilateral( 3.5, 3.5, $dimMinusThree, 3.5, $sourceBottomRightX, $sourceBottomRightY, diff --git a/src/Detector/PerspectiveTransform.php b/src/Detector/PerspectiveTransform.php index cabce5911..fb6d2db17 100644 --- a/src/Detector/PerspectiveTransform.php +++ b/src/Detector/PerspectiveTransform.php @@ -32,11 +32,11 @@ final class PerspectiveTransform{ private float $a32; private float $a33; - private function __construct( + private function set( float $a11, float $a21, float $a31, float $a12, float $a22, float $a32, float $a13, float $a23, float $a33 - ){ + ):PerspectiveTransform{ $this->a11 = $a11; $this->a12 = $a12; $this->a13 = $a13; @@ -46,30 +46,32 @@ final class PerspectiveTransform{ $this->a31 = $a31; $this->a32 = $a32; $this->a33 = $a33; + + return $this; } - public static function quadrilateralToQuadrilateral( + public function quadrilateralToQuadrilateral( float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3, float $x0p, float $y0p, float $x1p, float $y1p, float $x2p, float $y2p, float $x3p, float $y3p ):PerspectiveTransform{ - - $qToS = self::quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3); - $sToQ = self::squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p); - - return $sToQ->times($qToS); + return (new self) + ->squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p) + ->times($this->quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)); } - public static function quadrilateralToSquare( + private function quadrilateralToSquare( float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3 ):PerspectiveTransform{ // Here, the adjoint serves as the inverse: - return self::squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)->buildAdjoint(); + return $this + ->squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) + ->buildAdjoint(); } - public function buildAdjoint():PerspectiveTransform{ + private function buildAdjoint():PerspectiveTransform{ // Adjoint is the transpose of the cofactor matrix: - return new self( + return $this->set( $this->a22 * $this->a33 - $this->a23 * $this->a32, $this->a23 * $this->a31 - $this->a21 * $this->a33, $this->a21 * $this->a32 - $this->a22 * $this->a31, @@ -82,7 +84,7 @@ final class PerspectiveTransform{ ); } - public static function squareToQuadrilateral( + private function squareToQuadrilateral( float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3 ):PerspectiveTransform{ @@ -91,27 +93,26 @@ final class PerspectiveTransform{ if($dx3 === 0.0 && $dy3 === 0.0){ // Affine - return new self($x1 - $x0, $x2 - $x1, $x0, $y1 - $y0, $y2 - $y1, $y0, 0.0, 0.0, 1.0); + return $this->set($x1 - $x0, $x2 - $x1, $x0, $y1 - $y0, $y2 - $y1, $y0, 0.0, 0.0, 1.0); } - else{ - $dx1 = $x1 - $x2; - $dx2 = $x3 - $x2; - $dy1 = $y1 - $y2; - $dy2 = $y3 - $y2; - $denominator = $dx1 * $dy2 - $dx2 * $dy1; - $a13 = ($dx3 * $dy2 - $dx2 * $dy3) / $denominator; - $a23 = ($dx1 * $dy3 - $dx3 * $dy1) / $denominator; - return new self( - $x1 - $x0 + $a13 * $x1, $x3 - $x0 + $a23 * $x3, $x0, - $y1 - $y0 + $a13 * $y1, $y3 - $y0 + $a23 * $y3, $y0, - $a13, $a23, 1.0 - ); - } + $dx1 = $x1 - $x2; + $dx2 = $x3 - $x2; + $dy1 = $y1 - $y2; + $dy2 = $y3 - $y2; + $denominator = $dx1 * $dy2 - $dx2 * $dy1; + $a13 = ($dx3 * $dy2 - $dx2 * $dy3) / $denominator; + $a23 = ($dx1 * $dy3 - $dx3 * $dy1) / $denominator; + + return $this->set( + $x1 - $x0 + $a13 * $x1, $x3 - $x0 + $a23 * $x3, $x0, + $y1 - $y0 + $a13 * $y1, $y3 - $y0 + $a23 * $y3, $y0, + $a13, $a23, 1.0 + ); } - public function times(PerspectiveTransform $other):PerspectiveTransform{ - return new self( + private function times(PerspectiveTransform $other):PerspectiveTransform{ + return $this->set( $this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13, $this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23, $this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33, From f4401a4b2b3482bf37209e0a85ee319deed0adc2 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 01:16:21 +0200 Subject: [PATCH 62/78] :bath: cleanup --- src/Decoder/Binarizer.php | 4 +-- src/Decoder/BitMatrix.php | 3 +-- src/Decoder/BitMatrixParser.php | 33 +++++++++++++++++++++---- src/Decoder/LuminanceSource.php | 8 ++++-- src/Detector/AlignmentPatternFinder.php | 5 +--- src/Detector/Detector.php | 9 +++---- src/Detector/FinderPattern.php | 25 +++++++++++++------ src/Detector/FinderPatternFinder.php | 19 ++++++-------- src/Detector/GridSampler.php | 4 +-- src/Detector/ResultPoint.php | 4 +-- 10 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/Decoder/Binarizer.php b/src/Decoder/Binarizer.php index d3c7fdaf3..7173b5add 100644 --- a/src/Decoder/Binarizer.php +++ b/src/Decoder/Binarizer.php @@ -36,8 +36,8 @@ final class Binarizer{ // This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels. // So this is the smallest dimension in each axis we can accept. private const BLOCK_SIZE_POWER = 3; - private const BLOCK_SIZE = 8; // ...0100...00 - private const BLOCK_SIZE_MASK = 7; // ...0011...11 + private const BLOCK_SIZE = 8; // ...0100...00 + private const BLOCK_SIZE_MASK = 7; // ...0011...11 private const MINIMUM_DIMENSION = 40; private const MIN_DYNAMIC_RANGE = 24; diff --git a/src/Decoder/BitMatrix.php b/src/Decoder/BitMatrix.php index a8d41eefa..0f3b902fa 100644 --- a/src/Decoder/BitMatrix.php +++ b/src/Decoder/BitMatrix.php @@ -13,7 +13,6 @@ namespace chillerlan\QRCode\Decoder; use chillerlan\QRCode\Common\{MaskPattern, Version}; use InvalidArgumentException; -use function chillerlan\QRCode\Common\uRShift; use function array_fill, count; final class BitMatrix{ @@ -110,7 +109,7 @@ final class BitMatrix{ $this->bits[$offset] ??= 0; - return (uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0; + return (BitMatrixParser::uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0; } /** diff --git a/src/Decoder/BitMatrixParser.php b/src/Decoder/BitMatrixParser.php index f56165cca..da9f26666 100644 --- a/src/Decoder/BitMatrixParser.php +++ b/src/Decoder/BitMatrixParser.php @@ -13,8 +13,7 @@ namespace chillerlan\QRCode\Decoder; use RuntimeException; use chillerlan\QRCode\Common\{Version, FormatInformation}; -use function chillerlan\QRCode\Common\numBitsDiffering; -use const PHP_INT_MAX; +use const PHP_INT_MAX, PHP_INT_SIZE; /** * @author Sean Owen @@ -219,7 +218,7 @@ final class BitMatrixParser{ return new FormatInformation($maskedBits); } - $bitsDifference = numBitsDiffering($maskedFormatInfo1, $dataBits); + $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $dataBits); if($bitsDifference < $bestDifference){ $bestFormatInfo = $maskedBits; @@ -228,7 +227,7 @@ final class BitMatrixParser{ if($maskedFormatInfo1 !== $maskedFormatInfo2){ // also try the other option - $bitsDifference = numBitsDiffering($maskedFormatInfo2, $dataBits); + $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $dataBits); if($bitsDifference < $bestDifference){ $bestFormatInfo = $maskedBits; @@ -319,7 +318,7 @@ final class BitMatrixParser{ // 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 = numBitsDiffering($versionBits, $targetVersionPattern); + $bitsDifference = self::numBitsDiffering($versionBits, $targetVersionPattern); if($bitsDifference < $bestDifference){ $bestVersion = $i; @@ -336,4 +335,28 @@ final class BitMatrixParser{ return null; } + public static function uRShift(int $a, int $b):int{ + + if($b === 0){ + return $a; + } + + return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)); + } + + private static function numBitsDiffering(int $a, int $b):int{ + // a now has a 1 bit exactly where its bit differs with b's + $a ^= $b; + // Offset i holds the number of 1 bits in the binary representation of i + $BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4]; + // Count bits set quickly with a series of lookups: + $count = 0; + + for($i = 0; $i < 32; $i += 4){ + $count += $BITS_SET_IN_HALF_BYTE[self::uRShift($a, $i) & 0x0F]; + } + + return $count; + } + } diff --git a/src/Decoder/LuminanceSource.php b/src/Decoder/LuminanceSource.php index fba88ab9e..5ed658b97 100644 --- a/src/Decoder/LuminanceSource.php +++ b/src/Decoder/LuminanceSource.php @@ -12,7 +12,7 @@ namespace chillerlan\QRCode\Decoder; use InvalidArgumentException; -use function chillerlan\QRCode\Common\arraycopy; +use function array_slice, array_splice; /** * The purpose of this class hierarchy is to abstract different bitmap implementations across @@ -82,7 +82,11 @@ abstract class LuminanceSource{ throw new InvalidArgumentException('Requested row is outside the image: '.$y); } - return arraycopy($this->luminances, $y * $this->width, [], 0, $this->width); + $arr = []; + + array_splice($arr, 0, $this->width, array_slice($this->luminances, $y * $this->width, $this->width)); + + return $arr; } /** diff --git a/src/Detector/AlignmentPatternFinder.php b/src/Detector/AlignmentPatternFinder.php index fcbb26296..a68e11589 100644 --- a/src/Detector/AlignmentPatternFinder.php +++ b/src/Detector/AlignmentPatternFinder.php @@ -14,7 +14,6 @@ namespace chillerlan\QRCode\Detector; use chillerlan\QRCode\Decoder\BitMatrix; use function abs, count; - /** *

This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder * patterns but are smaller and appear at regular intervals throughout the image.

@@ -35,7 +34,6 @@ final class AlignmentPatternFinder{ private float $moduleSize; /** @var \chillerlan\QRCode\Detector\AlignmentPattern[] */ private array $possibleCenters; - private array $crossCheckStateCount; /** *

Creates a finder that will look in a portion of the whole image.

@@ -47,7 +45,6 @@ final class AlignmentPatternFinder{ $this->bitMatrix = $image; $this->moduleSize = $moduleSize; $this->possibleCenters = []; - $this->crossCheckStateCount = []; } /** @@ -228,7 +225,7 @@ final class AlignmentPatternFinder{ */ private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{ $maxI = $this->bitMatrix->getDimension(); - $stateCount = $this->crossCheckStateCount; + $stateCount = []; $stateCount[0] = 0; $stateCount[1] = 0; $stateCount[2] = 0; diff --git a/src/Detector/Detector.php b/src/Detector/Detector.php index 810a98170..46dba837a 100644 --- a/src/Detector/Detector.php +++ b/src/Detector/Detector.php @@ -15,7 +15,6 @@ use RuntimeException; use chillerlan\QRCode\Common\Version; use chillerlan\QRCode\Decoder\BitMatrix; use function abs, is_nan, max, min, round; -use function chillerlan\QRCode\Common\distance; use const NAN; /** @@ -210,7 +209,7 @@ final class Detector{ if(($state === 1) === $this->bitMatrix->get($realX, $realY)){ if($state === 2){ - return distance($x, $y, $fromX, $fromY); + return FinderPattern::distance($x, $y, $fromX, $fromY); } $state++; @@ -233,7 +232,7 @@ final class Detector{ // is "white" so this last po$at (toX+xStep,toY) is the right ending. This is really a // small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this. if($state === 2){ - return distance($toX + $xstep, $toY, $fromX, $fromY); + return FinderPattern::distance($toX + $xstep, $toY, $fromX, $fromY); } // else we didn't find even black-white-black; no estimate is really possible @@ -252,8 +251,8 @@ final class Detector{ FinderPattern $bottomLeft, float $moduleSize ):int{ - $tltrCentersDimension = (int)round($topLeft->distance($topRight) / $moduleSize); - $tlblCentersDimension = (int)round($topLeft->distance($bottomLeft) / $moduleSize); + $tltrCentersDimension = (int)round($topLeft->getDistance($topRight) / $moduleSize); + $tlblCentersDimension = (int)round($topLeft->getDistance($bottomLeft) / $moduleSize); $dimension = (int)((($tltrCentersDimension + $tlblCentersDimension) / 2) + 7); switch($dimension % 4){ diff --git a/src/Detector/FinderPattern.php b/src/Detector/FinderPattern.php index e522d6cfd..6691b6dae 100644 --- a/src/Detector/FinderPattern.php +++ b/src/Detector/FinderPattern.php @@ -11,7 +11,7 @@ namespace chillerlan\QRCode\Detector; -use function chillerlan\QRCode\Common\{distance, squaredDistance}; +use function sqrt; /** *

Encapsulates a finder pattern, which are the three square patterns found in @@ -24,10 +24,10 @@ final class FinderPattern extends ResultPoint{ private int $count; - public function __construct(float $posX, float $posY, float $estimatedModuleSize, int $count = 1){ + public function __construct(float $posX, float $posY, float $estimatedModuleSize, int $count = null){ parent::__construct($posX, $posY, $estimatedModuleSize); - $this->count = $count; + $this->count = $count ?? 1; } public function getCount():int{ @@ -39,15 +39,15 @@ final class FinderPattern extends ResultPoint{ * * @return float distance between two points */ - public function distance(FinderPattern $b):float{ - return distance($this->getX(), $this->getY(), $b->getX(), $b->getY()); + public function getDistance(FinderPattern $b):float{ + return self::distance($this->x, $this->y, $b->x, $b->y); } /** * Get square of distance between a and b. */ - public function squaredDistance(FinderPattern $b):float{ - return squaredDistance($this->getX(), $this->getY(), $b->getX(), $b->getY()); + public function getSquaredDistance(FinderPattern $b):float{ + return self::squaredDistance($this->x, $this->y, $b->x, $b->y); } /** @@ -66,4 +66,15 @@ final class FinderPattern extends ResultPoint{ ); } + private static function squaredDistance(float $aX, float $aY, float $bX, float $bY):float{ + $xDiff = $aX - $bX; + $yDiff = $aY - $bY; + + return $xDiff * $xDiff + $yDiff * $yDiff; + } + + public static function distance(float $aX, float $aY, float $bX, float $bY):float{ + return sqrt(self::squaredDistance($aX, $aY, $bX, $bY)); + } + } diff --git a/src/Detector/FinderPatternFinder.php b/src/Detector/FinderPatternFinder.php index 374688931..6cd188c96 100644 --- a/src/Detector/FinderPatternFinder.php +++ b/src/Detector/FinderPatternFinder.php @@ -35,8 +35,6 @@ final class FinderPatternFinder{ /** @var \chillerlan\QRCode\Detector\FinderPattern[] */ private array $possibleCenters; private bool $hasSkipped = false; - /** @var int[] */ - private array $crossCheckStateCount; /** *

Creates a finder that will search the image for three finder patterns.

@@ -44,9 +42,8 @@ final class FinderPatternFinder{ * @param BitMatrix $bitMatrix image to search */ public function __construct(BitMatrix $bitMatrix){ - $this->bitMatrix = $bitMatrix; - $this->possibleCenters = []; - $this->crossCheckStateCount = $this->getCrossCheckStateCount(); + $this->bitMatrix = $bitMatrix; + $this->possibleCenters = []; } /** @@ -649,7 +646,7 @@ final class FinderPatternFinder{ for($j = $i + 1; $j < $startSize - 1; $j++){ $fpj = $this->possibleCenters[$j]; - $squares0 = $fpi->squaredDistance($fpj); + $squares0 = $fpi->getSquaredDistance($fpj); for($k = $j + 1; $k < $startSize; $k++){ $fpk = $this->possibleCenters[$k]; @@ -661,8 +658,8 @@ final class FinderPatternFinder{ } $a = $squares0; - $b = $fpj->squaredDistance($fpk); - $c = $fpi->squaredDistance($fpk); + $b = $fpj->getSquaredDistance($fpk); + $c = $fpi->getSquaredDistance($fpk); // sorts ascending - inlined if($a < $b){ @@ -734,9 +731,9 @@ final class FinderPatternFinder{ private function orderBestPatterns(array $patterns):array{ // Find distances between pattern centers - $zeroOneDistance = $patterns[0]->distance($patterns[1]); - $oneTwoDistance = $patterns[1]->distance($patterns[2]); - $zeroTwoDistance = $patterns[0]->distance($patterns[2]); + $zeroOneDistance = $patterns[0]->getDistance($patterns[1]); + $oneTwoDistance = $patterns[1]->getDistance($patterns[2]); + $zeroTwoDistance = $patterns[0]->getDistance($patterns[2]); // Assume one closest to other two is B; A and C will just be guesses at first if($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance){ diff --git a/src/Detector/GridSampler.php b/src/Detector/GridSampler.php index 7f03c9436..2f252c9f9 100644 --- a/src/Detector/GridSampler.php +++ b/src/Detector/GridSampler.php @@ -132,10 +132,10 @@ final class GridSampler{ for($y = 0; $y < $dimension; $y++){ $max = count($points); - $iValue = (float)$y + 0.5; + $iValue = $y + 0.5; for($x = 0; $x < $max; $x += 2){ - $points[$x] = (float)($x / 2) + 0.5; + $points[$x] = ($x / 2) + 0.5; $points[$x + 1] = $iValue; } diff --git a/src/Detector/ResultPoint.php b/src/Detector/ResultPoint.php index f57dbaf25..4d69116b1 100644 --- a/src/Detector/ResultPoint.php +++ b/src/Detector/ResultPoint.php @@ -32,11 +32,11 @@ abstract class ResultPoint{ } public function getX():float{ - return (float)$this->x; + return $this->x; } public function getY():float{ - return (float)$this->y; + return $this->y; } public function getEstimatedModuleSize():float{ From f6e68e097cecb002ddb9b95f8099d81bc5162399 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 01:17:04 +0200 Subject: [PATCH 63/78] :shower: remove extra includes --- composer.json | 5 +--- src/Common/functions.php | 58 ---------------------------------------- src/includes.php | 16 ----------- 3 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 src/Common/functions.php delete mode 100644 src/includes.php diff --git a/composer.json b/composer.json index 311442500..536bdf56f 100644 --- a/composer.json +++ b/composer.json @@ -48,10 +48,7 @@ "autoload": { "psr-4": { "chillerlan\\QRCode\\": "src/" - }, - "files": [ - "src/includes.php" - ] + } }, "autoload-dev": { "psr-4": { diff --git a/src/Common/functions.php b/src/Common/functions.php deleted file mode 100644 index ebfa03094..000000000 --- a/src/Common/functions.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @copyright 2021 Smiley - * @license Apache-2.0 - */ - -namespace chillerlan\QRCode\Common; - -use function array_slice, array_splice, sqrt; -use const PHP_INT_SIZE; - -const QRCODE_DECODER_INCLUDES = true; - -function arraycopy(array $srcArray, int $srcPos, array $destArray, int $destPos, int $length):array{ - array_splice($destArray, $destPos, $length, array_slice($srcArray, $srcPos, $length)); - - return $destArray; -} - -function uRShift(int $a, int $b):int{ - - if($b === 0){ - return $a; - } - - return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)); -} - -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[uRShift($a, $i) & 0x0F]; - } - - return $count; -} - -function squaredDistance(float $aX, float $aY, float $bX, float $bY):float{ - $xDiff = $aX - $bX; - $yDiff = $aY - $bY; - - return $xDiff * $xDiff + $yDiff * $yDiff; -} - -function distance(float $aX, float $aY, float $bX, float $bY):float{ - return sqrt(squaredDistance($aX, $aY, $bX, $bY)); -} - diff --git a/src/includes.php b/src/includes.php deleted file mode 100644 index 0e326fcaa..000000000 --- a/src/includes.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @copyright 2021 smiley - * @license MIT - */ - -namespace chillerlan\QRCode; - -// @codeCoverageIgnoreStart -if(!\defined('QRCODE_DECODER_INCLUDES')){ - require_once __DIR__.'/Common/functions.php'; -} - -// @codeCoverageIgnoreEnd From 6a2bad4e3484ec74104b506bfb6ac6d5064c9138 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 01:17:31 +0200 Subject: [PATCH 64/78] :sparkles: +editorconfig --- .editorconfig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..93a2ee965 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space +indent_size = 2 From 70140f1c2b879b0d3ddd82405e378bbe0afd1c6e Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 01:17:52 +0200 Subject: [PATCH 65/78] :fire: remove phpmd.xml --- phpmd.xml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 phpmd.xml diff --git a/phpmd.xml b/phpmd.xml deleted file mode 100644 index a70f7e3a1..000000000 --- a/phpmd.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - codemasher/php-qrcode PMD ruleset - */examples/* - */tests/* - - - - - 1 - - - - - - - - - - - - - - - - - - - - From 4604627403a3bde5862f8324ead7cae4fdbf37ee Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 01:18:28 +0200 Subject: [PATCH 66/78] :octocat: --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bab593b18..fce953f42 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ namespaced, cleaned up, improved and other stuff. [![CodeCov][coverage-badge]][coverage] [![Scrunitizer CI][scrutinizer-badge]][scrutinizer] [![Packagist downloads][downloads-badge]][downloads]
-[![Continuous Integration][gh-action-badge]][gh-action] +[![Continuous Integration][gh-action-badge]][gh-action] [![phpDocs][gh-docs-badge]][gh-docs] [php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-qrcode?logo=php&color=8892BF [php]: https://www.php.net/supported-versions.php -[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg +[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg?logo=packagist [packagist]: https://packagist.org/packages/chillerlan/php-qrcode [license-badge]: https://img.shields.io/github/license/chillerlan/php-qrcode.svg [license]: https://github.com/chillerlan/php-qrcode/blob/main/LICENSE @@ -25,7 +25,7 @@ namespaced, cleaned up, improved and other stuff. [coverage]: https://codecov.io/github/chillerlan/php-qrcode [scrutinizer-badge]: https://img.shields.io/scrutinizer/g/chillerlan/php-qrcode.svg?logo=scrutinizer [scrutinizer]: https://scrutinizer-ci.com/g/chillerlan/php-qrcode -[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode.svg +[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode.svg?logo=packagist [downloads]: https://packagist.org/packages/chillerlan/php-qrcode/stats [gh-action-badge]: https://github.com/chillerlan/php-qrcode/workflows/Continuous%20Integration/badge.svg [gh-action]: https://github.com/chillerlan/php-qrcode/actions?query=workflow%3A%22Continuous+Integration%22 @@ -34,13 +34,13 @@ namespaced, cleaned up, improved and other stuff. ## Documentation -See [the wiki](https://github.com/chillerlan/php-qrcode/wiki) for advanced documentation. +See [the wiki](https://github.com/chillerlan/php-qrcode/wiki) for advanced documentation. An API documentation created with [phpDocumentor](https://www.phpdoc.org/) can be found at https://chillerlan.github.io/php-qrcode/ (WIP). ### Requirements - PHP 7.4+ - `ext-mbstring` - - optional: + - optional: - `ext-json`, `ext-gd` - `ext-imagick` with [ImageMagick](https://imagemagick.org) installed - [`setasign/fpdf`](https://github.com/setasign/fpdf) for the PDF output module @@ -50,7 +50,7 @@ An API documentation created with [phpDocumentor](https://www.phpdoc.org/) can b via terminal: `composer require chillerlan/php-qrcode` -*composer.json* +*composer.json* ```json { "require": { @@ -61,7 +61,7 @@ via terminal: `composer require chillerlan/php-qrcode` ``` Note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^4.3` - see [releases](https://github.com/chillerlan/php-qrcode/releases) for valid versions. -For PHP version ... +For PHP version ... - 7.4+ use `^4.3` - 7.2+ use `^3.3` - 7.0+ use `^2.0` (PHP 7.0 and 7.1 are EOL!) @@ -87,9 +87,9 @@ Wait, what was that? Please again, slower! See [Advanced usage](https://github.c - Drupal [Google Authenticator Login `ga_login`](https://www.drupal.org/project/ga_login) - WordPress [`wp-two-factor-auth`](https://github.com/sjinks/wp-two-factor-auth) - WordPress [Simple 2FA `simple-2fa`](https://wordpress.org/plugins/simple-2fa/) -- WoltLab Suite [two-step-verification](http://pluginstore.woltlab.com/file/3007-two-step-verification/) +- WoltLab Suite [two-step-verification](http://pluginstore.woltlab.com/file/3007-two-step-verification/) - [Cachet](https://github.com/CachetHQ/Cachet) -- [Appwrite](https://github.com/appwrite/appwrite) +- [Appwrite](https://github.com/appwrite/appwrite) - other uses: [dependents](https://github.com/chillerlan/php-qrcode/network/dependents) / [packages](https://github.com/chillerlan/php-qrcode/network/dependents?dependent_type=PACKAGE) ### Shameless advertising From 33c1e2d88a9830bf8a0756fd5e2cff2be2f70342 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 01:19:59 +0200 Subject: [PATCH 67/78] :octocat: update gh-pages deploy --- .github/workflows/docs.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0b37d0c2d..0eafa4d69 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,9 @@ jobs: uses: ./.github/actions/build-docs - name: "Publish Docs to gh-pages" - uses: maxheld83/ghpages@v0.3.0 - env: - BUILD_DIR: docs/ - GH_PAT: ${{ secrets.GH_PAT }} + uses: JamesIves/github-pages-deploy-action@4.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: dist + CLEAN: true From d1936de3ba6f732243d7c54e88d58fa686fb1911 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 18:19:55 +0200 Subject: [PATCH 68/78] :octocat: change visibility in final classes to private --- src/Common/ECICharset.php | 5 ++++- src/Common/Version.php | 2 +- src/Data/AlphaNum.php | 2 +- src/Data/ECI.php | 2 +- src/Data/MaskPatternTester.php | 10 +++++----- src/Data/Number.php | 2 +- src/Data/QRData.php | 18 +++++++++--------- src/Data/QRMatrix.php | 10 +++++----- src/QRCodeReader.php | 2 +- 9 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/Common/ECICharset.php b/src/Common/ECICharset.php index 9193c0343..e7e45433d 100644 --- a/src/Common/ECICharset.php +++ b/src/Common/ECICharset.php @@ -13,7 +13,10 @@ namespace chillerlan\QRCode\Common; use InvalidArgumentException; use function array_key_exists; -class ECICharset{ +/** + * + */ +final class ECICharset{ public const CP437 = 0; // Code page 437, DOS Latin US public const ISO_IEC_8859_1_GLI = 1; // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1 diff --git a/src/Common/Version.php b/src/Common/Version.php index 1c0ba45d4..bab6c3fe7 100644 --- a/src/Common/Version.php +++ b/src/Common/Version.php @@ -256,7 +256,7 @@ final class Version{ /** * QR Code version number */ - protected int $version; + private int $version; /** * Version constructor. diff --git a/src/Data/AlphaNum.php b/src/Data/AlphaNum.php index 87291d967..409351cbe 100644 --- a/src/Data/AlphaNum.php +++ b/src/Data/AlphaNum.php @@ -27,7 +27,7 @@ final class AlphaNum extends QRDataModeAbstract{ * * @var int[] */ - protected const CHAR_MAP_ALPHANUM = [ + private const CHAR_MAP_ALPHANUM = [ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15, 'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, diff --git a/src/Data/ECI.php b/src/Data/ECI.php index 86ebf7fdb..86fe22e0a 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -24,7 +24,7 @@ final class ECI extends QRDataModeAbstract{ /** * The current ECI encoding id */ - protected int $encoding; + private int $encoding; /** * @inheritDoc diff --git a/src/Data/MaskPatternTester.php b/src/Data/MaskPatternTester.php index 792920b32..942d09244 100644 --- a/src/Data/MaskPatternTester.php +++ b/src/Data/MaskPatternTester.php @@ -27,7 +27,7 @@ final class MaskPatternTester{ /** * The data interface that contains the data matrix to test */ - protected QRData $qrData; + private QRData $qrData; /** * Receives the QRData object @@ -74,7 +74,7 @@ final class MaskPatternTester{ /** * Checks for each group of five or more same-colored modules in a row (or column) */ - protected function testLevel1(array $m, int $size):int{ + private function testLevel1(array $m, int $size):int{ $penalty = 0; foreach($m as $y => $row){ @@ -113,7 +113,7 @@ final class MaskPatternTester{ /** * Checks for each 2x2 area of same-colored modules in the matrix */ - protected function testLevel2(array $m, int $size):int{ + private function testLevel2(array $m, int $size):int{ $penalty = 0; foreach($m as $y => $row){ @@ -144,7 +144,7 @@ final class MaskPatternTester{ /** * Checks if there are patterns that look similar to the finder patterns (1:1:3:1:1 ratio) */ - protected function testLevel3(array $m, int $size):int{ + private function testLevel3(array $m, int $size):int{ $penalties = 0; foreach($m as $y => $row){ @@ -185,7 +185,7 @@ final class MaskPatternTester{ /** * Checks if more than half of the modules are dark or light, with a larger penalty for a larger difference */ - protected function testLevel4(array $m, int $size):float{ + private function testLevel4(array $m, int $size):float{ $count = 0; foreach($m as $y => $row){ diff --git a/src/Data/Number.php b/src/Data/Number.php index d31605826..46fac07a7 100644 --- a/src/Data/Number.php +++ b/src/Data/Number.php @@ -25,7 +25,7 @@ final class Number extends QRDataModeAbstract{ /** * @var int[] */ - protected const CHAR_MAP_NUMBER = [ + private const CHAR_MAP_NUMBER = [ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, ]; diff --git a/src/Data/QRData.php b/src/Data/QRData.php index cf6c1d274..a54f78474 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -26,34 +26,34 @@ final class QRData{ * * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions */ - protected SettingsContainerInterface $options; + private SettingsContainerInterface $options; /** * a BitBuffer instance */ - protected BitBuffer $bitBuffer; + private BitBuffer $bitBuffer; /** * an EccLevel instance */ - protected EccLevel $eccLevel; + private EccLevel $eccLevel; /** * current QR Code version */ - protected Version $version; + private Version $version; /** * @var \chillerlan\QRCode\Data\QRDataModeInterface[] */ - protected array $dataSegments = []; + private array $dataSegments = []; /** * Max bits for the current ECC mode * * @var int[] */ - protected array $maxBitsForEcc; + private array $maxBitsForEcc; /** * QRData constructor. @@ -108,7 +108,7 @@ final class QRData{ * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - protected function estimateTotalBitLength():int{ + private function estimateTotalBitLength():int{ $length = 0; $margin = 0; @@ -142,7 +142,7 @@ final class QRData{ * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - protected function getMinimumVersion():int{ + private function getMinimumVersion():int{ $total = $this->estimateTotalBitLength(); // guess the version number within the given range @@ -162,7 +162,7 @@ final class QRData{ * * @throws \chillerlan\QRCode\QRCodeException on data overflow */ - protected function writeBitBuffer():void{ + private function writeBitBuffer():void{ $version = $this->version->getVersionNumber(); $MAX_BITS = $this->maxBitsForEcc[$version]; diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index 11fd3445e..3fc35675f 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -55,29 +55,29 @@ final class QRMatrix{ /** * the used mask pattern, set via QRMatrix::mask() */ - protected ?MaskPattern $maskPattern = null; + private ?MaskPattern $maskPattern = null; /** * the size (side length) of the matrix, including quiet zone (if created) */ - protected int $moduleCount; + private int $moduleCount; /** * the actual matrix data array * * @var int[][] */ - protected array $matrix; + private array $matrix; /** * the current ECC level */ - protected EccLevel $eccLevel; + private EccLevel $eccLevel; /** * a Version instance */ - protected Version $version; + private Version $version; /** * QRMatrix constructor. diff --git a/src/QRCodeReader.php b/src/QRCodeReader.php index 6a88b9d0a..d510818f9 100644 --- a/src/QRCodeReader.php +++ b/src/QRCodeReader.php @@ -30,7 +30,7 @@ final class QRCodeReader{ * * @return \chillerlan\QRCode\Decoder\DecoderResult */ - protected function decode($im):DecoderResult{ + private function decode($im):DecoderResult{ $source = $this->useImagickIfAvailable ? new IMagickLuminanceSource($im) From 9710a30448621d8bb9821d96c18b01d8c5ad3b65 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 18:25:44 +0200 Subject: [PATCH 69/78] :octocat: moved MaskPatternTester to Common --- src/{Data => Common}/MaskPatternTester.php | 4 ++-- src/QRCode.php | 4 ++-- tests/{Data => Common}/MaskPatternTesterTest.php | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/{Data => Common}/MaskPatternTester.php (98%) rename tests/{Data => Common}/MaskPatternTesterTest.php (86%) diff --git a/src/Data/MaskPatternTester.php b/src/Common/MaskPatternTester.php similarity index 98% rename from src/Data/MaskPatternTester.php rename to src/Common/MaskPatternTester.php index 942d09244..abda352a7 100644 --- a/src/Data/MaskPatternTester.php +++ b/src/Common/MaskPatternTester.php @@ -10,9 +10,9 @@ * @noinspection PhpUnused */ -namespace chillerlan\QRCode\Data; +namespace chillerlan\QRCode\Common; -use chillerlan\QRCode\Common\MaskPattern; +use chillerlan\QRCode\Data\QRData; use function abs, array_search, call_user_func_array, min; /** diff --git a/src/QRCode.php b/src/QRCode.php index 1ab3e4844..6e655450b 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -11,9 +11,9 @@ namespace chillerlan\QRCode; use chillerlan\QRCode\Data\{ - AlphaNum, Byte, ECI, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix + AlphaNum, Byte, ECI, Kanji, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix }; -use chillerlan\QRCode\Common\{ECICharset, MaskPattern, Mode}; +use chillerlan\QRCode\Common\{ECICharset, MaskPattern, MaskPatternTester, Mode}; use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString}; use chillerlan\Settings\SettingsContainerInterface; use function class_exists, in_array, mb_convert_encoding, mb_detect_encoding; diff --git a/tests/Data/MaskPatternTesterTest.php b/tests/Common/MaskPatternTesterTest.php similarity index 86% rename from tests/Data/MaskPatternTesterTest.php rename to tests/Common/MaskPatternTesterTest.php index cf162be38..d08889255 100644 --- a/tests/Data/MaskPatternTesterTest.php +++ b/tests/Common/MaskPatternTesterTest.php @@ -8,11 +8,11 @@ * @license MIT */ -namespace chillerlan\QRCodeTest\Data; +namespace chillerlan\QRCodeTest\Common; -use chillerlan\QRCode\Common\MaskPattern; +use chillerlan\QRCode\Common\{MaskPattern, MaskPatternTester}; +use chillerlan\QRCode\Data\{Byte, QRData}; use chillerlan\QRCode\QROptions; -use chillerlan\QRCode\Data\{Byte, MaskPatternTester, QRData}; use PHPUnit\Framework\TestCase; /** From 3969de33bb3c3de291a110568a8520b306cf822b Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 18:38:58 +0200 Subject: [PATCH 70/78] :shower: --- src/QRCode.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/QRCode.php b/src/QRCode.php index 6e655450b..8501cbe81 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -10,10 +10,8 @@ namespace chillerlan\QRCode; -use chillerlan\QRCode\Data\{ - AlphaNum, Byte, ECI, Kanji, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix -}; use chillerlan\QRCode\Common\{ECICharset, MaskPattern, MaskPatternTester, Mode}; +use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix}; use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString}; use chillerlan\Settings\SettingsContainerInterface; use function class_exists, in_array, mb_convert_encoding, mb_detect_encoding; From 72a8fd6adc310f68d107610f8347cac34496f459 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 20:48:47 +0200 Subject: [PATCH 71/78] :octocat: extract custom output module init to spearate method --- src/QRCode.php | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/QRCode.php b/src/QRCode.php index 8501cbe81..0267bfda2 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -14,7 +14,7 @@ use chillerlan\QRCode\Common\{ECICharset, MaskPattern, MaskPatternTester, Mode}; use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix}; use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString}; use chillerlan\Settings\SettingsContainerInterface; -use function class_exists, in_array, mb_convert_encoding, mb_detect_encoding; +use function class_exists, class_implements, in_array, mb_convert_encoding, mb_detect_encoding; /** * Turns a text string into a Model 2 QR Code @@ -153,7 +153,7 @@ class QRCode{ $matrix = $this->dataInterface->writeMatrix($maskPattern); - if((bool)$this->options->addQuietzone){ + if($this->options->addQuietzone){ $matrix->setQuietZone($this->options->quietzoneSize); } @@ -167,13 +167,13 @@ class QRCode{ */ protected function initOutputInterface():QROutputInterface{ - if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){ - return new $this->options->outputInterface($this->options, $this->getMatrix()); + if($this->options->outputType === $this::OUTPUT_CUSTOM){ + return $this->initCustomOutputInterface(); } foreach($this::OUTPUT_MODES as $outputInterface => $modes){ - if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){ + if(in_array($this->options->outputType, $modes)){ return new $outputInterface($this->options, $this->getMatrix()); } @@ -182,6 +182,24 @@ class QRCode{ throw new QRCodeOutputException('invalid output type'); } + /** + * initializes a custom output module after checking the existence of the class and if it implemnts the required interface + * + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + */ + protected function initCustomOutputInterface():QROutputInterface{ + + if(!class_exists($this->options->outputInterface)){ + throw new QRCodeOutputException('invalid custom output module'); + } + + if(!in_array(QROutputInterface::class, class_implements($this->options->outputInterface))){ + throw new QRCodeOutputException('custom output module does not implement QROutputInterface'); + } + + return new $this->options->outputInterface($this->options, $this->getMatrix()); + } + /** * checks if a string qualifies as numeric (convenience method) */ From f092e868471e04b53ac0ae79751c7c5332b33efe Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 21:18:06 +0200 Subject: [PATCH 72/78] :octocat: will this test now work on CI? --- tests/Output/QROutputTestAbstract.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index c3937ae65..05dfed4e7 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -71,17 +71,14 @@ abstract class QROutputTestAbstract extends TestCase{ */ public function testSaveException():void{ - if(PHP_OS_FAMILY === 'Windows'){ - $this::markTestSkipped('why does this fail on CI??'); - - /** @noinspection PhpUnreachableStatementInspection */ - return; - } +# if(PHP_OS_FAMILY === 'Windows'){ +# $this::markTestSkipped('why does this fail on CI??'); +# } $this->expectException(QRCodeOutputException::class); - $this->expectExceptionMessage('Could not write data to cache file: /foo'); + $this->expectExceptionMessage('Could not write data to cache file: /foo.bar'); - $this->options->cachefile = '/foo'; + $this->options->cachefile = '/foo.bar'; $this->outputInterface = $this->getOutputInterface($this->options); $this->outputInterface->dump(); } From 4f6002ab7b93d03462e25a8152ef743f19ea0db7 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sat, 5 Jun 2021 21:34:00 +0200 Subject: [PATCH 73/78] :octocat: trying a non-existent directory --- tests/Output/QROutputTestAbstract.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index 05dfed4e7..c41f979bb 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -76,9 +76,9 @@ abstract class QROutputTestAbstract extends TestCase{ # } $this->expectException(QRCodeOutputException::class); - $this->expectExceptionMessage('Could not write data to cache file: /foo.bar'); + $this->expectExceptionMessage('Could not write data to cache file: /foo/bar.test'); - $this->options->cachefile = '/foo.bar'; + $this->options->cachefile = '/foo/bar.test'; $this->outputInterface = $this->getOutputInterface($this->options); $this->outputInterface->dump(); } From f294c3a6d6082900d4f12894faf8c99e69f36983 Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 6 Jun 2021 01:12:08 +0200 Subject: [PATCH 74/78] :octocat: +Version::__toString() --- src/Common/Version.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Common/Version.php b/src/Common/Version.php index bab6c3fe7..3efcb44a8 100644 --- a/src/Common/Version.php +++ b/src/Common/Version.php @@ -272,6 +272,13 @@ final class Version{ $this->version = $version; } + /** + * returns the current version number as string + */ + public function __toString():string{ + return (string)$this->version; + } + /** * returns the current version number */ From 5f23962763e0457e6e3e580a75f79780a4565fce Mon Sep 17 00:00:00 2001 From: codemasher Date: Sun, 6 Jun 2021 01:16:22 +0200 Subject: [PATCH 75/78] :shower: test cleanup --- tests/Output/QROutputTestAbstract.php | 5 ----- tests/QRCodeReaderTest.php | 27 +++++++++++++------------- tests/qrcodes/rotated.png | Bin 0 -> 786 bytes tests/qrcodes/tilted.png | Bin 0 -> 28148 bytes 4 files changed, 14 insertions(+), 18 deletions(-) create mode 100644 tests/qrcodes/rotated.png create mode 100644 tests/qrcodes/tilted.png diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index c41f979bb..fa93a6729 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -70,11 +70,6 @@ abstract class QROutputTestAbstract extends TestCase{ * Tests if an exception is thrown when trying to write a cache file to an invalid destination */ public function testSaveException():void{ - -# if(PHP_OS_FAMILY === 'Windows'){ -# $this::markTestSkipped('why does this fail on CI??'); -# } - $this->expectException(QRCodeOutputException::class); $this->expectExceptionMessage('Could not write data to cache file: /foo/bar.test'); diff --git a/tests/QRCodeReaderTest.php b/tests/QRCodeReaderTest.php index c0524da8a..4965d8599 100644 --- a/tests/QRCodeReaderTest.php +++ b/tests/QRCodeReaderTest.php @@ -46,6 +46,8 @@ class QRCodeReaderTest extends TestCase{ 'damaged' => ['damaged.png', 'https://smiley.codes/qrcode/'], // covers Binarizer::getHistogramBlackMatrix() 'smol' => ['smol.png', 'https://smiley.codes/qrcode/'], + 'tilted' => ['tilted.png', 'Hello world!'], // tilted 22° CCW + 'rotated' => ['rotated.png', 'Hello world!'], // rotated 90° CW ]; } @@ -55,7 +57,7 @@ class QRCodeReaderTest extends TestCase{ public function testReaderGD(string $img, string $expected):void{ $reader = new QRCodeReader(false); - self::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img)); + $this::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img)); } /** @@ -64,17 +66,17 @@ class QRCodeReaderTest extends TestCase{ public function testReaderImagick(string $img, string $expected):void{ if(!extension_loaded('imagick')){ - self::markTestSkipped('imagick not installed'); + $this::markTestSkipped('imagick not installed'); } $reader = new QRCodeReader(true); - self::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img)); + $this::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img)); } public function dataTestProvider():array{ $data = []; - $str = str_repeat(self::loremipsum, 5); + $str = str_repeat($this::loremipsum, 5); foreach(range(1, 40) as $v){ $version = new Version($v); @@ -82,7 +84,7 @@ class QRCodeReaderTest extends TestCase{ foreach(EccLevel::MODES as $ecc => $_){ $eccLevel = new EccLevel($ecc); - $data['version: '.$version->getVersionNumber().$eccLevel->__toString()] = [ + $data['version: '.$version.$eccLevel] = [ $version, $eccLevel, /** @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal */ @@ -103,20 +105,19 @@ class QRCodeReaderTest extends TestCase{ $options->eccLevel = $ecc->getLevel(); $options->version = $version->getVersionNumber(); $options->imageBase64 = false; - $options->scale = 1; // what's interesting is that a smaller scale seems to produce less errors??? - - $imagedata = (new QRCode($options))->render($expected); + $options->scale = 1; // what's interesting is that a smaller scale seems to produce fewer reader errors??? try{ - $result = (new QRCodeReader(true))->readBlob($imagedata); + $imagedata = (new QRCode($options))->render($expected); + $result = (new QRCodeReader(true))->readBlob($imagedata); } catch(Exception $e){ - self::markTestSkipped($version->getVersionNumber().$ecc->__toString().': '.$e->getMessage()); + $this::markTestSkipped($version.$ecc.': '.$e->getMessage()); } - self::assertSame($expected, $result->getText()); - self::assertSame($version->getVersionNumber(), $result->getVersion()->getVersionNumber()); - self::assertSame($ecc->getLevel(), $result->getEccLevel()->getLevel()); + $this::assertSame($expected, $result->getText()); + $this::assertSame($version->getVersionNumber(), $result->getVersion()->getVersionNumber()); + $this::assertSame($ecc->getLevel(), $result->getEccLevel()->getLevel()); } } diff --git a/tests/qrcodes/rotated.png b/tests/qrcodes/rotated.png new file mode 100644 index 0000000000000000000000000000000000000000..194a3c22c45a901f2c7798f37929b4222600dd99 GIT binary patch literal 786 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>f$65Fi(^Q|t+#grv)&l+I9%M7 z@;~}pPJ)7glgFm>Kl5j&+&kJY*mwP2^?%0rztt=X0s8vniASHa?QvNr-EN;1d_utP*M}$VE#*6b_ai91n zxBFN1pBI1c+O@CPSFeq1jlr$Qf?v}1cZtGmW&qm$ApM!b$wj{<7d@{}K(R+J%#MBL zKihwAf1CQvKOBK-)1~JM&seO#)NHE1hwi*>+r+iM{%hDHewqvBKad|kto~3p;niQJ zo5c-pey_H(SNZ*X^L+nrb56ijfkL3?+P|6!FTTII`2OVX{o$*s`~>#ydxd60pV++S zkoV#z_U(K3+jrG-mz&??|K8I_^=Z`a?@6iWBUjb`FS+~NCk#2bA7nj!z9js-;T-)( zKT&;^-1=PM?sxB!`I}4TGen8R7lTJ4 z(A44yFYkXYw^z6O{cqR*-y53{x>=aQxz9W9nt$H#@7|D$f3>#Hf1ijF*6XJ2lTZ0o z*SF%8B`d09lO9@nd@nw<`u+JihruDm08KLiYJc}eT%6y3&U4fH_r71hrd!vZOGYu& z&AVRz+Lyl~H;a2-{FJ}Ae}3(Mh}qDz*XWd8T_}ta1fX<=>}|E54Cn3s^zMEB?-4Mq OGkCiCxvX5n#i&@|Qc=y5*m=XK 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 aMw<<#(t>N1v3Q#s)nTophaMiZ4gEihib6O5 literal 0 HcmV?d00001 From 3042f4dcc975c00d64cfbb9de7c9fc19cf9e1fc2 Mon Sep 17 00:00:00 2001 From: codemasher Date: Fri, 23 Jul 2021 18:36:15 +0200 Subject: [PATCH 76/78] :octocat: moved example images --- README.md | 4 ++-- {examples => docs}/example_image.png | Bin {examples => docs}/example_svg.png | Bin 3 files changed, 2 insertions(+), 2 deletions(-) rename {examples => docs}/example_image.png (100%) rename {examples => docs}/example_svg.png (100%) diff --git a/README.md b/README.md index fce953f42..f5f7780b6 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ echo 'QR Code'; ```

- QR codes are awesome! - QR codes are awesome! + QR codes are awesome! + QR codes are awesome!

Wait, what was that? Please again, slower! See [Advanced usage](https://github.com/chillerlan/php-qrcode/wiki/Advanced-usage) on the wiki. diff --git a/examples/example_image.png b/docs/example_image.png similarity index 100% rename from examples/example_image.png rename to docs/example_image.png diff --git a/examples/example_svg.png b/docs/example_svg.png similarity index 100% rename from examples/example_svg.png rename to docs/example_svg.png From dbc85a7cd12512829f5f178824ca0ba71f377b50 Mon Sep 17 00:00:00 2001 From: codemasher Date: Fri, 3 Sep 2021 20:04:51 +0200 Subject: [PATCH 77/78] :octocat: --- .github/workflows/tests.yml | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2263dd2cc..3a6f17c3a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,6 +14,10 @@ jobs: runs-on: ubuntu-latest + env: + PHAN_ALLOW_XDEBUG: 0 + PHAN_DISABLE_XDEBUG_WARN: 1 + steps: - name: "Checkout" uses: actions/checkout@v2 @@ -30,11 +34,6 @@ jobs: - name: "Update dependencies with composer" run: composer update --no-interaction --no-ansi --no-progress --no-suggest - - name: "phan env" - run: | - echo "PHAN_ALLOW_XDEBUG=0" >> $GITHUB_ENV - echo "PHAN_DISABLE_XDEBUG_WARN=1" >> $GITHUB_ENV - - name: "Run phan" run: php vendor/bin/phan @@ -52,6 +51,7 @@ jobs: php-version: - "7.4" - "8.0" + - "8.1" steps: # - name: "Configure git to avoid issues with line endings" @@ -70,21 +70,6 @@ jobs: extensions: gd, imagick, json, mbstring # ini-values: - - name: "Determine composer cache directory on Linux" - if: matrix.os == 'ubuntu-latest' - run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - - - name: "Determine composer cache directory on Windows" - if: matrix.os == 'windows-latest' - run: echo "COMPOSER_CACHE_DIR=%LOCALAPPDATA%\Composer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - - name: "Cache dependencies installed with composer" - uses: actions/cache@v2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: php${{ matrix.php-version }}-composer- - - name: "Install dependencies with composer" run: composer update --no-ansi --no-interaction --no-progress --no-suggest From 238d0fbd22d8f8638f818e3670e212659c44e251 Mon Sep 17 00:00:00 2001 From: codemasher Date: Fri, 3 Sep 2021 20:05:36 +0200 Subject: [PATCH 78/78] :octocat: whatever, phpstorm --- .idea/codeStyles/Project.xml | 51 +++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 511ae8d84..0d9148f39 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -125,6 +125,43 @@