From b231433d56512eb1fd31895e14e4f8c7c6a459fe Mon Sep 17 00:00:00 2001 From: smiley Date: Sat, 4 May 2024 22:52:48 +0200 Subject: [PATCH] :sparkles: QRInterventionImage (intervention/image) --- composer.json | 2 + examples/intervention-image.php | 80 +++++++++++ src/Output/QRInterventionImage.php | 162 +++++++++++++++++++++++ tests/Output/QRInterventionImageTest.php | 70 ++++++++++ 4 files changed, 314 insertions(+) create mode 100644 examples/intervention-image.php create mode 100644 src/Output/QRInterventionImage.php create mode 100644 tests/Output/QRInterventionImageTest.php diff --git a/composer.json b/composer.json index 6ea9fd4d6..f31649e3f 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ }, "require-dev": { "chillerlan/php-authenticator": "^5.1", + "intervention/image": "^3.5", "phpbench/phpbench": "^1.2.15", "phan/phan": "^5.4", "phpunit/phpunit": "^11.0", @@ -61,6 +62,7 @@ }, "suggest": { "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", + "intervention/image": "More advanced GD and ImageMagick output.", "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" }, diff --git a/examples/intervention-image.php b/examples/intervention-image.php new file mode 100644 index 000000000..3593216b6 --- /dev/null +++ b/examples/intervention-image.php @@ -0,0 +1,80 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +use chillerlan\QRCode\Data\QRMatrix; +use chillerlan\QRCode\Output\QRInterventionImage; +use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\QROptions; +use Intervention\Image\Drivers\Gd\Driver as GdDriver; + +require_once __DIR__.'/../vendor/autoload.php'; + +$options = new QROptions; + +$options->version = 7; +$options->outputInterface = QRInterventionImage::class; +$options->scale = 20; +$options->outputBase64 = false; +$options->bgColor = '#cccccc'; +$options->imageTransparent = false; +$options->transparencyColor = '#cccccc'; +$options->drawLightModules = true; +$options->drawCircularModules = true; +$options->circleRadius = 0.4; +$options->keepAsSquare = [ + QRMatrix::M_FINDER_DARK, + QRMatrix::M_FINDER_DOT, + QRMatrix::M_ALIGNMENT_DARK, +]; +$options->moduleValues = [ + // finder + QRMatrix::M_FINDER_DARK => '#A71111', // dark (true) + QRMatrix::M_FINDER_DOT => '#A71111', // finder dot, dark (true) + QRMatrix::M_FINDER => '#FFBFBF', // light (false) + // alignment + QRMatrix::M_ALIGNMENT_DARK => '#A70364', + QRMatrix::M_ALIGNMENT => '#FFC9C9', + // timing + QRMatrix::M_TIMING_DARK => '#98005D', + QRMatrix::M_TIMING => '#FFB8E9', + // format + QRMatrix::M_FORMAT_DARK => '#003804', + QRMatrix::M_FORMAT => '#CCFB12', + // version + QRMatrix::M_VERSION_DARK => '#650098', + QRMatrix::M_VERSION => '#E0B8FF', + // data + QRMatrix::M_DATA_DARK => '#4A6000', + QRMatrix::M_DATA => '#ECF9BE', + // darkmodule + QRMatrix::M_DARKMODULE => '#080063', + // separator + QRMatrix::M_SEPARATOR => '#DDDDDD', + // quietzone + QRMatrix::M_QUIETZONE => '#DDDDDD', +]; + +$qrcode = new QRCode($options); +$qrcode->addByteSegment('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); + +$qrOutputInterface = new QRInterventionImage($options, $qrcode->getQRMatrix()); +// set a different driver +$qrOutputInterface->setDriver(new GdDriver); + +$out = $qrOutputInterface->dump(); + +header('Content-type: image/png'); + +echo $out; + +exit; + + + diff --git a/src/Output/QRInterventionImage.php b/src/Output/QRInterventionImage.php new file mode 100644 index 000000000..e8d6dcc11 --- /dev/null +++ b/src/Output/QRInterventionImage.php @@ -0,0 +1,162 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Output; + +use chillerlan\QRCode\Data\QRMatrix; +use chillerlan\QRCode\QROptions; +use chillerlan\Settings\SettingsContainerInterface; +use Intervention\Image\Drivers\Gd\Driver as GdDriver; +use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver; +use Intervention\Image\Geometry\Factories\CircleFactory; +use Intervention\Image\Geometry\Factories\RectangleFactory; +use Intervention\Image\ImageManager; +use Intervention\Image\Interfaces\DriverInterface; +use Intervention\Image\Interfaces\ImageInterface; +use Intervention\Image\Interfaces\ImageManagerInterface; +use UnhandledMatchError; +use function class_exists; +use function extension_loaded; +use function intdiv; + +/** + * intervention/image (GD/ImageMagick) output + * + * note: this output class works very slow compared to the native GD/Imagick output classes for obvious reasons. + * use only if you must. + * + * @see https://github.com/Intervention/image + * @see https://image.intervention.io/ + */ +class QRInterventionImage extends QROutputAbstract{ + use CssColorModuleValueTrait; + + protected DriverInterface $driver; + protected ImageManagerInterface $manager; + protected ImageInterface $image; + + /** + * QRInterventionImage constructor. + * + * @throws \chillerlan\QRCode\Output\QRCodeOutputException + */ + public function __construct(SettingsContainerInterface|QROptions $options, QRMatrix $matrix){ + + if(!class_exists(ImageManager::class)){ + // @codeCoverageIgnoreStart + throw new QRCodeOutputException( + 'The QRInterventionImage output requires Intervention/image (https://github.com/Intervention/image)'. + ' as dependency but the class "\\Intervention\\Image\\ImageManager" could not be found.', + ); + // @codeCoverageIgnoreEnd + } + + try{ + + $this->driver = match(true){ + extension_loaded('gd') => new GdDriver, + extension_loaded('imagick') => new ImagickDriver, + }; + + $this->setDriver($this->driver); + } + catch(UnhandledMatchError){ + throw new QRCodeOutputException('no image processing extension loaded (gd, imagick)'); // @codeCoverageIgnore + } + + parent::__construct($options, $matrix); + } + + /** + * Sets a DriverInterface + */ + public function setDriver(DriverInterface $driver):static{ + $this->manager = new ImageManager($driver); + + return $this; + } + + /** + * @inheritDoc + */ + public function dump(string $file = null):string|ImageInterface{ + [$width, $height] = $this->getOutputDimensions(); + + $this->image = $this->manager->create($width, $height); + + $this->image->fill($this->getDefaultModuleValue(false)); + + if($this->options->imageTransparent && $this::moduleValueIsValid($this->options->transparencyColor)){ + $this->image->setBlendingColor($this->prepareModuleValue($this->options->transparencyColor)); + } + + if($this::moduleValueIsValid($this->options->bgColor)){ + $this->image->fill($this->prepareModuleValue($this->options->bgColor)); + } + + foreach($this->matrix->getMatrix() as $y => $row){ + foreach($row as $x => $M_TYPE){ + $this->module($x, $y, $M_TYPE); + } + } + + if($this->options->returnResource){ + return $this->image; + } + + $image = $this->image->toPng(); + $imageData = $image->toString(); + + $this->saveToFile($imageData, $file); + + if($this->options->outputBase64){ + return $image->toDataUri(); + } + + return $imageData; + } + + + /** + * 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; + } + + $color = $this->getModuleValue($M_TYPE); + + if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ + + $this->image->drawCircle( + (($x * $this->scale) + intdiv($this->scale, 2)), + (($y * $this->scale) + intdiv($this->scale, 2)), + function(CircleFactory $circle) use ($color):void{ + $circle->radius((int)($this->circleRadius * $this->scale)); + $circle->background($color); + } + ); + + return; + } + + $this->image->drawRectangle( + ($x * $this->scale), + ($y * $this->scale), + function(RectangleFactory $rectangle) use ($color):void{ + $rectangle->size($this->scale, $this->scale); + $rectangle->background($color); + } + ); + } + +} diff --git a/tests/Output/QRInterventionImageTest.php b/tests/Output/QRInterventionImageTest.php new file mode 100644 index 000000000..eac185a6c --- /dev/null +++ b/tests/Output/QRInterventionImageTest.php @@ -0,0 +1,70 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +namespace chillerlan\QRCodeTest\Output; + +use chillerlan\QRCode\Data\QRMatrix; +use chillerlan\QRCode\Output\QRInterventionImage; +use chillerlan\QRCode\Output\QROutputInterface; +use chillerlan\QRCode\QROptions; +use chillerlan\Settings\SettingsContainerInterface; +use Intervention\Image\Interfaces\ImageInterface; +use function extension_loaded; + +/** + * Tests the QRInterventionImage output module + */ +class QRInterventionImageTest extends QROutputTestAbstract{ + use CssColorModuleValueProviderTrait; + + /** + * @inheritDoc + */ + protected function setUp():void{ + + if(!extension_loaded('gd')){ + $this::markTestSkipped('ext-gd not loaded'); + } + + parent::setUp(); + } + + protected function getOutputInterface( + SettingsContainerInterface|QROptions $options, + QRMatrix $matrix + ):QROutputInterface{ + return new QRInterventionImage($options, $matrix); + } + + /** + * @inheritDoc + */ + public function testSetModuleValues():void{ + + $this->options->moduleValues = [ + // data + QRMatrix::M_DATA_DARK => '#4A6000', + QRMatrix::M_DATA => '#ECF9BE', + ]; + + $this->outputInterface = $this->getOutputInterface($this->options, $this->matrix); + $this->outputInterface->dump(); + + $this::assertTrue(true); // tricking the code coverage + } + + public function testOutputGetResource():void{ + $this->options->returnResource = true; + $this->outputInterface = $this->getOutputInterface($this->options, $this->matrix); + + $this::assertInstanceOf(ImageInterface::class, $this->outputInterface->dump()); + } + +}