浏览代码

:sparkles: QRInterventionImage (intervention/image)

smiley 1 年之前
父节点
当前提交
b231433d56
共有 4 个文件被更改,包括 314 次插入0 次删除
  1. 2 0
      composer.json
  2. 80 0
      examples/intervention-image.php
  3. 162 0
      src/Output/QRInterventionImage.php
  4. 70 0
      tests/Output/QRInterventionImageTest.php

+ 2 - 0
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"
 	},

+ 80 - 0
examples/intervention-image.php

@@ -0,0 +1,80 @@
+<?php
+/**
+ * intervention/image output example
+ *
+ * @created      04.05.2024
+ * @author       smiley <smiley@chillerlan.net>
+ * @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;
+
+
+

+ 162 - 0
src/Output/QRInterventionImage.php

@@ -0,0 +1,162 @@
+<?php
+/**
+ * Class QRInterventionImage
+ *
+ * @created      21.01.2024
+ * @author       smiley <smiley@chillerlan.net>
+ * @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);
+			}
+		);
+	}
+
+}

+ 70 - 0
tests/Output/QRInterventionImageTest.php

@@ -0,0 +1,70 @@
+<?php
+/**
+ * Class QRInterventionImageTest
+ *
+ * @created      04.05.2024
+ * @author       smiley <smiley@chillerlan.net>
+ * @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());
+	}
+
+}