mirror of
https://github.com/chillerlan/php-qrcode.git
synced 2026-08-31 12:38:22 +00:00
:octocat: QRGdImage & QRImagick background/transparency rework
This commit is contained in:
+3
-1
@@ -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,
|
||||
|
||||
@@ -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
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
*
|
||||
|
||||
@@ -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[][]
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user