Forráskód Böngészése

:octocat: QRGdImage & QRImagick background/transparency rework

smiley 2 éve
szülő
commit
4147dae999

+ 3 - 1
examples/image.php

@@ -20,8 +20,10 @@ $options = new QROptions([
 	'scale'               => 20,
 	'imageBase64'         => false,
 	'bgColor'             => [200, 150, 200],
-	'imageTransparent'    => false,
+	'imageTransparent'    => true,
+#	'transparencyColor'   => [233, 233, 233],
 	'drawCircularModules' => true,
+	'drawLightModules'    => true,
 	'circleRadius'        => 0.4,
 	'keepAsSquare'        => [
 		QRMatrix::M_FINDER_DARK,

+ 3 - 2
examples/imagick.php

@@ -17,10 +17,11 @@ $options = new QROptions([
 	'version'             => 7,
 	'outputType'          => QROutputInterface::IMAGICK,
 	'eccLevel'            => EccLevel::L,
+	'scale'               => 20,
 	'imageBase64'         => false,
-	'bgColor'             => '#ccccaa', // overrides the imageTransparent setting
+	'bgColor'             => '#ccccaa',
 	'imageTransparent'    => true,
-	'scale'               => 20,
+#	'transparencyColor'   => '#ECF9BE',
 	'drawLightModules'    => true,
 	'drawCircularModules' => true,
 	'circleRadius'        => 0.4,

+ 106 - 46
src/Output/QRGdImage.php

@@ -18,7 +18,6 @@ use ErrorException, Throwable;
 use function count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor,
 	imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, imagescale, is_array, is_numeric,
 	max, min, ob_end_clean, ob_get_contents, ob_start, restore_error_handler, set_error_handler;
-use const IMG_BILINEAR_FIXED;
 
 /**
  * Converts the matrix into GD images, raw or base64 output (requires ext-gd)
@@ -35,10 +34,25 @@ class QRGdImage extends QROutputAbstract{
 	 */
 	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){
 
@@ -46,7 +60,23 @@ class QRGdImage extends QROutputAbstract{
 			throw new QRCodeOutputException('ext-gd not loaded'); // @codeCoverageIgnore
 		}
 
-		parent::__construct($options, $matrix);
+		$this->options = $options;
+		$this->matrix  = $matrix;
+
+		$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
+		if($this->options->drawCircularModules && $this->options->scale < 20){
+			// increase the initial image size by 10
+			$this->length    = (($this->length + 2) * 10);
+			$this->scale    *= 10;
+			$this->upscaled  = true;
+		}
+
+		$this->image = imagecreatetruecolor($this->length, $this->length);
+		// set module values after image creation because we need the GdImage instance
+		$this->setModuleValues();
 	}
 
 	/**
@@ -70,8 +100,9 @@ class QRGdImage extends QROutputAbstract{
 
 	/**
 	 * @inheritDoc
+	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
 	 */
-	protected function getModuleValue($value):array{
+	protected function getModuleValue($value):int{
 		$v = [];
 
 		for($i = 0; $i < 3; $i++){
@@ -79,14 +110,21 @@ class QRGdImage extends QROutputAbstract{
 			$v[] = (int)max(0, min(255, $value[$i]));
 		}
 
-		return $v;
+		/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
+		$color = imagecolorallocate($this->image, ...$v);
+
+		if($color === false){
+			throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
+		}
+
+		return $color;
 	}
 
 	/**
 	 * @inheritDoc
 	 */
-	protected function getDefaultModuleValue(bool $isDark):array{
-		return ($isDark) ? [0, 0, 0] : [255, 255, 255];
+	protected function getDefaultModuleValue(bool $isDark):int{
+		return $this->getModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
 	}
 
 	/**
@@ -104,47 +142,20 @@ class QRGdImage extends QROutputAbstract{
 			throw new ErrorException($msg, 0, $severity, $file, $line);
 		});
 
-		// we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
-		if($this->options->drawCircularModules && $this->options->scale <= 20){
-			$this->length  = (($this->length + 2) * 10);
-			$this->scale  *= 10;
-		}
+		$this->setBgColor();
 
-		$this->image = imagecreatetruecolor($this->length, $this->length);
+		imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
 
-		// avoid: "Indirect modification of overloaded property $x has no effect"
-		// https://stackoverflow.com/a/10455217
-		$bgColor = $this->options->imageTransparencyBG;
+		$this->drawImage();
 
-		if($this->moduleValueIsValid($this->options->bgColor)){
-			$bgColor = $this->getModuleValue($this->options->bgColor);
-		}
-
-		/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
-		$background = imagecolorallocate($this->image, ...$bgColor);
-
-		if(
-			   $this->options->imageTransparent
-			&& $this->options->outputType !== QROutputInterface::GDIMAGE_JPG
-			&& $this->moduleValueIsValid($this->options->imageTransparencyBG)
-		){
-			$tbg = $this->getModuleValue($this->options->imageTransparencyBG);
-			/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
-			imagecolortransparent($this->image, imagecolorallocate($this->image, ...$tbg));
-		}
-
-		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, $M_TYPE);
-			}
+		if($this->upscaled){
+			// scale down to the expected size
+			$this->image    = imagescale($this->image, ($this->length / 10), ($this->length / 10));
+			$this->upscaled = false;
 		}
 
-		// scale down to the expected size
-		if($this->options->drawCircularModules && $this->options->scale <= 20){
-			$this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10), IMG_BILINEAR_FIXED);
-		}
+		// set transparency after scaling, otherwise it would be undone
+		$this->setTransparencyColor();
 
 		if($this->options->returnResource){
 			restore_error_handler();
@@ -165,12 +176,61 @@ class QRGdImage extends QROutputAbstract{
 		return $imageData;
 	}
 
+	/**
+	 * Sets the background color
+	 */
+	protected function setBgColor():void{
+
+		if(isset($this->background)){
+			return;
+		}
+
+		if($this->moduleValueIsValid($this->options->bgColor)){
+			$this->background = $this->getModuleValue($this->options->bgColor);
+
+			return;
+		}
+
+		$this->background = $this->getModuleValue([255, 255, 255]);
+	}
+
+	/**
+	 * Sets the transparency color
+	 */
+	protected function setTransparencyColor():void{
+
+		if($this->options->outputType === QROutputInterface::GDIMAGE_JPG || !$this->options->imageTransparent){
+			return;
+		}
+
+		$transparencyColor = $this->background;
+
+		if($this->moduleValueIsValid($this->options->transparencyColor)){
+			$transparencyColor = $this->getModuleValue($this->options->transparencyColor);
+		}
+
+		imagecolortransparent($this->image, $transparencyColor);
+	}
+
+	/**
+	 * Creates the QR image
+	 */
+	protected function drawImage():void{
+		foreach($this->matrix->matrix() as $y => $row){
+			foreach($row as $x => $M_TYPE){
+				$this->setPixel($x, $y, $M_TYPE);
+			}
+		}
+	}
+
 	/**
 	 * Creates a single QR pixel with the given settings
 	 */
 	protected function setPixel(int $x, int $y, int $M_TYPE):void{
-		/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
-		$color = imagecolorallocate($this->image, ...$this->moduleValues[$M_TYPE]);
+
+		if(!$this->options->drawLightModules && !$this->matrix->check($x, $y)){
+			return;
+		}
 
 		$this->options->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->options->keepAsSquare)
 			? imagefilledellipse(
@@ -179,7 +239,7 @@ class QRGdImage extends QROutputAbstract{
 				(int)(($y * $this->scale) + ($this->scale / 2)),
 				(int)(2 * $this->options->circleRadius * $this->scale),
 				(int)(2 * $this->options->circleRadius * $this->scale),
-				$color
+				$this->moduleValues[$M_TYPE]
 			)
 			: imagefilledrectangle(
 				$this->image,
@@ -187,7 +247,7 @@ class QRGdImage extends QROutputAbstract{
 				($y * $this->scale),
 				(($x + 1) * $this->scale),
 				(($y + 1) * $this->scale),
-				$color
+				$this->moduleValues[$M_TYPE]
 			);
 	}
 

+ 55 - 8
src/Output/QRImagick.php

@@ -26,9 +26,21 @@ use const FILEINFO_MIME_TYPE;
  */
 class QRImagick extends QROutputAbstract{
 
+	/**
+	 * The main image instance
+	 */
 	protected Imagick $imagick;
+
+	/**
+	 * The main draw instance
+	 */
 	protected ImagickDraw $imagickDraw;
 
+	/**
+	 * The allocated background color
+	 */
+	protected ImagickPixel $background;
+
 	/**
 	 * @inheritDoc
 	 *
@@ -48,6 +60,8 @@ class QRImagick extends QROutputAbstract{
 	}
 
 	/**
+	 * @todo: check/validate possible values
+	 * @see https://www.php.net/manual/imagickpixel.construct.php
 	 * @inheritDoc
 	 */
 	protected function moduleValueIsValid($value):bool{
@@ -65,7 +79,7 @@ class QRImagick extends QROutputAbstract{
 	 * @inheritDoc
 	 */
 	protected function getDefaultModuleValue(bool $isDark):ImagickPixel{
-		return new ImagickPixel(($isDark) ? $this->options->markupDark : $this->options->markupLight);
+		return $this->getModuleValue(($isDark) ? $this->options->markupDark : $this->options->markupLight);
 	}
 
 	/**
@@ -76,16 +90,13 @@ class QRImagick extends QROutputAbstract{
 	public function dump(string $file = null){
 		$this->imagick = new Imagick;
 
-		$bgColor = ($this->options->imageTransparent) ? 'transparent' : 'white';
+		$this->setBgColor();
 
-		// keep the imagickBG property for now (until v6)
-		if($this->moduleValueIsValid($this->options->bgColor ?? $this->options->imagickBG)){
-			$bgColor = ($this->options->bgColor ?? $this->options->imagickBG);
-		}
-
-		$this->imagick->newImage($this->length, $this->length, new ImagickPixel($bgColor), $this->options->imagickFormat);
+		$this->imagick->newImage($this->length, $this->length, $this->background, $this->options->imagickFormat);
 
 		$this->drawImage();
+		// set transparency color after all operations
+		$this->setTransparencyColor();
 
 		if($this->options->returnResource){
 			return $this->imagick;
@@ -104,6 +115,42 @@ class QRImagick extends QROutputAbstract{
 		return $imageData;
 	}
 
+	/**
+	 * Sets the background color
+	 */
+	protected function setBgColor():void{
+
+		if(isset($this->background)){
+			return;
+		}
+
+		if($this->moduleValueIsValid($this->options->bgColor)){
+			$this->background = $this->getModuleValue($this->options->bgColor);
+
+			return;
+		}
+
+		$this->background = $this->getModuleValue('white');
+	}
+
+	/**
+	 * Sets the transparency color
+	 */
+	protected function setTransparencyColor():void{
+
+		if(!$this->options->imageTransparent){
+			return;
+		}
+
+		$transparencyColor = $this->background;
+
+		if($this->moduleValueIsValid($this->options->transparencyColor)){
+			$transparencyColor = $this->getModuleValue($this->options->transparencyColor);
+		}
+
+		$this->imagick->transparentPaintImage($transparencyColor, 0.0, 10, false);
+	}
+
 	/**
 	 * Creates the QR image via ImagickDraw
 	 */

+ 12 - 57
src/QROptionsTrait.php

@@ -174,9 +174,8 @@ trait QROptionsTrait{
 	 *
 	 * a note for GDImage output:
 	 *
-	 * if QROptions::$scale is less or equal than 20, the image will be upscaled internally, then the modules will be drawn
-	 * using imagefilledellipse() and then scaled back to the expected size using IMG_BICUBIC which in turn produces
-	 * unexpected outcomes in combination with transparency - to avoid this, set scale to a value greater than 20.
+	 * 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
 	 *
 	 * @see https://github.com/chillerlan/php-qrcode/issues/23
 	 * @see https://github.com/chillerlan/php-qrcode/discussions/122
@@ -235,11 +234,9 @@ trait QROptionsTrait{
 	protected bool $imageBase64 = true;
 
 	/**
-	 * toggle background transparency
-	 *
-	 * - GdImage: (png, gif) it sets imagecolortransparent() with {@see \chillerlan\QRCode\QROptions::$imageTransparencyBG}
-	 *
+	 * toggle transparency
 	 *
+	 * @see \chillerlan\QRCode\QROptions::$transparencyColor
 	 * @see https://github.com/chillerlan/php-qrcode/discussions/121
  	 */
 	protected bool $imageTransparent = true;
@@ -252,21 +249,21 @@ trait QROptionsTrait{
 	protected bool $drawLightModules = true;
 
 	/**
-	 * Sets the background color in GD mode: [R, G, B].
+	 * Sets a transparency color for when {@see \chillerlan\QRCode\QROptions::$imageTransparent QROptions::$imageTransparent} is set to true.
+	 * Defaults to {@see \chillerlan\QRCode\QROptions::$bgColor QROptions::$bgColor}.
 	 *
-	 * When $imageTransparent is set to true, this color is set as transparent in imagecolortransparent()
+	 * - QRGdImage: [R, G, B], this color is set as transparent in {@see imagecolortransparent()}
+	 * - QRImagick: "color_str", this color is set in {@see Imagick::transparentPaintImage()}
 	 *
-	 * @see \chillerlan\QRCode\Output\QRGdImage
-	 * @see \chillerlan\QRCode\QROptions::$imageTransparent
-	 * @see imagecolortransparent()
+	 * @var mixed|null
 	 */
-	protected array $imageTransparencyBG = [255, 255, 255];
+	protected $transparencyColor = null;
 
 	/**
 	 * Sets the image background color (if applicable)
 	 *
-	 * - Imagick: defaults to "transparent" or "white", depending on $imageTransparent, {@see \ImagickPixel::__construct()}
-	 * - GdImage: defaults to $imageTransparencyBG, {@see \chillerlan\QRCode\QROptions::$imageTransparencyBG}
+	 * - QRGdImage: defaults to "white"
+	 * - QRImagick: defaults to [255, 255, 255]
 	 *
 	 * @var mixed|null
 	 */
@@ -290,15 +287,6 @@ trait QROptionsTrait{
 	 */
 	protected string $imagickFormat = 'png32';
 
-	/**
-	 * Imagick background color
-	 *
-	 * @deprecated 5.0.0 use QROptions::$bgColor instead
-	 * @see \chillerlan\QRCode\QROptions::$bgColor
-	 * @see \ImagickPixel::__construct()
-	 */
-	protected ?string $imagickBG = null;
-
 	/**
 	 * Measurement unit for FPDF output: pt, mm, cm, in (defaults to "pt")
 	 *
@@ -399,39 +387,6 @@ trait QROptionsTrait{
 		$this->quietzoneSize = max(0, min($quietzoneSize, 75));
 	}
 
-	/**
-	 * sets the transparency background color
-	 *
-	 * @throws \chillerlan\QRCode\QRCodeException
-	 */
-	protected function set_imageTransparencyBG(array $imageTransparencyBG):void{
-
-		// invalid value - set to white as default
-		if(count($imageTransparencyBG) < 3){
-			$this->imageTransparencyBG = [255, 255, 255];
-
-			return;
-		}
-
-		foreach($imageTransparencyBG as $k => $v){
-
-			// cut off exceeding items
-			if($k > 2){
-				break;
-			}
-
-			if(!is_numeric($v)){
-				throw new QRCodeException('Invalid RGB value.');
-			}
-
-			// clamp the values
-			$this->imageTransparencyBG[$k] = max(0, min(255, (int)$v));
-		}
-
-		// use the array values to not run into errors with the spread operator (...$arr)
-		$this->imageTransparencyBG = array_values($this->imageTransparencyBG);
-	}
-
 	/**
 	 * sets the FPDF measurement unit
 	 *

+ 0 - 21
tests/QROptionsTest.php

@@ -79,27 +79,6 @@ final class QROptionsTest extends TestCase{
 		];
 	}
 
-	/**
-	 * Tests clamping of the RGB values for $imageTransparencyBG
-	 *
-	 * @dataProvider RGBProvider
-	 */
-	public function testClampRGBValues(array $rgb, array $expected):void{
-		$o = new QROptions(['imageTransparencyBG' => $rgb]);
-
-		$this::assertSame($expected, $o->imageTransparencyBG);
-	}
-
-	/**
-	 * Tests if an exception is thrown when a non-numeric RGB value was encoutered
-	 */
-	public function testInvalidRGBValueException():void{
-		$this->expectException(QRCodeException::class);
-		$this->expectExceptionMessage('Invalid RGB value.');
-		/** @phan-suppress-next-line PhanNoopNew */
-		new QROptions(['imageTransparencyBG' => ['r', 'g', 'b']]);
-	}
-
 	/**
 	 * @return int[][]
 	 */