diff options
Diffstat (limited to 'vendor/chillerlan/php-qrcode/src')
67 files changed, 9921 insertions, 2309 deletions
diff --git a/vendor/chillerlan/php-qrcode/src/Common/BitBuffer.php b/vendor/chillerlan/php-qrcode/src/Common/BitBuffer.php new file mode 100644 index 000000000..4a59f2b58 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/BitBuffer.php @@ -0,0 +1,180 @@ +<?php +/** + * Class BitBuffer + * + * @created 25.11.2015 + * @author Smiley <smiley@chillerlan.net> + * @copyright 2015 Smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; +use function count, floor, min; + +/** + * Holds the raw binary data + */ +final class BitBuffer{ + + /** + * The buffer content + * + * @var int[] + */ + private array $buffer; + + /** + * Length of the content (bits) + */ + private int $length; + + /** + * Read count (bytes) + */ + private int $bytesRead = 0; + + /** + * Read count (bits) + */ + private int $bitsRead = 0; + + /** + * BitBuffer constructor. + * + * @param int[] $bytes + */ + public function __construct(array $bytes = []){ + $this->buffer = $bytes; + $this->length = count($this->buffer); + } + + /** + * appends a sequence of bits + */ + public function put(int $bits, int $length):self{ + + for($i = 0; $i < $length; $i++){ + $this->putBit((($bits >> ($length - $i - 1)) & 1) === 1); + } + + return $this; + } + + /** + * appends a single bit + */ + public function putBit(bool $bit):self{ + $bufIndex = (int)floor($this->length / 8); + + if(count($this->buffer) <= $bufIndex){ + $this->buffer[] = 0; + } + + if($bit === true){ + $this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8)); + } + + $this->length++; + + return $this; + } + + /** + * returns the current buffer length + */ + public function getLength():int{ + return $this->length; + } + + /** + * returns the buffer content + * + * to debug: array_map(fn($v) => sprintf('%08b', $v), $bitBuffer->getBuffer()) + */ + public function getBuffer():array{ + 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 \chillerlan\QRCode\QRCodeException if numBits isn't in [1,32] or more than is available + */ + public function read(int $numBits):int{ + + if($numBits < 1 || $numBits > $this->available()){ + throw new QRCodeException('invalid $numBits: '.$numBits); + } + + $result = 0; + + // First, read remainder from current byte + if($this->bitsRead > 0){ + $bitsLeft = (8 - $this->bitsRead); + $toRead = min($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; + } + + /** + * Clears the buffer and resets the stats + */ + public function clear():self{ + $this->buffer = []; + $this->length = 0; + + return $this->rewind(); + } + + /** + * Resets the read-counters + */ + public function rewind():self{ + $this->bytesRead = 0; + $this->bitsRead = 0; + + return $this; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/ECICharset.php b/vendor/chillerlan/php-qrcode/src/Common/ECICharset.php new file mode 100644 index 000000000..0c98e36da --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/ECICharset.php @@ -0,0 +1,125 @@ +<?php +/** + * Class ECICharset + * + * @created 21.01.2021 + * @author ZXing Authors + * @author smiley <smiley@chillerlan.net> + * @copyright 2021 smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; +use function sprintf; + +/** + * ISO/IEC 18004:2000 - 8.4.1 Extended Channel Interpretation (ECI) Mode + */ +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 + 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', + ]; + + /** + * The current ECI character set ID + */ + private int $charsetID; + + /** + * @throws \chillerlan\QRCode\QRCodeException + */ + public function __construct(int $charsetID){ + + if($charsetID < 0 || $charsetID > 999999){ + throw new QRCodeException(sprintf('invalid charset id: "%s"', $charsetID)); + } + + $this->charsetID = $charsetID; + } + + /** + * Returns the current character set ID + */ + public function getID():int{ + return $this->charsetID; + } + + /** + * Returns the name of the current character set or null if no name is available + * + * @see \mb_convert_encoding() + * @see \iconv() + */ + public function getName():?string{ + return (self::MB_ENCODINGS[$this->charsetID] ?? null); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/EccLevel.php b/vendor/chillerlan/php-qrcode/src/Common/EccLevel.php new file mode 100644 index 000000000..789d7f79d --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/EccLevel.php @@ -0,0 +1,223 @@ +<?php +/** + * Class EccLevel + * + * @created 19.11.2020 + * @author smiley <smiley@chillerlan.net> + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; +use function array_column; + +/** + * This class encapsulates the four error correction levels defined by the QR code standard. + */ +final 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%. + + /** + * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40 + * + * @var int[][] + */ + private const MAX_BITS = [ + // [ L, M, Q, H] // v => modules + [ 0, 0, 0, 0], // 0 => will be ignored, index starts at 1 + [ 152, 128, 104, 72], // 1 => 21 + [ 272, 224, 176, 128], // 2 => 25 + [ 440, 352, 272, 208], // 3 => 29 + [ 640, 512, 384, 288], // 4 => 33 + [ 864, 688, 496, 368], // 5 => 37 + [ 1088, 864, 608, 480], // 6 => 41 + [ 1248, 992, 704, 528], // 7 => 45 + [ 1552, 1232, 880, 688], // 8 => 49 + [ 1856, 1456, 1056, 800], // 9 => 53 + [ 2192, 1728, 1232, 976], // 10 => 57 + [ 2592, 2032, 1440, 1120], // 11 => 61 + [ 2960, 2320, 1648, 1264], // 12 => 65 + [ 3424, 2672, 1952, 1440], // 13 => 69 NICE! + [ 3688, 2920, 2088, 1576], // 14 => 73 + [ 4184, 3320, 2360, 1784], // 15 => 77 + [ 4712, 3624, 2600, 2024], // 16 => 81 + [ 5176, 4056, 2936, 2264], // 17 => 85 + [ 5768, 4504, 3176, 2504], // 18 => 89 + [ 6360, 5016, 3560, 2728], // 19 => 93 + [ 6888, 5352, 3880, 3080], // 20 => 97 + [ 7456, 5712, 4096, 3248], // 21 => 101 + [ 8048, 6256, 4544, 3536], // 22 => 105 + [ 8752, 6880, 4912, 3712], // 23 => 109 + [ 9392, 7312, 5312, 4112], // 24 => 113 + [10208, 8000, 5744, 4304], // 25 => 117 + [10960, 8496, 6032, 4768], // 26 => 121 + [11744, 9024, 6464, 5024], // 27 => 125 + [12248, 9544, 6968, 5288], // 28 => 129 + [13048, 10136, 7288, 5608], // 29 => 133 + [13880, 10984, 7880, 5960], // 30 => 137 + [14744, 11640, 8264, 6344], // 31 => 141 + [15640, 12328, 8920, 6760], // 32 => 145 + [16568, 13048, 9368, 7208], // 33 => 149 + [17528, 13800, 9848, 7688], // 34 => 153 + [18448, 14496, 10288, 7888], // 35 => 157 + [19472, 15312, 10832, 8432], // 36 => 161 + [20528, 15936, 11408, 8768], // 37 => 165 + [21616, 16816, 12016, 9136], // 38 => 169 + [22496, 17728, 12656, 9776], // 39 => 173 + [23648, 18672, 13328, 10208], // 40 => 177 + ]; + + /** + * ISO/IEC 18004:2000 Section 8.9 - Format Information + * + * ECC level -> mask pattern + * + * @var int[][] + */ + private const FORMAT_PATTERN = [ + [ // 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 value + * + * L: 0b01 + * M: 0b00 + * Q: 0b11 + * H: 0b10 + */ + private int $eccLevel; + + /** + * @param int $eccLevel containing the two bits encoding a QR Code's error correction level + * + * @todo: accept string values (PHP8+) + * @see https://github.com/chillerlan/php-qrcode/discussions/160 + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function __construct(int $eccLevel){ + + if((0b11 & $eccLevel) !== $eccLevel){ + throw new QRCodeException('invalid ECC level'); + } + + $this->eccLevel = $eccLevel; + } + + /** + * returns the string representation of the current ECC level + */ + public function __toString():string{ + return [ + self::L => 'L', + self::M => 'M', + self::Q => 'Q', + self::H => 'H', + ][$this->eccLevel]; + } + + /** + * returns the current ECC level + */ + public function getLevel():int{ + return $this->eccLevel; + } + + /** + * returns the ordinal value of the current ECC level + * + * references to the keys of the following tables: + * + * @see \chillerlan\QRCode\Common\EccLevel::MAX_BITS + * @see \chillerlan\QRCode\Common\EccLevel::FORMAT_PATTERN + * @see \chillerlan\QRCode\Common\Version::RSBLOCKS + */ + public function getOrdinal():int{ + return [ + self::L => 0, + self::M => 1, + self::Q => 2, + self::H => 3, + ][$this->eccLevel]; + } + + /** + * returns the format pattern for the given $eccLevel and $maskPattern + */ + public function getformatPattern(MaskPattern $maskPattern):int{ + return self::FORMAT_PATTERN[$this->getOrdinal()][$maskPattern->getPattern()]; + } + + /** + * returns an array with the max bit lengths for version 1-40 and the current ECC level + * + * @return int[] + */ + public function getMaxBits():array{ + $col = array_column(self::MAX_BITS, $this->getOrdinal()); + + unset($col[0]); // remove the inavlid index 0 + + return $col; + } + + /** + * Returns the maximum bit length for the given version and current ECC level + */ + public function getMaxBitsForVersion(Version $version):int{ + return self::MAX_BITS[$version->getVersionNumber()][$this->getOrdinal()]; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/GDLuminanceSource.php b/vendor/chillerlan/php-qrcode/src/Common/GDLuminanceSource.php new file mode 100644 index 000000000..0702a67bb --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/GDLuminanceSource.php @@ -0,0 +1,97 @@ +<?php +/** + * Class GDLuminanceSource + * + * @created 17.01.2021 + * @author Ashot Khanamiryan + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\Decoder\QRCodeDecoderException; +use chillerlan\Settings\SettingsContainerInterface; +use function file_get_contents, get_resource_type, imagecolorat, imagecolorsforindex, + imagecreatefromstring, imagefilter, imagesx, imagesy, is_resource; +use const IMG_FILTER_BRIGHTNESS, IMG_FILTER_CONTRAST, IMG_FILTER_GRAYSCALE, IMG_FILTER_NEGATE, PHP_MAJOR_VERSION; + +/** + * This class is used to help decode images from files which arrive as GD Resource + * It does not support rotation. + */ +class GDLuminanceSource extends LuminanceSourceAbstract{ + + /** + * @var resource|\GdImage + */ + protected $gdImage; + + /** + * GDLuminanceSource constructor. + * + * @param resource|\GdImage $gdImage + * @param \chillerlan\Settings\SettingsContainerInterface|null $options + * + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + public function __construct($gdImage, ?SettingsContainerInterface $options = null){ + + /** @noinspection PhpFullyQualifiedNameUsageInspection */ + if( + (PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage) // @todo: remove version check in v6 + || (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd')) + ){ + throw new QRCodeDecoderException('Invalid GD image source.'); // @codeCoverageIgnore + } + + parent::__construct(imagesx($gdImage), imagesy($gdImage), $options); + + $this->gdImage = $gdImage; + + if($this->options->readerGrayscale){ + imagefilter($this->gdImage, IMG_FILTER_GRAYSCALE); + } + + if($this->options->readerInvertColors){ + imagefilter($this->gdImage, IMG_FILTER_NEGATE); + } + + if($this->options->readerIncreaseContrast){ + imagefilter($this->gdImage, IMG_FILTER_BRIGHTNESS, -100); + imagefilter($this->gdImage, IMG_FILTER_CONTRAST, -100); + } + + $this->setLuminancePixels(); + } + + /** + * + */ + protected 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']); + } + } + + } + + /** @inheritDoc */ + public static function fromFile(string $path, ?SettingsContainerInterface $options = null):self{ + return new self(imagecreatefromstring(file_get_contents(self::checkFile($path))), $options); + } + + /** @inheritDoc */ + public static function fromBlob(string $blob, ?SettingsContainerInterface $options = null):self{ + return new self(imagecreatefromstring($blob), $options); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/GF256.php b/vendor/chillerlan/php-qrcode/src/Common/GF256.php new file mode 100644 index 000000000..d8ba0950b --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/GF256.php @@ -0,0 +1,154 @@ +<?php +/** + * Class GF256 + * + * @created 16.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; + +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 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 = [ + 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, + 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 + * @throws \chillerlan\QRCode\QRCodeException + */ + public static function buildMonomial(int $degree, int $coefficient):GenericGFPoly{ + + if($degree < 0){ + throw new QRCodeException('degree < 0'); + } + + $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) + * @throws \chillerlan\QRCode\QRCodeException + */ + public static function log(int $a):int{ + + if($a < 1){ + throw new QRCodeException('$a < 1'); + } + + return self::logTable[$a]; + } + + /** + * @return int multiplicative inverse of a + * @throws \chillerlan\QRCode\QRCodeException + */ + public static function inverse(int $a):int{ + + if($a === 0){ + throw new QRCodeException('$a === 0'); + } + + 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/vendor/chillerlan/php-qrcode/src/Common/GenericGFPoly.php b/vendor/chillerlan/php-qrcode/src/Common/GenericGFPoly.php new file mode 100644 index 000000000..4023e74d2 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/GenericGFPoly.php @@ -0,0 +1,263 @@ +<?php +/** + * Class GenericGFPoly + * + * @created 16.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; +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 $coefficients array coefficients as ints representing elements of GF(size), arranged + * from most significant (highest-power term) coefficient to the least significant + * @param int|null $degree + * + * @throws \chillerlan\QRCode\QRCodeException 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 QRCodeException('arg $coefficients is empty'); + } + + if($degree < 0){ + throw new QRCodeException('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++; + } + + $this->coefficients = [0]; + + if($firstNonZero !== $coefficientsLength){ + $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; + } + + /** + * + */ + public function multiply(GenericGFPoly $other):self{ + + 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); + } + + /** + * @return \chillerlan\QRCode\Common\GenericGFPoly[] [quotient, remainder] + * @throws \chillerlan\QRCode\QRCodeException + */ + public function divide(GenericGFPoly $other):array{ + + if($other->isZero()){ + throw new QRCodeException('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]; + + } + + /** + * + */ + public function multiplyInt(int $scalar):self{ + + 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); + } + + /** + * @throws \chillerlan\QRCode\QRCodeException + */ + public function multiplyByMonomial(int $degree, int $coefficient):self{ + + if($degree < 0){ + throw new QRCodeException('degree < 0'); + } + + 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); + } + + /** + * + */ + public function mod(GenericGFPoly $other):self{ + + 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); + } + + /** + * + */ + public function addOrSubtract(GenericGFPoly $other):self{ + + 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); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/IMagickLuminanceSource.php b/vendor/chillerlan/php-qrcode/src/Common/IMagickLuminanceSource.php new file mode 100644 index 000000000..ade994a78 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/IMagickLuminanceSource.php @@ -0,0 +1,78 @@ +<?php +/** + * Class IMagickLuminanceSource + * + * @created 17.01.2021 + * @author Ashot Khanamiryan + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\Settings\SettingsContainerInterface; +use Imagick; +use function count; + +/** + * This class is used to help decode images from files which arrive as Imagick Resource + * It does not support rotation. + */ +class IMagickLuminanceSource extends LuminanceSourceAbstract{ + + protected Imagick $imagick; + + /** + * IMagickLuminanceSource constructor. + */ + public function __construct(Imagick $imagick, ?SettingsContainerInterface $options = null){ + parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight(), $options); + + $this->imagick = $imagick; + + if($this->options->readerGrayscale){ + $this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY); + } + + if($this->options->readerInvertColors){ + $this->imagick->negateImage($this->options->readerGrayscale); + } + + if($this->options->readerIncreaseContrast){ + for($i = 0; $i < 10; $i++){ + $this->imagick->contrastImage(false); // misleading docs + } + } + + $this->setLuminancePixels(); + } + + /** + * + */ + protected function setLuminancePixels():void{ + $pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR); + $count = count($pixels); + + for($i = 0; $i < $count; $i += 3){ + $this->setLuminancePixel(($pixels[$i] & 0xff), ($pixels[($i + 1)] & 0xff), ($pixels[($i + 2)] & 0xff)); + } + } + + /** @inheritDoc */ + public static function fromFile(string $path, ?SettingsContainerInterface $options = null):self{ + return new self(new Imagick(self::checkFile($path)), $options); + } + + /** @inheritDoc */ + public static function fromBlob(string $blob, ?SettingsContainerInterface $options = null):self{ + $im = new Imagick; + $im->readImageBlob($blob); + + return new self($im, $options); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/LuminanceSourceAbstract.php b/vendor/chillerlan/php-qrcode/src/Common/LuminanceSourceAbstract.php new file mode 100644 index 000000000..e4373b87d --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/LuminanceSourceAbstract.php @@ -0,0 +1,107 @@ +<?php +/** + * Class LuminanceSourceAbstract + * + * @created 24.01.2021 + * @author ZXing Authors + * @author Ashot Khanamiryan + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\Decoder\QRCodeDecoderException; +use chillerlan\QRCode\QROptions; +use chillerlan\Settings\SettingsContainerInterface; +use function array_slice, array_splice, file_exists, is_file, is_readable, realpath; + +/** + * The purpose of this class hierarchy is to abstract different bitmap implementations across + * platforms into a standard interface for requesting greyscale luminance values. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{ + + /** @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface */ + protected SettingsContainerInterface $options; + protected array $luminances; + protected int $width; + protected int $height; + + /** + * + */ + public function __construct(int $width, int $height, ?SettingsContainerInterface $options = null){ + $this->width = $width; + $this->height = $height; + $this->options = ($options ?? new QROptions); + + $this->luminances = []; + } + + /** @inheritDoc */ + public function getLuminances():array{ + return $this->luminances; + } + + /** @inheritDoc */ + public function getWidth():int{ + return $this->width; + } + + /** @inheritDoc */ + public function getHeight():int{ + return $this->height; + } + + /** + * @inheritDoc + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + public function getRow(int $y):array{ + + if($y < 0 || $y >= $this->getHeight()){ + throw new QRCodeDecoderException('Requested row is outside the image: '.$y); + } + + $arr = []; + + array_splice($arr, 0, $this->width, array_slice($this->luminances, ($y * $this->width), $this->width)); + + return $arr; + } + + /** + * + */ + 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; + } + + /** + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + protected static function checkFile(string $path):string{ + $path = trim($path); + + if(!file_exists($path) || !is_file($path) || !is_readable($path)){ + throw new QRCodeDecoderException('invalid file: '.$path); + } + + $realpath = realpath($path); + + if($realpath === false){ + throw new QRCodeDecoderException('unable to resolve path: '.$path); + } + + return $realpath; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/LuminanceSourceInterface.php b/vendor/chillerlan/php-qrcode/src/Common/LuminanceSourceInterface.php new file mode 100644 index 000000000..64409e36a --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/LuminanceSourceInterface.php @@ -0,0 +1,61 @@ +<?php +/** + * Interface LuminanceSourceInterface + * + * @created 18.11.2021 + * @author smiley <smiley@chillerlan.net> + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +/** + */ +interface LuminanceSourceInterface{ + + /** + * Fetches luminance data for the underlying bitmap. Values should be fetched using: + * `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 getLuminances():array; + + /** + * @return int The width of the bitmap. + */ + public function getWidth():int; + + /** + * @return int The height of the bitmap. + */ + public function getHeight():int; + + /** + * 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 + * getLuminances() 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. + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + public function getRow(int $y):array; + + /** + * Creates a LuminanceSource instance from the given file + */ + public static function fromFile(string $path):self; + + /** + * Creates a LuminanceSource instance from the given data blob + */ + public static function fromBlob(string $blob):self; + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/MaskPattern.php b/vendor/chillerlan/php-qrcode/src/Common/MaskPattern.php new file mode 100644 index 000000000..5c3ea93c1 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/MaskPattern.php @@ -0,0 +1,329 @@ +<?php +/** + * Class MaskPattern + * + * @created 19.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; +use chillerlan\QRCode\Data\QRMatrix; +use Closure; +use function abs, array_column, array_search, intdiv, min; + +/** + * ISO/IEC 18004:2000 Section 8.8.1 + * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results + * + * @see http://www.thonky.com/qr-code-tutorial/data-masking + * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java + */ +final class MaskPattern{ + + /** + * @see \chillerlan\QRCode\QROptionsTrait::$maskPattern + * + * @var int + */ + public const AUTO = -1; + + 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; + + /** + * @var int[] + */ + 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, + ]; + + /* + * Penalty scores + * + * ISO/IEC 18004:2000 Section 8.8.1 - Table 24 + */ + private const PENALTY_N1 = 3; + private const PENALTY_N2 = 3; + private const PENALTY_N3 = 40; + private const PENALTY_N4 = 10; + + /** + * The current mask pattern value (0-7) + */ + private int $maskPattern; + + /** + * MaskPattern constructor. + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function __construct(int $maskPattern){ + + if(($maskPattern & 0b111) !== $maskPattern){ + throw new QRCodeException('invalid mask pattern'); + } + + $this->maskPattern = $maskPattern; + } + + /** + * Returns the current mask pattern + */ + public function getPattern():int{ + return $this->maskPattern; + } + + /** + * Returns a closure that applies the mask for the chosen mask pattern. + * + * 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. + * + * @see https://www.thonky.com/qr-code-tutorial/mask-patterns + * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java#L32-L117 + */ + public function getMask():Closure{ + // $x = column (width), $y = row (height) + return [ + self::PATTERN_000 => fn(int $x, int $y):bool => (($x + $y) % 2) === 0, + self::PATTERN_001 => fn(int $x, int $y):bool => ($y % 2) === 0, + self::PATTERN_010 => fn(int $x, int $y):bool => ($x % 3) === 0, + self::PATTERN_011 => fn(int $x, int $y):bool => (($x + $y) % 3) === 0, + self::PATTERN_100 => fn(int $x, int $y):bool => ((intdiv($y, 2) + intdiv($x, 3)) % 2) === 0, + self::PATTERN_101 => fn(int $x, int $y):bool => (($x * $y) % 6) === 0, + self::PATTERN_110 => fn(int $x, int $y):bool => (($x * $y) % 6) < 3, + self::PATTERN_111 => fn(int $x, int $y):bool => (($x + $y + (($x * $y) % 3)) % 2) === 0, + ][$this->maskPattern]; + } + + /** + * Evaluates the matrix of the given data interface and returns a new mask pattern instance for the best result + */ + public static function getBestPattern(QRMatrix $QRMatrix):self{ + $penalties = []; + $size = $QRMatrix->getSize(); + + foreach(self::PATTERNS as $pattern){ + $mp = new self($pattern); + $matrix = (clone $QRMatrix)->setFormatInfo($mp)->mask($mp)->getMatrix(true); + $penalty = 0; + + for($level = 1; $level <= 4; $level++){ + $penalty += self::{'testRule'.$level}($matrix, $size, $size); + } + + $penalties[$pattern] = (int)$penalty; + } + + return new self(array_search(min($penalties), $penalties, true)); + } + + /** + * Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and + * give penalty to them. Example: 00000 or 11111. + */ + public static function testRule1(array $matrix, int $height, int $width):int{ + $penalty = 0; + + // horizontal + foreach($matrix as $row){ + $penalty += self::applyRule1($row); + } + + // vertical + for($x = 0; $x < $width; $x++){ + $penalty += self::applyRule1(array_column($matrix, $x)); + } + + return $penalty; + } + + /** + * + */ + private static function applyRule1(array $rc):int{ + $penalty = 0; + $numSameBitCells = 0; + $prevBit = null; + + foreach($rc as $val){ + + if($val === $prevBit){ + $numSameBitCells++; + } + else{ + + if($numSameBitCells >= 5){ + $penalty += (self::PENALTY_N1 + $numSameBitCells - 5); + } + + $numSameBitCells = 1; // Include the cell itself. + $prevBit = $val; + } + } + + if($numSameBitCells >= 5){ + $penalty += (self::PENALTY_N1 + $numSameBitCells - 5); + } + + return $penalty; + } + + /** + * Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give + * penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a + * penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block. + */ + public static function testRule2(array $matrix, int $height, int $width):int{ + $penalty = 0; + + foreach($matrix as $y => $row){ + + if($y > ($height - 2)){ + break; + } + + foreach($row as $x => $val){ + + if($x > ($width - 2)){ + break; + } + + if( + $val === $row[($x + 1)] + && $val === $matrix[($y + 1)][$x] + && $val === $matrix[($y + 1)][($x + 1)] + ){ + $penalty++; + } + } + } + + return (self::PENALTY_N2 * $penalty); + } + + /** + * Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4 + * starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we + * find patterns like 000010111010000, we give penalty once. + */ + public static function testRule3(array $matrix, int $height, int $width):int{ + $penalties = 0; + + foreach($matrix as $y => $row){ + foreach($row as $x => $val){ + + if( + ($x + 6) < $width + && $val + && !$row[($x + 1)] + && $row[($x + 2)] + && $row[($x + 3)] + && $row[($x + 4)] + && !$row[($x + 5)] + && $row[($x + 6)] + && ( + self::isWhiteHorizontal($row, $width, ($x - 4), $x) + || self::isWhiteHorizontal($row, $width, ($x + 7), ($x + 11)) + ) + ){ + $penalties++; + } + + if( + ($y + 6) < $height + && $val + && !$matrix[($y + 1)][$x] + && $matrix[($y + 2)][$x] + && $matrix[($y + 3)][$x] + && $matrix[($y + 4)][$x] + && !$matrix[($y + 5)][$x] + && $matrix[($y + 6)][$x] + && ( + self::isWhiteVertical($matrix, $height, $x, ($y - 4), $y) + || self::isWhiteVertical($matrix, $height, $x, ($y + 7), ($y + 11)) + ) + ){ + $penalties++; + } + + } + } + + return ($penalties * self::PENALTY_N3); + } + + /** + * + */ + private static function isWhiteHorizontal(array $row, int $width, int $from, int $to):bool{ + + if($from < 0 || $width < $to){ + return false; + } + + for($x = $from; $x < $to; $x++){ + if($row[$x]){ + return false; + } + } + + return true; + } + + /** + * + */ + private static function isWhiteVertical(array $matrix, int $height, int $x, int $from, int $to):bool{ + + if($from < 0 || $height < $to){ + return false; + } + + for($y = $from; $y < $to; $y++){ + if($matrix[$y][$x] === true){ + return false; + } + } + + return true; + } + + /** + * Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give + * penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance. + */ + public static function testRule4(array $matrix, int $height, int $width):int{ + $darkCells = 0; + $totalCells = ($height * $width); + + foreach($matrix as $row){ + foreach($row as $val){ + if($val === true){ + $darkCells++; + } + } + } + + return (intdiv((abs($darkCells * 2 - $totalCells) * 10), $totalCells) * self::PENALTY_N4); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/Mode.php b/vendor/chillerlan/php-qrcode/src/Common/Mode.php new file mode 100644 index 000000000..523d37919 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/Mode.php @@ -0,0 +1,96 @@ +<?php +/** + * Class Mode + * + * @created 19.11.2020 + * @author smiley <smiley@chillerlan.net> + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\Data\{AlphaNum, Byte, Hanzi, Kanji, Number}; +use chillerlan\QRCode\QRCodeException; + +/** + * Data mode information - ISO 18004:2006, 6.4.1, Tables 2 and 3 + */ +final class Mode{ + + // ISO/IEC 18004:2000 Table 2 + + /** @var int */ + public const TERMINATOR = 0b0000; + /** @var int */ + public const NUMBER = 0b0001; + /** @var int */ + public const ALPHANUM = 0b0010; + /** @var int */ + public const BYTE = 0b0100; + /** @var int */ + public const KANJI = 0b1000; + /** @var int */ + public const HANZI = 0b1101; + /** @var int */ + public const STRCTURED_APPEND = 0b0011; + /** @var int */ + public const FNC1_FIRST = 0b0101; + /** @var int */ + public const FNC1_SECOND = 0b1001; + /** @var int */ + public const 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::NUMBER => [10, 12, 14], + self::ALPHANUM => [ 9, 11, 13], + self::BYTE => [ 8, 16, 16], + self::KANJI => [ 8, 10, 12], + self::HANZI => [ 8, 10, 12], + self::ECI => [ 0, 0, 0], + ]; + + /** + * Map of data mode => interface (detection order) + * + * @var string[] + */ + public const INTERFACES = [ + self::NUMBER => Number::class, + self::ALPHANUM => AlphaNum::class, + self::KANJI => Kanji::class, + self::HANZI => Hanzi::class, + self::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'); + } + + $minVersion = 0; + + foreach([9, 26, 40] as $key => $breakpoint){ + + if($version > $minVersion && $version <= $breakpoint){ + return self::LENGTH_BITS[$mode][$key]; + } + + $minVersion = $breakpoint; + } + + throw new QRCodeException(sprintf('invalid version number: %d', $version)); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Common/Version.php b/vendor/chillerlan/php-qrcode/src/Common/Version.php new file mode 100644 index 000000000..fe7240f8a --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Common/Version.php @@ -0,0 +1,287 @@ +<?php +/** + * Class Version + * + * @created 19.11.2020 + * @author smiley <smiley@chillerlan.net> + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; + +/** + * Version related tables and methods + */ +final class Version{ + + /** + * Enable version auto detection + * + * @see \chillerlan\QRCode\QROptionsTrait::$version + * + * @var int + */ + public const AUTO = -1; + + /** + * ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns + * + * version -> pattern + * + * @var int[][] + */ + private 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[] + */ + private 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, + ]; + + /** + * ISO/IEC 18004:2000 Tables 13-22 - Error correction characteristics + * + * @see http://www.thonky.com/qr-code-tutorial/error-correction-table + */ + private const RSBLOCKS = [ + 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]]]], + ]; + + /** + * ISO/IEC 18004:2000 Table 1 - Data capacity of all versions of QR Code + */ + 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 + */ + private int $version; + + /** + * Version constructor. + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function __construct(int $version){ + + if($version < 1 || $version > 40){ + throw new QRCodeException('invalid version given'); + } + + $this->version = $version; + } + + /** + * returns the current version number as string + */ + public function __toString():string{ + return (string)$this->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]; + } + + /** + * returns ECC block information for the given $version and $eccLevel + */ + public function getRSBlocks(EccLevel $eccLevel):array{ + return self::RSBLOCKS[$this->version][$eccLevel->getOrdinal()]; + } + + /** + * returns the maximum codewords for the current version + */ + public function getTotalCodewords():int{ + return self::TOTAL_CODEWORDS[$this->version]; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Data/AlphaNum.php b/vendor/chillerlan/php-qrcode/src/Data/AlphaNum.php index 28d9d7563..93ef9fc59 100644 --- a/vendor/chillerlan/php-qrcode/src/Data/AlphaNum.php +++ b/vendor/chillerlan/php-qrcode/src/Data/AlphaNum.php @@ -2,9 +2,7 @@ /** * Class AlphaNum * - * @filesource AlphaNum.php * @created 25.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -12,9 +10,8 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\QRCode; - -use function ord, sprintf; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; +use function ceil, intdiv, preg_match, strpos; /** * Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / : @@ -22,39 +19,118 @@ 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{ - protected int $datamode = QRCode::DATA_ALPHANUM; + /** + * ISO/IEC 18004:2000 Table 5 + * + * @var string + */ + private const CHAR_MAP = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; - protected array $lengthBits = [9, 11, 13]; + /** + * @inheritDoc + */ + public const DATAMODE = Mode::ALPHANUM; /** - * @inheritdoc + * @inheritDoc */ - protected function write(string $data):void{ + public function getLengthInBits():int{ + return (int)ceil($this->getCharCount() * (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{ + return (bool)preg_match('/^[A-Z\d %$*+-.:\/]+$/', $string); + } + + /** + * @inheritDoc + */ + public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{ + $len = $this->getCharCount(); + + $bitBuffer + ->put(self::DATAMODE, 4) + ->put($len, $this::getLengthBits($versionNumber)) + ; + + // encode 2 characters in 11 bits + for($i = 0; ($i + 1) < $len; $i += 2){ + $bitBuffer->put( + ($this->ord($this->data[$i]) * 45 + $this->ord($this->data[($i + 1)])), + 11, + ); } - if($i < $this->strlen){ - $this->bitBuffer->put($this->getCharCode($data[$i]), 6); + // encode a remaining character in 6 bits + if($i < $len){ + $bitBuffer->put($this->ord($this->data[$i]), 6); } + return $this; } /** - * get the code for the given character + * @inheritDoc * - * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $length = $bitBuffer->read(self::getLengthBits($versionNumber)); + $result = ''; + // Read two characters at a time + while($length > 1){ + + if($bitBuffer->available() < 11){ + throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore + } + + $nextTwoCharsBits = $bitBuffer->read(11); + $result .= self::chr(intdiv($nextTwoCharsBits, 45)); + $result .= self::chr($nextTwoCharsBits % 45); + $length -= 2; + } + + if($length === 1){ + // special case: one character left + if($bitBuffer->available() < 6){ + throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore + } + + $result .= self::chr($bitBuffer->read(6)); + } + + return $result; + } + + /** + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + private function ord(string $chr):int{ + /** @phan-suppress-next-line PhanParamSuspiciousOrder */ + $ord = strpos(self::CHAR_MAP, $chr); + + if($ord === false){ + throw new QRCodeDataException('invalid character'); // @codeCoverageIgnore + } + + return $ord; + } + + /** + * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - protected function getCharCode(string $chr):int{ + private static function chr(int $ord):string{ - if(!isset($this::CHAR_MAP_ALPHANUM[$chr])){ - throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, ord($chr))); + if($ord < 0 || $ord > 44){ + throw new QRCodeDataException('invalid character code'); // @codeCoverageIgnore } - return $this::CHAR_MAP_ALPHANUM[$chr]; + return self::CHAR_MAP[$ord]; } } diff --git a/vendor/chillerlan/php-qrcode/src/Data/Byte.php b/vendor/chillerlan/php-qrcode/src/Data/Byte.php index 02e76a639..10ab85262 100644 --- a/vendor/chillerlan/php-qrcode/src/Data/Byte.php +++ b/vendor/chillerlan/php-qrcode/src/Data/Byte.php @@ -2,9 +2,7 @@ /** * Class Byte * - * @filesource Byte.php * @created 25.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -12,33 +10,76 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\QRCode; - -use function ord; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; +use function chr, ord; /** - * Byte mode, ISO-8859-1 or UTF-8 + * 8-bit Byte mode, ISO-8859-1 or UTF-8 * * 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; + /** + * @inheritDoc + */ + public const DATAMODE = Mode::BYTE; - protected array $lengthBits = [8, 16, 16]; + /** + * @inheritDoc + */ + public function getLengthInBits():int{ + return ($this->getCharCount() * 8); + } /** - * @inheritdoc + * @inheritDoc */ - protected function write(string $data):void{ + public static function validateString(string $string):bool{ + return $string !== ''; + } + + /** + * @inheritDoc + */ + public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{ + $len = $this->getCharCount(); + + $bitBuffer + ->put(self::DATAMODE, 4) + ->put($len, $this::getLengthBits($versionNumber)) + ; + $i = 0; - while($i < $this->strlen){ - $this->bitBuffer->put(ord($data[$i]), 8); + while($i < $len){ + $bitBuffer->put(ord($this->data[$i]), 8); $i++; } + return $this; + } + + /** + * @inheritDoc + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $length = $bitBuffer->read(self::getLengthBits($versionNumber)); + + if($bitBuffer->available() < (8 * $length)){ + throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore + } + + $readBytes = ''; + + for($i = 0; $i < $length; $i++){ + $readBytes .= chr($bitBuffer->read(8)); + } + + return $readBytes; } } diff --git a/vendor/chillerlan/php-qrcode/src/Data/ECI.php b/vendor/chillerlan/php-qrcode/src/Data/ECI.php new file mode 100644 index 000000000..1aacd0b71 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Data/ECI.php @@ -0,0 +1,165 @@ +<?php +/** + * Class ECI + * + * @created 20.11.2020 + * @author smiley <smiley@chillerlan.net> + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode}; +use function mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, sprintf; + +/** + * Adds an ECI Designator + * + * ISO/IEC 18004:2000 8.4.1.1 + * + * Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment() + */ +final class ECI extends QRDataModeAbstract{ + + /** + * @inheritDoc + */ + public const DATAMODE = Mode::ECI; + + /** + * The current ECI encoding id + */ + private int $encoding; + + /** + * @inheritDoc + * @throws \chillerlan\QRCode\Data\QRCodeDataException + * @noinspection PhpMissingParentConstructorInspection + */ + public function __construct(int $encoding){ + + if($encoding < 0 || $encoding > 999999){ + throw new QRCodeDataException(sprintf('invalid encoding id: "%s"', $encoding)); + } + + $this->encoding = $encoding; + } + + /** + * @inheritDoc + */ + public function getLengthInBits():int{ + + if($this->encoding < 128){ + return 8; + } + + if($this->encoding < 16384){ + return 16; + } + + return 24; + } + + /** + * Writes an ECI designator to the bitbuffer + * + * @inheritDoc + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{ + $bitBuffer->put(self::DATAMODE, 4); + + if($this->encoding < 128){ + $bitBuffer->put($this->encoding, 8); + } + elseif($this->encoding < 16384){ + $bitBuffer->put(($this->encoding | 0x8000), 16); + } + elseif($this->encoding < 1000000){ + $bitBuffer->put(($this->encoding | 0xC00000), 24); + } + else{ + throw new QRCodeDataException('invalid ECI ID'); + } + + return $this; + } + + /** + * Reads and parses the value of an ECI designator + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function parseValue(BitBuffer $bitBuffer):ECICharset{ + $firstByte = $bitBuffer->read(8); + + // just one byte + if(($firstByte & 0b10000000) === 0){ + $id = ($firstByte & 0b01111111); + } + // two bytes + elseif(($firstByte & 0b11000000) === 0b10000000){ + $id = ((($firstByte & 0b00111111) << 8) | $bitBuffer->read(8)); + } + // three bytes + elseif(($firstByte & 0b11100000) === 0b11000000){ + $id = ((($firstByte & 0b00011111) << 16) | $bitBuffer->read(16)); + } + else{ + throw new QRCodeDataException(sprintf('error decoding ECI value first byte: %08b', $firstByte));// @codeCoverageIgnore + } + + return new ECICharset($id); + } + + /** + * @codeCoverageIgnore Unused, but required as per interface + */ + public static function validateString(string $string):bool{ + return true; + } + + /** + * Reads and decodes the ECI designator including the following byte sequence + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $eciCharset = self::parseValue($bitBuffer); + $nextMode = $bitBuffer->read(4); + $data = self::decodeModeSegment($nextMode, $bitBuffer, $versionNumber); + $encoding = $eciCharset->getName(); + + if($encoding === null){ + // The spec isn't clear on this mode; see + // section 6.4.5: it 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($data, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true); + + if($encoding === false){ + throw new QRCodeDataException('could not determine encoding in ECI mode'); // @codeCoverageIgnore + } + } + + return mb_convert_encoding($data, mb_internal_encoding(), $encoding); + } + + /** + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + private static function decodeModeSegment(int $mode, BitBuffer $bitBuffer, int $versionNumber):string{ + + switch(true){ + case $mode === Mode::NUMBER: return Number::decodeSegment($bitBuffer, $versionNumber); + case $mode === Mode::ALPHANUM: return AlphaNum::decodeSegment($bitBuffer, $versionNumber); + case $mode === Mode::BYTE: return Byte::decodeSegment($bitBuffer, $versionNumber); + } + + throw new QRCodeDataException(sprintf('ECI designator followed by invalid mode: "%04b"', $mode)); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Data/Hanzi.php b/vendor/chillerlan/php-qrcode/src/Data/Hanzi.php new file mode 100644 index 000000000..2df8ac1c6 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Data/Hanzi.php @@ -0,0 +1,206 @@ +<?php +/** + * Class Hanzi + * + * @created 19.11.2020 + * @author smiley <smiley@chillerlan.net> + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\Common\{BitBuffer, Mode}; +use Throwable; +use function chr, implode, intdiv, is_string, mb_convert_encoding, mb_detect_encoding, + mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen; + +/** + * Hanzi (simplified Chinese) mode, GBT18284-2000: 13-bit double-byte characters from the GB2312/GB18030 character set + * + * Please note that this is not part of the QR Code specification and may not be supported by all readers (ZXing-based ones do). + * + * @see https://en.wikipedia.org/wiki/GB_2312 + * @see http://www.herongyang.com/GB2312/Introduction-of-GB2312.html + * @see https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding + * @see https://gist.github.com/codemasher/91da33c44bfb48a81a6c1426bb8e4338 + * @see https://github.com/zxing/zxing/blob/dfb06fa33b17a9e68321be151c22846c7b78048f/core/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java#L172-L209 + * @see https://www.chinesestandard.net/PDF/English.aspx/GBT18284-2000 + */ +final class Hanzi extends QRDataModeAbstract{ + + /** + * possible values: GB2312, GB18030 + * + * @var string + */ + public const ENCODING = 'GB18030'; + + /** + * @todo: other subsets??? + * + * @var int + */ + public const GB2312_SUBSET = 0b0001; + + /** + * @inheritDoc + */ + public const DATAMODE = Mode::HANZI; + + /** + * @inheritDoc + */ + protected function getCharCount():int{ + return mb_strlen($this->data, self::ENCODING); + } + + /** + * @inheritDoc + */ + public function getLengthInBits():int{ + return ($this->getCharCount() * 13); + } + + /** + * @inheritDoc + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function convertEncoding(string $string):string{ + mb_detect_order([mb_internal_encoding(), 'UTF-8', 'GB2312', 'GB18030', 'CP936', 'EUC-CN', 'HZ']); + + $detected = mb_detect_encoding($string, null, true); + + if($detected === false){ + throw new QRCodeDataException('mb_detect_encoding error'); + } + + if($detected === self::ENCODING){ + return $string; + } + + $string = mb_convert_encoding($string, self::ENCODING, $detected); + + if(!is_string($string)){ + throw new QRCodeDataException('mb_convert_encoding error'); + } + + return $string; + } + + /** + * checks if a string qualifies as Hanzi/GB2312 + */ + public static function validateString(string $string):bool{ + + try{ + $string = self::convertEncoding($string); + } + catch(Throwable $e){ + return false; + } + + $len = strlen($string); + + if($len < 2 || ($len % 2) !== 0){ + return false; + } + + for($i = 0; $i < $len; $i += 2){ + $byte1 = ord($string[$i]); + $byte2 = ord($string[($i + 1)]); + + // byte 1 unused ranges + if($byte1 < 0xa1 || ($byte1 > 0xa9 && $byte1 < 0xb0) || $byte1 > 0xf7){ + return false; + } + + // byte 2 unused ranges + if($byte2 < 0xa1 || $byte2 > 0xfe){ + return false; + } + + } + + return true; + } + + /** + * @inheritDoc + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence + */ + public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{ + + $bitBuffer + ->put(self::DATAMODE, 4) + ->put($this::GB2312_SUBSET, 4) + ->put($this->getCharCount(), $this::getLengthBits($versionNumber)) + ; + + $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)]))); + + if($c >= 0xa1a1 && $c <= 0xaafe){ + $c -= 0x0a1a1; + } + elseif($c >= 0xb0a1 && $c <= 0xfafe){ + $c -= 0x0a6a1; + } + else{ + throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c)); + } + + $bitBuffer->put((((($c >> 8) & 0xff) * 0x060) + ($c & 0xff)), 13); + } + + if($i < $len){ + throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1))); + } + + return $this; + } + + /** + * See specification GBT 18284-2000 + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + + // Hanzi mode contains a subset indicator right after mode indicator + if($bitBuffer->read(4) !== self::GB2312_SUBSET){ + throw new QRCodeDataException('ecpected subset indicator for Hanzi mode'); + } + + $length = $bitBuffer->read(self::getLengthBits($versionNumber)); + + if($bitBuffer->available() < ($length * 13)){ + throw new QRCodeDataException('not enough bits available'); + } + + // Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as GB2312 afterwards + $buffer = []; + $offset = 0; + + while($length > 0){ + // Each 13 bits encodes a 2-byte character + $twoBytes = $bitBuffer->read(13); + $assembledTwoBytes = ((intdiv($twoBytes, 0x060) << 8) | ($twoBytes % 0x060)); + + $assembledTwoBytes += ($assembledTwoBytes < 0x00a00) // 0x003BF + ? 0x0a1a1 // In the 0xA1A1 to 0xAAFE range + : 0x0a6a1; // In the 0xB0A1 to 0xFAFE 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(), self::ENCODING); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Data/Kanji.php b/vendor/chillerlan/php-qrcode/src/Data/Kanji.php index e106c50f1..ccde82642 100644 --- a/vendor/chillerlan/php-qrcode/src/Data/Kanji.php +++ b/vendor/chillerlan/php-qrcode/src/Data/Kanji.php @@ -2,9 +2,7 @@ /** * Class Kanji * - * @filesource Kanji.php * @created 25.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -12,58 +10,183 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\QRCode; - -use function mb_strlen, ord, sprintf, strlen; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; +use Throwable; +use function chr, implode, intdiv, is_string, mb_convert_encoding, mb_detect_encoding, + mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen; /** - * Kanji mode: double-byte characters from the Shift JIS character set + * Kanji mode: 13-bit double-byte characters from the Shift-JIS character set * * ISO/IEC 18004:2000 Section 8.3.5 * ISO/IEC 18004:2000 Section 8.4.5 + * + * @see https://en.wikipedia.org/wiki/Shift_JIS#As_defined_in_JIS_X_0208:1997 + * @see http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml + * @see https://gist.github.com/codemasher/d07d3e6e9346c08e7a41b8b978784952 */ -final class Kanji extends QRDataAbstract{ +final class Kanji extends QRDataModeAbstract{ - protected int $datamode = QRCode::DATA_KANJI; + /** + * possible values: SJIS, SJIS-2004 + * + * SJIS-2004 may produce errors in PHP < 8 + * + * @var string + */ + public const ENCODING = 'SJIS'; - protected array $lengthBits = [8, 10, 12]; + /** + * @inheritDoc + */ + public const DATAMODE = Mode::KANJI; + + /** + * @inheritDoc + */ + protected function getCharCount():int{ + return mb_strlen($this->data, self::ENCODING); + } + + /** + * @inheritDoc + */ + public function getLengthInBits():int{ + return ($this->getCharCount() * 13); + } /** - * @inheritdoc + * @inheritDoc + * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - protected function getLength(string $data):int{ - return mb_strlen($data, 'SJIS'); + public static function convertEncoding(string $string):string{ + mb_detect_order([mb_internal_encoding(), 'UTF-8', 'SJIS', 'SJIS-2004']); + + $detected = mb_detect_encoding($string, null, true); + + if($detected === false){ + throw new QRCodeDataException('mb_detect_encoding error'); + } + + if($detected === self::ENCODING){ + return $string; + } + + $string = mb_convert_encoding($string, self::ENCODING, $detected); + + if(!is_string($string)){ + throw new QRCodeDataException(sprintf('invalid encoding: %s', $detected)); + } + + return $string; } /** - * @inheritdoc + * checks if a string qualifies as SJIS Kanji + */ + public static function validateString(string $string):bool{ + + try{ + $string = self::convertEncoding($string); + } + catch(Throwable $e){ + return false; + } + + $len = strlen($string); + + if($len < 2 || ($len % 2) !== 0){ + return false; + } + + for($i = 0; $i < $len; $i += 2){ + $byte1 = ord($string[$i]); + $byte2 = ord($string[($i + 1)]); + + // byte 1 unused and vendor ranges + if($byte1 < 0x81 || ($byte1 > 0x84 && $byte1 < 0x88) || ($byte1 > 0x9f && $byte1 < 0xe0) || $byte1 > 0xea){ + return false; + } + + // byte 2 unused ranges + if($byte2 < 0x40 || $byte2 === 0x7f || $byte2 > 0xfc){ + return false; + } + + } + + return true; + } + + /** + * @inheritDoc * * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence */ - protected function write(string $data):void{ - $len = strlen($data); + public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{ + + $bitBuffer + ->put(self::DATAMODE, 4) + ->put($this->getCharCount(), $this::getLengthBits($versionNumber)) + ; + + $len = strlen($this->data); - for($i = 0; $i + 1 < $len; $i += 2){ - $c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1])); + 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)); + 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){ - throw new QRCodeDataException(sprintf('illegal char at %d', $i + 1)); + throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1))); + } + + return $this; + } + + /** + * @inheritDoc + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $length = $bitBuffer->read(self::getLengthBits($versionNumber)); + + if($bitBuffer->available() < ($length * 13)){ + throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore + } + + // Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as SJIS afterwards + $buffer = []; + $offset = 0; + + while($length > 0){ + // Each 13 bits encodes a 2-byte character + $twoBytes = $bitBuffer->read(13); + $assembledTwoBytes = ((intdiv($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(), self::ENCODING); } } diff --git a/vendor/chillerlan/php-qrcode/src/Data/MaskPatternTester.php b/vendor/chillerlan/php-qrcode/src/Data/MaskPatternTester.php deleted file mode 100644 index 7874cb53d..000000000 --- a/vendor/chillerlan/php-qrcode/src/Data/MaskPatternTester.php +++ /dev/null @@ -1,203 +0,0 @@ -<?php -/** - * Class MaskPatternTester - * - * @filesource MaskPatternTester.php - * @created 22.11.2017 - * @package chillerlan\QRCode\Data - * @author Smiley <smiley@chillerlan.net> - * @copyright 2017 Smiley - * @license MIT - * - * @noinspection PhpUnused - */ - -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. - * - * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results - * - * @see http://www.thonky.com/qr-code-tutorial/data-masking - */ -final class MaskPatternTester{ - - /** - * The data interface that contains the data matrix to test - */ - protected QRDataInterface $dataInterface; - - /** - * Receives the QRDataInterface - * - * @see \chillerlan\QRCode\QROptions::$maskPattern - * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern - */ - public function __construct(QRDataInterface $dataInterface){ - $this->dataInterface = $dataInterface; - } - - /** - * shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern - * - * @see \chillerlan\QRCode\Data\MaskPatternTester - */ - public function getBestMaskPattern():int{ - $penalties = []; - - for($pattern = 0; $pattern < 8; $pattern++){ - $penalties[$pattern] = $this->testPattern($pattern); - } - - return array_search(min($penalties), $penalties, true); - } - - /** - * Returns the penalty for the given mask pattern - * - * @see \chillerlan\QRCode\QROptions::$maskPattern - * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern - */ - public function testPattern(int $pattern):int{ - $matrix = $this->dataInterface->initMatrix($pattern, true); - $penalty = 0; - - for($level = 1; $level <= 4; $level++){ - $penalty += call_user_func_array([$this, 'testLevel'.$level], [$matrix->matrix(true), $matrix->size()]); - } - - return (int)$penalty; - } - - /** - * Checks for each group of five or more same-colored modules in a row (or column) - */ - protected function testLevel1(array $m, int $size):int{ - $penalty = 0; - - foreach($m as $y => $row){ - foreach($row as $x => $val){ - $count = 0; - - for($ry = -1; $ry <= 1; $ry++){ - - if($y + $ry < 0 || $size <= $y + $ry){ - continue; - } - - for($rx = -1; $rx <= 1; $rx++){ - - if(($ry === 0 && $rx === 0) || (($x + $rx) < 0 || $size <= ($x + $rx))){ - continue; - } - - if($m[$y + $ry][$x + $rx] === $val){ - $count++; - } - - } - } - - if($count > 5){ - $penalty += (3 + $count - 5); - } - - } - } - - return $penalty; - } - - /** - * Checks for each 2x2 area of same-colored modules in the matrix - */ - protected function testLevel2(array $m, int $size):int{ - $penalty = 0; - - foreach($m as $y => $row){ - - if($y > $size - 2){ - break; - } - - foreach($row as $x => $val){ - - if($x > $size - 2){ - break; - } - - if( - $val === $m[$y][$x + 1] - && $val === $m[$y + 1][$x] - && $val === $m[$y + 1][$x + 1] - ){ - $penalty++; - } - } - } - - return 3 * $penalty; - } - - /** - * 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{ - $penalties = 0; - - foreach($m as $y => $row){ - foreach($row as $x => $val){ - - if( - $x + 6 < $size - && $val - && !$m[$y][$x + 1] - && $m[$y][$x + 2] - && $m[$y][$x + 3] - && $m[$y][$x + 4] - && !$m[$y][$x + 5] - && $m[$y][$x + 6] - ){ - $penalties++; - } - - if( - $y + 6 < $size - && $val - && !$m[$y + 1][$x] - && $m[$y + 2][$x] - && $m[$y + 3][$x] - && $m[$y + 4][$x] - && !$m[$y + 5][$x] - && $m[$y + 6][$x] - ){ - $penalties++; - } - - } - } - - return $penalties * 40; - } - - /** - * 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{ - $count = 0; - - foreach($m as $y => $row){ - foreach($row as $x => $val){ - if($val){ - $count++; - } - } - } - - return (abs(100 * $count / $size / $size - 50) / 5) * 10; - } - -} diff --git a/vendor/chillerlan/php-qrcode/src/Data/Number.php b/vendor/chillerlan/php-qrcode/src/Data/Number.php index 0a905b13e..3e4238fdc 100644 --- a/vendor/chillerlan/php-qrcode/src/Data/Number.php +++ b/vendor/chillerlan/php-qrcode/src/Data/Number.php @@ -2,9 +2,7 @@ /** * Class Number * - * @filesource Number.php * @created 26.11.2015 - * @package chillerlan\QRCode\Data * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -12,9 +10,8 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\QRCode; - -use function ord, sprintf, str_split, substr; +use chillerlan\QRCode\Common\{BitBuffer, Mode}; +use function ceil, intdiv, substr, unpack; /** * Numeric mode: decimal digits 0 to 9 @@ -22,56 +19,142 @@ 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{ - protected int $datamode = QRCode::DATA_NUMBER; + /** + * @inheritDoc + */ + public const DATAMODE = Mode::NUMBER; + + /** + * @inheritDoc + */ + public function getLengthInBits():int{ + return (int)ceil($this->getCharCount() * (10 / 3)); + } - protected array $lengthBits = [10, 12, 14]; + /** + * @inheritDoc + */ + public static function validateString(string $string):bool{ + return (bool)preg_match('/^\d+$/', $string); + } /** - * @inheritdoc + * @inheritDoc */ - protected function write(string $data):void{ + public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{ + $len = $this->getCharCount(); + + $bitBuffer + ->put(self::DATAMODE, 4) + ->put($len, $this::getLengthBits($versionNumber)) + ; + $i = 0; - while($i + 2 < $this->strlen){ - $this->bitBuffer->put($this->parseInt(substr($data, $i, 3)), 10); + // encode numeric triplets in 10 bits + while(($i + 2) < $len){ + $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); + // encode 2 remaining numbers in 7 bits + if(($len - $i) === 2){ + $bitBuffer->put($this->parseInt(substr($this->data, $i, 2)), 7); } - elseif($this->strlen - $i === 2){ - $this->bitBuffer->put($this->parseInt(substr($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); } } + return $this; } /** * get the code for the given numeric string * - * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence + * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - protected function parseInt(string $string):int{ + private function parseInt(string $string):int{ $num = 0; - foreach(str_split($string) as $chr){ - $c = ord($chr); + $ords = unpack('C*', $string); - if(!isset($this::CHAR_MAP_NUMBER[$chr])){ - throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, $c)); - } + if($ords === false){ + throw new QRCodeDataException('unpack() error'); + } - $c = $c - 48; // ord('0') - $num = $num * 10 + $c; + foreach($ords as $ord){ + $num = ($num * 10 + $ord - 48); } return $num; } + /** + * @inheritDoc + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ + $length = $bitBuffer->read(self::getLengthBits($versionNumber)); + $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'); // @codeCoverageIgnore + } + + $threeDigitsBits = $bitBuffer->read(10); + + if($threeDigitsBits >= 1000){ + throw new QRCodeDataException('error decoding numeric value'); + } + + $result .= intdiv($threeDigitsBits, 100); + $result .= (intdiv($threeDigitsBits, 10) % 10); + $result .= ($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'); // @codeCoverageIgnore + } + + $twoDigitsBits = $bitBuffer->read(7); + + if($twoDigitsBits >= 100){ + throw new QRCodeDataException('error decoding numeric value'); + } + + $result .= intdiv($twoDigitsBits, 10); + $result .= ($twoDigitsBits % 10); + } + elseif($length === 1){ + // One digit left over to read + if($bitBuffer->available() < 4){ + throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore + } + + $digitBits = $bitBuffer->read(4); + + if($digitBits >= 10){ + throw new QRCodeDataException('error decoding numeric value'); + } + + $result .= $digitBits; + } + + return $result; + } + } diff --git a/vendor/chillerlan/php-qrcode/src/Data/QRCodeDataException.php b/vendor/chillerlan/php-qrcode/src/Data/QRCodeDataException.php index 862f57ba0..04ffbd7ed 100644 --- a/vendor/chillerlan/php-qrcode/src/Data/QRCodeDataException.php +++ b/vendor/chillerlan/php-qrcode/src/Data/QRCodeDataException.php @@ -2,9 +2,7 @@ /** * Class QRCodeDataException * - * @filesource QRCodeDataException.php * @created 09.12.2015 - * @package chillerlan\QRCode\Data * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -14,4 +12,9 @@ namespace chillerlan\QRCode\Data; use chillerlan\QRCode\QRCodeException; -class QRCodeDataException extends QRCodeException{} +/** + * An exception container + */ +final class QRCodeDataException extends QRCodeException{ + +} diff --git a/vendor/chillerlan/php-qrcode/src/Data/QRData.php b/vendor/chillerlan/php-qrcode/src/Data/QRData.php new file mode 100644 index 000000000..9d610b357 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Data/QRData.php @@ -0,0 +1,263 @@ +<?php +/** + * Class QRData + * + * @created 25.11.2015 + * @author Smiley <smiley@chillerlan.net> + * @copyright 2015 Smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, Version}; +use chillerlan\Settings\SettingsContainerInterface; +use function count, sprintf; + +/** + * Processes the binary data and maps it on a QRMatrix which is then being returned + */ +final class QRData{ + + /** + * the options instance + * + * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions + */ + private SettingsContainerInterface $options; + + /** + * a BitBuffer instance + */ + private BitBuffer $bitBuffer; + + /** + * an EccLevel instance + */ + private EccLevel $eccLevel; + + /** + * current QR Code version + */ + private Version $version; + + /** + * @var \chillerlan\QRCode\Data\QRDataModeInterface[] + */ + private array $dataSegments = []; + + /** + * Max bits for the current ECC mode + * + * @var int[] + */ + private array $maxBitsForEcc; + + /** + * QRData constructor. + */ + public function __construct(SettingsContainerInterface $options, array $dataSegments = []){ + $this->options = $options; + $this->bitBuffer = new BitBuffer; + $this->eccLevel = new EccLevel($this->options->eccLevel); + $this->maxBitsForEcc = $this->eccLevel->getMaxBits(); + + $this->setData($dataSegments); + } + + /** + * Sets the data string (internally called by the constructor) + * + * Subsequent calls will overwrite the current state - use the QRCode::add*Segement() method instead + * + * @param \chillerlan\QRCode\Data\QRDataModeInterface[] $dataSegments + */ + public function setData(array $dataSegments):self{ + $this->dataSegments = $dataSegments; + $this->version = $this->getMinimumVersion(); + + $this->bitBuffer->clear(); + $this->writeBitBuffer(); + + return $this; + } + + /** + * Returns the current BitBuffer instance + * + * @codeCoverageIgnore + */ + public function getBitBuffer():BitBuffer{ + return $this->bitBuffer; + } + + /** + * Sets a BitBuffer object + * + * This can be used instead of setData(), however, the version auto-detection is not available in this case. + * The version needs to match the length bits range for the data mode the data has been encoded with, + * additionally the bit array needs to contain enough pad bits. + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function setBitBuffer(BitBuffer $bitBuffer):self{ + + if($this->options->version === Version::AUTO){ + throw new QRCodeDataException('version auto detection is not available'); + } + + if($bitBuffer->getLength() === 0){ + throw new QRCodeDataException('the given BitBuffer is empty'); + } + + $this->dataSegments = []; + $this->bitBuffer = $bitBuffer; + $this->version = new Version($this->options->version); + + return $this; + } + + /** + * returns a fresh matrix object with the data written and masked with the given $maskPattern + */ + public function writeMatrix():QRMatrix{ + return (new QRMatrix($this->version, $this->eccLevel)) + ->initFunctionalPatterns() + ->writeCodewords($this->bitBuffer) + ; + } + + /** + * estimates the total length of the several mode segments in order to guess the minimum version + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function estimateTotalBitLength():int{ + $length = 0; + + foreach($this->dataSegments as $segment){ + // data length of the current segment + $length += $segment->getLengthInBits(); + // +4 bits for the mode descriptor + $length += 4; + // Hanzi mode sets an additional 4 bit long subset identifier + if($segment instanceof Hanzi){ + $length += 4; + } + } + + $provisionalVersion = null; + + foreach($this->maxBitsForEcc as $version => $maxBits){ + + if($length <= $maxBits){ + $provisionalVersion = $version; + } + + } + + if($provisionalVersion !== null){ + + // add character count indicator bits for the provisional version + foreach($this->dataSegments as $segment){ + $length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion); + } + + // it seems that in some cases the estimated total length is not 100% accurate, + // so we substract 4 bits from the total when not in mixed mode + if(count($this->dataSegments) <= 1){ + $length -= 4; + } + + // we've got a match! + // or let's see if there's a higher version number available + if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){ + 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 + */ + public function getMinimumVersion():Version{ + + if($this->options->version !== Version::AUTO){ + return new Version($this->options->version); + } + + $total = $this->estimateTotalBitLength(); + + // guess the version number within the given range + for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){ + if($total <= ($this->maxBitsForEcc[$version] - 4)){ + return new Version($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\Data\QRCodeDataException on data overflow + */ + private function writeBitBuffer():void{ + $MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version); + + foreach($this->dataSegments as $segment){ + $segment->write($this->bitBuffer, $this->version->getVersionNumber()); + } + + // 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(Mode::TERMINATOR, 4); + } + + // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion + + // if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long + // by the addition of padding bits with binary value 0 + while(($this->bitBuffer->getLength() % 8) !== 0){ + + if($this->bitBuffer->getLength() === $MAX_BITS){ + break; + } + + $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. + $alternate = false; + + while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){ + $this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8); + + $alternate = !$alternate; + } + + // In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros) + // to the end of the message in order exactly to fill the symbol capacity + while($this->bitBuffer->getLength() <= $MAX_BITS){ + $this->bitBuffer->putBit(false); + } + + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Data/QRDataAbstract.php b/vendor/chillerlan/php-qrcode/src/Data/QRDataAbstract.php deleted file mode 100644 index 72b67b7b9..000000000 --- a/vendor/chillerlan/php-qrcode/src/Data/QRDataAbstract.php +++ /dev/null @@ -1,311 +0,0 @@ -<?php -/** - * Class QRDataAbstract - * - * @filesource QRDataAbstract.php - * @created 25.11.2015 - * @package chillerlan\QRCode\Data - * @author Smiley <smiley@chillerlan.net> - * @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/vendor/chillerlan/php-qrcode/src/Data/QRDataInterface.php b/vendor/chillerlan/php-qrcode/src/Data/QRDataInterface.php deleted file mode 100644 index 93ad6221d..000000000 --- a/vendor/chillerlan/php-qrcode/src/Data/QRDataInterface.php +++ /dev/null @@ -1,200 +0,0 @@ -<?php -/** - * Interface QRDataInterface - * - * @filesource QRDataInterface.php - * @created 01.12.2015 - * @package chillerlan\QRCode\Data - * @author Smiley <smiley@chillerlan.net> - * @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/vendor/chillerlan/php-qrcode/src/Data/QRDataModeAbstract.php b/vendor/chillerlan/php-qrcode/src/Data/QRDataModeAbstract.php new file mode 100644 index 000000000..94b93ac0e --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Data/QRDataModeAbstract.php @@ -0,0 +1,61 @@ +<?php +/** + * Class QRDataModeAbstract + * + * @created 19.11.2020 + * @author smiley <smiley@chillerlan.net> + * @copyright 2020 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\Common\Mode; + +/** + * abstract methods for the several data modes + */ +abstract class QRDataModeAbstract implements QRDataModeInterface{ + + /** + * The data to write + */ + protected string $data; + + /** + * QRDataModeAbstract constructor. + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function __construct(string $data){ + $data = $this::convertEncoding($data); + + if(!$this::validateString($data)){ + throw new QRCodeDataException('invalid data'); + } + + $this->data = $data; + } + + /** + * returns the character count of the $data string + */ + protected function getCharCount():int{ + return strlen($this->data); + } + + /** + * @inheritDoc + */ + public static function convertEncoding(string $string):string{ + return $string; + } + + /** + * shortcut + */ + protected static function getLengthBits(int $versionNumber):int{ + return Mode::getLengthBitsForVersion(static::DATAMODE, $versionNumber); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Data/QRDataModeInterface.php b/vendor/chillerlan/php-qrcode/src/Data/QRDataModeInterface.php new file mode 100644 index 000000000..321cf60b6 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Data/QRDataModeInterface.php @@ -0,0 +1,63 @@ +<?php +/** + * Interface QRDataModeInterface + * + * @created 01.12.2015 + * @author Smiley <smiley@chillerlan.net> + * @copyright 2015 Smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\Common\BitBuffer; + +/** + * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji) + */ +interface QRDataModeInterface{ + + /** + * the current data mode: Number, Alphanum, Kanji, Hanzi, Byte, ECI + * + * tbh I hate this constant here, but it's part of the interface, so I can't just declare it in the abstract class. + * (phan will complain about a PhanAccessOverridesFinalConstant) + * + * @see https://wiki.php.net/rfc/final_class_const + * + * @var int + * @see \chillerlan\QRCode\Common\Mode + * @internal do not call this constant from the interface, but rather from one of the child classes + */ + public const DATAMODE = -1; + + /** + * retruns the length in bits of the data string + */ + public function getLengthInBits():int; + + /** + * encoding conversion helper + * + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public static function convertEncoding(string $string):string; + + /** + * 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(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface; + + /** + * reads a segment from the BitBuffer and decodes in the current data mode + */ + public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string; + +} diff --git a/vendor/chillerlan/php-qrcode/src/Data/QRMatrix.php b/vendor/chillerlan/php-qrcode/src/Data/QRMatrix.php index 05c8b9069..e32633a99 100755 --- a/vendor/chillerlan/php-qrcode/src/Data/QRMatrix.php +++ b/vendor/chillerlan/php-qrcode/src/Data/QRMatrix.php @@ -2,9 +2,7 @@ /** * Class QRMatrix * - * @filesource QRMatrix.php * @created 15.11.2017 - * @package chillerlan\QRCode\Data * @author Smiley <smiley@chillerlan.net> * @copyright 2017 Smiley * @license MIT @@ -12,207 +10,124 @@ namespace chillerlan\QRCode\Data; -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 chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Version}; +use function array_fill, array_map, array_reverse, count, intdiv; /** - * Holds a numerical representation of the final QR Code; + * Holds an array representation of the final QR Code that contains numerical values for later output modifications; * maps the ECC coded binary data and applies the mask pattern * * @see http://www.thonky.com/qr-code-tutorial/format-version-information */ -final class QRMatrix{ +class QRMatrix{ + + /* + * special values + */ /** @var int */ - public const M_NULL = 0x00; + public const IS_DARK = 0b100000000000; + /** @var int */ + public const M_NULL = 0b000000000000; + /** @var int */ + public const M_LOGO = 0b001000000000; + /** @var int */ + public const M_LOGO_DARK = 0b101000000000; + + /* + * light values + */ + + /** @var int */ + public const M_DATA = 0b000000000010; + /** @var int */ + public const M_FINDER = 0b000000000100; + /** @var int */ + public const M_SEPARATOR = 0b000000001000; + /** @var int */ + public const M_ALIGNMENT = 0b000000010000; /** @var int */ - public const M_DARKMODULE = 0x02; + public const M_TIMING = 0b000000100000; /** @var int */ - public const M_DATA = 0x04; + public const M_FORMAT = 0b000001000000; /** @var int */ - public const M_FINDER = 0x06; + public const M_VERSION = 0b000010000000; /** @var int */ - public const M_SEPARATOR = 0x08; + public const M_QUIETZONE = 0b000100000000; + + /* + * dark values + */ + /** @var int */ - public const M_ALIGNMENT = 0x0a; + public const M_DARKMODULE = 0b100000000001; /** @var int */ - public const M_TIMING = 0x0c; + public const M_DATA_DARK = 0b100000000010; /** @var int */ - public const M_FORMAT = 0x0e; + public const M_FINDER_DARK = 0b100000000100; /** @var int */ - public const M_VERSION = 0x10; + public const M_ALIGNMENT_DARK = 0b100000010000; /** @var int */ - public const M_QUIETZONE = 0x12; + public const M_TIMING_DARK = 0b100000100000; /** @var int */ - public const M_LOGO = 0x14; + public const M_FORMAT_DARK = 0b100001000000; /** @var int */ - public const M_FINDER_DOT = 0x16; + public const M_VERSION_DARK = 0b100010000000; /** @var int */ - public const M_TEST = 0xff; + public const M_FINDER_DOT = 0b110000000000; - /** - * ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns - * - * version -> pattern - * - * @var int[][] + /* + * values used for reversed reflectance */ - 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, - ]; + /** @var int */ + public const M_DARKMODULE_LIGHT = 0b000000000001; + /** @var int */ + public const M_FINDER_DOT_LIGHT = 0b010000000000; + /** @var int */ + public const M_SEPARATOR_DARK = 0b100000001000; + /** @var int */ + public const M_QUIETZONE_DARK = 0b100100000000; /** - * ISO/IEC 18004:2000 Section 8.9 - Format Information + * Map of flag => coord * - * ECC level -> mask pattern + * @see \chillerlan\QRCode\Data\QRMatrix::checkNeighbours() * - * @var int[][] + * @var array */ - 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, - ], + protected const neighbours = [ + 0b00000001 => [-1, -1], + 0b00000010 => [ 0, -1], + 0b00000100 => [ 1, -1], + 0b00001000 => [ 1, 0], + 0b00010000 => [ 1, 1], + 0b00100000 => [ 0, 1], + 0b01000000 => [-1, 1], + 0b10000000 => [-1, 0], ]; /** - * the current QR Code version number + * the matrix version - always set in QRMatrix, may be null in BitMatrix */ - protected int $version; + protected ?Version $version = null; /** - * the current ECC level + * the current ECC level - always set in QRMatrix, may be null in BitMatrix */ - protected int $eclevel; + protected ?EccLevel $eccLevel = null; /** - * the used mask pattern, set via QRMatrix::mapData() + * the mask pattern that was used in the most recent operation, set via: + * + * - QRMatrix::setFormatInfo() + * - QRMatrix::mask() + * - BitMatrix::readFormatInformation() */ - protected int $maskPattern = QRCode::MASK_PATTERN_AUTO; + protected ?MaskPattern $maskPattern = null; /** - * the size (side length) of the matrix + * the size (side length) of the matrix, including quiet zone (if created) */ protected int $moduleCount; @@ -225,37 +140,33 @@ final class QRMatrix{ /** * 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'); - } - - if(!array_key_exists($eclevel, QRCode::ECC_MODES)){ - throw new QRCodeDataException('invalid ecc level'); - } - + public function __construct(Version $version, EccLevel $eccLevel){ $this->version = $version; - $this->eclevel = $eclevel; - $this->moduleCount = $this->version * 4 + 17; - $this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL)); + $this->eccLevel = $eccLevel; + $this->moduleCount = $this->version->getDimension(); + $this->matrix = $this->createMatrix($this->moduleCount, $this::M_NULL); } /** - * shortcut to initialize the matrix + * Creates a 2-dimensional array (square) of the given $size */ - public function init(int $maskPattern, bool $test = null):QRMatrix{ + protected function createMatrix(int $size, int $value):array{ + return array_fill(0, $size, array_fill(0, $size, $value)); + } + + /** + * shortcut to initialize the functional patterns + */ + public function initFunctionalPatterns():self{ return $this ->setFinderPattern() ->setSeparators() ->setAlignmentPattern() ->setTimingPattern() - ->setVersionNumber($test) - ->setFormatInfo($maskPattern, $test) ->setDarkModule() + ->setVersionNumber() + ->setFormatInfo() ; } @@ -264,93 +175,245 @@ final class QRMatrix{ * * @return int[][]|bool[][] */ - public function matrix(bool $boolean = false):array{ + public function getMatrix(?bool $boolean = null):array{ - if(!$boolean){ + if($boolean !== true){ return $this->matrix; } - $matrix = []; + $matrix = $this->matrix; - foreach($this->matrix as $y => $row){ - $matrix[$y] = []; - - foreach($row as $x => $val){ - $matrix[$y][$x] = ($val >> 8) > 0; - } + foreach($matrix as &$row){ + $row = array_map([$this, 'isDark'], $row); } return $matrix; } /** + * @deprecated 5.0.0 use QRMatrix::getMatrix() instead + * @see \chillerlan\QRCode\Data\QRMatrix::getMatrix() + * @codeCoverageIgnore + */ + public function matrix(?bool $boolean = null):array{ + return $this->getMatrix($boolean); + } + + /** * Returns the current version number */ - public function version():int{ + public function getVersion():?Version{ return $this->version; } /** + * @deprecated 5.0.0 use QRMatrix::getVersion() instead + * @see \chillerlan\QRCode\Data\QRMatrix::getVersion() + * @codeCoverageIgnore + */ + public function version():?Version{ + return $this->getVersion(); + } + + /** * Returns the current ECC level */ - public function eccLevel():int{ - return $this->eclevel; + public function getEccLevel():?EccLevel{ + return $this->eccLevel; + } + + /** + * @deprecated 5.0.0 use QRMatrix::getEccLevel() instead + * @see \chillerlan\QRCode\Data\QRMatrix::getEccLevel() + * @codeCoverageIgnore + */ + public function eccLevel():?EccLevel{ + return $this->getEccLevel(); } /** * Returns the current mask pattern */ - public function maskPattern():int{ + public function getMaskPattern():?MaskPattern{ return $this->maskPattern; } /** + * @deprecated 5.0.0 use QRMatrix::getMaskPattern() instead + * @see \chillerlan\QRCode\Data\QRMatrix::getMaskPattern() + * @codeCoverageIgnore + */ + public function maskPattern():?MaskPattern{ + return $this->getMaskPattern(); + } + + /** * Returns the absoulute size of the matrix, including quiet zone (after setting it). * * size = version * 4 + 17 [ + 2 * quietzone size] */ - public function size():int{ + public function getSize():int{ return $this->moduleCount; } /** - * Returns the value of the module at position [$x, $y] + * @deprecated 5.0.0 use QRMatrix::getSize() instead + * @see \chillerlan\QRCode\Data\QRMatrix::getSize() + * @codeCoverageIgnore + */ + public function size():int{ + return $this->getSize(); + } + + /** + * Returns the value of the module at position [$x, $y] or -1 if the coordinate is outside the matrix */ public function get(int $x, int $y):int{ + + if(!isset($this->matrix[$y][$x])){ + return -1; + } + return $this->matrix[$y][$x]; } /** * 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); + public function set(int $x, int $y, bool $value, int $M_TYPE):self{ + + if(isset($this->matrix[$y][$x])){ + // we don't know whether the input is dark, so we remove the dark bit + $M_TYPE &= ~$this::IS_DARK; + + if($value === true){ + $M_TYPE |= $this::IS_DARK; + } + + $this->matrix[$y][$x] = $M_TYPE; + } return $this; } /** - * Checks whether a module is true (dark) or false (light) + * Fills an area of $width * $height, from the given starting point [$startX, $startY] (top left) with $value for $M_TYPE. + */ + public function setArea(int $startX, int $startY, int $width, int $height, bool $value, int $M_TYPE):self{ + + for($y = $startY; $y < ($startY + $height); $y++){ + for($x = $startX; $x < ($startX + $width); $x++){ + $this->set($x, $y, $value, $M_TYPE); + } + } + + return $this; + } + + /** + * Flips the value of the module at ($x, $y) + */ + public function flip(int $x, int $y):self{ + + if(isset($this->matrix[$y][$x])){ + $this->matrix[$y][$x] ^= $this::IS_DARK; + } + + return $this; + } + + /** + * Checks whether the module at ($x, $y) is of the given $M_TYPE * - * true => $value >> 8 === $M_TYPE - * $value >> 8 > 0 + * true => $value & $M_TYPE === $M_TYPE * - * false => $value === $M_TYPE - * $value >> 8 === 0 + * Also, returns false if the given coordinates are out of range. + */ + public function checkType(int $x, int $y, int $M_TYPE):bool{ + + if(isset($this->matrix[$y][$x])){ + return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE; + } + + return false; + } + + /** + * Checks whether the module at ($x, $y) is in the given array of $M_TYPES, + * returns true if a match is found, otherwise false. + */ + public function checkTypeIn(int $x, int $y, array $M_TYPES):bool{ + + foreach($M_TYPES as $type){ + if($this->checkType($x, $y, $type)){ + return true; + } + } + + return false; + } + + /** + * Checks whether the module at ($x, $y) is true (dark) or false (light) + * + * Also, returns false if the given coordinates are out of range. */ public function check(int $x, int $y):bool{ - return ($this->matrix[$y][$x] >> 8) > 0; + + if(isset($this->matrix[$y][$x])){ + return $this->isDark($this->matrix[$y][$x]); + } + + return false; } + /** + * Checks whether the given $M_TYPE is a dark value + */ + public function isDark(int $M_TYPE):bool{ + return ($M_TYPE & $this::IS_DARK) === $this::IS_DARK; + } + + /** + * Checks the status of the neighbouring modules for the module at ($x, $y) and returns a bitmask with the results. + * + * The 8 flags of the bitmask represent the status of each of the neighbouring fields, + * starting with the lowest bit for top left, going clockwise: + * + * 0 1 2 + * 7 # 3 + * 6 5 4 + */ + public function checkNeighbours(int $x, int $y, ?int $M_TYPE = null):int{ + $bits = 0; + + foreach($this::neighbours as $bit => [$ix, $iy]){ + $ix += $x; + $iy += $y; + + // $M_TYPE is given, skip if the field is not the same type + if($M_TYPE !== null && !$this->checkType($ix, $iy, $M_TYPE)){ + continue; + } + + if($this->checkType($ix, $iy, $this::IS_DARK)){ + $bits |= $bit; + } + } + + return $bits; + } /** * Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder + * + * 4 * version + 9 or moduleCount - 8 */ - public function setDarkModule():QRMatrix{ - $this->set(8, 4 * $this->version + 9, true, $this::M_DARKMODULE); + public function setDarkModule():self{ + $this->set(8, ($this->moduleCount - 8), true, $this::M_DARKMODULE); return $this; } @@ -360,31 +423,20 @@ final class QRMatrix{ * * ISO/IEC 18004:2000 Section 7.3.2 */ - public function setFinderPattern():QRMatrix{ + public function setFinderPattern():self{ $pos = [ [0, 0], // top left - [$this->moduleCount - 7, 0], // bottom left - [0, $this->moduleCount - 7], // top right + [($this->moduleCount - 7), 0], // top right + [0, ($this->moduleCount - 7)], // bottom left ]; foreach($pos as $c){ - for($y = 0; $y < 7; $y++){ - for($x = 0; $x < 7; $x++){ - // outer (dark) 7*7 square - if($x === 0 || $x === 6 || $y === 0 || $y === 6){ - $this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER); - } - // inner (light) 5*5 square - elseif($x === 1 || $x === 5 || $y === 1 || $y === 5){ - $this->set($c[0] + $y, $c[1] + $x, false, $this::M_FINDER); - } - // 3*3 dot - else{ - $this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER_DOT); - } - } - } + $this + ->setArea( $c[0] , $c[1] , 7, 7, true, $this::M_FINDER) + ->setArea(($c[0] + 1), ($c[1] + 1), 5, 5, false, $this::M_FINDER) + ->setArea(($c[0] + 2), ($c[1] + 2), 3, 3, true, $this::M_FINDER_DOT) + ; } return $this; @@ -395,24 +447,25 @@ final class QRMatrix{ * * ISO/IEC 18004:2000 Section 7.3.3 */ - public function setSeparators():QRMatrix{ + public function setSeparators():self{ $h = [ [7, 0], - [$this->moduleCount - 8, 0], - [7, $this->moduleCount - 8], + [($this->moduleCount - 8), 0], + [7, ($this->moduleCount - 8)], ]; $v = [ [7, 7], - [$this->moduleCount - 1, 7], - [7, $this->moduleCount - 8], + [($this->moduleCount - 1), 7], + [7, ($this->moduleCount - 8)], ]; for($c = 0; $c < 3; $c++){ for($i = 0; $i < 8; $i++){ - $this->set($h[$c][0] , $h[$c][1] + $i, false, $this::M_SEPARATOR); - $this->set($v[$c][0] - $i, $v[$c][1] , false, $this::M_SEPARATOR); + // phpcs:ignore + $this->set( $h[$c][0] , ($h[$c][1] + $i), false, $this::M_SEPARATOR); + $this->set(($v[$c][0] - $i), $v[$c][1] , false, $this::M_SEPARATOR); } } @@ -425,23 +478,22 @@ final class QRMatrix{ * * ISO/IEC 18004:2000 Section 7.3.5 */ - public function setAlignmentPattern():QRMatrix{ + public function setAlignmentPattern():self{ + $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){ continue; } - for($ry = -2; $ry <= 2; $ry++){ - for($rx = -2; $rx <= 2; $rx++){ - $v = ($ry === 0 && $rx === 0) || $ry === 2 || $ry === -2 || $rx === 2 || $rx === -2; - - $this->set($x + $rx, $y + $ry, $v, $this::M_ALIGNMENT); - } - } + $this + ->setArea(($x - 2), ($y - 2), 5, 5, true, $this::M_ALIGNMENT) + ->setArea(($x - 1), ($y - 1), 3, 3, false, $this::M_ALIGNMENT) + ->set($x, $y, true, $this::M_ALIGNMENT) + ; } } @@ -455,15 +507,15 @@ final class QRMatrix{ * * ISO/IEC 18004:2000 Section 7.3.4 */ - public function setTimingPattern():QRMatrix{ + public function setTimingPattern():self{ - foreach(range(8, $this->moduleCount - 8 - 1) as $i){ + for($i = 8; $i < ($this->moduleCount - 8); $i++){ if($this->matrix[6][$i] !== $this::M_NULL || $this->matrix[$i][6] !== $this::M_NULL){ continue; } - $v = $i % 2 === 0; + $v = ($i % 2) === 0; $this->set($i, 6, $v, $this::M_TIMING); // h $this->set(6, $i, $v, $this::M_TIMING); // v @@ -477,15 +529,15 @@ final class QRMatrix{ * * ISO/IEC 18004:2000 Section 8.10 */ - public function setVersionNumber(bool $test = null):QRMatrix{ - $bits = $this::versionPattern[$this->version] ?? false; + public function setVersionNumber():self{ + $bits = $this->version->getVersionPattern(); - if($bits !== false){ + if($bits !== null){ for($i = 0; $i < 18; $i++){ - $a = (int)floor($i / 3); - $b = $i % 3 + $this->moduleCount - 8 - 3; - $v = !$test && (($bits >> $i) & 1) === 1; + $a = intdiv($i, 3); + $b = (($i % 3) + ($this->moduleCount - 8 - 3)); + $v = (($bits >> $i) & 1) === 1; $this->set($b, $a, $v, $this::M_VERSION); // ne $this->set($a, $b, $v, $this::M_VERSION); // sw @@ -497,40 +549,43 @@ final class QRMatrix{ } /** - * Draws the format info along the finder patterns + * Draws the format info along the finder patterns. If no $maskPattern, all format info modules will be set to false. * * 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; + public function setFormatInfo(?MaskPattern $maskPattern = null):self{ + $this->maskPattern = $maskPattern; + $bits = 0; // sets all format fields to false (test mode) + + if($this->maskPattern instanceof MaskPattern){ + $bits = $this->eccLevel->getformatPattern($this->maskPattern); + } for($i = 0; $i < 15; $i++){ - $v = !$test && (($bits >> $i) & 1) === 1; + $v = (($bits >> $i) & 1) === 1; if($i < 6){ $this->set(8, $i, $v, $this::M_FORMAT); } elseif($i < 8){ - $this->set(8, $i + 1, $v, $this::M_FORMAT); + $this->set(8, ($i + 1), $v, $this::M_FORMAT); } else{ - $this->set(8, $this->moduleCount - 15 + $i, $v, $this::M_FORMAT); + $this->set(8, ($this->moduleCount - 15 + $i), $v, $this::M_FORMAT); } if($i < 8){ - $this->set($this->moduleCount - $i - 1, 8, $v, $this::M_FORMAT); + $this->set(($this->moduleCount - $i - 1), 8, $v, $this::M_FORMAT); } elseif($i < 9){ - $this->set(15 - $i, 8, $v, $this::M_FORMAT); + $this->set(((15 - $i)), 8, $v, $this::M_FORMAT); } else{ - $this->set(15 - $i - 1, 8, $v, $this::M_FORMAT); + $this->set((15 - $i - 1), 8, $v, $this::M_FORMAT); } } - $this->set(8, $this->moduleCount - 8, !$test, $this::M_FORMAT); - return $this; } @@ -541,30 +596,62 @@ final class QRMatrix{ * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - public function setQuietZone(int $size = null):QRMatrix{ + public function setQuietZone(int $quietZoneSize):self{ + + // early exit if there's nothing to add + if($quietZoneSize < 1){ + return $this; + } - if($this->matrix[$this->moduleCount - 1][$this->moduleCount - 1] === $this::M_NULL){ + if($this->matrix[($this->moduleCount - 1)][($this->moduleCount - 1)] === $this::M_NULL){ throw new QRCodeDataException('use only after writing data'); } - $size = $size !== null - ? max(0, min($size, floor($this->moduleCount / 2))) - : 4; + // create a matrix with the new size + $newSize = ($this->moduleCount + ($quietZoneSize * 2)); + $newMatrix = $this->createMatrix($newSize, $this::M_QUIETZONE); - for($y = 0; $y < $this->moduleCount; $y++){ - for($i = 0; $i < $size; $i++){ - array_unshift($this->matrix[$y], $this::M_QUIETZONE); - array_push($this->matrix[$y], $this::M_QUIETZONE); + // copy over the current matrix + foreach($this->matrix as $y => $row){ + foreach($row as $x => $val){ + $newMatrix[($y + $quietZoneSize)][($x + $quietZoneSize)] = $val; } } - $this->moduleCount += ($size * 2); + // set the new values + $this->moduleCount = $newSize; + $this->matrix = $newMatrix; + + return $this; + } - $r = array_fill(0, $this->moduleCount, $this::M_QUIETZONE); + /** + * Rotates the matrix by 90 degrees clock wise + */ + public function rotate90():self{ + /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */ + $this->matrix = array_map((fn(int ...$a):array => array_reverse($a)), ...$this->matrix); - for($i = 0; $i < $size; $i++){ - array_unshift($this->matrix, $r); - array_push($this->matrix, $r); + return $this; + } + + /** + * Inverts the values of the whole matrix + * + * ISO/IEC 18004:2015 Section 6.2 - Reflectance reversal + */ + public function invert():self{ + + foreach($this->matrix as $y => $row){ + foreach($row as $x => $val){ + + // skip null fields + if($val === $this::M_NULL){ + continue; + } + + $this->flip($x, $y); + } } return $this; @@ -572,11 +659,15 @@ final class QRMatrix{ /** * Clears a space of $width * $height in order to add a logo or text. + * If no $height is given, the space will be assumed a square of $width. * - * Additionally, the logo space can be positioned within the QR Code - respecting the main functional patterns - - * using $startX and $startY. If either of these are null, the logo space will be centered in that direction. + * Additionally, the logo space can be positioned within the QR Code using $startX and $startY. + * If either of these are null, the logo space will be centered in that direction. * ECC level "H" (30%) is required. * + * The coordinates of $startX and $startY do not include the quiet zone: + * [0, 0] is always the top left module of the top left finder pattern, negative values go into the quiet zone top and left. + * * Please note that adding a logo space minimizes the error correction capacity of the QR Code and * created images may become unreadable, especially when printed with a chance to receive damage. * Please test thoroughly before using this feature in production. @@ -588,13 +679,27 @@ final class QRMatrix{ * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):QRMatrix{ + public function setLogoSpace(int $width, ?int $height = null, ?int $startX = null, ?int $startY = null):self{ + $height ??= $width; - // for logos we operate in ECC H (30%) only - if($this->eclevel !== QRCode::ECC_H){ + // if width and height happen to be negative or 0 (default value), just return - nothing to do + if($width <= 0 || $height <= 0){ + return $this; // @codeCoverageIgnore + } + + // for logos, we operate in ECC H (30%) only + if($this->eccLevel->getLevel() !== EccLevel::H){ throw new QRCodeDataException('ECC level "H" required to add logo space'); } + // $this->moduleCount includes the quiet zone (if created), we need the QR size here + $dimension = $this->version->getDimension(); + + // throw if the size exceeds the qrcode size + if($width > $dimension || $height > $dimension){ + throw new QRCodeDataException('logo dimensions exceed matrix size'); + } + // we need uneven sizes to center the logo space, adjust if needed if($startX === null && ($width % 2) === 0){ $width++; @@ -604,36 +709,29 @@ final class QRMatrix{ $height++; } - // $this->moduleCount includes the quiet zone (if created), we need the QR size here - $length = $this->version * 4 + 17; - // throw if the logo space exceeds the maximum error correction capacity - if($width * $height > floor($length * $length * 0.2)){ + if(($width * $height) > (int)($dimension * $dimension * 0.25)){ throw new QRCodeDataException('logo space exceeds the maximum error correction capacity'); } - // quiet zone size - $qz = ($this->moduleCount - $length) / 2; - // skip quiet zone and the first 9 rows/columns (finder-, mode-, version- and timing patterns) - $start = $qz + 9; - // skip quiet zone - $end = $this->moduleCount - $qz; + $quietzone = (($this->moduleCount - $dimension) / 2); + $end = ($this->moduleCount - $quietzone); // determine start coordinates - $startX = ($startX !== null ? $startX : ($length - $width) / 2) + $qz; - $startY = ($startY !== null ? $startY : ($length - $height) / 2) + $qz; + $startX ??= (($dimension - $width) / 2); + $startY ??= (($dimension - $height) / 2); + $endX = ($quietzone + $startX + $width); + $endY = ($quietzone + $startY + $height); // clear the space - foreach($this->matrix as $y => $row){ - foreach($row as $x => $val){ + for($y = ($quietzone + $startY); $y < $endY; $y++){ + for($x = ($quietzone + $startX); $x < $endX; $x++){ // out of bounds, skip - if($x < $start || $y < $start ||$x >= $end || $y >= $end){ + if($x < $quietzone || $y < $quietzone ||$x >= $end || $y >= $end){ continue; } - // a match - if($x >= $startX && $x < ($startX + $width) && $y >= $startY && $y < ($startY + $height)){ - $this->set($x, $y, false, $this::M_LOGO); - } + + $this->set($x, $y, false, $this::M_LOGO); } } @@ -641,100 +739,75 @@ final class QRMatrix{ } /** - * Maps the binary $data array from QRDataInterface::maskECC() on the matrix, - * masking the data using $maskPattern (ISO/IEC 18004:2000 Section 8.8) - * - * @see \chillerlan\QRCode\Data\QRDataAbstract::maskECC() - * - * @param int[] $data - * @param int $maskPattern - * - * @return \chillerlan\QRCode\Data\QRMatrix + * Maps the interleaved binary $data on the matrix */ - public function mapData(array $data, int $maskPattern):QRMatrix{ - $this->maskPattern = $maskPattern; - $byteCount = count($data); - $y = $this->moduleCount - 1; - $inc = -1; - $byteIndex = 0; - $bitIndex = 7; - $mask = $this->getMask($this->maskPattern); + public function writeCodewords(BitBuffer $bitBuffer):self{ + $data = (new ReedSolomonEncoder($this->version, $this->eccLevel))->interleaveEcBytes($bitBuffer); + $byteCount = count($data); + $iByte = 0; + $iBit = 7; + $direction = true; - for($i = $y; $i > 0; $i -= 2){ + for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){ + // skip vertical alignment pattern if($i === 6){ $i--; } - while(true){ - for($c = 0; $c < 2; $c++){ - $x = $i - $c; - - if($this->matrix[$y][$x] === $this::M_NULL){ - $v = false; - - if($byteIndex < $byteCount){ - $v = (($data[$byteIndex] >> $bitIndex) & 1) === 1; - } - - if($mask($x, $y) === 0){ - $v = !$v; - } + for($count = 0; $count < $this->moduleCount; $count++){ + $y = $count; - $this->matrix[$y][$x] = $this::M_DATA << ($v ? 8 : 0); - $bitIndex--; + if($direction){ + $y = ($this->moduleCount - 1 - $count); + } - if($bitIndex === -1){ - $byteIndex++; - $bitIndex = 7; - } + for($col = 0; $col < 2; $col++){ + $x = ($i - $col); + // skip functional patterns + if($this->matrix[$y][$x] !== $this::M_NULL){ + continue; } - } - $y += $inc; + $this->matrix[$y][$x] = $this::M_DATA; - if($y < 0 || $this->moduleCount <= $y){ - $y -= $inc; - $inc = -$inc; + if($iByte < $byteCount && (($data[$iByte] >> $iBit--) & 1) === 1){ + $this->matrix[$y][$x] |= $this::IS_DARK; + } - break; + if($iBit === -1){ + $iByte++; + $iBit = 7; + } } - } + + $direction = !$direction; // switch directions } 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() + * Applies/reverses the mask pattern * - * @internal - * - * @throws \chillerlan\QRCode\Data\QRCodeDataException + * ISO/IEC 18004:2000 Section 8.8.1 */ - protected function getMask(int $maskPattern):Closure{ + public function mask(MaskPattern $maskPattern):self{ + $this->maskPattern = $maskPattern; + $mask = $this->maskPattern->getMask(); - if((0b111 & $maskPattern) !== $maskPattern){ - throw new QRCodeDataException('invalid mask pattern'); // @codeCoverageIgnore + foreach($this->matrix as $y => $row){ + foreach($row as $x => $val){ + // skip non-data modules + if(($val & $this::M_DATA) === $this::M_DATA && $mask($x, $y)){ + $this->flip($x, $y); + } + } } - 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]; + return $this; } } diff --git a/vendor/chillerlan/php-qrcode/src/Data/ReedSolomonEncoder.php b/vendor/chillerlan/php-qrcode/src/Data/ReedSolomonEncoder.php new file mode 100644 index 000000000..60444378c --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Data/ReedSolomonEncoder.php @@ -0,0 +1,127 @@ +<?php +/** + * Class ReedSolomonEncoder + * + * @created 07.01.2021 + * @author smiley <smiley@chillerlan.net> + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Data; + +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, GenericGFPoly, GF256, Version}; +use function array_fill, array_merge, count, max; + +/** + * Reed-Solomon encoding - ISO/IEC 18004:2000 Section 8.5 ff + * + * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding + */ +final class ReedSolomonEncoder{ + + private Version $version; + private EccLevel $eccLevel; + + private array $interleavedData; + private int $interleavedDataIndex; + + /** + * ReedSolomonDecoder constructor + */ + public function __construct(Version $version, EccLevel $eccLevel){ + $this->version = $version; + $this->eccLevel = $eccLevel; + } + + /** + * ECC encoding and interleaving + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function interleaveEcBytes(BitBuffer $bitBuffer):array{ + [$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $this->version->getRSBlocks($this->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 => [$rsBlockTotal, $dataByteCount]){ + $dataBytes[$key] = []; + + for($i = 0; $i < $dataByteCount; $i++){ + $dataBytes[$key][$i] = ($bitBufferData[($i + $dataByteOffset)] & 0xff); + } + + $ecByteCount = ($rsBlockTotal - $dataByteCount); + $ecBytes[$key] = $this->encode($dataBytes[$key], $ecByteCount); + $maxDataBytes = max($maxDataBytes, $dataByteCount); + $maxEcBytes = max($maxEcBytes, $ecByteCount); + $dataByteOffset += $dataByteCount; + } + + $this->interleavedData = array_fill(0, $this->version->getTotalCodewords(), 0); + $this->interleavedDataIndex = 0; + $numRsBlocks = ($l1 + $l2); + + $this->interleave($dataBytes, $maxDataBytes, $numRsBlocks); + $this->interleave($ecBytes, $maxEcBytes, $numRsBlocks); + + return $this->interleavedData; + } + + /** + * + */ + private function encode(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(); + + $modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree)) + ->mod($rsPoly) + ->getCoefficients() + ; + + $ecBytes = array_fill(0, $rsPolyDegree, 0); + $count = (count($modCoefficients) - $rsPolyDegree); + + foreach($ecBytes as $i => &$val){ + $modIndex = ($i + $count); + $val = 0; + + if($modIndex >= 0){ + $val = $modCoefficients[$modIndex]; + } + } + + 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/vendor/chillerlan/php-qrcode/src/Decoder/Binarizer.php b/vendor/chillerlan/php-qrcode/src/Decoder/Binarizer.php new file mode 100644 index 000000000..7b7b49f65 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Decoder/Binarizer.php @@ -0,0 +1,361 @@ +<?php +/** + * Class Binarizer + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use chillerlan\QRCode\Common\LuminanceSourceInterface; +use chillerlan\QRCode\Data\QRMatrix; +use function array_fill, count, intdiv, 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 LuminanceSourceInterface $source; + private array $luminances; + + /** + * + */ + public function __construct(LuminanceSourceInterface $source){ + $this->source = $source; + $this->luminances = $this->source->getLuminances(); + } + + /** + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + 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 QRCodeDecoderException('no meaningful dark point found'); // @codeCoverageIgnore + } + + // 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); + } + + /** + * + */ + private function getHistogramBlackMatrix(int $width, int $height):BitMatrix{ + + // 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); + $right = intdiv(($width * 4), 5); + $x = intdiv($width, 5); + + for($y = 1; $y < 5; $y++){ + $row = intdiv(($height * $y), 5); + $localLuminances = $this->source->getRow($row); + + for(; $x < $right; $x++){ + $pixel = ($localLuminances[$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. + $matrix = new BitMatrix(max($width, $height)); + + for($y = 0; $y < $height; $y++){ + $offset = ($y * $width); + + for($x = 0; $x < $width; $x++){ + $matrix->set($x, $y, (($this->luminances[($offset + $x)] & 0xff) < $blackPoint), QRMatrix::M_DATA); + } + } + + 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(int $subWidth, int $subHeight, int $width, int $height):array{ + $blackPoints = array_fill(0, $subHeight, 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)($this->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 += ((int)($this->luminances[(int)($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 = ($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 = ( + ($blackPoints[($y - 1)][$x] + (2 * $blackPoints[$y][($x - 1)]) + $blackPoints[($y - 1)][($x - 1)]) / 4 + ); + + if($min < $averageNeighborBlackPoint){ + $average = $averageNeighborBlackPoint; + } + } + } + + $blackPoints[$y][$x] = $average; + } + } + + return $blackPoints; + } + + /** + * For each block in the image, calculate the average black point using a 5x5 grid + * of the surrounding blocks. 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)); + $blackPoints = $this->calculateBlackPoints($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++){ + $br = $blackPoints[($top + $z)]; + $sum += ($br[($left - 2)] + $br[($left - 1)] + $br[$left] + $br[($left + 1)] + $br[($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. + $v = (((int)($this->luminances[($o + $i)]) & 0xff) <= $average); + + $matrix->set(($xoffset + $i), ($yoffset + $j), $v, QRMatrix::M_DATA); + } + } + } + } + + return $matrix; + } + + /** + * @noinspection PhpSameParameterValueInspection + */ + 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/vendor/chillerlan/php-qrcode/src/Decoder/BitMatrix.php b/vendor/chillerlan/php-qrcode/src/Decoder/BitMatrix.php new file mode 100644 index 000000000..21f504e72 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Decoder/BitMatrix.php @@ -0,0 +1,430 @@ +<?php +/** + * Class BitMatrix + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version}; +use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix}; +use function array_fill, array_reverse, count; +use const PHP_INT_MAX, PHP_INT_SIZE; + +/** + * Extended QRMatrix to map read data from the Binarizer + */ +final class BitMatrix extends QRMatrix{ + + /** + * See ISO 18004:2006, Annex C, Table C.1 + * + * [data bits, sequence after masking] + */ + private const DECODE_LOOKUP = [ + 0x5412, // 0101010000010010 + 0x5125, // 0101000100100101 + 0x5E7C, // 0101111001111100 + 0x5B4B, // 0101101101001011 + 0x45F9, // 0100010111111001 + 0x40CE, // 0100000011001110 + 0x4F97, // 0100111110010111 + 0x4AA0, // 0100101010100000 + 0x77C4, // 0111011111000100 + 0x72F3, // 0111001011110011 + 0x7DAA, // 0111110110101010 + 0x789D, // 0111100010011101 + 0x662F, // 0110011000101111 + 0x6318, // 0110001100011000 + 0x6C41, // 0110110001000001 + 0x6976, // 0110100101110110 + 0x1689, // 0001011010001001 + 0x13BE, // 0001001110111110 + 0x1CE7, // 0001110011100111 + 0x19D0, // 0001100111010000 + 0x0762, // 0000011101100010 + 0x0255, // 0000001001010101 + 0x0D0C, // 0000110100001100 + 0x083B, // 0000100000111011 + 0x355F, // 0011010101011111 + 0x3068, // 0011000001101000 + 0x3F31, // 0011111100110001 + 0x3A06, // 0011101000000110 + 0x24B4, // 0010010010110100 + 0x2183, // 0010000110000011 + 0x2EDA, // 0010111011011010 + 0x2BED, // 0010101111101101 + ]; + + private const FORMAT_INFO_MASK_QR = 0x5412; // 0101010000010010 + + /** + * This flag has effect only on the copyVersionBit() method. + * Before proceeding with readCodewords() the resetInfo() method should be called. + */ + private bool $mirror = false; + + /** + * @noinspection PhpMissingParentConstructorInspection + */ + public function __construct(int $dimension){ + $this->moduleCount = $dimension; + $this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL)); + } + + /** + * Resets the current version info in order to attempt another reading + */ + public function resetVersionInfo():self{ + $this->version = null; + $this->eccLevel = null; + $this->maskPattern = null; + + return $this; + } + + /** + * Mirror the bit matrix diagonally in order to attempt a second reading. + */ + public function mirrorDiagonal():self{ + $this->mirror = !$this->mirror; + + // mirror vertically + $this->matrix = array_reverse($this->matrix); + // rotate by 90 degrees clockwise + /** @phan-suppress-next-line PhanTypeMismatchReturnSuperType */ + return $this->rotate90(); + } + + /** + * Reads the bits in the BitMatrix representing the finder pattern in the + * correct order in order to reconstruct the codewords bytes contained within the + * QR Code. Throws if the exact number of bytes expected is not read. + * + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + public function readCodewords():array{ + + $this + ->readFormatInformation() + ->readVersion() + ->mask($this->maskPattern) // reverse the mask pattern + ; + + // invoke a fresh matrix with only the function & format patterns to compare against + $matrix = (new QRMatrix($this->version, $this->eccLevel)) + ->initFunctionalPatterns() + ->setFormatInfo($this->maskPattern) + ; + + $result = []; + $byte = 0; + $bitsRead = 0; + $direction = true; + + // Read columns in pairs, from right to left + for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){ + + // Skip whole column with vertical alignment pattern; + // saves time and makes the other code proceed more cleanly + if($i === 6){ + $i--; + } + // Read alternatingly from bottom to top then top to bottom + for($count = 0; $count < $this->moduleCount; $count++){ + $y = ($direction) ? ($this->moduleCount - 1 - $count) : $count; + + for($col = 0; $col < 2; $col++){ + $x = ($i - $col); + + // Ignore bits covered by the function pattern + if($matrix->get($x, $y) !== $this::M_NULL){ + continue; + } + + $bitsRead++; + $byte <<= 1; + + if($this->check($x, $y)){ + $byte |= 1; + } + // If we've made a whole byte, save it off + if($bitsRead === 8){ + $result[] = $byte; + $bitsRead = 0; + $byte = 0; + } + } + } + + $direction = !$direction; // switch directions + } + + if(count($result) !== $this->version->getTotalCodewords()){ + throw new QRCodeDecoderException('result count differs from total codewords for version'); + } + + // bytes encoded within the QR Code + return $result; + } + + /** + * Reads format information from one of its two locations within the QR Code. + * Throws if both format information locations cannot be parsed as the valid encoding of format information. + * + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + private function readFormatInformation():self{ + + if($this->eccLevel !== null && $this->maskPattern !== null){ + return $this; + } + + // Read top-left format info bits + $formatInfoBits1 = 0; + + for($i = 0; $i < 6; $i++){ + $formatInfoBits1 = $this->copyVersionBit($i, 8, $formatInfoBits1); + } + + // ... and skip a bit in the timing pattern ... + $formatInfoBits1 = $this->copyVersionBit(7, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyVersionBit(8, 8, $formatInfoBits1); + $formatInfoBits1 = $this->copyVersionBit(8, 7, $formatInfoBits1); + // ... and skip a bit in the timing pattern ... + for($j = 5; $j >= 0; $j--){ + $formatInfoBits1 = $this->copyVersionBit(8, $j, $formatInfoBits1); + } + + // Read the top-right/bottom-left pattern too + $formatInfoBits2 = 0; + $jMin = ($this->moduleCount - 7); + + for($j = ($this->moduleCount - 1); $j >= $jMin; $j--){ + $formatInfoBits2 = $this->copyVersionBit(8, $j, $formatInfoBits2); + } + + for($i = ($this->moduleCount - 8); $i < $this->moduleCount; $i++){ + $formatInfoBits2 = $this->copyVersionBit($i, 8, $formatInfoBits2); + } + + $formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2); + + if($formatInfo === null){ + + // Should return null, but, some QR codes apparently do not mask this info. + // Try again by actually masking the pattern first. + $formatInfo = $this->doDecodeFormatInformation( + ($formatInfoBits1 ^ $this::FORMAT_INFO_MASK_QR), + ($formatInfoBits2 ^ $this::FORMAT_INFO_MASK_QR) + ); + + // still nothing??? + if($formatInfo === null){ + throw new QRCodeDecoderException('failed to read format info'); // @codeCoverageIgnore + } + + } + + $this->eccLevel = new EccLevel(($formatInfo >> 3) & 0x03); // Bits 3,4 + $this->maskPattern = new MaskPattern($formatInfo & 0x07); // Bottom 3 bits + + return $this; + } + + /** + * + */ + private function copyVersionBit(int $i, int $j, int $versionBits):int{ + + $bit = $this->mirror + ? $this->check($j, $i) + : $this->check($i, $j); + + return ($bit) ? (($versionBits << 1) | 0x1) : ($versionBits << 1); + } + + /** + * Returns information about the format it specifies, or null if it doesn't seem to match any known pattern + */ + private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?int{ + $bestDifference = PHP_INT_MAX; + $bestFormatInfo = 0; + + // Find the int in FORMAT_INFO_DECODE_LOOKUP with the fewest bits differing + foreach($this::DECODE_LOOKUP as $maskedBits => $dataBits){ + + if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){ + // Found an exact match + return $maskedBits; + } + + $bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits); + + if($bitsDifference < $bestDifference){ + $bestFormatInfo = $maskedBits; + $bestDifference = $bitsDifference; + } + + if($maskedFormatInfo1 !== $maskedFormatInfo2){ + // also try the other option + $bitsDifference = $this->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 $bestFormatInfo; + } + + return null; + } + + /** + * Reads version information from one of its two locations within the QR Code. + * Throws if both version information locations cannot be parsed as the valid encoding of version information. + * + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + * @noinspection DuplicatedCode + */ + private function readVersion():self{ + + if($this->version !== null){ + return $this; + } + + $provisionalVersion = (($this->moduleCount - 17) / 4); + + // no version info if v < 7 + if($provisionalVersion < 7){ + $this->version = new Version($provisionalVersion); + + return $this; + } + + // Read top-right version info: 3 wide by 6 tall + $versionBits = 0; + $ijMin = ($this->moduleCount - 11); + + for($y = 5; $y >= 0; $y--){ + for($x = ($this->moduleCount - 9); $x >= $ijMin; $x--){ + $versionBits = $this->copyVersionBit($x, $y, $versionBits); + } + } + + $this->version = $this->decodeVersionInformation($versionBits); + + if($this->version !== null && $this->version->getDimension() === $this->moduleCount){ + return $this; + } + + // Hmm, failed. Try bottom left: 6 wide by 3 tall + $versionBits = 0; + + for($x = 5; $x >= 0; $x--){ + for($y = ($this->moduleCount - 9); $y >= $ijMin; $y--){ + $versionBits = $this->copyVersionBit($x, $y, $versionBits); + } + } + + $this->version = $this->decodeVersionInformation($versionBits); + + if($this->version !== null && $this->version->getDimension() === $this->moduleCount){ + return $this; + } + + throw new QRCodeDecoderException('failed to read version'); + } + + /** + * Decodes the version information from the given bit sequence, returns null if no valid match is found. + */ + 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 + /** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */ + $bitsDifference = $this->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; + } + + /** + * + */ + private function uRShift(int $a, int $b):int{ + + if($b === 0){ + return $a; + } + + return (($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1))); + } + + /** + * + */ + private 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[($this->uRShift($a, $i) & 0x0F)]; + } + + return $count; + } + + /** + * @codeCoverageIgnore + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function setQuietZone(?int $quietZoneSize = null):self{ + throw new QRCodeDataException('not supported'); + } + + /** + * @codeCoverageIgnore + * @throws \chillerlan\QRCode\Data\QRCodeDataException + */ + public function setLogoSpace(int $width, ?int $height = null, ?int $startX = null, ?int $startY = null):self{ + throw new QRCodeDataException('not supported'); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Decoder/Decoder.php b/vendor/chillerlan/php-qrcode/src/Decoder/Decoder.php new file mode 100644 index 000000000..6f369a6df --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Decoder/Decoder.php @@ -0,0 +1,173 @@ +<?php +/** + * Class Decoder + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, LuminanceSourceInterface, MaskPattern, Mode, Version}; +use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number}; +use chillerlan\QRCode\Detector\Detector; +use Throwable; +use function chr, str_replace; + +/** + * 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 ?Version $version = null; + private ?EccLevel $eccLevel = null; + private ?MaskPattern $maskPattern = null; + private BitBuffer $bitBuffer; + + /** + * Decodes a QR Code represented as a BitMatrix. + * A 1 or "true" is taken to mean a black module. + * + * @throws \Throwable|\chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + public function decode(LuminanceSourceInterface $source):DecoderResult{ + $matrix = (new Detector($source))->detect(); + + try{ + // clone the BitMatrix to avoid errors in case we run into mirroring + return $this->decodeMatrix(clone $matrix); + } + catch(Throwable $e){ + + try{ + /* + * Prepare for a mirrored reading. + * + * 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. + */ + return $this->decodeMatrix($matrix->resetVersionInfo()->mirrorDiagonal()); + } + catch(Throwable $f){ + // Throw the exception from the original reading + throw $e; + } + + } + + } + + /** + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + private function decodeMatrix(BitMatrix $matrix):DecoderResult{ + // Read raw codewords + $rawCodewords = $matrix->readCodewords(); + $this->version = $matrix->getVersion(); + $this->eccLevel = $matrix->getEccLevel(); + $this->maskPattern = $matrix->getMaskPattern(); + + if($this->version === null || $this->eccLevel === null || $this->maskPattern === null){ + throw new QRCodeDecoderException('unable to read version or format info'); // @codeCoverageIgnore + } + + $resultBytes = (new ReedSolomonDecoder($this->version, $this->eccLevel))->decode($rawCodewords); + + return $this->decodeBitStream($resultBytes); + } + + /** + * Decode the contents of that stream of bytes + * + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + private function decodeBitStream(BitBuffer $bitBuffer):DecoderResult{ + $this->bitBuffer = $bitBuffer; + $versionNumber = $this->version->getVersionNumber(); + $symbolSequence = -1; + $parityData = -1; + $fc1InEffect = false; + $result = ''; + + // While still another segment to read... + while($this->bitBuffer->available() >= 4){ + $datamode = $this->bitBuffer->read(4); // mode is encoded by 4 bits + + // OK, assume we're done + if($datamode === Mode::TERMINATOR){ + break; + } + elseif($datamode === Mode::NUMBER){ + $result .= Number::decodeSegment($this->bitBuffer, $versionNumber); + } + elseif($datamode === Mode::ALPHANUM){ + $result .= $this->decodeAlphanumSegment($versionNumber, $fc1InEffect); + } + elseif($datamode === Mode::BYTE){ + $result .= Byte::decodeSegment($this->bitBuffer, $versionNumber); + } + elseif($datamode === Mode::KANJI){ + $result .= Kanji::decodeSegment($this->bitBuffer, $versionNumber); + } + elseif($datamode === Mode::STRCTURED_APPEND){ + + if($this->bitBuffer->available() < 16){ + throw new QRCodeDecoderException('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 = $this->bitBuffer->read(8); + $parityData = $this->bitBuffer->read(8); + } + elseif($datamode === Mode::FNC1_FIRST || $datamode === Mode::FNC1_SECOND){ + // We do little with FNC1 except alter the parsed result a bit according to the spec + $fc1InEffect = true; + } + elseif($datamode === Mode::ECI){ + $result .= ECI::decodeSegment($this->bitBuffer, $versionNumber); + } + elseif($datamode === Mode::HANZI){ + $result .= Hanzi::decodeSegment($this->bitBuffer, $versionNumber); + } + else{ + throw new QRCodeDecoderException('invalid data mode'); + } + + } + + return new DecoderResult([ + 'rawBytes' => $this->bitBuffer, + 'data' => $result, + 'version' => $this->version, + 'eccLevel' => $this->eccLevel, + 'maskPattern' => $this->maskPattern, + 'structuredAppendParity' => $parityData, + 'structuredAppendSequence' => $symbolSequence, + ]); + } + + /** + * + */ + private function decodeAlphanumSegment(int $versionNumber, bool $fc1InEffect):string{ + $str = AlphaNum::decodeSegment($this->bitBuffer, $versionNumber); + + // See section 6.4.8.1, 6.4.8.2 + if($fc1InEffect){ // ??? + // We need to massage the result a bit if in an FNC1 mode: + $str = str_replace(chr(0x1d), '%', $str); + $str = str_replace('%%', '%', $str); + } + + return $str; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Decoder/DecoderResult.php b/vendor/chillerlan/php-qrcode/src/Decoder/DecoderResult.php new file mode 100644 index 000000000..13fd24ba8 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Decoder/DecoderResult.php @@ -0,0 +1,99 @@ +<?php +/** + * Class DecoderResult + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Version}; +use chillerlan\QRCode\Data\QRMatrix; +use function property_exists; + +/** + * 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. + * + * @property \chillerlan\QRCode\Common\BitBuffer $rawBytes + * @property string $data + * @property \chillerlan\QRCode\Common\Version $version + * @property \chillerlan\QRCode\Common\EccLevel $eccLevel + * @property \chillerlan\QRCode\Common\MaskPattern $maskPattern + * @property int $structuredAppendParity + * @property int $structuredAppendSequence + */ +final class DecoderResult{ + + private BitBuffer $rawBytes; + private Version $version; + private EccLevel $eccLevel; + private MaskPattern $maskPattern; + private string $data = ''; + private int $structuredAppendParity = -1; + private int $structuredAppendSequence = -1; + + /** + * DecoderResult constructor. + */ + public function __construct(?iterable $properties = null){ + + if(!empty($properties)){ + + foreach($properties as $property => $value){ + + if(!property_exists($this, $property)){ + continue; + } + + $this->{$property} = $value; + } + + } + + } + + /** + * @return mixed|null + */ + public function __get(string $property){ + + if(property_exists($this, $property)){ + return $this->{$property}; + } + + return null; + } + + /** + * + */ + public function __toString():string{ + return $this->data; + } + + /** + * + */ + public function hasStructuredAppend():bool{ + return $this->structuredAppendParity >= 0 && $this->structuredAppendSequence >= 0; + } + + /** + * Returns a QRMatrix instance with the settings and data of the reader result + */ + public function getQRMatrix():QRMatrix{ + return (new QRMatrix($this->version, $this->eccLevel)) + ->initFunctionalPatterns() + ->writeCodewords($this->rawBytes) + ->setFormatInfo($this->maskPattern) + ->mask($this->maskPattern) + ; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Decoder/QRCodeDecoderException.php b/vendor/chillerlan/php-qrcode/src/Decoder/QRCodeDecoderException.php new file mode 100644 index 000000000..11157afc1 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Decoder/QRCodeDecoderException.php @@ -0,0 +1,20 @@ +<?php +/** + * Class QRCodeDecoderException + * + * @created 01.12.2021 + * @author smiley <smiley@chillerlan.net> + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Decoder; + +use chillerlan\QRCode\QRCodeException; + +/** + * An exception container + */ +final class QRCodeDecoderException extends QRCodeException{ + +} diff --git a/vendor/chillerlan/php-qrcode/src/Decoder/ReedSolomonDecoder.php b/vendor/chillerlan/php-qrcode/src/Decoder/ReedSolomonDecoder.php new file mode 100644 index 000000000..5f104a1c8 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Decoder/ReedSolomonDecoder.php @@ -0,0 +1,313 @@ +<?php +/** + * Class ReedSolomonDecoder + * + * @created 24.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Decoder; + +use chillerlan\QRCode\Common\{BitBuffer, EccLevel, GenericGFPoly, GF256, Version}; +use function array_fill, array_reverse, count; + +/** + * Implements Reed-Solomon decoding + * + * The algorithm will not be explained here, but the following references were helpful + * in creating this implementation: + * + * - Bruce Maggs "Decoding Reed-Solomon Codes" (see discussion of Forney's Formula) + * http://www.cs.cmu.edu/afs/cs.cmu.edu/project/pscico-guyb/realworld/www/rs_decode.ps + * - J.I. Hall. "Chapter 5. Generalized Reed-Solomon Codes" (see discussion of Euclidean algorithm) + * https://users.math.msu.edu/users/halljo/classes/codenotes/GRS.pdf + * + * 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{ + + private Version $version; + private EccLevel $eccLevel; + + /** + * ReedSolomonDecoder constructor + */ + public function __construct(Version $version, EccLevel $eccLevel){ + $this->version = $version; + $this->eccLevel = $eccLevel; + } + + /** + * Error-correct and copy data blocks together into a stream of bytes + */ + public function decode(array $rawCodewords):BitBuffer{ + $dataBlocks = $this->deinterleaveRawBytes($rawCodewords); + $dataBytes = []; + + foreach($dataBlocks as [$numDataCodewords, $codewordBytes]){ + $corrected = $this->correctErrors($codewordBytes, $numDataCodewords); + + for($i = 0; $i < $numDataCodewords; $i++){ + $dataBytes[] = $corrected[$i]; + } + } + + return new BitBuffer($dataBytes); + } + + /** + * 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. + * + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + private function deinterleaveRawBytes(array $rawCodewords):array{ + // Figure out the number and size of data blocks used by this version and + // error correction level + [$numEccCodewords, $eccBlocks] = $this->version->getRSBlocks($this->eccLevel); + + // Now establish DataBlocks of the appropriate size and number of data codewords + $result = [];//new DataBlock[$totalBlocks]; + $numResultBlocks = 0; + + foreach($eccBlocks as [$numEccBlocks, $eccPerBlock]){ + 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. + /** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */ + $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 + /** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */ + $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++]; + } + } + + // DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code + 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 $codewordByte){ + $codewordsInts[] = ($codewordByte & 0xFF); + } + + $decoded = $this->decodeWords($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; + } + + /** + * 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 \chillerlan\QRCode\Decoder\QRCodeDecoderException if decoding fails for any reason + */ + private function decodeWords(array $received, int $numEccCodewords):array{ + $poly = new GenericGFPoly($received); + $syndromeCoefficients = []; + $error = false; + + for($i = 0; $i < $numEccCodewords; $i++){ + $syndromeCoefficients[$i] = $poly->evaluateAt(GF256::exp($i)); + + if($syndromeCoefficients[$i] !== 0){ + $error = true; + } + } + + if(!$error){ + return $received; + } + + [$sigma, $omega] = $this->runEuclideanAlgorithm( + GF256::buildMonomial($numEccCodewords, 1), + new GenericGFPoly(array_reverse($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 QRCodeDecoderException('Bad error location'); + } + + $received[$position] ^= $errorMagnitudes[$i]; + } + + return $received; + } + + /** + * @return \chillerlan\QRCode\Common\GenericGFPoly[] [sigma, omega] + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + private function runEuclideanAlgorithm(GenericGFPoly $a, GenericGFPoly $b, int $z):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 z/2 + while((2 * $r->getDegree()) >= $z){ + $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 QRCodeDecoderException('Division algorithm failed to reduce polynomial?'); + } + } + + $sigmaTildeAtZero = $t->getCoefficient(0); + + if($sigmaTildeAtZero === 0){ + throw new QRCodeDecoderException('sigmaTilde(0) was zero'); + } + + $inverse = GF256::inverse($sigmaTildeAtZero); + + return [$t->multiplyInt($inverse), $r->multiplyInt($inverse)]; + } + + /** + * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException + */ + 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 QRCodeDecoderException('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/vendor/chillerlan/php-qrcode/src/Detector/AlignmentPattern.php b/vendor/chillerlan/php-qrcode/src/Detector/AlignmentPattern.php new file mode 100644 index 000000000..72feafdfa --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/AlignmentPattern.php @@ -0,0 +1,34 @@ +<?php +/** + * Class AlignmentPattern + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @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 FinderPattern containing an average of the two. + */ + public function combineEstimate(float $i, float $j, float $newModuleSize):self{ + return new self( + (($this->x + $j) / 2.0), + (($this->y + $i) / 2.0), + (($this->estimatedModuleSize + $newModuleSize) / 2.0) + ); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Detector/AlignmentPatternFinder.php b/vendor/chillerlan/php-qrcode/src/Detector/AlignmentPatternFinder.php new file mode 100644 index 000000000..d9edc50bb --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/AlignmentPatternFinder.php @@ -0,0 +1,284 @@ +<?php +/** + * Class AlignmentPatternFinder + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @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 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 $matrix; + private float $moduleSize; + /** @var \chillerlan\QRCode\Detector\AlignmentPattern[] */ + private array $possibleCenters; + + /** + * Creates a finder that will look in a portion of the whole image. + * + * @param \chillerlan\QRCode\Decoder\BitMatrix $matrix image to search + * @param float $moduleSize estimated module size so far + */ + public function __construct(BitMatrix $matrix, float $moduleSize){ + $this->matrix = $matrix; + $this->moduleSize = $moduleSize; + $this->possibleCenters = []; + } + + /** + * 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->matrix->check($j, $i)){ + $j++; + } + + $currentState = 0; + + while($j < $maxJ){ + + if($this->matrix->check($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{ + $maxVariance = ($this->moduleSize / 2.0); + + for($i = 0; $i < 3; $i++){ + if(abs($this->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 = (($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); + } + + /** + * 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->matrix->getSize(); + $stateCount = []; + $stateCount[0] = 0; + $stateCount[1] = 0; + $stateCount[2] = 0; + + // Start counting up from center + $i = $startI; + while($i >= 0 && $this->matrix->check($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->matrix->check($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->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){ + $stateCount[1]++; + $i++; + } + + if($i === $maxI || $stateCount[1] > $maxCount){ + return null; + } + + while($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[2] <= $maxCount){ + $stateCount[2]++; + $i++; + } + + if($stateCount[2] > $maxCount){ + return null; + } + + // phpcs:ignore + 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/vendor/chillerlan/php-qrcode/src/Detector/Detector.php b/vendor/chillerlan/php-qrcode/src/Detector/Detector.php new file mode 100644 index 000000000..123b685c6 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/Detector.php @@ -0,0 +1,350 @@ +<?php +/** + * Class Detector + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use chillerlan\QRCode\Common\{LuminanceSourceInterface, Version}; +use chillerlan\QRCode\Decoder\{Binarizer, BitMatrix}; +use function abs, intdiv, is_nan, max, min, round; +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 $matrix; + + /** + * Detector constructor. + */ + public function __construct(LuminanceSourceInterface $source){ + $this->matrix = (new Binarizer($source))->getBlackMatrix(); + } + + /** + * Detects a QR Code in an image. + */ + public function detect():BitMatrix{ + [$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->matrix))->find(); + + $moduleSize = $this->calculateModuleSize($topLeft, $topRight, $bottomLeft); + $dimension = $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize); + $provisionalVersion = new Version(intdiv(($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->matrix, $dimension, $transform); + } + + /** + * Computes an average estimated module size based on estimated derived from the positions + * of the three finder patterns. + * + * @throws \chillerlan\QRCode\Detector\QRCodeDetectorException + */ + 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 QRCodeDetectorException('module size < 1.0'); + } + + return $moduleSize; + } + + /** + * Estimates module size based on two finder patterns -- it uses + * #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int) to figure the + * width of each, measuring along the axis between their centers. + */ + private function calculateModuleSizeOneWay(FinderPattern $a, FinderPattern $b):float{ + + $moduleSizeEst1 = $this->sizeOfBlackWhiteBlackRunBothWays($a->getX(), $a->getY(), $b->getX(), $b->getY()); + $moduleSizeEst2 = $this->sizeOfBlackWhiteBlackRunBothWays($b->getX(), $b->getY(), $a->getX(), $a->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 #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. + * + * @noinspection DuplicatedCode + */ + 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->matrix->getSize(); + // 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, $otherToX, $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 https://en.wikipedia.org/wiki/Bresenham%27s_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->matrix->check($realX, $realY)){ + + if($state === 2){ + return FinderPattern::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 FinderPattern::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 \chillerlan\QRCode\Detector\QRCodeDetectorException + */ + private function computeDimension(FinderPattern $nw, FinderPattern $ne, FinderPattern $sw, float $size):int{ + $tltrCentersDimension = (int)round($nw->getDistance($ne) / $size); + $tlblCentersDimension = (int)round($nw->getDistance($sw) / $size); + $dimension = (int)((($tltrCentersDimension + $tlblCentersDimension) / 2) + 7); + + switch($dimension % 4){ + case 0: + $dimension++; + break; + // 1? do nothing + case 2: + $dimension--; + break; + case 3: + throw new QRCodeDetectorException('estimated dimension: '.$dimension); + } + + if(($dimension % 4) !== 1){ + throw new QRCodeDetectorException('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->matrix->getSize(); + $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->matrix, $overallEstModuleSize))->find( + $alignmentAreaLeftX, + $alignmentAreaTopY, + ($alignmentAreaRightX - $alignmentAreaLeftX), + ($alignmentAreaBottomY - $alignmentAreaTopY), + ); + } + + /** + * + */ + private function createTransform( + FinderPattern $nw, + FinderPattern $ne, + FinderPattern $sw, + int $size, + ?AlignmentPattern $ap = null + ):PerspectiveTransform{ + $dimMinusThree = ($size - 3.5); + + if($ap instanceof AlignmentPattern){ + $bottomRightX = $ap->getX(); + $bottomRightY = $ap->getY(); + $sourceBottomRightX = ($dimMinusThree - 3.0); + $sourceBottomRightY = $sourceBottomRightX; + } + else{ + // Don't have an alignment pattern, just make up the bottom-right point + $bottomRightX = ($ne->getX() - $nw->getX() + $sw->getX()); + $bottomRightY = ($ne->getY() - $nw->getY() + $sw->getY()); + $sourceBottomRightX = $dimMinusThree; + $sourceBottomRightY = $dimMinusThree; + } + + return (new PerspectiveTransform)->quadrilateralToQuadrilateral( + 3.5, + 3.5, + $dimMinusThree, + 3.5, + $sourceBottomRightX, + $sourceBottomRightY, + 3.5, + $dimMinusThree, + $nw->getX(), + $nw->getY(), + $ne->getX(), + $ne->getY(), + $bottomRightX, + $bottomRightY, + $sw->getX(), + $sw->getY() + ); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Detector/FinderPattern.php b/vendor/chillerlan/php-qrcode/src/Detector/FinderPattern.php new file mode 100644 index 000000000..3ae4650ad --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/FinderPattern.php @@ -0,0 +1,92 @@ +<?php +/** + * Class FinderPattern + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use function sqrt; + +/** + * 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 = null){ + parent::__construct($posX, $posY, $estimatedModuleSize); + + $this->count = ($count ?? 1); + } + + /** + * + */ + public function getCount():int{ + return $this->count; + } + + /** + * @param \chillerlan\QRCode\Detector\FinderPattern $b second pattern + * + * @return float distance between two points + */ + 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 getSquaredDistance(FinderPattern $b):float{ + return self::squaredDistance($this->x, $this->y, $b->x, $b->y); + } + + /** + * Combines this object's current estimate of a finder pattern position and module size + * with a new estimate. It returns a new FinderPattern containing a weighted average + * based on count. + */ + public function combineEstimate(float $i, float $j, float $newModuleSize):self{ + $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 + ); + } + + /** + * + */ + 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/vendor/chillerlan/php-qrcode/src/Detector/FinderPatternFinder.php b/vendor/chillerlan/php-qrcode/src/Detector/FinderPatternFinder.php new file mode 100644 index 000000000..61628d063 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/FinderPatternFinder.php @@ -0,0 +1,773 @@ +<?php +/** + * Class FinderPatternFinder + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + * + * @phan-file-suppress PhanTypePossiblyInvalidDimOffset + */ + +namespace chillerlan\QRCode\Detector; + +use chillerlan\QRCode\Decoder\BitMatrix; +use function abs, count, intdiv, 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 $matrix; + /** @var \chillerlan\QRCode\Detector\FinderPattern[] */ + private array $possibleCenters; + private bool $hasSkipped = false; + + /** + * Creates a finder that will search the image for three finder patterns. + * + * @param BitMatrix $matrix image to search + */ + public function __construct(BitMatrix $matrix){ + $this->matrix = $matrix; + $this->possibleCenters = []; + } + + /** + * @return \chillerlan\QRCode\Detector\FinderPattern[] + */ + public function find():array{ + $dimension = $this->matrix->getSize(); + + // 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 = intdiv((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->matrix->check($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 + */ + private function centerFromEnd(array $stateCount, int $end):float{ + return (float)(($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2); + } + + /** + * @param int[] $stateCount + */ + 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 + */ + 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 int $centerI row where a finder pattern was detected + * @param int $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->matrix->check(($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->matrix->check(($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->matrix->check(($centerJ - $i), ($centerI - $i))){ + $stateCount[0]++; + $i++; + } + + if($stateCount[0] === 0){ + return false; + } + + $dimension = $this->matrix->getSize(); + + // Now also count down, right from center + $i = 1; + // phpcs:ignore + while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && $this->matrix->check(($centerJ + $i), ($centerI + $i))){ + $stateCount[2]++; + $i++; + } + + // phpcs:ignore + while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && !$this->matrix->check(($centerJ + $i), ($centerI + $i))){ + $stateCount[3]++; + $i++; + } + + if($stateCount[3] === 0){ + return false; + } + + // phpcs:ignore + while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && $this->matrix->check(($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 + * @noinspection DuplicatedCode + */ + private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{ + $maxI = $this->matrix->getSize(); + $stateCount = $this->getCrossCheckStateCount(); + + // Start counting up from center + $i = $startI; + while($i >= 0 && $this->matrix->check($centerJ, $i)){ + $stateCount[2]++; + $i--; + } + + if($i < 0){ + return null; + } + + while($i >= 0 && !$this->matrix->check($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->matrix->check($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->matrix->check($centerJ, $i)){ + $stateCount[2]++; + $i++; + } + + if($i === $maxI){ + return null; + } + + while($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[3] < $maxCount){ + $stateCount[3]++; + $i++; + } + + if($i === $maxI || $stateCount[3] >= $maxCount){ + return null; + } + + while($i < $maxI && $this->matrix->check($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 from + // 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 #crossCheckVertical(int, int, int, int), and in fact is basically identical, + * except it reads horizontally instead of vertically. This is used to cross-cross + * check a vertical cross-check and locate the real center of the alignment pattern. + * @noinspection DuplicatedCode + */ + private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{ + $maxJ = $this->matrix->getSize(); + $stateCount = $this->getCrossCheckStateCount(); + + $j = $startJ; + while($j >= 0 && $this->matrix->check($j, $centerI)){ + $stateCount[2]++; + $j--; + } + + if($j < 0){ + return null; + } + + while($j >= 0 && !$this->matrix->check($j, $centerI) && $stateCount[1] <= $maxCount){ + $stateCount[1]++; + $j--; + } + + if($j < 0 || $stateCount[1] > $maxCount){ + return null; + } + + while($j >= 0 && $this->matrix->check($j, $centerI) && $stateCount[0] <= $maxCount){ + $stateCount[0]++; + $j--; + } + + if($stateCount[0] > $maxCount){ + return null; + } + + $j = ($startJ + 1); + while($j < $maxJ && $this->matrix->check($j, $centerI)){ + $stateCount[2]++; + $j++; + } + + if($j === $maxJ){ + return null; + } + + while($j < $maxJ && !$this->matrix->check($j, $centerI) && $stateCount[3] < $maxCount){ + $stateCount[3]++; + $j++; + } + + if($j === $maxJ || $stateCount[3] >= $maxCount){ + return null; + } + + while($j < $maxJ && $this->matrix->check($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 from + // 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; + + // cautious (was in for fool in which $this->possibleCenters is updated) + $count = count($this->possibleCenters); + + for($index = 0; $index < $count; $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 #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 FinderPatterns from our list of candidates. The "best" are + * those that have been detected at least #CENTER_QUORUM times, and whose module + * size differs from the average among those patterns the least + * @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if 3 such finder patterns do not exist + */ + private function selectBestPatterns():array{ + $startSize = count($this->possibleCenters); + + if($startSize < 3){ + throw new QRCodeDetectorException('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->getSquaredDistance($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->getSquaredDistance($fpk); + $c = $fpi->getSquaredDistance($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 QRCodeDetectorException('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]->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){ + [$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/vendor/chillerlan/php-qrcode/src/Detector/GridSampler.php b/vendor/chillerlan/php-qrcode/src/Detector/GridSampler.php new file mode 100644 index 000000000..f70bb0eca --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/GridSampler.php @@ -0,0 +1,181 @@ +<?php +/** + * Class GridSampler + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @copyright 2021 Smiley + * @license Apache-2.0 + */ + +namespace chillerlan\QRCode\Detector; + +use chillerlan\QRCode\Data\QRMatrix; +use chillerlan\QRCode\Decoder\BitMatrix; +use function array_fill, count, intdiv, 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 #setGridSampler(GridSampler) + * with an instance of a class which implements this interface. + * + * @author Sean Owen + */ +final class GridSampler{ + + private array $points; + + /** + * 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 int $dimension matrix width/height + * + * @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if an endpoint is lies outside the image boundaries + */ + private function checkAndNudgePoints(int $dimension):void{ + $nudged = true; + $max = count($this->points); + + // Check and nudge points from start until we see some that are OK: + for($offset = 0; $offset < $max && $nudged; $offset += 2){ + $x = (int)$this->points[$offset]; + $y = (int)$this->points[($offset + 1)]; + + if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){ + throw new QRCodeDetectorException(sprintf('checkAndNudgePoints 1, x: %s, y: %s, d: %s', $x, $y, $dimension)); + } + + $nudged = false; + + if($x === -1){ + $this->points[$offset] = 0.0; + $nudged = true; + } + elseif($x === $dimension){ + $this->points[$offset] = ($dimension - 1); + $nudged = true; + } + + if($y === -1){ + $this->points[($offset + 1)] = 0.0; + $nudged = true; + } + elseif($y === $dimension){ + $this->points[($offset + 1)] = ($dimension - 1); + $nudged = true; + } + + } + + // Check and nudge points from end: + $nudged = true; + + for($offset = ($max - 2); $offset >= 0 && $nudged; $offset -= 2){ + $x = (int)$this->points[$offset]; + $y = (int)$this->points[($offset + 1)]; + + if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){ + throw new QRCodeDetectorException(sprintf('checkAndNudgePoints 2, x: %s, y: %s, d: %s', $x, $y, $dimension)); + } + + $nudged = false; + + if($x === -1){ + $this->points[$offset] = 0.0; + $nudged = true; + } + elseif($x === $dimension){ + $this->points[$offset] = ($dimension - 1); + $nudged = true; + } + + if($y === -1){ + $this->points[($offset + 1)] = 0.0; + $nudged = true; + } + elseif($y === $dimension){ + $this->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 \chillerlan\QRCode\Detector\QRCodeDetectorException if image can't be sampled, for example, if the transformation defined + * by the given points is invalid or results in sampling outside the image boundaries + */ + public function sampleGrid(BitMatrix $matrix, int $dimension, PerspectiveTransform $transform):BitMatrix{ + + if($dimension <= 0){ + throw new QRCodeDetectorException('invalid matrix size'); + } + + $bits = new BitMatrix($dimension); + $this->points = array_fill(0, (2 * $dimension), 0.0); + + for($y = 0; $y < $dimension; $y++){ + $max = count($this->points); + $iValue = ($y + 0.5); + + for($x = 0; $x < $max; $x += 2){ + $this->points[$x] = (($x / 2) + 0.5); + $this->points[($x + 1)] = $iValue; + } + // phpcs:ignore + [$this->points, ] = $transform->transformPoints($this->points); + // Quick check to see if points transformed to something inside the image; + // sufficient to check the endpoints + $this->checkAndNudgePoints($matrix->getSize()); + + // no need to try/catch as QRMatrix::set() will silently discard out of bounds values +# try{ + for($x = 0; $x < $max; $x += 2){ + // Black(-ish) pixel + $bits->set( + intdiv($x, 2), + $y, + $matrix->check((int)$this->points[$x], (int)$this->points[($x + 1)]), + QRMatrix::M_DATA + ); + } +# } +# catch(\Throwable $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 QRCodeDetectorException('ArrayIndexOutOfBoundsException'); +# } + + } + + return $bits; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Detector/PerspectiveTransform.php b/vendor/chillerlan/php-qrcode/src/Detector/PerspectiveTransform.php new file mode 100644 index 000000000..7964092e3 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/PerspectiveTransform.php @@ -0,0 +1,182 @@ +<?php +/** + * Class PerspectiveTransform + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @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 set( + float $a11, float $a21, float $a31, + float $a12, float $a22, float $a32, + float $a13, float $a23, float $a33 + ):self{ + $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; + + return $this; + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + 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 + ):self{ + return (new self) + ->squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p) + ->times($this->quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)); + } + + /** + * + */ + private function quadrilateralToSquare( + float $x0, float $y0, float $x1, float $y1, + float $x2, float $y2, float $x3, float $y3 + ):self{ + // Here, the adjoint serves as the inverse: + return $this + ->squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) + ->buildAdjoint(); + } + + /** + * + */ + private function buildAdjoint():self{ + // Adjoint is the transpose of the cofactor matrix: + 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), + ($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) + ); + } + + /** + * + */ + private function squareToQuadrilateral( + float $x0, float $y0, float $x1, float $y1, + float $x2, float $y2, float $x3, float $y3 + ):self{ + $dx3 = ($x0 - $x1 + $x2 - $x3); + $dy3 = ($y0 - $y1 + $y2 - $y3); + + if($dx3 === 0.0 && $dy3 === 0.0){ + // Affine + return $this->set(($x1 - $x0), ($x2 - $x1), $x0, ($y1 - $y0), ($y2 - $y1), $y0, 0.0, 0.0, 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 + ); + } + + /** + * + */ + private function times(PerspectiveTransform $other):self{ + 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), + ($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) + ); + } + + /** + * @return array[] [$xValues, $yValues] + */ + public function transformPoints(array $xValues, ?array $yValues = null):array{ + $max = count($xValues); + + if($yValues !== null){ // unused + + 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 [$xValues, $yValues]; + } + + 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); + } + + return [$xValues, []]; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Detector/QRCodeDetectorException.php b/vendor/chillerlan/php-qrcode/src/Detector/QRCodeDetectorException.php new file mode 100644 index 000000000..2444e193c --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/QRCodeDetectorException.php @@ -0,0 +1,20 @@ +<?php +/** + * Class QRCodeDetectorException + * + * @created 01.12.2021 + * @author smiley <smiley@chillerlan.net> + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Detector; + +use chillerlan\QRCode\QRCodeException; + +/** + * An exception container + */ +final class QRCodeDetectorException extends QRCodeException{ + +} diff --git a/vendor/chillerlan/php-qrcode/src/Detector/ResultPoint.php b/vendor/chillerlan/php-qrcode/src/Detector/ResultPoint.php new file mode 100644 index 000000000..92997a746 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Detector/ResultPoint.php @@ -0,0 +1,73 @@ +<?php +/** + * Class ResultPoint + * + * @created 17.01.2021 + * @author ZXing Authors + * @author Smiley <smiley@chillerlan.net> + * @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 $this->x; + } + + /** + * + */ + public function getY():float{ + return $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/vendor/chillerlan/php-qrcode/src/Helpers/BitBuffer.php b/vendor/chillerlan/php-qrcode/src/Helpers/BitBuffer.php deleted file mode 100644 index de47f20f4..000000000 --- a/vendor/chillerlan/php-qrcode/src/Helpers/BitBuffer.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php -/** - * Class BitBuffer - * - * @filesource BitBuffer.php - * @created 25.11.2015 - * @package chillerlan\QRCode\Helpers - * @author Smiley <smiley@chillerlan.net> - * @copyright 2015 Smiley - * @license MIT - */ - -namespace chillerlan\QRCode\Helpers; - -use function count, floor; - -/** - * Holds the raw binary data - */ -final class BitBuffer{ - - /** - * The buffer content - * - * @var int[] - */ - protected array $buffer = []; - - /** - * Length of the content (bits) - */ - protected int $length = 0; - - /** - * clears the buffer - */ - public function clear():BitBuffer{ - $this->buffer = []; - $this->length = 0; - - return $this; - } - - /** - * appends a sequence of bits - */ - public function put(int $num, int $length):BitBuffer{ - - for($i = 0; $i < $length; $i++){ - $this->putBit((($num >> ($length - $i - 1)) & 1) === 1); - } - - return $this; - } - - /** - * appends a single bit - */ - public function putBit(bool $bit):BitBuffer{ - $bufIndex = floor($this->length / 8); - - if(count($this->buffer) <= $bufIndex){ - $this->buffer[] = 0; - } - - if($bit === true){ - $this->buffer[(int)$bufIndex] |= (0x80 >> ($this->length % 8)); - } - - $this->length++; - - return $this; - } - - /** - * returns the current buffer length - */ - public function getLength():int{ - return $this->length; - } - - /** - * returns the buffer content - */ - public function getBuffer():array{ - return $this->buffer; - } - -} diff --git a/vendor/chillerlan/php-qrcode/src/Helpers/Polynomial.php b/vendor/chillerlan/php-qrcode/src/Helpers/Polynomial.php deleted file mode 100644 index c42e0831c..000000000 --- a/vendor/chillerlan/php-qrcode/src/Helpers/Polynomial.php +++ /dev/null @@ -1,178 +0,0 @@ -<?php -/** - * Class Polynomial - * - * @filesource Polynomial.php - * @created 25.11.2015 - * @package chillerlan\QRCode\Helpers - * @author Smiley <smiley@chillerlan.net> - * @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/vendor/chillerlan/php-qrcode/src/Output/QRCodeOutputException.php b/vendor/chillerlan/php-qrcode/src/Output/QRCodeOutputException.php index 639bdd111..bf30f1bb0 100644 --- a/vendor/chillerlan/php-qrcode/src/Output/QRCodeOutputException.php +++ b/vendor/chillerlan/php-qrcode/src/Output/QRCodeOutputException.php @@ -2,9 +2,7 @@ /** * Class QRCodeOutputException * - * @filesource QRCodeOutputException.php * @created 09.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -14,4 +12,9 @@ namespace chillerlan\QRCode\Output; use chillerlan\QRCode\QRCodeException; -class QRCodeOutputException extends QRCodeException{} +/** + * An exception container + */ +final class QRCodeOutputException extends QRCodeException{ + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QREps.php b/vendor/chillerlan/php-qrcode/src/Output/QREps.php new file mode 100644 index 000000000..76dd50b37 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QREps.php @@ -0,0 +1,173 @@ +<?php +/** + * Class QREps + * + * @created 09.05.2022 + * @author smiley <smiley@chillerlan.net> + * @copyright 2022 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Output; + +use function array_values, count, date, implode, is_array, is_numeric, max, min, round, sprintf; + +/** + * Encapsulated Postscript (EPS) output + * + * @see https://github.com/t0k4rt/phpqrcode/blob/bb29e6eb77e0a2a85bb0eb62725e0adc11ff5a90/qrvect.php#L52-L137 + * @see https://web.archive.org/web/20170818010030/http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/postscript/pdfs/5002.EPSF_Spec.pdf + * @see https://web.archive.org/web/20210419003859/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf + * @see https://github.com/chillerlan/php-qrcode/discussions/148 + */ +class QREps extends QROutputAbstract{ + + public const MIME_TYPE = 'application/postscript'; + + /** + * @inheritDoc + */ + public static function moduleValueIsValid($value):bool{ + + if(!is_array($value) || count($value) < 3){ + return false; + } + + // check the first values of the array + foreach(array_values($value) as $i => $val){ + + if($i > 3){ + break; + } + + if(!is_numeric($val)){ + return false; + } + + } + + return true; + } + + /** + * @param array $value + * + * @inheritDoc + */ + protected function prepareModuleValue($value):string{ + $values = []; + + foreach(array_values($value) as $i => $val){ + + if($i > 3){ + break; + } + + // clamp value and convert from int 0-255 to float 0-1 RGB/CMYK range + $values[] = round((max(0, min(255, intval($val))) / 255), 6); + } + + return $this->formatColor($values); + } + + /** + * @inheritDoc + */ + protected function getDefaultModuleValue(bool $isDark):string{ + return $this->formatColor(($isDark) ? [0.0, 0.0, 0.0] : [1.0, 1.0, 1.0]); + } + + /** + * Set the color format string + * + * 4 values in the color array will be interpreted as CMYK, 3 as RGB + * + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + */ + protected function formatColor(array $values):string{ + $count = count($values); + + if($count < 3){ + throw new QRCodeOutputException('invalid color value'); + } + + $format = ($count === 4) + // CMYK + ? '%f %f %f %f C' + // RGB + : '%f %f %f R'; + + return sprintf($format, ...$values); + } + + /** + * @inheritDoc + */ + public function dump(?string $file = null):string{ + [$width, $height] = $this->getOutputDimensions(); + + $eps = [ + // main header + '%!PS-Adobe-3.0 EPSF-3.0', + '%%Creator: php-qrcode (https://github.com/chillerlan/php-qrcode)', + '%%Title: QR Code', + sprintf('%%%%CreationDate: %1$s', date('c')), + '%%DocumentData: Clean7Bit', + '%%LanguageLevel: 3', + sprintf('%%%%BoundingBox: 0 0 %s %s', $width, $height), + '%%EndComments', + // function definitions + '%%BeginProlog', + '/F { rectfill } def', + '/R { setrgbcolor } def', + '/C { setcmykcolor } def', + '%%EndProlog', + ]; + + if($this::moduleValueIsValid($this->options->bgColor)){ + $eps[] = $this->prepareModuleValue($this->options->bgColor); + $eps[] = sprintf('0 0 %s %s F', $width, $height); + } + + // create the path elements + $paths = $this->collectModules(fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE)); + + foreach($paths as $M_TYPE => $path){ + + if(empty($path)){ + continue; + } + + $eps[] = $this->getModuleValue($M_TYPE); + $eps[] = implode("\n", $path); + } + + // end file + $eps[] = '%%EOF'; + + $data = implode("\n", $eps); + + $this->saveToFile($data, $file); + + return $data; + } + + /** + * Returns a path segment for a single module + */ + protected function module(int $x, int $y, int $M_TYPE):string{ + + if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ + return ''; + } + + $outputX = ($x * $this->scale); + // Actual size - one block = Topmost y pos. + $top = ($this->length - $this->scale); + // Apparently y-axis is inverted (y0 is at bottom and not top) in EPS, so we have to switch the y-axis here + $outputY = ($top - ($y * $this->scale)); + + return sprintf('%d %d %d %d F', $outputX, $outputY, $this->scale, $this->scale); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRFpdf.php b/vendor/chillerlan/php-qrcode/src/Output/QRFpdf.php index a15ae9ff3..8f2482cba 100644 --- a/vendor/chillerlan/php-qrcode/src/Output/QRFpdf.php +++ b/vendor/chillerlan/php-qrcode/src/Output/QRFpdf.php @@ -2,24 +2,20 @@ /** * 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; use chillerlan\QRCode\Data\QRMatrix; -use chillerlan\QRCode\QRCodeException; use chillerlan\Settings\SettingsContainerInterface; use FPDF; -use function array_values, class_exists, count, is_array; +use function array_values, class_exists, count, intval, is_array, is_numeric, max, min; /** * QRFpdf output module (requires fpdf) @@ -29,12 +25,23 @@ use function array_values, class_exists, count, is_array; */ class QRFpdf extends QROutputAbstract{ + public const MIME_TYPE = 'application/pdf'; + + protected FPDF $fpdf; + protected ?array $prevColor = null; + + /** + * QRFpdf constructor. + * + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + */ public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){ if(!class_exists(FPDF::class)){ // @codeCoverageIgnoreStart - throw new QRCodeException( - 'The QRFpdf output requires FPDF as dependency but the class "\FPDF" couldn\'t be found.' + throw new QRCodeOutputException( + 'The QRFpdf output requires FPDF (https://github.com/Setasign/FPDF)'. + ' as dependency but the class "\\FPDF" couldn\'t be found.' ); // @codeCoverageIgnoreEnd } @@ -45,69 +52,128 @@ class QRFpdf extends QROutputAbstract{ /** * @inheritDoc */ - protected function setModuleValues():void{ + public static function moduleValueIsValid($value):bool{ + + if(!is_array($value) || count($value) < 3){ + return false; + } - foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){ - $v = $this->options->moduleValues[$M_TYPE] ?? null; + // check the first 3 values of the array + foreach(array_values($value) as $i => $val){ - if(!is_array($v) || count($v) < 3){ - $this->moduleValues[$M_TYPE] = $defaultValue - ? [0, 0, 0] - : [255, 255, 255]; + if($i > 2){ + break; } - else{ - $this->moduleValues[$M_TYPE] = array_values($v); + + if(!is_numeric($val)){ + return false; } } + return true; } /** - * @inheritDoc + * @param array $value * - * @return string|\FPDF + * @inheritDoc + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + */ + protected function prepareModuleValue($value):array{ + $values = []; + + foreach(array_values($value) as $i => $val){ + + if($i > 2){ + break; + } + + $values[] = max(0, min(255, intval($val))); + } + + if(count($values) !== 3){ + throw new QRCodeOutputException('invalid color value'); + } + + return $values; + } + + /** + * @inheritDoc */ - public function dump(string $file = null){ - $file ??= $this->options->cachefile; + protected function getDefaultModuleValue(bool $isDark):array{ + return ($isDark) ? [0, 0, 0] : [255, 255, 255]; + } - $fpdf = new FPDF('P', $this->options->fpdfMeasureUnit, [$this->length, $this->length]); + /** + * Initializes an FPDF instance + */ + protected function initFPDF():FPDF{ + $fpdf = new FPDF('P', $this->options->fpdfMeasureUnit, $this->getOutputDimensions()); $fpdf->AddPage(); - $prevColor = null; + return $fpdf; + } - foreach($this->matrix->matrix() as $y => $row){ + /** + * @inheritDoc + * + * @return string|\FPDF + */ + public function dump(?string $file = null, ?FPDF $fpdf = null){ + $this->fpdf = ($fpdf ?? $this->initFPDF()); - foreach($row as $x => $M_TYPE){ - /** @var int $M_TYPE */ - $color = $this->moduleValues[$M_TYPE]; + if($this::moduleValueIsValid($this->options->bgColor)){ + $bgColor = $this->prepareModuleValue($this->options->bgColor); + [$width, $height] = $this->getOutputDimensions(); - if($prevColor === null || $prevColor !== $color){ - /** @phan-suppress-next-line PhanParamTooFewUnpack */ - $fpdf->SetFillColor(...$color); - $prevColor = $color; - } + /** @phan-suppress-next-line PhanParamTooFewUnpack */ + $this->fpdf->SetFillColor(...$bgColor); + $this->fpdf->Rect(0, 0, $width, $height, 'F'); + } - $fpdf->Rect($x * $this->scale, $y * $this->scale, 1 * $this->scale, 1 * $this->scale, 'F'); - } + $this->prevColor = null; + foreach($this->matrix->getMatrix() as $y => $row){ + foreach($row as $x => $M_TYPE){ + $this->module($x, $y, $M_TYPE); + } } if($this->options->returnResource){ - return $fpdf; + return $this->fpdf; } - $pdfData = $fpdf->Output('S'); + $pdfData = $this->fpdf->Output('S'); - if($file !== null){ - $this->saveToFile($pdfData, $file); - } + $this->saveToFile($pdfData, $file); - if($this->options->imageBase64){ - $pdfData = sprintf('data:application/pdf;base64,%s', base64_encode($pdfData)); + if($this->options->outputBase64){ + $pdfData = $this->toBase64DataURI($pdfData); } return $pdfData; } + /** + * Renders a single module + */ + protected function module(int $x, int $y, int $M_TYPE):void{ + + if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ + return; + } + + $color = $this->getModuleValue($M_TYPE); + + if($color !== null && $color !== $this->prevColor){ + /** @phan-suppress-next-line PhanParamTooFewUnpack */ + $this->fpdf->SetFillColor(...$color); + $this->prevColor = $color; + } + + $this->fpdf->Rect(($x * $this->scale), ($y * $this->scale), $this->scale, $this->scale, 'F'); + } + } diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRGdImage.php b/vendor/chillerlan/php-qrcode/src/Output/QRGdImage.php new file mode 100644 index 000000000..25db1c902 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRGdImage.php @@ -0,0 +1,400 @@ +<?php +/** + * Class QRGdImage + * + * @created 05.12.2015 + * @author Smiley <smiley@chillerlan.net> + * @copyright 2015 Smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Output; + +use chillerlan\QRCode\Data\QRMatrix; +use chillerlan\Settings\SettingsContainerInterface; +use ErrorException; +use Throwable; +use function array_values, count, extension_loaded, imagebmp, imagecolorallocate, imagecolortransparent, + imagecreatetruecolor, imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, + imagescale, imagetypes, imagewebp, intdiv, intval, is_array, is_numeric, max, min, ob_end_clean, ob_get_contents, ob_start, + restore_error_handler, set_error_handler, sprintf; +use const IMG_BMP, IMG_GIF, IMG_JPG, IMG_PNG, IMG_WEBP; + +/** + * Converts the matrix into GD images, raw or base64 output (requires ext-gd) + * + * @see https://php.net/manual/book.image.php + * + * @deprecated 5.0.0 this class will be made abstract in future versions, + * calling it directly is deprecated - use one of the child classes instead + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ +class QRGdImage extends QROutputAbstract{ + + /** + * The GD image resource + * + * @see imagecreatetruecolor() + * @var resource|\GdImage + * + * @todo: add \GdImage type in v6 + */ + protected $image; + + /** + * The allocated background color + * + * @see \imagecolorallocate() + */ + protected int $background; + + /** + * Whether we're running in upscale mode (scale < 20) + * + * @see \chillerlan\QRCode\QROptions::$drawCircularModules + */ + protected bool $upscaled = false; + + /** + * @inheritDoc + * + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + * @noinspection PhpMissingParentConstructorInspection + */ + public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){ + $this->options = $options; + $this->matrix = $matrix; + + $this->checkGD(); + + if($this->options->invertMatrix){ + $this->matrix->invert(); + } + + $this->copyVars(); + $this->setMatrixDimensions(); + } + + /** + * Checks whether GD is installed and if the given mode is supported + * + * @return void + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + * @codeCoverageIgnore + */ + protected function checkGD():void{ + + if(!extension_loaded('gd')){ + throw new QRCodeOutputException('ext-gd not loaded'); + } + + $modes = [ + self::GDIMAGE_BMP => IMG_BMP, + self::GDIMAGE_GIF => IMG_GIF, + self::GDIMAGE_JPG => IMG_JPG, + self::GDIMAGE_PNG => IMG_PNG, + self::GDIMAGE_WEBP => IMG_WEBP, + ]; + + // likely using default or custom output + if(!isset($modes[$this->options->outputType])){ + return; + } + + $mode = $modes[$this->options->outputType]; + + if((imagetypes() & $mode) !== $mode){ + throw new QRCodeOutputException(sprintf('output mode "%s" not supported', $this->options->outputType)); + } + + } + + /** + * @inheritDoc + */ + public static function moduleValueIsValid($value):bool{ + + if(!is_array($value) || count($value) < 3){ + return false; + } + + // check the first 3 values of the array + foreach(array_values($value) as $i => $val){ + + if($i > 2){ + break; + } + + if(!is_numeric($val)){ + return false; + } + + } + + return true; + } + + /** + * @param array $value + * + * @inheritDoc + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + */ + protected function prepareModuleValue($value):int{ + $values = []; + + foreach(array_values($value) as $i => $val){ + + if($i > 2){ + break; + } + + $values[] = max(0, min(255, intval($val))); + } + + /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */ + $color = imagecolorallocate($this->image, ...$values); + + if($color === false){ + throw new QRCodeOutputException('could not set color: imagecolorallocate() error'); + } + + return $color; + } + + /** + * @inheritDoc + */ + protected function getDefaultModuleValue(bool $isDark):int{ + return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]); + } + + /** + * @inheritDoc + * + * @return string|resource|\GdImage + * + * @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn + * @throws \ErrorException + */ + public function dump(?string $file = null){ + + set_error_handler(function(int $errno, string $errstr):bool{ + throw new ErrorException($errstr, $errno); + }); + + $this->image = $this->createImage(); + // set module values after image creation because we need the GdImage instance + $this->setModuleValues(); + $this->setBgColor(); + + imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background); + + $this->drawImage(); + + if($this->upscaled){ + // scale down to the expected size + $this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10)); + $this->upscaled = false; + } + + // set transparency after scaling, otherwise it would be undone + // @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099 + $this->setTransparencyColor(); + + if($this->options->returnResource){ + restore_error_handler(); + + return $this->image; + } + + $imageData = $this->dumpImage(); + + $this->saveToFile($imageData, $file); + + if($this->options->outputBase64){ + // @todo: remove mime parameter in v6 + $imageData = $this->toBase64DataURI($imageData, 'image/'.$this->options->outputType); + } + + restore_error_handler(); + + return $imageData; + } + + /** + * Creates a new GdImage resource and scales it if necessary + * + * we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales + * + * @see https://github.com/chillerlan/php-qrcode/issues/23 + * + * @return \GdImage|resource + */ + protected function createImage(){ + + if($this->drawCircularModules && $this->options->gdImageUseUpscale && $this->options->scale < 20){ + // increase the initial image size by 10 + $this->length *= 10; + $this->scale *= 10; + $this->upscaled = true; + } + + return imagecreatetruecolor($this->length, $this->length); + } + + /** + * Sets the background color + */ + protected function setBgColor():void{ + + if(isset($this->background)){ + return; + } + + if($this::moduleValueIsValid($this->options->bgColor)){ + $this->background = $this->prepareModuleValue($this->options->bgColor); + + return; + } + + $this->background = $this->prepareModuleValue([255, 255, 255]); + } + + /** + * Sets the transparency color + */ + protected function setTransparencyColor():void{ + + // @todo: the jpg skip can be removed in v6 + if($this->options->outputType === QROutputInterface::GDIMAGE_JPG || !$this->options->imageTransparent){ + return; + } + + $transparencyColor = $this->background; + + if($this::moduleValueIsValid($this->options->transparencyColor)){ + $transparencyColor = $this->prepareModuleValue($this->options->transparencyColor); + } + + imagecolortransparent($this->image, $transparencyColor); + } + + /** + * Draws the QR image + */ + protected function drawImage():void{ + foreach($this->matrix->getMatrix() as $y => $row){ + foreach($row as $x => $M_TYPE){ + $this->module($x, $y, $M_TYPE); + } + } + } + + /** + * Creates a single QR pixel with the given settings + */ + protected function module(int $x, int $y, int $M_TYPE):void{ + + if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ + return; + } + + $color = $this->getModuleValue($M_TYPE); + + if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ + imagefilledellipse( + $this->image, + (($x * $this->scale) + intdiv($this->scale, 2)), + (($y * $this->scale) + intdiv($this->scale, 2)), + (int)($this->circleDiameter * $this->scale), + (int)($this->circleDiameter * $this->scale), + $color + ); + + return; + } + + imagefilledrectangle( + $this->image, + ($x * $this->scale), + ($y * $this->scale), + (($x + 1) * $this->scale), + (($y + 1) * $this->scale), + $color + ); + } + + /** + * Renders the image with the gdimage function for the desired output + * + * @see \imagebmp() + * @see \imagegif() + * @see \imagejpeg() + * @see \imagepng() + * @see \imagewebp() + * + * @todo: v6.0: make abstract and call from child classes + * @see https://github.com/chillerlan/php-qrcode/issues/223 + * @codeCoverageIgnore + */ + protected function renderImage():void{ + + switch($this->options->outputType){ + case QROutputInterface::GDIMAGE_BMP: + imagebmp($this->image, null, ($this->options->quality > 0)); + break; + case QROutputInterface::GDIMAGE_GIF: + imagegif($this->image); + break; + case QROutputInterface::GDIMAGE_JPG: + imagejpeg($this->image, null, max(-1, min(100, $this->options->quality))); + break; + case QROutputInterface::GDIMAGE_WEBP: + imagewebp($this->image, null, max(-1, min(100, $this->options->quality))); + break; + // silently default to png output + case QROutputInterface::GDIMAGE_PNG: + default: + imagepng($this->image, null, max(-1, min(9, $this->options->quality))); + } + + } + + /** + * Creates the final image by calling the desired GD output function + * + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + */ + protected function dumpImage():string{ + $exception = null; + $imageData = null; + + ob_start(); + + try{ + $this->renderImage(); + + $imageData = ob_get_contents(); + imagedestroy($this->image); + } + // not going to cover edge cases + // @codeCoverageIgnoreStart + catch(Throwable $e){ + $exception = $e; + } + // @codeCoverageIgnoreEnd + + ob_end_clean(); + + // throw here in case an exception happened within the output buffer + if($exception instanceof Throwable){ + throw new QRCodeOutputException($exception->getMessage()); + } + + return $imageData; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRGdImageBMP.php b/vendor/chillerlan/php-qrcode/src/Output/QRGdImageBMP.php new file mode 100644 index 000000000..268ebe7c2 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRGdImageBMP.php @@ -0,0 +1,33 @@ +<?php +/** + * Class QRGdImageBMP + * + * @created 25.10.2023 + * @author smiley <smiley@chillerlan.net> + * @copyright 2023 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Output; + +use function imagebmp; + +/** + * GdImage bmp output + * + * @see \imagebmp() + */ +class QRGdImageBMP extends QRGdImage{ + + public const MIME_TYPE = 'image/bmp'; + + /** + * @inheritDoc + */ + protected function renderImage():void{ + imagebmp($this->image, null, ($this->options->quality > 0)); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRGdImageGIF.php b/vendor/chillerlan/php-qrcode/src/Output/QRGdImageGIF.php new file mode 100644 index 000000000..a02130907 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRGdImageGIF.php @@ -0,0 +1,33 @@ +<?php +/** + * Class QRGdImageGIF + * + * @created 25.10.2023 + * @author smiley <smiley@chillerlan.net> + * @copyright 2023 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Output; + +use function imagegif; + +/** + * GdImage gif output + * + * @see \imagegif() + */ +class QRGdImageGIF extends QRGdImage{ + + public const MIME_TYPE = 'image/gif'; + + /** + * @inheritDoc + */ + protected function renderImage():void{ + imagegif($this->image); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRGdImageJPEG.php b/vendor/chillerlan/php-qrcode/src/Output/QRGdImageJPEG.php new file mode 100644 index 000000000..6be36e2fe --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRGdImageJPEG.php @@ -0,0 +1,40 @@ +<?php +/** + * Class QRGdImageJPEG + * + * @created 25.10.2023 + * @author smiley <smiley@chillerlan.net> + * @copyright 2023 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Output; + +use function imagejpeg, max, min; + +/** + * GdImage jpeg output + * + * @see \imagejpeg() + */ +class QRGdImageJPEG extends QRGdImage{ + + public const MIME_TYPE = 'image/jpg'; + + /** + * @inheritDoc + */ + protected function setTransparencyColor():void{ + // noop - transparency is not supported + } + + /** + * @inheritDoc + */ + protected function renderImage():void{ + imagejpeg($this->image, null, max(-1, min(100, $this->options->quality))); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRGdImagePNG.php b/vendor/chillerlan/php-qrcode/src/Output/QRGdImagePNG.php new file mode 100644 index 000000000..2db3fd5b4 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRGdImagePNG.php @@ -0,0 +1,33 @@ +<?php +/** + * Class QRGdImagePNG + * + * @created 25.10.2023 + * @author smiley <smiley@chillerlan.net> + * @copyright 2023 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Output; + +use function imagepng, max, min; + +/** + * GdImage png output + * + * @see \imagepng() + */ +class QRGdImagePNG extends QRGdImage{ + + public const MIME_TYPE = 'image/png'; + + /** + * @inheritDoc + */ + protected function renderImage():void{ + imagepng($this->image, null, max(-1, min(9, $this->options->quality))); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRGdImageWEBP.php b/vendor/chillerlan/php-qrcode/src/Output/QRGdImageWEBP.php new file mode 100644 index 000000000..cf8dfa9a5 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRGdImageWEBP.php @@ -0,0 +1,33 @@ +<?php +/** + * Class QRGdImageWEBP + * + * @created 25.10.2023 + * @author smiley <smiley@chillerlan.net> + * @copyright 2023 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Output; + +use function imagewebp, max, min; + +/** + * GdImage webp output + * + * @see \imagewebp() + */ +class QRGdImageWEBP extends QRGdImage{ + + public const MIME_TYPE = 'image/webp'; + + /** + * @inheritDoc + */ + protected function renderImage():void{ + imagewebp($this->image, null, max(-1, min(100, $this->options->quality))); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRImage.php b/vendor/chillerlan/php-qrcode/src/Output/QRImage.php index 8f533d341..cda496d36 100644 --- a/vendor/chillerlan/php-qrcode/src/Output/QRImage.php +++ b/vendor/chillerlan/php-qrcode/src/Output/QRImage.php @@ -2,216 +2,18 @@ /** * Class QRImage * - * @filesource QRImage.php - * @created 05.12.2015 - * @package chillerlan\QRCode\Output - * @author Smiley <smiley@chillerlan.net> - * @copyright 2015 Smiley + * @created 14.12.2021 + * @author smiley <smiley@chillerlan.net> + * @copyright 2021 smiley * @license MIT - * - * @noinspection PhpComposerExtensionStubsInspection */ namespace chillerlan\QRCode\Output; -use chillerlan\QRCode\Data\QRMatrix; -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, - imagecreatetruecolor, imagedestroy, imagefilledrectangle, imagegif, imagejpeg, imagepng, in_array, - is_array, ob_end_clean, ob_get_contents, ob_start, range, sprintf; - /** - * Converts the matrix into GD images, raw or base64 output (requires ext-gd) - * - * @see http://php.net/manual/book.image.php + * @deprecated 5.0.0 backward compatibility, use QRGdImage instead + * @see \chillerlan\QRCode\Output\QRGdImage */ -class QRImage extends QROutputAbstract{ - - /** - * GD image types that support transparency - * - * @var string[] - */ - protected const TRANSPARENCY_TYPES = [ - QRCode::OUTPUT_IMAGE_PNG, - QRCode::OUTPUT_IMAGE_GIF, - ]; - - protected string $defaultMode = QRCode::OUTPUT_IMAGE_PNG; - - /** - * The GD image resource - * - * @see imagecreatetruecolor() - * @var resource|\GdImage - * - * @phan-suppress PhanUndeclaredTypeProperty - */ - protected $image; - - /** - * @inheritDoc - * - * @throws \chillerlan\QRCode\QRCodeException - */ - public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){ - - if(!extension_loaded('gd')){ - throw new QRCodeException('ext-gd not loaded'); // @codeCoverageIgnore - } - - parent::__construct($options, $matrix); - } - - /** - * @inheritDoc - */ - protected function setModuleValues():void{ - - foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){ - $v = $this->options->moduleValues[$M_TYPE] ?? null; - - if(!is_array($v) || count($v) < 3){ - $this->moduleValues[$M_TYPE] = $defaultValue - ? [0, 0, 0] - : [255, 255, 255]; - } - else{ - $this->moduleValues[$M_TYPE] = array_values($v); - } - - } - - } - - /** - * @inheritDoc - * - * @return string|resource|\GdImage - * - * @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn - */ - public function dump(string $file = null){ - $file ??= $this->options->cachefile; - - $this->image = imagecreatetruecolor($this->length, $this->length); - - // avoid: Indirect modification of overloaded property $imageTransparencyBG has no effect - // https://stackoverflow.com/a/10455217 - $tbg = $this->options->imageTransparencyBG; - /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */ - $background = imagecolorallocate($this->image, ...$tbg); - - if((bool)$this->options->imageTransparent && in_array($this->options->outputType, $this::TRANSPARENCY_TYPES, true)){ - imagecolortransparent($this->image, $background); - } - - imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $background); - - foreach($this->matrix->matrix() as $y => $row){ - foreach($row as $x => $M_TYPE){ - $this->setPixel($x, $y, $this->moduleValues[$M_TYPE]); - } - } - - if($this->options->returnResource){ - return $this->image; - } - - $imageData = $this->dumpImage(); - - if($file !== null){ - $this->saveToFile($imageData, $file); - } - - if($this->options->imageBase64){ - $imageData = sprintf('data:image/%s;base64,%s', $this->options->outputType, base64_encode($imageData)); - } - - return $imageData; - } - - /** - * Creates a single QR pixel with the given settings - */ - protected function setPixel(int $x, int $y, array $rgb):void{ - imagefilledrectangle( - $this->image, - $x * $this->scale, - $y * $this->scale, - ($x + 1) * $this->scale, - ($y + 1) * $this->scale, - /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */ - imagecolorallocate($this->image, ...$rgb) - ); - } - - /** - * Creates the final image by calling the desired GD output function - * - * @throws \chillerlan\QRCode\Output\QRCodeOutputException - */ - protected function dumpImage():string{ - ob_start(); - - try{ - call_user_func([$this, $this->outputMode ?? $this->defaultMode]); - } - // not going to cover edge cases - // @codeCoverageIgnoreStart - catch(Exception $e){ - throw new QRCodeOutputException($e->getMessage()); - } - // @codeCoverageIgnoreEnd - - $imageData = ob_get_contents(); - imagedestroy($this->image); - - ob_end_clean(); - - return $imageData; - } - - /** - * PNG output - * - * @return void - */ - protected function png():void{ - imagepng( - $this->image, - null, - in_array($this->options->pngCompression, range(-1, 9), true) - ? $this->options->pngCompression - : -1 - ); - } - - /** - * Jiff - like... JitHub! - * - * @return void - */ - protected function gif():void{ - imagegif($this->image); - } - - /** - * JPG output - * - * @return void - */ - protected function jpg():void{ - imagejpeg( - $this->image, - null, - in_array($this->options->jpegQuality, range(0, 100), true) - ? $this->options->jpegQuality - : 85 - ); - } +class QRImage extends QRGdImage{ } diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRImagick.php b/vendor/chillerlan/php-qrcode/src/Output/QRImagick.php index 49516d30e..214311a93 100644 --- a/vendor/chillerlan/php-qrcode/src/Output/QRImagick.php +++ b/vendor/chillerlan/php-qrcode/src/Output/QRImagick.php @@ -2,9 +2,7 @@ /** * Class QRImagick * - * @filesource QRImagick.php * @created 04.07.2018 - * @package chillerlan\QRCode\Output * @author smiley <smiley@chillerlan.net> * @copyright 2018 smiley * @license MIT @@ -15,51 +13,98 @@ namespace chillerlan\QRCode\Output; use chillerlan\QRCode\Data\QRMatrix; -use chillerlan\QRCode\QRCodeException; use chillerlan\Settings\SettingsContainerInterface; -use Imagick, ImagickDraw, ImagickPixel; - -use function extension_loaded, is_string; +use finfo, Imagick, ImagickDraw, ImagickPixel; +use function extension_loaded, in_array, is_string, max, min, preg_match, strlen; +use const FILEINFO_MIME_TYPE; /** * ImageMagick output module (requires ext-imagick) * - * @see http://php.net/manual/book.imagick.php - * @see http://phpimagick.com + * @see https://php.net/manual/book.imagick.php + * @see https://phpimagick.com */ class QRImagick extends QROutputAbstract{ + /** + * The main image instance + */ protected Imagick $imagick; /** + * The main draw instance + */ + protected ImagickDraw $imagickDraw; + + /** + * The allocated background color + */ + protected ImagickPixel $backgroundColor; + + /** * @inheritDoc + * + * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){ - if(!extension_loaded('imagick')){ - throw new QRCodeException('ext-imagick not loaded'); // @codeCoverageIgnore + foreach(['fileinfo', 'imagick'] as $ext){ + if(!extension_loaded($ext)){ + throw new QRCodeOutputException(sprintf('ext-%s not loaded', $ext)); // @codeCoverageIgnore + } } parent::__construct($options, $matrix); } /** + * note: we're not necessarily validating the several values, just checking the general syntax + * + * @see https://www.php.net/manual/imagickpixel.construct.php * @inheritDoc */ - protected function setModuleValues():void{ + public static function moduleValueIsValid($value):bool{ - foreach($this::DEFAULT_MODULE_VALUES as $type => $defaultValue){ - $v = $this->options->moduleValues[$type] ?? null; + if(!is_string($value)){ + return false; + } - if(!is_string($v)){ - $this->moduleValues[$type] = $defaultValue - ? new ImagickPixel($this->options->markupDark) - : new ImagickPixel($this->options->markupLight); - } - else{ - $this->moduleValues[$type] = new ImagickPixel($v); - } + $value = trim($value); + + // hex notation + // #rgb(a) + // #rrggbb(aa) + // #rrrrggggbbbb(aaaa) + // ... + if(preg_match('/^#[a-f\d]+$/i', $value) && in_array((strlen($value) - 1), [3, 4, 6, 8, 9, 12, 16, 24, 32], true)){ + return true; + } + + // css (-like) func(...values) + if(preg_match('#^(graya?|hs(b|la?)|rgba?)\([\d .,%]+\)$#i', $value)){ + return true; } + + // predefined css color + if(preg_match('/^[a-z]+$/i', $value)){ + return true; + } + + return false; + } + + /** + * @inheritDoc + */ + protected function prepareModuleValue($value):ImagickPixel{ + return new ImagickPixel($value); + } + + /** + * @inheritDoc + */ + protected function getDefaultModuleValue(bool $isDark):ImagickPixel{ + return $this->prepareModuleValue(($isDark) ? '#000' : '#fff'); } /** @@ -67,18 +112,14 @@ class QRImagick extends QROutputAbstract{ * * @return string|\Imagick */ - public function dump(string $file = null){ - $file ??= $this->options->cachefile; - $this->imagick = new Imagick; - - $this->imagick->newImage( - $this->length, - $this->length, - new ImagickPixel($this->options->imagickBG ?? 'transparent'), - $this->options->imagickFormat - ); + public function dump(?string $file = null){ + $this->setBgColor(); + + $this->imagick = $this->createImage(); $this->drawImage(); + // set transparency color after all operations + $this->setTransparencyColor(); if($this->options->returnResource){ return $this->imagick; @@ -86,34 +127,109 @@ class QRImagick extends QROutputAbstract{ $imageData = $this->imagick->getImageBlob(); - if($file !== null){ - $this->saveToFile($imageData, $file); + $this->imagick->destroy(); + + $this->saveToFile($imageData, $file); + + if($this->options->outputBase64){ + $imageData = $this->toBase64DataURI($imageData, (new finfo(FILEINFO_MIME_TYPE))->buffer($imageData)); } return $imageData; } /** + * Sets the background color + */ + protected function setBgColor():void{ + + if($this::moduleValueIsValid($this->options->bgColor)){ + $this->backgroundColor = $this->prepareModuleValue($this->options->bgColor); + + return; + } + + $this->backgroundColor = $this->prepareModuleValue('white'); + } + + /** + * Creates a new Imagick instance + */ + protected function createImage():Imagick{ + $imagick = new Imagick; + [$width, $height] = $this->getOutputDimensions(); + + $imagick->newImage($width, $height, $this->backgroundColor, $this->options->imagickFormat); + + if($this->options->quality > -1){ + $imagick->setImageCompressionQuality(max(0, min(100, $this->options->quality))); + } + + return $imagick; + } + + /** + * Sets the transparency color + */ + protected function setTransparencyColor():void{ + + if(!$this->options->imageTransparent){ + return; + } + + $transparencyColor = $this->backgroundColor; + + if($this::moduleValueIsValid($this->options->transparencyColor)){ + $transparencyColor = $this->prepareModuleValue($this->options->transparencyColor); + } + + $this->imagick->transparentPaintImage($transparencyColor, 0.0, 10, false); + } + + /** * Creates the QR image via ImagickDraw */ protected function drawImage():void{ - $draw = new ImagickDraw; + $this->imagickDraw = new ImagickDraw; + $this->imagickDraw->setStrokeWidth(0); - foreach($this->matrix->matrix() as $y => $row){ + foreach($this->matrix->getMatrix() as $y => $row){ foreach($row as $x => $M_TYPE){ - $draw->setStrokeColor($this->moduleValues[$M_TYPE]); - $draw->setFillColor($this->moduleValues[$M_TYPE]); - $draw->rectangle( - $x * $this->scale, - $y * $this->scale, - ($x + 1) * $this->scale, - ($y + 1) * $this->scale - ); - + $this->module($x, $y, $M_TYPE); } } - $this->imagick->drawImage($draw); + $this->imagick->drawImage($this->imagickDraw); + } + + /** + * draws a single pixel at the given position + */ + protected function module(int $x, int $y, int $M_TYPE):void{ + + if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ + return; + } + + $this->imagickDraw->setFillColor($this->getModuleValue($M_TYPE)); + + if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ + $this->imagickDraw->circle( + (($x + 0.5) * $this->scale), + (($y + 0.5) * $this->scale), + (($x + 0.5 + $this->circleRadius) * $this->scale), + (($y + 0.5) * $this->scale) + ); + + return; + } + + $this->imagickDraw->rectangle( + ($x * $this->scale), + ($y * $this->scale), + ((($x + 1) * $this->scale) - 1), + ((($y + 1) * $this->scale) - 1) + ); } } diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRMarkup.php b/vendor/chillerlan/php-qrcode/src/Output/QRMarkup.php index 06d6e88cb..240bd45ad 100644 --- a/vendor/chillerlan/php-qrcode/src/Output/QRMarkup.php +++ b/vendor/chillerlan/php-qrcode/src/Output/QRMarkup.php @@ -2,9 +2,7 @@ /** * Class QRMarkup * - * @filesource QRMarkup.php * @created 17.12.2016 - * @package chillerlan\QRCode\Output * @author Smiley <smiley@chillerlan.net> * @copyright 2016 Smiley * @license MIT @@ -12,149 +10,85 @@ namespace chillerlan\QRCode\Output; -use chillerlan\QRCode\QRCode; - -use function is_string, sprintf, strip_tags, trim; +use function is_string, preg_match, strip_tags, trim; /** - * Converts the matrix into markup types: HTML, SVG, ... + * Abstract for markup types: HTML, SVG, ... XML anyone? */ -class QRMarkup extends QROutputAbstract{ - - protected string $defaultMode = QRCode::OUTPUT_MARKUP_SVG; - - /** - * @see \sprintf() - */ - protected string $svgHeader = '<svg xmlns="http://www.w3.org/2000/svg" class="qr-svg %1$s" '. - 'style="width: 100%%; height: auto;" viewBox="0 0 %2$d %2$d">'; +abstract class QRMarkup extends QROutputAbstract{ /** + * note: we're not necessarily validating the several values, just checking the general syntax + * note: css4 colors are not included + * + * @todo: XSS proof + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value * @inheritDoc */ - protected function setModuleValues():void{ - - foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){ - $v = $this->options->moduleValues[$M_TYPE] ?? null; - - if(!is_string($v)){ - $this->moduleValues[$M_TYPE] = $defaultValue - ? $this->options->markupDark - : $this->options->markupLight; - } - else{ - $this->moduleValues[$M_TYPE] = trim(strip_tags($v), '\'"'); - } + public static function moduleValueIsValid($value):bool{ + if(!is_string($value)){ + return false; } - } - - /** - * HTML output - */ - protected function html(string $file = null):string{ + $value = trim(strip_tags($value), " '\"\r\n\t"); - $html = empty($this->options->cssClass) - ? '<div>' - : '<div class="'.$this->options->cssClass.'">'; - - $html .= $this->options->eol; - - foreach($this->matrix->matrix() as $row){ - $html .= '<div>'; - - foreach($row as $M_TYPE){ - $html .= '<span style="background: '.$this->moduleValues[$M_TYPE].';"></span>'; - } - - $html .= '</div>'.$this->options->eol; + // hex notation + // #rgb(a) + // #rrggbb(aa) + if(preg_match('/^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i', $value)){ + return true; } - $html .= '</div>'.$this->options->eol; + // css: hsla/rgba(...values) + if(preg_match('#^(hsla?|rgba?)\([\d .,%/]+\)$#i', $value)){ + return true; + } - if($file !== null){ - return '<!DOCTYPE html>'. - '<head><meta charset="UTF-8"><title>QR Code</title></head>'. - '<body>'.$this->options->eol.$html.'</body>'; + // predefined css color + if(preg_match('/^[a-z]+$/i', $value)){ + return true; } - return $html; + return false; } /** - * SVG output - * - * @see https://github.com/codemasher/php-qrcode/pull/5 + * @inheritDoc */ - protected function svg(string $file = null):string{ - $matrix = $this->matrix->matrix(); - - $svg = sprintf($this->svgHeader, $this->options->cssClass, $this->options->svgViewBoxSize ?? $this->moduleCount) - .$this->options->eol - .'<defs>'.$this->options->svgDefs.'</defs>' - .$this->options->eol; - - foreach($this->moduleValues as $M_TYPE => $value){ - $path = ''; - - foreach($matrix as $y => $row){ - //we'll combine active blocks within a single row as a lightweight compression technique - $start = null; - $count = 0; - - foreach($row as $x => $module){ - - if($module === $M_TYPE){ - $count++; - - if($start === null){ - $start = $x; - } - - if(isset($row[$x + 1])){ - continue; - } - } - - if($count > 0){ - $len = $count; - $start ??= 0; // avoid type coercion in sprintf() - phan happy - - $path .= sprintf('M%s %s h%s v1 h-%sZ ', $start, $y, $len, $len); - - // reset count - $count = 0; - $start = null; - } - - } - - } - - if(!empty($path)){ - $svg .= sprintf( - '<path class="qr-%s %s" stroke="transparent" fill="%s" fill-opacity="%s" d="%s" />', - $M_TYPE, $this->options->cssClass, $value, $this->options->svgOpacity, $path - ); - } + protected function prepareModuleValue($value):string{ + return trim(strip_tags($value), " '\"\r\n\t"); + } - } + /** + * @inheritDoc + */ + protected function getDefaultModuleValue(bool $isDark):string{ + return ($isDark) ? '#000' : '#fff'; + } - // close svg - $svg .= '</svg>'.$this->options->eol; + /** + * @inheritDoc + */ + public function dump(?string $file = null):string{ + $data = $this->createMarkup($file !== null); - // if saving to file, append the correct headers - if($file !== null){ - return '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'. - $this->options->eol.$svg; - } + $this->saveToFile($data, $file); - if($this->options->imageBase64){ - $svg = sprintf('data:image/svg+xml;base64,%s', base64_encode($svg)); - } + return $data; + } - return $svg; + /** + * returns a string with all css classes for the current element + */ + protected function getCssClass(int $M_TYPE = 0):string{ + return $this->options->cssClass; } + /** + * returns the fully parsed and rendered markup string for the given input + */ + abstract protected function createMarkup(bool $saveToFile):string; + } diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRMarkupHTML.php b/vendor/chillerlan/php-qrcode/src/Output/QRMarkupHTML.php new file mode 100644 index 000000000..65dc49a8a --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRMarkupHTML.php @@ -0,0 +1,51 @@ +<?php +/** + * Class QRMarkupHTML + * + * @created 06.06.2022 + * @author smiley <smiley@chillerlan.net> + * @copyright 2022 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Output; + +use function implode, sprintf; + +/** + * HTML output (a cheap markup substitute when SVG is not available or not an option) + */ +class QRMarkupHTML extends QRMarkup{ + + public const MIME_TYPE = 'text/html'; + + /** + * @inheritDoc + */ + protected function createMarkup(bool $saveToFile):string{ + $rows = []; + $cssClass = $this->getCssClass(); + + foreach($this->matrix->getMatrix() as $row){ + $element = '<span style="background: %s;"></span>'; + $modules = array_map(fn(int $M_TYPE):string => sprintf($element, $this->getModuleValue($M_TYPE)), $row); + + $rows[] = sprintf('<div>%s</div>%s', implode('', $modules), $this->eol); + } + + $html = sprintf('<div class="%1$s">%3$s%2$s</div>%3$s', $cssClass, implode('', $rows), $this->eol); + + // wrap the snippet into a body when saving to file + if($saveToFile){ + $html = sprintf( + '<!DOCTYPE html><html lang="none">%2$s<head>%2$s<meta charset="UTF-8">%2$s'. + '<title>QR Code</title></head>%2$s<body>%1$s</body>%2$s</html>', + $html, + $this->eol + ); + } + + return $html; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRMarkupSVG.php b/vendor/chillerlan/php-qrcode/src/Output/QRMarkupSVG.php new file mode 100644 index 000000000..735c4180b --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRMarkupSVG.php @@ -0,0 +1,200 @@ +<?php +/** + * Class QRMarkupSVG + * + * @created 06.06.2022 + * @author smiley <smiley@chillerlan.net> + * @copyright 2022 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Output; + +use function array_chunk, implode, is_string, preg_match, sprintf, trim; + +/** + * SVG output + * + * @see https://github.com/codemasher/php-qrcode/pull/5 + * @see https://developer.mozilla.org/en-US/docs/Web/SVG + * @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/ + * @see https://lea.verou.me/blog/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/ + * @see https://codepen.io/leaverou/full/RmwzKv + * @see https://jakearchibald.github.io/svgomg/ + * @see https://web.archive.org/web/20200220211445/http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html + */ +class QRMarkupSVG extends QRMarkup{ + + public const MIME_TYPE = 'image/svg+xml'; + + /** + * @todo: XSS proof + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill + * @inheritDoc + */ + public static function moduleValueIsValid($value):bool{ + + if(!is_string($value)){ + return false; + } + + $value = trim($value); + + // url(...) + if(preg_match('~^url\([-/#a-z\d]+\)$~i', $value)){ + return true; + } + + // otherwise check for standard css notation + return parent::moduleValueIsValid($value); + } + + /** + * @inheritDoc + */ + protected function getOutputDimensions():array{ + return [$this->moduleCount, $this->moduleCount]; + } + + /** + * @inheritDoc + */ + protected function getCssClass(int $M_TYPE = 0):string{ + return implode(' ', [ + 'qr-'.($this::LAYERNAMES[$M_TYPE] ?? $M_TYPE), + $this->matrix->isDark($M_TYPE) ? 'dark' : 'light', + $this->options->cssClass, + ]); + } + + /** + * @inheritDoc + */ + protected function createMarkup(bool $saveToFile):string{ + $svg = $this->header(); + + if(!empty($this->options->svgDefs)){ + $svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->eol); + } + + $svg .= $this->paths(); + + // close svg + $svg .= sprintf('%1$s</svg>%1$s', $this->eol); + + // transform to data URI only when not saving to file + if(!$saveToFile && $this->options->outputBase64){ + $svg = $this->toBase64DataURI($svg); + } + + return $svg; + } + + /** + * returns the value for the SVG viewBox attribute + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox + * @see https://css-tricks.com/scale-svg/#article-header-id-3 + */ + protected function getViewBox():string{ + [$width, $height] = $this->getOutputDimensions(); + + return sprintf('0 0 %s %s', $width, $height); + } + + /** + * returns the <svg> header with the given options parsed + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg + */ + protected function header():string{ + + $header = sprintf( + '<svg xmlns="http://www.w3.org/2000/svg" class="qr-svg %1$s" viewBox="%2$s" preserveAspectRatio="%3$s">%4$s', + $this->options->cssClass, + $this->getViewBox(), + $this->options->svgPreserveAspectRatio, + $this->eol + ); + + if($this->options->svgAddXmlHeader){ + $header = sprintf('<?xml version="1.0" encoding="UTF-8"?>%s%s', $this->eol, $header); + } + + return $header; + } + + /** + * returns one or more SVG <path> elements + */ + protected function paths():string{ + $paths = $this->collectModules(fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE)); + $svg = []; + + // create the path elements + foreach($paths as $M_TYPE => $modules){ + // limit the total line length + $chunks = array_chunk($modules, 100); + $chonks = []; + + foreach($chunks as $chunk){ + $chonks[] = implode(' ', $chunk); + } + + $path = implode($this->eol, $chonks); + + if(empty($path)){ + continue; + } + + $svg[] = $this->path($path, $M_TYPE); + } + + return implode($this->eol, $svg); + } + + /** + * renders and returns a single <path> element + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path + */ + protected function path(string $path, int $M_TYPE):string{ + + if($this->options->svgUseFillAttributes){ + return sprintf( + '<path class="%s" fill="%s" d="%s"/>', + $this->getCssClass($M_TYPE), + $this->getModuleValue($M_TYPE), + $path + ); + } + + return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path); + } + + /** + * returns a path segment for a single module + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d + */ + protected function module(int $x, int $y, int $M_TYPE):string{ + + if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ + return ''; + } + + if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ + // string interpolation: ugly and fast + $ix = ($x + 0.5 - $this->circleRadius); + $iy = ($y + 0.5); + + // phpcs:ignore + return "M$ix $iy a$this->circleRadius $this->circleRadius 0 1 0 $this->circleDiameter 0 a$this->circleRadius $this->circleRadius 0 1 0 -$this->circleDiameter 0Z"; + } + + // phpcs:ignore + return "M$x $y h1 v1 h-1Z"; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QROutputAbstract.php b/vendor/chillerlan/php-qrcode/src/Output/QROutputAbstract.php index d4ed3d0c9..a2757ac8c 100644 --- a/vendor/chillerlan/php-qrcode/src/Output/QROutputAbstract.php +++ b/vendor/chillerlan/php-qrcode/src/Output/QROutputAbstract.php @@ -2,9 +2,7 @@ /** * Class QROutputAbstract * - * @filesource QROutputAbstract.php * @created 09.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -12,10 +10,10 @@ namespace chillerlan\QRCode\Output; -use chillerlan\QRCode\{Data\QRMatrix, QRCode}; +use chillerlan\QRCode\Data\QRMatrix; use chillerlan\Settings\SettingsContainerInterface; - -use function call_user_func_array, dirname, file_put_contents, get_called_class, in_array, is_writable, sprintf; +use Closure; +use function base64_encode, dirname, file_put_contents, is_writable, ksort, sprintf; /** * common output abstract @@ -25,30 +23,11 @@ abstract class QROutputAbstract implements QROutputInterface{ /** * the current size of the QR matrix * - * @see \chillerlan\QRCode\Data\QRMatrix::size() + * @see \chillerlan\QRCode\Data\QRMatrix::getSize() */ protected int $moduleCount; /** - * the current output mode - * - * @see \chillerlan\QRCode\QROptions::$outputType - */ - protected string $outputMode; - - /** - * the default output mode of the current output module - */ - protected string $defaultMode; - - /** - * the current scaling for a QR pixel - * - * @see \chillerlan\QRCode\QROptions::$scale - */ - protected int $scale; - - /** * the side length of the QR image (modules * scale) */ protected int $length; @@ -68,62 +47,215 @@ abstract class QROutputAbstract implements QROutputInterface{ */ protected SettingsContainerInterface $options; + /** @see \chillerlan\QRCode\QROptions::$scale */ + protected int $scale; + /** @see \chillerlan\QRCode\QROptions::$connectPaths */ + protected bool $connectPaths; + /** @see \chillerlan\QRCode\QROptions::$excludeFromConnect */ + protected array $excludeFromConnect; + /** @see \chillerlan\QRCode\QROptions::$eol */ + protected string $eol; + /** @see \chillerlan\QRCode\QROptions::$drawLightModules */ + protected bool $drawLightModules; + /** @see \chillerlan\QRCode\QROptions::$drawCircularModules */ + protected bool $drawCircularModules; + /** @see \chillerlan\QRCode\QROptions::$keepAsSquare */ + protected array $keepAsSquare; + /** @see \chillerlan\QRCode\QROptions::$circleRadius */ + protected float $circleRadius; + protected float $circleDiameter; + /** * QROutputAbstract constructor. */ public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){ - $this->options = $options; - $this->matrix = $matrix; - $this->moduleCount = $this->matrix->size(); + $this->options = $options; + $this->matrix = $matrix; + + if($this->options->invertMatrix){ + $this->matrix->invert(); + } + + $this->copyVars(); + $this->setMatrixDimensions(); + $this->setModuleValues(); + } + + /** + * Creates copies of several QROptions values to avoid calling the magic getters + * in long loops for a significant performance increase. + * + * These variables are usually used in the "module" methods and are called up to 31329 times (at version 40). + */ + protected function copyVars():void{ + + $vars = [ + 'connectPaths', + 'excludeFromConnect', + 'eol', + 'drawLightModules', + 'drawCircularModules', + 'keepAsSquare', + 'circleRadius', + ]; + + foreach($vars as $property){ + $this->{$property} = $this->options->{$property}; + } + + $this->circleDiameter = ($this->circleRadius * 2); + } + + /** + * Sets/updates the matrix dimensions + * + * Call this method if you modify the matrix from within your custom module in case the dimensions have been changed + */ + protected function setMatrixDimensions():void{ + $this->moduleCount = $this->matrix->getSize(); $this->scale = $this->options->scale; - $this->length = $this->moduleCount * $this->scale; + $this->length = ($this->moduleCount * $this->scale); + } + + /** + * Returns a 2 element array with the current output width and height + * + * The type and units of the values depend on the output class. The default value is the current module count * scale. + */ + protected function getOutputDimensions():array{ + return [$this->length, $this->length]; + } - $class = get_called_class(); + /** + * Sets the initial module values + */ + protected function setModuleValues():void{ - if(isset(QRCode::OUTPUT_MODES[$class]) && in_array($this->options->outputType, QRCode::OUTPUT_MODES[$class])){ - $this->outputMode = $this->options->outputType; + // first fill the map with the default values + foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){ + $this->moduleValues[$M_TYPE] = $this->getDefaultModuleValue($defaultValue); } - $this->setModuleValues(); + // now loop over the options values to replace defaults and add extra values + foreach($this->options->moduleValues as $M_TYPE => $value){ + if($this::moduleValueIsValid($value)){ + $this->moduleValues[$M_TYPE] = $this->prepareModuleValue($value); + } + } + + } + + /** + * Prepares the value for the given input (return value depends on the output class) + * + * @param mixed $value + * + * @return mixed|null + */ + abstract protected function prepareModuleValue($value); + + /** + * Returns a default value for either dark or light modules (return value depends on the output class) + * + * @return mixed|null + */ + abstract protected function getDefaultModuleValue(bool $isDark); + + /** + * Returns the prepared value for the given $M_TYPE + * + * @return mixed return value depends on the output class + * @throws \chillerlan\QRCode\Output\QRCodeOutputException if $moduleValues[$M_TYPE] doesn't exist + */ + protected function getModuleValue(int $M_TYPE){ + + if(!isset($this->moduleValues[$M_TYPE])){ + throw new QRCodeOutputException(sprintf('$M_TYPE %012b not found in module values map', $M_TYPE)); + } + + return $this->moduleValues[$M_TYPE]; + } + + /** + * Returns the prepared module value at the given coordinate [$x, $y] (convenience) + * + * @return mixed|null + */ + protected function getModuleValueAt(int $x, int $y){ + return $this->getModuleValue($this->matrix->get($x, $y)); } /** - * Sets the initial module values (clean-up & defaults) + * Returns a base64 data URI for the given string and mime type */ - abstract protected function setModuleValues():void; + protected function toBase64DataURI(string $data, ?string $mime = null):string{ + return sprintf('data:%s;base64,%s', ($mime ?? $this::MIME_TYPE), base64_encode($data)); + } /** - * saves the qr data to a file + * Saves the qr $data to a $file. If $file is null, nothing happens. * * @see file_put_contents() - * @see \chillerlan\QRCode\QROptions::cachefile + * @see \chillerlan\QRCode\QROptions::$cachefile * * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ - protected function saveToFile(string $data, string $file):bool{ + protected function saveToFile(string $data, ?string $file = null):void{ + + if($file === null){ + return; + } if(!is_writable(dirname($file))){ - throw new QRCodeOutputException(sprintf('Could not write data to cache file: %s', $file)); + throw new QRCodeOutputException(sprintf('Cannot write data to cache file: %s', $file)); } - return (bool)file_put_contents($file, $data); + if(file_put_contents($file, $data) === false){ + throw new QRCodeOutputException(sprintf('Cannot write data to cache file: %s (file_put_contents error)', $file)); + } } /** - * @inheritDoc + * collects the modules per QRMatrix::M_* type and runs a $transform function on each module and + * returns an array with the transformed modules + * + * The transform callback is called with the following parameters: + * + * $x - current column + * $y - current row + * $M_TYPE - field value + * $M_TYPE_LAYER - (possibly modified) field value that acts as layer id */ - public function dump(string $file = null){ - $file ??= $this->options->cachefile; + protected function collectModules(Closure $transform):array{ + $paths = []; - // call the built-in output method with the optional file path as parameter - // to make the called method aware if a cache file was given - $data = call_user_func_array([$this, $this->outputMode ?? $this->defaultMode], [$file]); + // collect the modules for each type + foreach($this->matrix->getMatrix() as $y => $row){ + foreach($row as $x => $M_TYPE){ + $M_TYPE_LAYER = $M_TYPE; - if($file !== null){ - $this->saveToFile($data, $file); + if($this->connectPaths && !$this->matrix->checkTypeIn($x, $y, $this->excludeFromConnect)){ + // to connect paths we'll redeclare the $M_TYPE_LAYER to data only + $M_TYPE_LAYER = QRMatrix::M_DATA; + + if($this->matrix->isDark($M_TYPE)){ + $M_TYPE_LAYER = QRMatrix::M_DATA_DARK; + } + } + + // collect the modules per $M_TYPE + $module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER); + + if(!empty($module)){ + $paths[$M_TYPE_LAYER][] = $module; + } + } } - return $data; + // beautify output + ksort($paths); + + return $paths; } } diff --git a/vendor/chillerlan/php-qrcode/src/Output/QROutputInterface.php b/vendor/chillerlan/php-qrcode/src/Output/QROutputInterface.php index b07b8e7a5..7d2315180 100644 --- a/vendor/chillerlan/php-qrcode/src/Output/QROutputInterface.php +++ b/vendor/chillerlan/php-qrcode/src/Output/QROutputInterface.php @@ -2,9 +2,7 @@ /** * Interface QROutputInterface, * - * @filesource QROutputInterface.php * @created 02.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -19,36 +17,210 @@ use chillerlan\QRCode\Data\QRMatrix; */ interface QROutputInterface{ - const DEFAULT_MODULE_VALUES = [ + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const MARKUP_HTML = 'html'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const MARKUP_SVG = 'svg'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const GDIMAGE_BMP = 'bmp'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const GDIMAGE_GIF = 'gif'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const GDIMAGE_JPG = 'jpg'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const GDIMAGE_PNG = 'png'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const GDIMAGE_WEBP = 'webp'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const STRING_JSON = 'json'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const STRING_TEXT = 'text'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const IMAGICK = 'imagick'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const FPDF = 'fpdf'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const EPS = 'eps'; + + /** + * @var string + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const CUSTOM = 'custom'; + + /** + * Map of built-in output modes => class FQN + * + * @var string[] + * @deprecated 5.0.0 <no replacement> + * @see https://github.com/chillerlan/php-qrcode/issues/223 + */ + public const MODES = [ + self::MARKUP_SVG => QRMarkupSVG::class, + self::MARKUP_HTML => QRMarkupHTML::class, + self::GDIMAGE_BMP => QRGdImageBMP::class, + self::GDIMAGE_GIF => QRGdImageGIF::class, + self::GDIMAGE_JPG => QRGdImageJPEG::class, + self::GDIMAGE_PNG => QRGdImagePNG::class, + self::GDIMAGE_WEBP => QRGdImageWEBP::class, + self::STRING_JSON => QRStringJSON::class, + self::STRING_TEXT => QRStringText::class, + self::IMAGICK => QRImagick::class, + self::FPDF => QRFpdf::class, + self::EPS => QREps::class, + ]; + + /** + * Map of module type => default value + * + * @var bool[] + */ + public 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_DARKMODULE_LIGHT => 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_FINDER_DOT_LIGHT => 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 => true, + QRMatrix::M_DATA_DARK => true, + QRMatrix::M_FINDER_DARK => true, + QRMatrix::M_SEPARATOR_DARK => true, + QRMatrix::M_ALIGNMENT_DARK => true, + QRMatrix::M_TIMING_DARK => true, + QRMatrix::M_FORMAT_DARK => true, + QRMatrix::M_VERSION_DARK => true, + QRMatrix::M_QUIETZONE_DARK => true, + QRMatrix::M_LOGO_DARK => true, + QRMatrix::M_FINDER_DOT => true, ]; /** - * generates the output, optionally dumps it to a file, and returns it + * Map of module type => readable name (for CSS etc.) + * + * @var string[] + */ + public const LAYERNAMES = [ + // light + QRMatrix::M_NULL => 'null', + QRMatrix::M_DARKMODULE_LIGHT => 'darkmodule-light', + QRMatrix::M_DATA => 'data', + QRMatrix::M_FINDER => 'finder', + QRMatrix::M_SEPARATOR => 'separator', + QRMatrix::M_ALIGNMENT => 'alignment', + QRMatrix::M_TIMING => 'timing', + QRMatrix::M_FORMAT => 'format', + QRMatrix::M_VERSION => 'version', + QRMatrix::M_QUIETZONE => 'quietzone', + QRMatrix::M_LOGO => 'logo', + QRMatrix::M_FINDER_DOT_LIGHT => 'finder-dot-light', + // dark + QRMatrix::M_DARKMODULE => 'darkmodule', + QRMatrix::M_DATA_DARK => 'data-dark', + QRMatrix::M_FINDER_DARK => 'finder-dark', + QRMatrix::M_SEPARATOR_DARK => 'separator-dark', + QRMatrix::M_ALIGNMENT_DARK => 'alignment-dark', + QRMatrix::M_TIMING_DARK => 'timing-dark', + QRMatrix::M_FORMAT_DARK => 'format-dark', + QRMatrix::M_VERSION_DARK => 'version-dark', + QRMatrix::M_QUIETZONE_DARK => 'quietzone-dark', + QRMatrix::M_LOGO_DARK => 'logo-dark', + QRMatrix::M_FINDER_DOT => 'finder-dot', + ]; + + /** + * @var string + * @see \chillerlan\QRCode\Output\QROutputAbstract::toBase64DataURI() + * @internal do not call this constant from the interface, but rather from one of the child classes + */ + public const MIME_TYPE = ''; + + /** + * Determines whether the given value is valid + * + * @param mixed $value + */ + public static function moduleValueIsValid($value):bool; + + /** + * Generates the output, optionally dumps it to a file, and returns it + * + * please note that the value of QROptions::$cachefile is already evaluated at this point. + * if the output module is invoked manually, it has no effect at all. + * you need to supply the $file parameter here in that case (or handle the option value in your custom output module). + * + * @see \chillerlan\QRCode\QRCode::renderMatrix() * * @return mixed */ - public function dump(string $file = null); + public function dump(?string $file = null); } diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRString.php b/vendor/chillerlan/php-qrcode/src/Output/QRString.php index 3ed5153e1..2d6d052d6 100644 --- a/vendor/chillerlan/php-qrcode/src/Output/QRString.php +++ b/vendor/chillerlan/php-qrcode/src/Output/QRString.php @@ -2,75 +2,110 @@ /** * Class QRString * - * @filesource QRString.php * @created 05.12.2015 - * @package chillerlan\QRCode\Output * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT * - * @noinspection PhpUnusedParameterInspection * @noinspection PhpComposerExtensionStubsInspection */ namespace chillerlan\QRCode\Output; -use chillerlan\QRCode\QRCode; - -use function implode, is_string, json_encode; +use function implode, is_string, json_encode, max, min, sprintf; +use const JSON_THROW_ON_ERROR; /** * Converts the matrix data into string types + * + * @deprecated 5.0.0 this class will be removed in future versions, use one of QRStringText or QRStringJSON instead */ class QRString extends QROutputAbstract{ - protected string $defaultMode = QRCode::OUTPUT_STRING_TEXT; - /** * @inheritDoc */ - protected function setModuleValues():void{ + public static function moduleValueIsValid($value):bool{ + return is_string($value); + } - foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){ - $v = $this->options->moduleValues[$M_TYPE] ?? null; + /** + * @inheritDoc + */ + protected function prepareModuleValue($value):string{ + return $value; + } - if(!is_string($v)){ - $this->moduleValues[$M_TYPE] = $defaultValue - ? $this->options->textDark - : $this->options->textLight; - } - else{ - $this->moduleValues[$M_TYPE] = $v; - } + /** + * @inheritDoc + */ + protected function getDefaultModuleValue(bool $isDark):string{ + return ($isDark) ? '██' : 'â–‘â–‘'; + } + /** + * @inheritDoc + */ + public function dump(?string $file = null):string{ + + switch($this->options->outputType){ + case QROutputInterface::STRING_TEXT: + $data = $this->text(); + break; + case QROutputInterface::STRING_JSON: + default: + $data = $this->json(); } + $this->saveToFile($data, $file); + + return $data; } /** * string output */ - protected function text(string $file = null):string{ - $str = []; + protected function text():string{ + $lines = []; + $linestart = $this->options->textLineStart; - foreach($this->matrix->matrix() as $row){ + for($y = 0; $y < $this->moduleCount; $y++){ $r = []; - foreach($row as $M_TYPE){ - $r[] = $this->moduleValues[$M_TYPE]; + for($x = 0; $x < $this->moduleCount; $x++){ + $r[] = $this->getModuleValueAt($x, $y); } - $str[] = implode('', $r); + $lines[] = $linestart.implode('', $r); } - return implode($this->options->eol, $str); + return implode($this->eol, $lines); } /** * JSON output + * + * @throws \JsonException */ - protected function json(string $file = null):string{ - return json_encode($this->matrix->matrix()); + protected function json():string{ + return json_encode($this->matrix->getMatrix($this->options->jsonAsBooleans), JSON_THROW_ON_ERROR); + } + + // + + /** + * a little helper to create a proper ANSI 8-bit color escape sequence + * + * @see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + * @see https://en.wikipedia.org/wiki/Block_Elements + * + * @codeCoverageIgnore + */ + public static function ansi8(string $str, int $color, ?bool $background = null):string{ + $color = max(0, min($color, 255)); + $background = ($background === true) ? 48 : 38; + + return sprintf("\x1b[%s;5;%sm%s\x1b[0m", $background, $color, $str); } } diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRStringJSON.php b/vendor/chillerlan/php-qrcode/src/Output/QRStringJSON.php new file mode 100644 index 000000000..87ed2d7ff --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRStringJSON.php @@ -0,0 +1,67 @@ +<?php +/** + * Class QRStringJSON + * + * @created 25.10.2023 + * @author smiley <smiley@chillerlan.net> + * @copyright 2023 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Output; + +use function json_encode; + +/** + * + */ +class QRStringJSON extends QROutputAbstract{ + + public const MIME_TYPE = 'application/json'; + + /** + * @inheritDoc + * @throws \JsonException + */ + public function dump(?string $file = null):string{ + $matrix = $this->matrix->getMatrix($this->options->jsonAsBooleans); + $data = json_encode($matrix, $this->options->jsonFlags); + + $this->saveToFile($data, $file); + + return $data; + } + + /** + * unused - required by interface + * + * @inheritDoc + * @codeCoverageIgnore + */ + protected function prepareModuleValue($value):string{ + return ''; + } + + /** + * unused - required by interface + * + * @inheritDoc + * @codeCoverageIgnore + */ + protected function getDefaultModuleValue(bool $isDark):string{ + return ''; + } + + /** + * unused - required by interface + * + * @inheritDoc + * @codeCoverageIgnore + */ + public static function moduleValueIsValid($value):bool{ + return true; + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/Output/QRStringText.php b/vendor/chillerlan/php-qrcode/src/Output/QRStringText.php new file mode 100644 index 000000000..a91591da7 --- /dev/null +++ b/vendor/chillerlan/php-qrcode/src/Output/QRStringText.php @@ -0,0 +1,76 @@ +<?php +/** + * Class QRStringText + * + * @created 25.10.2023 + * @author smiley <smiley@chillerlan.net> + * @copyright 2023 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Output; + +use function array_map, implode, is_string, max, min, sprintf; + +/** + * + */ +class QRStringText extends QROutputAbstract{ + + public const MIME_TYPE = 'text/plain'; + + /** + * @inheritDoc + */ + public static function moduleValueIsValid($value):bool{ + return is_string($value); + } + + /** + * @inheritDoc + */ + protected function prepareModuleValue($value):string{ + return $value; + } + + /** + * @inheritDoc + */ + protected function getDefaultModuleValue(bool $isDark):string{ + return ($isDark) ? '██' : 'â–‘â–‘'; + } + + /** + * @inheritDoc + */ + public function dump(?string $file = null):string{ + $lines = []; + $linestart = $this->options->textLineStart; + + foreach($this->matrix->getMatrix() as $row){ + $lines[] = $linestart.implode('', array_map([$this, 'getModuleValue'], $row)); + } + + $data = implode($this->eol, $lines); + + $this->saveToFile($data, $file); + + return $data; + } + + /** + * a little helper to create a proper ANSI 8-bit color escape sequence + * + * @see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + * @see https://en.wikipedia.org/wiki/Block_Elements + * + * @codeCoverageIgnore + */ + public static function ansi8(string $str, int $color, ?bool $background = null):string{ + $color = max(0, min($color, 255)); + $background = ($background === true) ? 48 : 38; + + return sprintf("\x1b[%s;5;%sm%s\x1b[0m", $background, $color, $str); + } + +} diff --git a/vendor/chillerlan/php-qrcode/src/QRCode.php b/vendor/chillerlan/php-qrcode/src/QRCode.php index 908030feb..235cb06d6 100755 --- a/vendor/chillerlan/php-qrcode/src/QRCode.php +++ b/vendor/chillerlan/php-qrcode/src/QRCode.php @@ -2,153 +2,161 @@ /** * Class QRCode * - * @filesource QRCode.php * @created 26.11.2015 - * @package chillerlan\QRCode * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ namespace chillerlan\QRCode; -use chillerlan\QRCode\Data\{ - AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRCodeDataException, QRDataInterface, QRMatrix -}; -use chillerlan\QRCode\Output\{ - QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString +use chillerlan\QRCode\Common\{ + EccLevel, ECICharset, GDLuminanceSource, IMagickLuminanceSource, LuminanceSourceInterface, MaskPattern, Mode, Version }; +use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number, QRData, QRDataModeInterface, QRMatrix}; +use chillerlan\QRCode\Decoder\{Decoder, DecoderResult}; +use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface}; use chillerlan\Settings\SettingsContainerInterface; - -use function call_user_func_array, class_exists, in_array, ord, strlen, strtolower, str_split; +use function class_exists, class_implements, in_array, mb_convert_encoding, mb_internal_encoding; /** * Turns a text string into a Model 2 QR Code * * @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php - * @see http://www.qrcode.com/en/codes/model12.html + * @see https://www.qrcode.com/en/codes/model12.html * @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf * @see https://en.wikipedia.org/wiki/QR_code - * @see http://www.thonky.com/qr-code-tutorial/ + * @see https://www.thonky.com/qr-code-tutorial/ */ class QRCode{ - /** @var int */ - public const VERSION_AUTO = -1; - /** @var int */ - public const MASK_PATTERN_AUTO = -1; + /** + * @deprecated 5.0.0 use Version::AUTO instead + * @see \chillerlan\QRCode\Common\Version::AUTO + * @var int + */ + public const VERSION_AUTO = Version::AUTO; - // ISO/IEC 18004:2000 Table 2 + /** + * @deprecated 5.0.0 use MaskPattern::AUTO instead + * @see \chillerlan\QRCode\Common\MaskPattern::AUTO + * @var int + */ + public const MASK_PATTERN_AUTO = MaskPattern::AUTO; - /** @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; + /** + * @deprecated 5.0.0 use EccLevel::L instead + * @see \chillerlan\QRCode\Common\EccLevel::L + * @var int + */ + public const ECC_L = EccLevel::L; /** - * References to the keys of the following tables: - * - * @see \chillerlan\QRCode\Data\QRDataInterface::MAX_LENGTH - * - * @var int[] + * @deprecated 5.0.0 use EccLevel::M instead + * @see \chillerlan\QRCode\Common\EccLevel::M + * @var int */ - public const DATA_MODES = [ - self::DATA_NUMBER => 0, - self::DATA_ALPHANUM => 1, - self::DATA_BYTE => 2, - self::DATA_KANJI => 3, - ]; + public const ECC_M = EccLevel::M; - // ISO/IEC 18004:2000 Tables 12, 25 + /** + * @deprecated 5.0.0 use EccLevel::Q instead + * @see \chillerlan\QRCode\Common\EccLevel::Q + * @var int + */ + public const ECC_Q = EccLevel::Q; - /** @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%. + /** + * @deprecated 5.0.0 use EccLevel::H instead + * @see \chillerlan\QRCode\Common\EccLevel::H + * @var int + */ + public const ECC_H = EccLevel::H; /** - * 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\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 */ - public const OUTPUT_MARKUP_SVG = 'svg'; - /** @var string */ - public const OUTPUT_IMAGE_PNG = 'png'; - /** @var string */ - public const OUTPUT_IMAGE_JPG = 'jpg'; - /** @var string */ - public const OUTPUT_IMAGE_GIF = 'gif'; - /** @var string */ - public const OUTPUT_STRING_JSON = 'json'; - /** @var string */ - public const OUTPUT_STRING_TEXT = 'text'; - /** @var string */ - public const OUTPUT_IMAGICK = 'imagick'; - /** @var string */ - public const OUTPUT_FPDF = 'fpdf'; - /** @var string */ - public const OUTPUT_CUSTOM = 'custom'; - - /** - * Map of built-in output modules => capabilities - * - * @var string[][] - */ - public const OUTPUT_MODES = [ - QRMarkup::class => [ - self::OUTPUT_MARKUP_SVG, - self::OUTPUT_MARKUP_HTML, - ], - QRImage::class => [ - self::OUTPUT_IMAGE_PNG, - self::OUTPUT_IMAGE_GIF, - self::OUTPUT_IMAGE_JPG, - ], - QRString::class => [ - self::OUTPUT_STRING_JSON, - self::OUTPUT_STRING_TEXT, - ], - QRImagick::class => [ - self::OUTPUT_IMAGICK, - ], - QRFpdf::class => [ - self::OUTPUT_FPDF - ] - ]; - - /** - * Map of data mode => interface - * + * @deprecated 5.0.0 use QROutputInterface::MARKUP_HTML instead + * @see \chillerlan\QRCode\Output\QROutputInterface::MARKUP_HTML + * @var string + */ + public const OUTPUT_MARKUP_HTML = QROutputInterface::MARKUP_HTML; + + /** + * @deprecated 5.0.0 use QROutputInterface::MARKUP_SVG instead + * @see \chillerlan\QRCode\Output\QROutputInterface::MARKUP_SVG + * @var string + */ + public const OUTPUT_MARKUP_SVG = QROutputInterface::MARKUP_SVG; + + /** + * @deprecated 5.0.0 use QROutputInterface::GDIMAGE_PNG instead + * @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_PNG + * @var string + */ + public const OUTPUT_IMAGE_PNG = QROutputInterface::GDIMAGE_PNG; + + /** + * @deprecated 5.0.0 use QROutputInterface::GDIMAGE_JPG instead + * @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_JPG + * @var string + */ + public const OUTPUT_IMAGE_JPG = QROutputInterface::GDIMAGE_JPG; + + /** + * @deprecated 5.0.0 use QROutputInterface::GDIMAGE_GIF instead + * @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_GIF + * @var string + */ + public const OUTPUT_IMAGE_GIF = QROutputInterface::GDIMAGE_GIF; + + /** + * @deprecated 5.0.0 use QROutputInterface::STRING_JSON instead + * @see \chillerlan\QRCode\Output\QROutputInterface::STRING_JSON + * @var string + */ + public const OUTPUT_STRING_JSON = QROutputInterface::STRING_JSON; + + /** + * @deprecated 5.0.0 use QROutputInterface::STRING_TEXT instead + * @see \chillerlan\QRCode\Output\QROutputInterface::STRING_TEXT + * @var string + */ + public const OUTPUT_STRING_TEXT = QROutputInterface::STRING_TEXT; + + /** + * @deprecated 5.0.0 use QROutputInterface::IMAGICK instead + * @see \chillerlan\QRCode\Output\QROutputInterface::IMAGICK + * @var string + */ + public const OUTPUT_IMAGICK = QROutputInterface::IMAGICK; + + /** + * @deprecated 5.0.0 use QROutputInterface::FPDF instead + * @see \chillerlan\QRCode\Output\QROutputInterface::FPDF + * @var string + */ + public const OUTPUT_FPDF = QROutputInterface::FPDF; + + /** + * @deprecated 5.0.0 use QROutputInterface::EPS instead + * @see \chillerlan\QRCode\Output\QROutputInterface::EPS + * @var string + */ + public const OUTPUT_EPS = QROutputInterface::EPS; + + /** + * @deprecated 5.0.0 use QROutputInterface::CUSTOM instead + * @see \chillerlan\QRCode\Output\QROutputInterface::CUSTOM + * @var string + */ + public const OUTPUT_CUSTOM = QROutputInterface::CUSTOM; + + /** + * @deprecated 5.0.0 use QROutputInterface::MODES instead + * @see \chillerlan\QRCode\Output\QROutputInterface::MODES * @var string[] */ - protected const DATA_INTERFACES = [ - 'number' => Number::class, - 'alphanum' => AlphaNum::class, - 'kanji' => Kanji::class, - 'byte' => Byte::class, - ]; + public const OUTPUT_MODES = QROutputInterface::MODES; /** * The settings container @@ -158,26 +166,73 @@ class QRCode{ protected SettingsContainerInterface $options; /** - * The selected data interface (Number, AlphaNum, Kanji, Byte) + * A collection of one or more data segments of QRDataModeInterface instances to write + * + * @var \chillerlan\QRCode\Data\QRDataModeInterface[] */ - protected QRDataInterface $dataInterface; + protected array $dataSegments = []; + + /** + * The luminance source for the reader + */ + protected string $luminanceSourceFQN = GDLuminanceSource::class; /** * QRCode constructor. * - * Sets the options instance, determines the current mb-encoding and sets it to UTF-8 + * PHP8: accept iterable + */ + public function __construct(?SettingsContainerInterface $options = null){ + $this->setOptions(($options ?? new QROptions)); + } + + /** + * Sets an options instance + */ + public function setOptions(SettingsContainerInterface $options):self{ + $this->options = $options; + + if($this->options->readerUseImagickIfAvailable){ + $this->luminanceSourceFQN = IMagickLuminanceSource::class; + } + + return $this; + } + + /** + * Renders a QR Code for the given $data and QROptions, saves $file optionally + * + * Note: it is possible to add several data segments before calling this method with a valid $data string + * which will result in a mixed-mode QR Code with the given parameter as last element. + * + * @see https://github.com/chillerlan/php-qrcode/issues/246 + * + * @return mixed */ - public function __construct(SettingsContainerInterface $options = null){ - $this->options = $options ?? new QROptions; + public function render(?string $data = null, ?string $file = null){ + + if($data !== null){ + /** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */ + foreach(Mode::INTERFACES as $dataInterface){ + + if($dataInterface::validateString($data)){ + $this->addSegment(new $dataInterface($data)); + + break; + } + } + } + + return $this->renderMatrix($this->getQRMatrix(), $file); } /** - * Renders a QR Code for the given $data and QROptions + * Renders a QR Code for the given QRMatrix and QROptions, saves $file optionally * * @return mixed */ - public function render(string $data, string $file = null){ - return $this->initOutputInterface($data)->dump($file); + public function renderMatrix(QRMatrix $matrix, ?string $file = null){ + return $this->initOutputInterface($matrix)->dump($file ?? $this->options->cachefile); } /** @@ -185,21 +240,37 @@ class QRCode{ * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ - public function getMatrix(string $data):QRMatrix{ + public function getQRMatrix():QRMatrix{ + $matrix = (new QRData($this->options, $this->dataSegments))->writeMatrix(); - if(empty($data)){ - throw new QRCodeDataException('QRCode::getMatrix() No data given.'); - } + $maskPattern = $this->options->maskPattern === MaskPattern::AUTO + ? MaskPattern::getBestPattern($matrix) + : new MaskPattern($this->options->maskPattern); - $this->dataInterface = $this->initDataInterface($data); + $matrix->setFormatInfo($maskPattern)->mask($maskPattern); - $maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO - ? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern() - : $this->options->maskPattern; + return $this->addMatrixModifications($matrix); + } - $matrix = $this->dataInterface->initMatrix($maskPattern); + /** + * add matrix modifications after mask pattern evaluation and before handing over to output + */ + protected function addMatrixModifications(QRMatrix $matrix):QRMatrix{ + + if($this->options->addLogoSpace){ + // check whether one of the dimensions was omitted + $logoSpaceWidth = ($this->options->logoSpaceWidth ?? $this->options->logoSpaceHeight ?? 0); + $logoSpaceHeight = ($this->options->logoSpaceHeight ?? $logoSpaceWidth); + + $matrix->setLogoSpace( + $logoSpaceWidth, + $logoSpaceHeight, + $this->options->logoSpaceStartX, + $this->options->logoSpaceStartY + ); + } - if((bool)$this->options->addQuietzone){ + if($this->options->addQuietzone){ $matrix->setQuietZone($this->options->quietzoneSize); } @@ -207,107 +278,207 @@ class QRCode{ } /** - * returns a fresh QRDataInterface for the given $data + * @deprecated 5.0.0 use QRCode::getQRMatrix() instead + * @see \chillerlan\QRCode\QRCode::getQRMatrix() + * @codeCoverageIgnore + */ + public function getMatrix():QRMatrix{ + return $this->getQRMatrix(); + } + + /** + * initializes a fresh built-in or custom QROutputInterface * - * @throws \chillerlan\QRCode\Data\QRCodeDataException + * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ - public function initDataInterface(string $data):QRDataInterface{ + protected function initOutputInterface(QRMatrix $matrix):QROutputInterface{ + // @todo: remove custom invocation in v6 + $outputInterface = (QROutputInterface::MODES[$this->options->outputType] ?? null); - // allow forcing the data mode - // see https://github.com/chillerlan/php-qrcode/issues/39 - $interface = $this::DATA_INTERFACES[strtolower($this->options->dataModeOverride)] ?? null; + if($this->options->outputType === QROutputInterface::CUSTOM){ + $outputInterface = $this->options->outputInterface; + } - if($interface !== null){ - return new $interface($this->options, $data); + if(!$outputInterface || !class_exists($outputInterface)){ + throw new QRCodeOutputException('invalid output module'); } - foreach($this::DATA_INTERFACES as $mode => $dataInterface){ + if(!in_array(QROutputInterface::class, class_implements($outputInterface), true)){ + throw new QRCodeOutputException('output module does not implement QROutputInterface'); + } - if(call_user_func_array([$this, 'is'.$mode], [$data])){ - return new $dataInterface($this->options, $data); - } + return new $outputInterface($this->options, $matrix); + } - } + /** + * checks if a string qualifies as numeric (convenience method) + * + * @deprecated 5.0.0 use Number::validateString() instead + * @see \chillerlan\QRCode\Data\Number::validateString() + * @codeCoverageIgnore + */ + public function isNumber(string $string):bool{ + return Number::validateString($string); + } - throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore + /** + * checks if a string qualifies as alphanumeric (convenience method) + * + * @deprecated 5.0.0 use AlphaNum::validateString() instead + * @see \chillerlan\QRCode\Data\AlphaNum::validateString() + * @codeCoverageIgnore + */ + public function isAlphaNum(string $string):bool{ + return AlphaNum::validateString($string); } /** - * returns a fresh (built-in) QROutputInterface + * checks if a string qualifies as Kanji (convenience method) * - * @throws \chillerlan\QRCode\Output\QRCodeOutputException + * @deprecated 5.0.0 use Kanji::validateString() instead + * @see \chillerlan\QRCode\Data\Kanji::validateString() + * @codeCoverageIgnore */ - protected function initOutputInterface(string $data):QROutputInterface{ + public function isKanji(string $string):bool{ + return Kanji::validateString($string); + } - if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){ - /** @phan-suppress-next-line PhanTypeExpectedObjectOrClassName */ - return new $this->options->outputInterface($this->options, $this->getMatrix($data)); - } + /** + * a dummy (convenience method) + * + * @deprecated 5.0.0 use Byte::validateString() instead + * @see \chillerlan\QRCode\Data\Byte::validateString() + * @codeCoverageIgnore + */ + public function isByte(string $string):bool{ + return Byte::validateString($string); + } - foreach($this::OUTPUT_MODES as $outputInterface => $modes){ + /** + * Adds a data segment + * + * ISO/IEC 18004:2000 8.3.6 - Mixing modes + * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length + */ + public function addSegment(QRDataModeInterface $segment):self{ + $this->dataSegments[] = $segment; - if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){ - return new $outputInterface($this->options, $this->getMatrix($data)); - } + return $this; + } - } + /** + * Clears the data segments array + * + * @codeCoverageIgnore + */ + public function clearSegments():self{ + $this->dataSegments = []; - throw new QRCodeOutputException('invalid output type'); + return $this; } /** - * checks if a string qualifies as numeric + * Adds a numeric data segment + * + * ISO/IEC 18004:2000 8.3.2 - Numeric Mode */ - public function isNumber(string $string):bool{ - return $this->checkString($string, QRDataInterface::CHAR_MAP_NUMBER); + public function addNumericSegment(string $data):self{ + return $this->addSegment(new Number($data)); } /** - * checks if a string qualifies as alphanumeric + * Adds an alphanumeric data segment + * + * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode */ - public function isAlphaNum(string $string):bool{ - return $this->checkString($string, QRDataInterface::CHAR_MAP_ALPHANUM); + public function addAlphaNumSegment(string $data):self{ + return $this->addSegment(new AlphaNum($data)); } /** - * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence. + * Adds a Kanji data segment (Japanese 13-bit double-byte characters, Shift-JIS) + * + * ISO/IEC 18004:2000 8.3.5 - Kanji Mode */ - protected function checkString(string $string, array $charmap):bool{ + public function addKanjiSegment(string $data):self{ + return $this->addSegment(new Kanji($data)); + } - foreach(str_split($string) as $chr){ - if(!isset($charmap[$chr])){ - return false; - } - } + /** + * Adds a Hanzi data segment (simplified Chinese 13-bit double-byte characters, GB2312/GB18030) + * + * GBT18284-2000 Hanzi Mode + */ + public function addHanziSegment(string $data):self{ + return $this->addSegment(new Hanzi($data)); + } - return true; + /** + * Adds an 8-bit byte data segment + * + * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode + */ + public function addByteSegment(string $data):self{ + return $this->addSegment(new Byte($data)); } /** - * checks if a string qualifies as Kanji + * Adds a standalone ECI designator + * + * The ECI designator must be followed by a Byte segment that contains the string encoded according to the given ECI charset + * + * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode */ - public function isKanji(string $string):bool{ - $i = 0; - $len = strlen($string); + public function addEciDesignator(int $encoding):self{ + return $this->addSegment(new ECI($encoding)); + } - while($i + 1 < $len){ - $c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1])); + /** + * Adds an ECI data segment (including designator) + * + * The given string will be encoded from mb_internal_encoding() to the given ECI character set + * + * I hate this somehow, but I'll leave it for now + * + * @throws \chillerlan\QRCode\QRCodeException + */ + public function addEciSegment(int $encoding, string $data):self{ + // 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()); + + return $this + ->addEciDesignator($eciCharset->getID()) + ->addByteSegment($data) + ; + } - if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){ - return false; - } + throw new QRCodeException('unable to add ECI segment'); + } - $i += 2; - } + /** + * Reads a QR Code from a given file + */ + public function readFromFile(string $path):DecoderResult{ + return $this->readFromSource($this->luminanceSourceFQN::fromFile($path, $this->options)); + } - return $i >= $len; + /** + * Reads a QR Code from the given data blob + */ + public function readFromBlob(string $blob):DecoderResult{ + return $this->readFromSource($this->luminanceSourceFQN::fromBlob($blob, $this->options)); } /** - * a dummy + * Reads a QR Code from the given luminance source */ - public function isByte(string $data):bool{ - return $data !== ''; + public function readFromSource(LuminanceSourceInterface $source):DecoderResult{ + return (new Decoder)->decode($source); } } diff --git a/vendor/chillerlan/php-qrcode/src/QRCodeException.php b/vendor/chillerlan/php-qrcode/src/QRCodeException.php index 737a0803e..600ce50ea 100644 --- a/vendor/chillerlan/php-qrcode/src/QRCodeException.php +++ b/vendor/chillerlan/php-qrcode/src/QRCodeException.php @@ -2,9 +2,7 @@ /** * Class QRCodeException * - * @filesource QRCodeException.php * @created 27.11.2015 - * @package chillerlan\QRCode * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -17,4 +15,6 @@ use Exception; /** * An exception container */ -class QRCodeException extends Exception{} +class QRCodeException extends Exception{ + +} diff --git a/vendor/chillerlan/php-qrcode/src/QROptions.php b/vendor/chillerlan/php-qrcode/src/QROptions.php index e36f6701a..91b5b4568 100644 --- a/vendor/chillerlan/php-qrcode/src/QROptions.php +++ b/vendor/chillerlan/php-qrcode/src/QROptions.php @@ -2,9 +2,7 @@ /** * Class QROptions * - * @filesource QROptions.php * @created 08.12.2015 - * @package chillerlan\QRCode * @author Smiley <smiley@chillerlan.net> * @copyright 2015 Smiley * @license MIT @@ -16,38 +14,6 @@ use chillerlan\Settings\SettingsContainerAbstract; /** * The QRCode settings container - * - * @property int $version - * @property int $versionMin - * @property int $versionMax - * @property int $eccLevel - * @property int $maskPattern - * @property bool $addQuietzone - * @property int $quietzoneSize - * @property string|null $dataModeOverride - * @property string $outputType - * @property string|null $outputInterface - * @property string|null $cachefile - * @property string $eol - * @property int $scale - * @property string $cssClass - * @property float $svgOpacity - * @property string $svgDefs - * @property int $svgViewBoxSize - * @property string $textDark - * @property string $textLight - * @property string $markupDark - * @property string $markupLight - * @property bool $returnResource - * @property bool $imageBase64 - * @property bool $imageTransparent - * @property array $imageTransparencyBG - * @property int $pngCompression - * @property int $jpegQuality - * @property string $imagickFormat - * @property string|null $imagickBG - * @property string $fpdfMeasureUnit - * @property array|null $moduleValues */ class QROptions extends SettingsContainerAbstract{ use QROptionsTrait; diff --git a/vendor/chillerlan/php-qrcode/src/QROptionsTrait.php b/vendor/chillerlan/php-qrcode/src/QROptionsTrait.php index 74c384b13..d2bc8c2ce 100644 --- a/vendor/chillerlan/php-qrcode/src/QROptionsTrait.php +++ b/vendor/chillerlan/php-qrcode/src/QROptionsTrait.php @@ -2,232 +2,493 @@ /** * Trait QROptionsTrait * - * @filesource QROptionsTrait.php + * Note: the docblocks in this file are optimized for readability in PhpStorm ond on readthedocs.io + * * @created 10.03.2018 - * @package chillerlan\QRCode * @author smiley <smiley@chillerlan.net> * @copyright 2018 smiley * @license MIT * - * @noinspection PhpUnused + * @noinspection PhpUnused, PhpComposerExtensionStubsInspection */ namespace chillerlan\QRCode; -use function array_values, count, in_array, is_numeric, max, min, sprintf, strtolower; +use chillerlan\QRCode\Output\QROutputInterface; +use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version}; +use function extension_loaded, in_array, max, min, strtolower; +use const JSON_THROW_ON_ERROR, PHP_EOL; /** * The QRCode plug-in settings & setter functionality */ trait QROptionsTrait{ + /* + * QR Code specific settings + */ + /** * QR Code version number * - * [1 ... 40] or QRCode::VERSION_AUTO + * `1 ... 40` or `Version::AUTO` (default) + * + * @see \chillerlan\QRCode\Common\Version */ - protected int $version = QRCode::VERSION_AUTO; + protected int $version = Version::AUTO; /** * Minimum QR version * - * if $version = QRCode::VERSION_AUTO + * if `QROptions::$version` is set to `Version::AUTO` (default: 1) */ protected int $versionMin = 1; /** * Maximum QR version + * + * if `QROptions::$version` is set to `Version::AUTO` (default: 40) */ protected int $versionMax = 40; /** * Error correct level * - * QRCode::ECC_X where X is: + * `EccLevel::X` where `X` is: + * + * - `L` => 7% (default) + * - `M` => 15% + * - `Q` => 25% + * - `H` => 30% * - * - L => 7% - * - M => 15% - * - Q => 25% - * - H => 30% + * @todo: accept string values (PHP8+) + * @see \chillerlan\QRCode\Common\EccLevel + * @see https://github.com/chillerlan/php-qrcode/discussions/160 */ - protected int $eccLevel = QRCode::ECC_L; + protected int $eccLevel = EccLevel::L; /** - * Mask Pattern to use + * Mask Pattern to use (no value in using, mostly for unit testing purposes) * - * [0...7] or QRCode::MASK_PATTERN_AUTO + * `0 ... 7` or `MaskPattern::PATTERN_AUTO` (default) + * + * @see \chillerlan\QRCode\Common\MaskPattern */ - protected int $maskPattern = QRCode::MASK_PATTERN_AUTO; + protected int $maskPattern = MaskPattern::AUTO; /** * Add a "quiet zone" (margin) according to the QR code spec + * + * @see https://www.qrcode.com/en/howto/code.html */ protected bool $addQuietzone = true; /** * Size of the quiet zone * - * internally clamped to [0 ... $moduleCount / 2], defaults to 4 modules + * internally clamped to `0 ... $moduleCount / 2` (default: 4) */ protected int $quietzoneSize = 4; + + /* + * General output settings + */ + /** - * Use this to circumvent the data mode detection and force the usage of the given mode. + * The built-in output type + * + * - `QROutputInterface::MARKUP_SVG` (default) + * - `QROutputInterface::MARKUP_HTML` + * - `QROutputInterface::GDIMAGE_BMP` + * - `QROutputInterface::GDIMAGE_GIF` + * - `QROutputInterface::GDIMAGE_JPG` + * - `QROutputInterface::GDIMAGE_PNG` + * - `QROutputInterface::GDIMAGE_WEBP` + * - `QROutputInterface::STRING_TEXT` + * - `QROutputInterface::STRING_JSON` + * - `QROutputInterface::IMAGICK` + * - `QROutputInterface::EPS` + * - `QROutputInterface::FPDF` + * - `QROutputInterface::CUSTOM` * - * valid modes are: Number, AlphaNum, Kanji, Byte (case insensitive) + * @see \chillerlan\QRCode\Output\QREps + * @see \chillerlan\QRCode\Output\QRFpdf + * @see \chillerlan\QRCode\Output\QRGdImage + * @see \chillerlan\QRCode\Output\QRImagick + * @see \chillerlan\QRCode\Output\QRMarkupHTML + * @see \chillerlan\QRCode\Output\QRMarkupSVG + * @see \chillerlan\QRCode\Output\QRString + * @see https://github.com/chillerlan/php-qrcode/issues/223 * - * @see https://github.com/chillerlan/php-qrcode/issues/39 - * @see https://github.com/chillerlan/php-qrcode/issues/97 (changed default value to '') + * @deprecated 5.0.0 see issue #223 */ - protected string $dataModeOverride = ''; + protected string $outputType = QROutputInterface::MARKUP_SVG; /** - * The output type + * The FQCN of the custom `QROutputInterface` * - * - QRCode::OUTPUT_MARKUP_XXXX where XXXX = HTML, SVG - * - QRCode::OUTPUT_IMAGE_XXX where XXX = PNG, GIF, JPG - * - QRCode::OUTPUT_STRING_XXXX where XXXX = TEXT, JSON - * - QRCode::OUTPUT_CUSTOM + * if `QROptions::$outputType` is set to `QROutputInterface::CUSTOM` (default: `null`) + * + * @deprecated 5.0.0 the nullable type will be removed in future versions + * and the default value will be set to `QRMarkupSVG::class` */ - protected string $outputType = QRCode::OUTPUT_IMAGE_PNG; + protected ?string $outputInterface = null; /** - * the FQCN of the custom QROutputInterface if $outputType is set to QRCode::OUTPUT_CUSTOM + * Return the image resource instead of a render if applicable. + * + * - `QRGdImage`: `resource` (PHP < 8), `GdImage` + * - `QRImagick`: `Imagick` + * - `QRFpdf`: `FPDF` + * + * This option overrides/ignores other output settings, such as `QROptions::$cachefile` + * and `QROptions::$outputBase64`. (default: `false`) + * + * @see \chillerlan\QRCode\Output\QROutputInterface::dump() */ - protected ?string $outputInterface = null; + protected bool $returnResource = false; /** - * /path/to/cache.file + * Optional cache file path `/path/to/cache.file` + * + * Please note that the `$file` parameter in `QRCode::render()` and `QRCode::renderMatrix()` + * takes precedence over the `QROptions::$cachefile` value. (default: `null`) + * + * @see \chillerlan\QRCode\QRCode::render() + * @see \chillerlan\QRCode\QRCode::renderMatrix() */ protected ?string $cachefile = null; /** - * newline string [HTML, SVG, TEXT] + * Toggle base64 data URI or raw data output (if applicable) + * + * (default: `true`) + * + * @see \chillerlan\QRCode\Output\QROutputAbstract::toBase64DataURI() */ - protected string $eol = PHP_EOL; + protected bool $outputBase64 = true; /** - * size of a QR code pixel [SVG, IMAGE_*], HTML via CSS + * Newline string + * + * (default: `PHP_EOL`) + */ + protected string $eol = PHP_EOL; + + /* + * Common visual modifications */ - protected int $scale = 5; /** - * a common css class + * Sets the image background color (if applicable) + * + * - `QRImagick`: defaults to `"white"` + * - `QRGdImage`: defaults to `[255, 255, 255]` + * - `QRFpdf`: defaults to blank internally (white page) + * + * @var mixed|null */ - protected string $cssClass = ''; + protected $bgColor = null; /** - * SVG opacity + * Whether to invert the matrix (reflectance reversal) + * + * (default: `false`) + * + * @see \chillerlan\QRCode\Data\QRMatrix::invert() */ - protected float $svgOpacity = 1.0; + protected bool $invertMatrix = false; /** - * anything between <defs> + * Whether to draw the light (false) modules * - * @see https://developer.mozilla.org/docs/Web/SVG/Element/defs + * (default: `true`) */ - protected string $svgDefs = '<style>rect{shape-rendering:crispEdges}</style>'; + protected bool $drawLightModules = true; /** - * SVG viewBox size. a single integer number which defines width/height of the viewBox attribute. + * Specify whether to draw the modules as filled circles + * + * a note for `GdImage` output: + * + * if `QROptions::$scale` is less than 20, the image will be upscaled internally, then the modules will be drawn + * using `imagefilledellipse()` and then scaled back to the expected size * - * viewBox="0 0 x x" + * No effect in: `QREps`, `QRFpdf`, `QRMarkupHTML` * - * @see https://css-tricks.com/scale-svg/#article-header-id-3 + * @see \imagefilledellipse() + * @see https://github.com/chillerlan/php-qrcode/issues/23 + * @see https://github.com/chillerlan/php-qrcode/discussions/122 */ - protected ?int $svgViewBoxSize = null; + protected bool $drawCircularModules = false; /** - * string substitute for dark + * Specifies the radius of the modules when `QROptions::$drawCircularModules` is set to `true` + * + * (default: 0.45) */ - protected string $textDark = '🔴'; + protected float $circleRadius = 0.45; /** - * string substitute for light + * Specifies which module types to exclude when `QROptions::$drawCircularModules` is set to `true` + * + * (default: `[]`) */ - protected string $textLight = 'â•'; + protected array $keepAsSquare = []; /** - * markup substitute for dark (CSS value) + * Whether to connect the paths for the several module types to avoid weird glitches when using gradients etc. + * + * This option is exclusive to output classes that use the module collector `QROutputAbstract::collectModules()`, + * which converts the `$M_TYPE` of all modules to `QRMatrix::M_DATA` and `QRMatrix::M_DATA_DARK` respectively. + * + * Module types that should not be added to the connected path can be excluded via `QROptions::$excludeFromConnect`. + * + * Currentty used in `QREps` and `QRMarkupSVG`. + * + * @see \chillerlan\QRCode\Output\QROutputAbstract::collectModules() + * @see \chillerlan\QRCode\QROptionsTrait::$excludeFromConnect + * @see https://github.com/chillerlan/php-qrcode/issues/57 */ - protected string $markupDark = '#000'; + protected bool $connectPaths = false; /** - * markup substitute for light (CSS value) + * Specify which paths/patterns to exclude from connecting if `QROptions::$connectPaths` is set to `true` + * + * @see \chillerlan\QRCode\QROptionsTrait::$connectPaths */ - protected string $markupLight = '#fff'; + protected array $excludeFromConnect = []; /** - * Return the image resource instead of a render if applicable. - * This option overrides other output options, such as $cachefile and $imageBase64. + * Module values map * - * Supported by the following modules: + * - `QRImagick`, `QRMarkupHTML`, `QRMarkupSVG`: #ABCDEF, cssname, rgb(), rgba()... + * - `QREps`, `QRFpdf`, `QRGdImage`: `[R, G, B]` // 0-255 + * - `QREps`: `[C, M, Y, K]` // 0-255 * - * - QRImage: resource (PHP < 8), GdImage - * - QRImagick: Imagick - * - QRFpdf: FPDF + * @see \chillerlan\QRCode\Output\QROutputAbstract::setModuleValues() + */ + protected array $moduleValues = []; + + /** + * Toggles logo space creation * - * @see \chillerlan\QRCode\Output\QROutputInterface::dump() + * @see \chillerlan\QRCode\QRCode::addMatrixModifications() + * @see \chillerlan\QRCode\Data\QRMatrix::setLogoSpace() + */ + protected bool $addLogoSpace = false; + + /** + * Width of the logo space * - * @var bool + * if only `QROptions::$logoSpaceWidth` is given, the logo space is assumed a square of that size */ - protected bool $returnResource = false; + protected ?int $logoSpaceWidth = null; + + /** + * Height of the logo space + * + * if only `QROptions::$logoSpaceHeight` is given, the logo space is assumed a square of that size + */ + protected ?int $logoSpaceHeight = null; /** - * toggle base64 or raw image data + * Optional horizontal start position of the logo space (top left corner) */ - protected bool $imageBase64 = true; + protected ?int $logoSpaceStartX = null; /** - * toggle transparency, not supported by jpg + * Optional vertical start position of the logo space (top left corner) */ - protected bool $imageTransparent = true; + protected ?int $logoSpaceStartY = null; + + + /* + * Common raster image settings (QRGdImage, QRImagick) + */ + + /** + * Pixel size of a QR code module + */ + protected int $scale = 5; /** - * @see imagecolortransparent() + * Toggle transparency + * + * - `QRGdImage` and `QRImagick`: the given `QROptions::$transparencyColor` is set as transparent * - * [R, G, B] + * @see https://github.com/chillerlan/php-qrcode/discussions/121 */ - protected array $imageTransparencyBG = [255, 255, 255]; + protected bool $imageTransparent = false; /** - * @see imagepng() + * Sets a transparency color for when `QROptions::$imageTransparent` is set to `true`. + * + * Defaults to `QROptions::$bgColor`. + * + * - `QRGdImage`: `[R, G, B]`, this color is set as transparent in `imagecolortransparent()` + * - `QRImagick`: `"color_str"`, this color is set in `Imagick::transparentPaintImage()` + * + * @see \imagecolortransparent() + * @see \Imagick::transparentPaintImage() + * + * @var mixed|null */ - protected int $pngCompression = -1; + protected $transparencyColor = null; /** - * @see imagejpeg() + * Compression quality + * + * The given value depends on the used output type: + * + * - `QRGdImageBMP`: `[0...1]` + * - `QRGdImageJPEG`: `[0...100]` + * - `QRGdImageWEBP`: `[0...9]` + * - `QRGdImagePNG`: `[0...100]` + * - `QRImagick`: `[0...100]` + * + * @see \imagebmp() + * @see \imagejpeg() + * @see \imagepng() + * @see \imagewebp() + * @see \Imagick::setImageCompressionQuality() + */ + protected int $quality = -1; + + /* + * QRGdImage settings + */ + + /** + * Toggles the usage of internal upscaling when `QROptions::$drawCircularModules` is set to `true` and + * `QROptions::$scale` is less than 20 + * + * @see \chillerlan\QRCode\Output\QRGdImage::createImage() + * @see https://github.com/chillerlan/php-qrcode/issues/23 + */ + protected bool $gdImageUseUpscale = true; + + /* + * QRImagick settings */ - protected int $jpegQuality = 85; /** * Imagick output format * - * @see \Imagick::setType() + * @see \Imagick::setImageFormat() + * @see https://www.imagemagick.org/script/formats.php + */ + protected string $imagickFormat = 'png32'; + + + /* + * Common markup output settings (QRMarkupSVG, QRMarkupHTML) + */ + + /** + * A common css class + */ + protected string $cssClass = 'qrcode'; + + /* + * QRMarkupSVG settings + */ + + /** + * Whether to add an XML header line or not, e.g. to embed the SVG directly in HTML + * + * `<?xml version="1.0" encoding="UTF-8"?>` */ - protected string $imagickFormat = 'png'; + protected bool $svgAddXmlHeader = true; /** - * Imagick background color (defaults to "transparent") + * Anything in the SVG `<defs>` tag + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs + */ + protected string $svgDefs = ''; + + /** + * Sets the value for the "preserveAspectRatio" on the `<svg>` element + * + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio + */ + protected string $svgPreserveAspectRatio = 'xMidYMid'; + + /** + * Whether to use the SVG `fill` attributes + * + * If set to `true` (default), the `fill` attribute will be set with the module value for the `<path>` element's `$M_TYPE`. + * When set to `false`, the module values map will be ignored and the QR Code may be styled via CSS. * - * @see \ImagickPixel::__construct() + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill + */ + protected bool $svgUseFillAttributes = true; + + /* + * QRStringText settings + */ + + /** + * An optional line prefix, e.g. empty space to align the QR Code in a console + */ + protected string $textLineStart = ''; + + /* + * QRStringJSON settings + */ + + /** + * Sets the flags to use for the `json_encode()` call + * + * @see https://www.php.net/manual/json.constants.php + */ + protected int $jsonFlags = JSON_THROW_ON_ERROR; + + /** + * Whether to return matrix values in JSON as booleans or `$M_TYPE` integers + */ + protected bool $jsonAsBooleans = false; + + /* + * QRFpdf settings */ - protected ?string $imagickBG = null; /** - * Measurement unit for FPDF output: pt, mm, cm, in (defaults to "pt") + * Measurement unit for `FPDF` output: `pt`, `mm`, `cm`, `in` (default: `pt`) * - * @see \FPDF::__construct() + * @see FPDF::__construct() */ protected string $fpdfMeasureUnit = 'pt'; + + /* + * QR Code reader settings + */ + /** - * Module values map + * Use Imagick (if available) when reading QR Codes + */ + protected bool $readerUseImagickIfAvailable = false; + + /** + * Grayscale the image before reading + */ + protected bool $readerGrayscale = false; + + /** + * Invert the colors of the image + */ + protected bool $readerInvertColors = false; + + /** + * Increase the contrast before reading * - * - HTML, IMAGICK: #ABCDEF, cssname, rgb(), rgba()... - * - IMAGE: [63, 127, 255] // R, G, B + * note that applying contrast works different in GD and Imagick, so mileage may vary */ - protected ?array $moduleValues = null; + protected bool $readerIncreaseContrast = false; + /** * clamp min/max version number @@ -255,87 +516,214 @@ trait QROptionsTrait{ } /** - * sets the error correction level + * sets/clamps the version number + */ + protected function set_version(int $version):void{ + $this->version = ($version !== Version::AUTO) ? max(1, min(40, $version)) : Version::AUTO; + } + + /** + * sets/clamps the quiet zone size + */ + protected function set_quietzoneSize(int $quietzoneSize):void{ + $this->quietzoneSize = max(0, min($quietzoneSize, 75)); + } + + /** + * sets the FPDF measurement unit * - * @throws \chillerlan\QRCode\QRCodeException + * @codeCoverageIgnore */ - protected function set_eccLevel(int $eccLevel):void{ + protected function set_fpdfMeasureUnit(string $unit):void{ + $unit = strtolower($unit); - if(!isset(QRCode::ECC_MODES[$eccLevel])){ - throw new QRCodeException(sprintf('Invalid error correct level: %s', $eccLevel)); + if(in_array($unit, ['cm', 'in', 'mm', 'pt'], true)){ + $this->fpdfMeasureUnit = $unit; } - $this->eccLevel = $eccLevel; + // @todo throw or ignore silently? + } + + /** + * enables Imagick for the QR Code reader if the extension is available + */ + protected function set_readerUseImagickIfAvailable(bool $useImagickIfAvailable):void{ + $this->readerUseImagickIfAvailable = ($useImagickIfAvailable && extension_loaded('imagick')); } /** - * sets/clamps the mask pattern + * clamp the logo space values between 0 and maximum length (177 modules at version 40) */ - protected function set_maskPattern(int $maskPattern):void{ + protected function clampLogoSpaceValue(?int $value):?int{ - if($maskPattern !== QRCode::MASK_PATTERN_AUTO){ - $this->maskPattern = max(0, min(7, $maskPattern)); + if($value === null){ + return null; } + return (int)max(0, min(177, $value)); } /** - * sets the transparency background color - * - * @throws \chillerlan\QRCode\QRCodeException + * clamp/set logo space width */ - protected function set_imageTransparencyBG(array $imageTransparencyBG):void{ + protected function set_logoSpaceWidth(?int $value):void{ + $this->logoSpaceWidth = $this->clampLogoSpaceValue($value); + } - // invalid value - set to white as default - if(count($imageTransparencyBG) < 3){ - $this->imageTransparencyBG = [255, 255, 255]; + /** + * clamp/set logo space height + */ + protected function set_logoSpaceHeight(?int $value):void{ + $this->logoSpaceHeight = $this->clampLogoSpaceValue($value); + } - return; - } + /** + * clamp/set horizontal logo space start + */ + protected function set_logoSpaceStartX(?int $value):void{ + $this->logoSpaceStartX = $this->clampLogoSpaceValue($value); + } - foreach($imageTransparencyBG as $k => $v){ + /** + * clamp/set vertical logo space start + */ + protected function set_logoSpaceStartY(?int $value):void{ + $this->logoSpaceStartY = $this->clampLogoSpaceValue($value); + } + + /** + * clamp/set SVG circle radius + */ + protected function set_circleRadius(float $circleRadius):void{ + $this->circleRadius = max(0.1, min(0.75, $circleRadius)); + } - // cut off exceeding items - if($k > 2){ - break; - } + /* + * redirect calls of deprecated variables to new/renamed property + */ - if(!is_numeric($v)){ - throw new QRCodeException('Invalid RGB value.'); - } + /** + * @deprecated 5.0.0 use QROptions::$outputBase64 instead + * @see \chillerlan\QRCode\QROptions::$outputBase64 + */ + protected bool $imageBase64; - // clamp the values - $this->imageTransparencyBG[$k] = max(0, min(255, (int)$v)); - } + /** + * redirect call to the new variable + * + * @deprecated 5.0.0 use QROptions::$outputBase64 instead + * @see \chillerlan\QRCode\QROptions::$outputBase64 + * @codeCoverageIgnore + */ + protected function set_imageBase64(bool $imageBase64):void{ + $this->outputBase64 = $imageBase64; + } - // use the array values to not run into errors with the spread operator (...$arr) - $this->imageTransparencyBG = array_values($this->imageTransparencyBG); + /** + * redirect call to the new variable + * + * @deprecated 5.0.0 use QROptions::$outputBase64 instead + * @see \chillerlan\QRCode\QROptions::$outputBase64 + * @codeCoverageIgnore + */ + protected function get_imageBase64():bool{ + return $this->outputBase64; } /** - * sets/clamps the version number + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality */ - protected function set_version(int $version):void{ + protected int $jpegQuality; - if($version !== QRCode::VERSION_AUTO){ - $this->version = max(1, min(40, $version)); - } + /** + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality + * @codeCoverageIgnore + */ + protected function set_jpegQuality(int $jpegQuality):void{ + $this->quality = $jpegQuality; + } + /** + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality + * @codeCoverageIgnore + */ + protected function get_jpegQuality():int{ + return $this->quality; } /** - * sets the FPDF measurement unit - * + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality + */ + protected int $pngCompression; + + /** + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality * @codeCoverageIgnore */ - protected function set_fpdfMeasureUnit(string $unit):void{ - $unit = strtolower($unit); + protected function set_pngCompression(int $pngCompression):void{ + $this->quality = $pngCompression; + } - if(in_array($unit, ['cm', 'in', 'mm', 'pt'], true)){ - $this->fpdfMeasureUnit = $unit; - } + /** + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality + * @codeCoverageIgnore + */ + protected function get_pngCompression():int{ + return $this->quality; + } - // @todo throw or ignore silently? + /** + * @deprecated 5.0.0 use QROptions::$transparencyColor instead + * @see \chillerlan\QRCode\QROptions::$transparencyColor + */ + protected array $imageTransparencyBG; + + /** + * @deprecated 5.0.0 use QROptions::$transparencyColor instead + * @see \chillerlan\QRCode\QROptions::$transparencyColor + * @codeCoverageIgnore + */ + protected function set_imageTransparencyBG(?array $imageTransparencyBG):void{ + $this->transparencyColor = $imageTransparencyBG; + } + + /** + * @deprecated 5.0.0 use QROptions::$transparencyColor instead + * @see \chillerlan\QRCode\QROptions::$transparencyColor + * @codeCoverageIgnore + */ + protected function get_imageTransparencyBG():?array{ + return $this->transparencyColor; + } + + /** + * @deprecated 5.0.0 use QROptions::$bgColor instead + * @see \chillerlan\QRCode\QROptions::$bgColor + */ + protected string $imagickBG; + + /** + * @deprecated 5.0.0 use QROptions::$bgColor instead + * @see \chillerlan\QRCode\QROptions::$bgColor + * @codeCoverageIgnore + */ + protected function set_imagickBG(?string $imagickBG):void{ + $this->bgColor = $imagickBG; + } + + /** + * @deprecated 5.0.0 use QROptions::$bgColor instead + * @see \chillerlan\QRCode\QROptions::$bgColor + * @codeCoverageIgnore + */ + protected function get_imagickBG():?string{ + return $this->bgColor; } } |