:octocat: QRGdImage & QRImagick background/transparency rework

This commit is contained in:
smiley
2023-03-15 07:57:17 +01:00
parent de27a999ae
commit 4147dae999
6 changed files with 183 additions and 139 deletions
+3 -1
View File
@@ -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,
+4 -3
View File
@@ -17,10 +17,11 @@ $options = new QROptions([
'version' => 7,
'outputType' => QROutputInterface::IMAGICK,
'eccLevel' => EccLevel::L,
'imageBase64' => false,
'bgColor' => '#ccccaa', // overrides the imageTransparent setting
'imageTransparent' => true,
'scale' => 20,
'imageBase64' => false,
'bgColor' => '#ccccaa',
'imageTransparent' => true,
# 'transparencyColor' => '#ECF9BE',
'drawLightModules' => true,
'drawCircularModules' => true,
'circleRadius' => 0.4,
+109 -49
View File
@@ -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();
imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
$this->drawImage();
if($this->upscaled){
// scale down to the expected size
$this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10));
$this->upscaled = false;
}
$this->image = imagecreatetruecolor($this->length, $this->length);
// avoid: "Indirect modification of overloaded property $x has no effect"
// https://stackoverflow.com/a/10455217
$bgColor = $this->options->imageTransparencyBG;
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);
}
}
// 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
View File
@@ -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
View File
@@ -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
*
-21
View File
@@ -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[][]
*/