From 6c135ba98132ccdcfd14130cd242e2c7da38df64 Mon Sep 17 00:00:00 2001 From: smiley Date: Tue, 5 Sep 2023 03:04:08 +0200 Subject: [PATCH] :octocat: QRGdImage: add BMP and WEBP support, deprecate QROptionsTrait::$jpegQuality and QROptionsTrait::$pngCompression in favor of QROptionsTrait::$quality --- src/Output/QRGdImage.php | 78 +++++++++++++++++++++++------- src/Output/QROutputInterface.php | 48 ++++++++++-------- src/QROptionsTrait.php | 66 +++++++++++++++++++++++-- tests/Output/QRGdImageBMPTest.php | 22 +++++++++ tests/Output/QRGdImageWEBPTest.php | 22 +++++++++ 5 files changed, 195 insertions(+), 41 deletions(-) create mode 100644 tests/Output/QRGdImageBMPTest.php create mode 100644 tests/Output/QRGdImageWEBPTest.php diff --git a/src/Output/QRGdImage.php b/src/Output/QRGdImage.php index 05e5c2fa5..bebb97653 100644 --- a/src/Output/QRGdImage.php +++ b/src/Output/QRGdImage.php @@ -14,10 +14,12 @@ namespace chillerlan\QRCode\Output; use chillerlan\QRCode\Data\QRMatrix; use chillerlan\Settings\SettingsContainerInterface; -use ErrorException, Throwable; -use function array_values, count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor, - imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, imagescale, intdiv, intval, - is_array, is_numeric, max, min, ob_end_clean, ob_get_contents, ob_start, restore_error_handler, set_error_handler; +use ErrorException; +use Throwable; +use function array_values, count, extension_loaded, gd_info, imagebmp, imagecolorallocate, imagecolortransparent, + imagecreatetruecolor, imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, + imagescale, imagewebp, intdiv, intval, is_array, is_numeric, max, min, ob_end_clean, ob_get_contents, ob_start, + restore_error_handler, set_error_handler, sprintf; /** * Converts the matrix into GD images, raw or base64 output (requires ext-gd) @@ -55,18 +57,56 @@ class QRGdImage extends QROutputAbstract{ * @noinspection PhpMissingParentConstructorInspection */ public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){ - - if(!extension_loaded('gd')){ - throw new QRCodeOutputException('ext-gd not loaded'); // @codeCoverageIgnore - } - $this->options = $options; $this->matrix = $matrix; + $this->checkGD(); $this->setMatrixDimensions(); - // 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 + $this->image = $this->createImage(); + // set module values after image creation because we need the GdImage instance + $this->setModuleValues(); + } + + /** + * 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'); + } + + $info = gd_info(); + $mode = [ + self::GDIMAGE_BMP => 'BMP Support', + self::GDIMAGE_GIF => 'GIF Create Support', + self::GDIMAGE_JPG => 'JPEG Support', + self::GDIMAGE_PNG => 'PNG Support', + self::GDIMAGE_WEBP => 'WebP Support', + ][$this->options->outputType]; + + if(!isset($info[$mode]) || $info[$mode] !== true){ + throw new QRCodeOutputException(sprintf('output mode "%s" not supported', $this->options->outputType)); + } + + } + + /** + * 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->options->drawCircularModules && $this->options->scale < 20){ // increase the initial image size by 10 $this->length = (($this->length + 2) * 10); @@ -74,9 +114,7 @@ class QRGdImage extends QROutputAbstract{ $this->upscaled = true; } - $this->image = imagecreatetruecolor($this->length, $this->length); - // set module values after image creation because we need the GdImage instance - $this->setModuleValues(); + return imagecreatetruecolor($this->length, $this->length); } /** @@ -225,7 +263,7 @@ class QRGdImage extends QROutputAbstract{ } /** - * Creates the QR image + * Draws the QR image */ protected function drawImage():void{ foreach($this->matrix->getMatrix() as $y => $row){ @@ -282,16 +320,22 @@ class QRGdImage extends QROutputAbstract{ try{ switch($this->options->outputType){ + case QROutputInterface::GDIMAGE_BMP: + imagebmp($this->image); + break; case QROutputInterface::GDIMAGE_GIF: imagegif($this->image); break; case QROutputInterface::GDIMAGE_JPG: - imagejpeg($this->image, null, max(0, min(100, $this->options->jpegQuality))); + 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->pngCompression))); + imagepng($this->image, null, max(-1, min(9, $this->options->quality))); } $imageData = ob_get_contents(); diff --git a/src/Output/QROutputInterface.php b/src/Output/QROutputInterface.php index c83d3775c..683658f96 100644 --- a/src/Output/QROutputInterface.php +++ b/src/Output/QROutputInterface.php @@ -18,27 +18,31 @@ use chillerlan\QRCode\Data\QRMatrix; interface QROutputInterface{ /** @var string */ - public const MARKUP_HTML = 'html'; + public const MARKUP_HTML = 'html'; /** @var string */ - public const MARKUP_SVG = 'svg'; + public const MARKUP_SVG = 'svg'; /** @var string */ - public const GDIMAGE_PNG = 'png'; + public const GDIMAGE_BMP = 'bmp'; /** @var string */ - public const GDIMAGE_JPG = 'jpg'; + public const GDIMAGE_GIF = 'gif'; /** @var string */ - public const GDIMAGE_GIF = 'gif'; + public const GDIMAGE_JPG = 'jpg'; /** @var string */ - public const STRING_JSON = 'json'; + public const GDIMAGE_PNG = 'png'; /** @var string */ - public const STRING_TEXT = 'text'; + public const GDIMAGE_WEBP = 'webp'; /** @var string */ - public const IMAGICK = 'imagick'; + public const STRING_JSON = 'json'; /** @var string */ - public const FPDF = 'fpdf'; + public const STRING_TEXT = 'text'; /** @var string */ - public const EPS = 'eps'; + public const IMAGICK = 'imagick'; /** @var string */ - public const CUSTOM = 'custom'; + public const FPDF = 'fpdf'; + /** @var string */ + public const EPS = 'eps'; + /** @var string */ + public const CUSTOM = 'custom'; /** * Map of built-in output modes => class FQN @@ -46,16 +50,18 @@ interface QROutputInterface{ * @var string[] */ public const MODES = [ - self::MARKUP_SVG => QRMarkupSVG::class, - self::MARKUP_HTML => QRMarkupHTML::class, - self::GDIMAGE_PNG => QRGdImage::class, - self::GDIMAGE_GIF => QRGdImage::class, - self::GDIMAGE_JPG => QRGdImage::class, - self::STRING_JSON => QRString::class, - self::STRING_TEXT => QRString::class, - self::IMAGICK => QRImagick::class, - self::FPDF => QRFpdf::class, - self::EPS => QREps::class, + self::MARKUP_SVG => QRMarkupSVG::class, + self::MARKUP_HTML => QRMarkupHTML::class, + self::GDIMAGE_BMP => QRGdImage::class, + self::GDIMAGE_GIF => QRGdImage::class, + self::GDIMAGE_JPG => QRGdImage::class, + self::GDIMAGE_PNG => QRGdImage::class, + self::GDIMAGE_WEBP => QRGdImage::class, + self::STRING_JSON => QRString::class, + self::STRING_TEXT => QRString::class, + self::IMAGICK => QRImagick::class, + self::FPDF => QRFpdf::class, + self::EPS => QREps::class, ]; /** diff --git a/src/QROptionsTrait.php b/src/QROptionsTrait.php index a6e1fbc62..fa73b73f4 100644 --- a/src/QROptionsTrait.php +++ b/src/QROptionsTrait.php @@ -89,7 +89,7 @@ trait QROptionsTrait{ * The built-in output type * * - QROutputInterface::MARKUP_XXXX where XXXX = HTML, SVG - * - QROutputInterface::GDIMAGE_XXX where XXX = PNG, GIF, JPG + * - QROutputInterface::GDIMAGE_XXX where XXX = BMP, GIF, JPG, PNG, WEBP * - QROutputInterface::STRING_XXXX where XXXX = TEXT, JSON * - QROutputInterface::IMAGICK * - QROutputInterface::EPS @@ -268,18 +268,32 @@ trait QROptionsTrait{ */ protected $transparencyColor = null; + /** + * Compression quality + * + * The given value depends on the used output type: + * + * @see \imagejpeg() + * @see \imagepng() + * @see \imagewebp() + * @see \Imagick::setImageCompressionQuality() + */ + protected int $quality = -1; + /* * QRGdImage settings */ /** - * @see imagepng() + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality */ protected int $pngCompression = -1; /** - * @see imagejpeg() + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality */ protected int $jpegQuality = 85; @@ -541,6 +555,7 @@ trait QROptionsTrait{ * * @deprecated 5.0.0 use QROptions::$outputBase64 instead * @see \chillerlan\QRCode\QROptions::$outputBase64 + * @codeCoverageIgnore */ protected function set_imageBase64(bool $imageBase64):void{ $this->outputBase64 = $imageBase64; @@ -551,9 +566,54 @@ trait QROptionsTrait{ * * @deprecated 5.0.0 use QROptions::$outputBase64 instead * @see \chillerlan\QRCode\QROptions::$outputBase64 + * @codeCoverageIgnore */ protected function get_imageBase64():bool{ return $this->outputBase64; } + /** + * redirect call to the new variable + * + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality + * @codeCoverageIgnore + */ + protected function set_jpegQuality(bool $jpegQuality):void{ + $this->quality = $jpegQuality; + } + + /** + * redirect call to the new variable + * + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality + * @codeCoverageIgnore + */ + protected function get_jpegQuality():bool{ + return $this->quality; + } + + /** + * redirect call to the new variable + * + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality + * @codeCoverageIgnore + */ + protected function set_pngCompression(bool $pngCompression):void{ + $this->quality = $pngCompression; + } + + /** + * redirect call to the new variable + * + * @deprecated 5.0.0 use QROptions::$quality instead + * @see \chillerlan\QRCode\QROptions::$quality + * @codeCoverageIgnore + */ + protected function get_pngCompression():bool{ + return $this->quality; + } + } diff --git a/tests/Output/QRGdImageBMPTest.php b/tests/Output/QRGdImageBMPTest.php new file mode 100644 index 000000000..b06eee6db --- /dev/null +++ b/tests/Output/QRGdImageBMPTest.php @@ -0,0 +1,22 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +namespace chillerlan\QRCodeTest\Output; + +use chillerlan\QRCode\Output\QROutputInterface; + +/** + * + */ +final class QRGdImageBMPTest extends QRGdImageTestAbstract{ + + protected string $type = QROutputInterface::GDIMAGE_BMP; + +} diff --git a/tests/Output/QRGdImageWEBPTest.php b/tests/Output/QRGdImageWEBPTest.php new file mode 100644 index 000000000..b5513d70b --- /dev/null +++ b/tests/Output/QRGdImageWEBPTest.php @@ -0,0 +1,22 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +namespace chillerlan\QRCodeTest\Output; + +use chillerlan\QRCode\Output\QROutputInterface; + +/** + * + */ +final class QRGdImageWEBPTest extends QRGdImageTestAbstract{ + + protected string $type = QROutputInterface::GDIMAGE_WEBP; + +}