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(); } }