diff --git a/composer.json b/composer.json index 0432ff73e..ad8139885 100644 --- a/composer.json +++ b/composer.json @@ -41,10 +41,11 @@ "prefer-stable": true, "require": { "php": "^8.2", - "ext-mbstring": "*", "chillerlan/php-settings-container": "^3.1" }, "require-dev": { + "ext-iconv": "*", + "ext-mbstring": "*", "chillerlan/php-authenticator": "^5.1", "phan/phan": "^5.4", "phpunit/phpunit": "^10.5", @@ -55,7 +56,8 @@ "suggest": { "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", "setasign/fpdf": "Required to use the QR FPDF output.", - "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" + "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code", + "symfony/polyfill-mbstring": "^1.24" }, "autoload": { "psr-4": { diff --git a/src/Common/CharacterEncodingHandlerInterface.php b/src/Common/CharacterEncodingHandlerInterface.php new file mode 100644 index 000000000..503fa0201 --- /dev/null +++ b/src/Common/CharacterEncodingHandlerInterface.php @@ -0,0 +1,35 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +/** + * Handles all multibyte character encoding/conversion operations + */ +interface CharacterEncodingHandlerInterface{ + + /** + * Get the string length + */ + public static function getCharCount(string $string, string $encoding = null):int; + + /** + * Gets the internal character encoding + */ + public static function getInternalEncoding():string; + + /** + * Converts the given `$string` to `$to_encoding`, optionally using the given encoding(s) in `$from_encoding` + * + * @throws \RuntimeException if an error occurred + */ + public static function convertEncoding(string $string, string $to_encoding, array|string $from_encoding = null):string; + +} diff --git a/src/Common/EncodingHandlerTrait.php b/src/Common/EncodingHandlerTrait.php new file mode 100644 index 000000000..94c587d12 --- /dev/null +++ b/src/Common/EncodingHandlerTrait.php @@ -0,0 +1,48 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\QRCodeException; +use Symfony\Polyfill\Mbstring\Mbstring; +use Throwable; +use function class_exists; +use function extension_loaded; + +trait EncodingHandlerTrait{ + + protected static CharacterEncodingHandlerInterface $encodingHandler; + + /** + * @throws \chillerlan\QRCode\QRCodeException + */ + protected static function getEncodingHandler():string{ + + try{ + return match(true){ + extension_loaded('mbstring') => MBStringHandler::class, + extension_loaded('iconv') && class_exists(Mbstring::class) => MBStringHandler::class, + extension_loaded('iconv') => IconvHandler::class, + }; + } + catch(Throwable){ + throw new QRCodeException('no character encoding handler available'); + } + + } + + /** + * We're setting an instance here so that the IDE stops yelling at the FQN + */ + protected function setEncodingHandler():void{ + static::$encodingHandler = new (static::getEncodingHandler()); + } + +} diff --git a/src/Common/IconvHandler.php b/src/Common/IconvHandler.php new file mode 100644 index 000000000..9753671e6 --- /dev/null +++ b/src/Common/IconvHandler.php @@ -0,0 +1,60 @@ + + * @copyright 2024 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Common; + +use RuntimeException; +use function iconv_get_encoding; +use function iconv_strlen; +use function is_array; +use function is_string; + +/** + * Handles iconv encoding operations (will probably fail) + */ +class IconvHandler implements CharacterEncodingHandlerInterface{ + + /** + * @inheritDoc + */ + public static function getCharCount(string $string, string $encoding = null):int{ + return iconv_strlen($string, $encoding); + } + + /** + * @inheritDoc + */ + public static function getInternalEncoding():string{ + return iconv_get_encoding('internal_encoding'); + } + + /** + * @inheritDoc + * @todo + */ + public static function convertEncoding(string $string, string $to_encoding, array|string $from_encoding = null):string{ + + // we don't have detect_encoding here, so we pick the first item, otherwise set to internal + if(is_array($from_encoding)){ + $from_encoding = ($from_encoding[0] ?? self::getInternalEncoding()); + } + + $str = iconv($from_encoding, $to_encoding, $string); + + if(!is_string($str)){ + throw new RuntimeException('iconv error'); + } + + return $str; + } + +} diff --git a/src/Common/MBStringHandler.php b/src/Common/MBStringHandler.php new file mode 100644 index 000000000..237706073 --- /dev/null +++ b/src/Common/MBStringHandler.php @@ -0,0 +1,54 @@ + + * @copyright 2024 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +namespace chillerlan\QRCode\Common; + +use RuntimeException; +use function is_string; +use function mb_convert_encoding; +use function mb_internal_encoding; +use function mb_strlen; + +/** + * Handles mbstring encoding operations + */ +class MBStringHandler implements CharacterEncodingHandlerInterface{ + + /** + * @inheritDoc + */ + public static function getCharCount(string $string, string $encoding = null):int{ + return mb_strlen($string, $encoding); + } + + /** + * @inheritDoc + */ + public static function getInternalEncoding():string{ + return mb_internal_encoding(); + } + + /** + * @inheritDoc + * @see \mb_detect_encoding() + */ + public static function convertEncoding(string $string, string $to_encoding, array|string $from_encoding = null):string{ + $str = mb_convert_encoding($string, $to_encoding, $from_encoding); + + if(!is_string($str)){ + throw new RuntimeException('mb_convert_encoding error'); + } + + return $str; + } + +} diff --git a/src/Data/ECI.php b/src/Data/ECI.php index 396d5bbf7..9320a2f27 100644 --- a/src/Data/ECI.php +++ b/src/Data/ECI.php @@ -10,8 +10,8 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode}; -use function mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, sprintf; +use chillerlan\QRCode\Common\{BitBuffer, ECICharset, EncodingHandlerTrait, Mode}; +use function sprintf; /** * Adds an ECI Designator @@ -21,6 +21,7 @@ * Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment() */ final class ECI extends QRDataModeAbstract{ + use EncodingHandlerTrait; /** * @inheritDoc @@ -43,6 +44,8 @@ public function __construct(int $encoding){ } $this->encoding = $encoding; + + $this->setEncodingHandler(); } /** @@ -142,14 +145,12 @@ public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):s // 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 - } + $encoding = ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8']; } - return mb_convert_encoding($data, mb_internal_encoding(), $encoding); + $to_encoding = self::$encodingHandler::getInternalEncoding(); + + return self::$encodingHandler::convertEncoding($data, $to_encoding, $encoding); } } diff --git a/src/Data/Hanzi.php b/src/Data/Hanzi.php index ec96793ce..caa8e1191 100644 --- a/src/Data/Hanzi.php +++ b/src/Data/Hanzi.php @@ -12,8 +12,7 @@ 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; +use function chr, implode, intdiv, ord, sprintf, strlen; /** * Hanzi (simplified Chinese) mode, GBT18284-2000: 13-bit double-byte characters from the GB2312/GB18030 character set @@ -52,7 +51,7 @@ final class Hanzi extends QRDataModeAbstract{ * @inheritDoc */ protected function getCharCount():int{ - return mb_strlen($this->data, self::ENCODING); + return self::$encodingHandler::getCharCount($this->data, self::ENCODING); } /** @@ -66,25 +65,9 @@ public function getLengthInBits():int{ * @inheritDoc */ public static function convertEncoding(string $string):string{ - mb_detect_order([mb_internal_encoding(), 'UTF-8', 'GB2312', 'GB18030', 'CP936', 'EUC-CN', 'HZ']); + $encodings = [self::$encodingHandler::getInternalEncoding(), '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; + return self::$encodingHandler::convertEncoding($string, self::ENCODING, $encodings); } /** @@ -199,7 +182,9 @@ public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):s $length--; } - return mb_convert_encoding(implode($buffer), mb_internal_encoding(), self::ENCODING); + $to_encoding = self::$encodingHandler::getInternalEncoding(); + + return self::$encodingHandler::convertEncoding(implode($buffer), $to_encoding, self::ENCODING); } } diff --git a/src/Data/Kanji.php b/src/Data/Kanji.php index 8c95cc71b..a3e773bd5 100644 --- a/src/Data/Kanji.php +++ b/src/Data/Kanji.php @@ -12,8 +12,7 @@ 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; +use function chr, implode, intdiv, ord, sprintf, strlen; /** * Kanji mode: 13-bit double-byte characters from the Shift-JIS character set @@ -45,7 +44,7 @@ final class Kanji extends QRDataModeAbstract{ * @inheritDoc */ protected function getCharCount():int{ - return mb_strlen($this->data, self::ENCODING); + return self::$encodingHandler::getCharCount($this->data, self::ENCODING); } /** @@ -59,25 +58,9 @@ public function getLengthInBits():int{ * @inheritDoc */ public static function convertEncoding(string $string):string{ - mb_detect_order([mb_internal_encoding(), 'UTF-8', 'SJIS', 'SJIS-2004']); + $encodings = [self::$encodingHandler::getInternalEncoding(), '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; + return self::$encodingHandler::convertEncoding($string, self::ENCODING, $encodings); } /** @@ -185,7 +168,9 @@ public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):s $length--; } - return mb_convert_encoding(implode($buffer), mb_internal_encoding(), self::ENCODING); + $to_encoding = self::$encodingHandler::getInternalEncoding(); + + return self::$encodingHandler::convertEncoding(implode($buffer), $to_encoding, self::ENCODING); } } diff --git a/src/Data/QRDataModeAbstract.php b/src/Data/QRDataModeAbstract.php index 94b93ac0e..4691a0bd3 100644 --- a/src/Data/QRDataModeAbstract.php +++ b/src/Data/QRDataModeAbstract.php @@ -10,12 +10,13 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\Mode; +use chillerlan\QRCode\Common\{EncodingHandlerTrait, Mode}; /** * abstract methods for the several data modes */ abstract class QRDataModeAbstract implements QRDataModeInterface{ + use EncodingHandlerTrait; /** * The data to write @@ -35,6 +36,8 @@ public function __construct(string $data){ } $this->data = $data; + + $this->setEncodingHandler(); } /** diff --git a/src/QRCode.php b/src/QRCode.php index da89114d5..a447c36fa 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -13,13 +13,14 @@ namespace chillerlan\QRCode; use chillerlan\QRCode\Common\{ - ECICharset, GDLuminanceSource, IMagickLuminanceSource, LuminanceSourceInterface, MaskPattern, Mode + EncodingHandlerTrait, ECICharset, GDLuminanceSource, + IMagickLuminanceSource, LuminanceSourceInterface, MaskPattern, Mode }; 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 class_exists, class_implements, in_array, mb_convert_encoding, mb_internal_encoding; +use function class_exists, class_implements, in_array; /** * Turns a text string into a Model 2 QR Code @@ -31,6 +32,7 @@ * @see https://www.thonky.com/qr-code-tutorial/ */ class QRCode{ + use EncodingHandlerTrait; /** * The settings container @@ -56,6 +58,7 @@ class QRCode{ */ public function __construct(SettingsContainerInterface|QROptions $options = new QROptions){ $this->setOptions($options); + $this->setEncodingHandler(); } /** @@ -239,7 +242,7 @@ public function addEciDesignator(int $encoding):static{ /** * Adds an ECI data segment (including designator) * - * The given string will be encoded from mb_internal_encoding() to the given ECI character set + * The given string will be encoded from internal_encoding to the given ECI character set * * I hate this somehow, but I'll leave it for now * @@ -252,7 +255,7 @@ public function addEciSegment(int $encoding, string $data):static{ $eciCharsetName = $eciCharset->getName(); // convert the string to the given charset if($eciCharsetName !== null){ - $data = mb_convert_encoding($data, $eciCharsetName, mb_internal_encoding()); + $data = static::$encodingHandler::convertEncoding($data, $eciCharsetName); return $this ->addEciDesignator($eciCharset->getID())