Skip to content

Commit 4147dae

Browse files
committed
:octocat: QRGdImage & QRImagick background/transparency rework
1 parent de27a99 commit 4147dae

File tree

6 files changed

+179
-135
lines changed

6 files changed

+179
-135
lines changed

examples/image.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
'scale' => 20,
2121
'imageBase64' => false,
2222
'bgColor' => [200, 150, 200],
23-
'imageTransparent' => false,
23+
'imageTransparent' => true,
24+
# 'transparencyColor' => [233, 233, 233],
2425
'drawCircularModules' => true,
26+
'drawLightModules' => true,
2527
'circleRadius' => 0.4,
2628
'keepAsSquare' => [
2729
QRMatrix::M_FINDER_DARK,

examples/imagick.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
'version' => 7,
1818
'outputType' => QROutputInterface::IMAGICK,
1919
'eccLevel' => EccLevel::L,
20+
'scale' => 20,
2021
'imageBase64' => false,
21-
'bgColor' => '#ccccaa', // overrides the imageTransparent setting
22+
'bgColor' => '#ccccaa',
2223
'imageTransparent' => true,
23-
'scale' => 20,
24+
# 'transparencyColor' => '#ECF9BE',
2425
'drawLightModules' => true,
2526
'drawCircularModules' => true,
2627
'circleRadius' => 0.4,

src/Output/QRGdImage.php

Lines changed: 106 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use function count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor,
1919
imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, imagescale, is_array, is_numeric,
2020
max, min, ob_end_clean, ob_get_contents, ob_start, restore_error_handler, set_error_handler;
21-
use const IMG_BILINEAR_FIXED;
2221

2322
/**
2423
* Converts the matrix into GD images, raw or base64 output (requires ext-gd)
@@ -35,18 +34,49 @@ class QRGdImage extends QROutputAbstract{
3534
*/
3635
protected $image;
3736

37+
/**
38+
* The allocated background color
39+
*
40+
* @see \imagecolorallocate()
41+
*/
42+
protected int $background;
43+
44+
/**
45+
* Whether we're running in upscale mode (scale < 20)
46+
*
47+
* @see \chillerlan\QRCode\QROptions::$drawCircularModules
48+
*/
49+
protected bool $upscaled = false;
50+
3851
/**
3952
* @inheritDoc
4053
*
4154
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
55+
* @noinspection PhpMissingParentConstructorInspection
4256
*/
4357
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
4458

4559
if(!extension_loaded('gd')){
4660
throw new QRCodeOutputException('ext-gd not loaded'); // @codeCoverageIgnore
4761
}
4862

49-
parent::__construct($options, $matrix);
63+
$this->options = $options;
64+
$this->matrix = $matrix;
65+
66+
$this->setMatrixDimensions();
67+
68+
// we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
69+
// @see https://github.com/chillerlan/php-qrcode/issues/23
70+
if($this->options->drawCircularModules && $this->options->scale < 20){
71+
// increase the initial image size by 10
72+
$this->length = (($this->length + 2) * 10);
73+
$this->scale *= 10;
74+
$this->upscaled = true;
75+
}
76+
77+
$this->image = imagecreatetruecolor($this->length, $this->length);
78+
// set module values after image creation because we need the GdImage instance
79+
$this->setModuleValues();
5080
}
5181

5282
/**
@@ -70,23 +100,31 @@ protected function moduleValueIsValid($value):bool{
70100

71101
/**
72102
* @inheritDoc
103+
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
73104
*/
74-
protected function getModuleValue($value):array{
105+
protected function getModuleValue($value):int{
75106
$v = [];
76107

77108
for($i = 0; $i < 3; $i++){
78109
// clamp value
79110
$v[] = (int)max(0, min(255, $value[$i]));
80111
}
81112

82-
return $v;
113+
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
114+
$color = imagecolorallocate($this->image, ...$v);
115+
116+
if($color === false){
117+
throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
118+
}
119+
120+
return $color;
83121
}
84122

85123
/**
86124
* @inheritDoc
87125
*/
88-
protected function getDefaultModuleValue(bool $isDark):array{
89-
return ($isDark) ? [0, 0, 0] : [255, 255, 255];
126+
protected function getDefaultModuleValue(bool $isDark):int{
127+
return $this->getModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
90128
}
91129

92130
/**
@@ -104,47 +142,20 @@ public function dump(string $file = null){
104142
throw new ErrorException($msg, 0, $severity, $file, $line);
105143
});
106144

107-
// we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
108-
if($this->options->drawCircularModules && $this->options->scale <= 20){
109-
$this->length = (($this->length + 2) * 10);
110-
$this->scale *= 10;
111-
}
145+
$this->setBgColor();
112146

113-
$this->image = imagecreatetruecolor($this->length, $this->length);
147+
imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
114148

115-
// avoid: "Indirect modification of overloaded property $x has no effect"
116-
// https://stackoverflow.com/a/10455217
117-
$bgColor = $this->options->imageTransparencyBG;
149+
$this->drawImage();
118150

119-
if($this->moduleValueIsValid($this->options->bgColor)){
120-
$bgColor = $this->getModuleValue($this->options->bgColor);
121-
}
122-
123-
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
124-
$background = imagecolorallocate($this->image, ...$bgColor);
125-
126-
if(
127-
$this->options->imageTransparent
128-
&& $this->options->outputType !== QROutputInterface::GDIMAGE_JPG
129-
&& $this->moduleValueIsValid($this->options->imageTransparencyBG)
130-
){
131-
$tbg = $this->getModuleValue($this->options->imageTransparencyBG);
132-
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
133-
imagecolortransparent($this->image, imagecolorallocate($this->image, ...$tbg));
134-
}
135-
136-
imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $background);
137-
138-
foreach($this->matrix->matrix() as $y => $row){
139-
foreach($row as $x => $M_TYPE){
140-
$this->setPixel($x, $y, $M_TYPE);
141-
}
151+
if($this->upscaled){
152+
// scale down to the expected size
153+
$this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10));
154+
$this->upscaled = false;
142155
}
143156

144-
// scale down to the expected size
145-
if($this->options->drawCircularModules && $this->options->scale <= 20){
146-
$this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10), IMG_BILINEAR_FIXED);
147-
}
157+
// set transparency after scaling, otherwise it would be undone
158+
$this->setTransparencyColor();
148159

149160
if($this->options->returnResource){
150161
restore_error_handler();
@@ -165,12 +176,61 @@ public function dump(string $file = null){
165176
return $imageData;
166177
}
167178

179+
/**
180+
* Sets the background color
181+
*/
182+
protected function setBgColor():void{
183+
184+
if(isset($this->background)){
185+
return;
186+
}
187+
188+
if($this->moduleValueIsValid($this->options->bgColor)){
189+
$this->background = $this->getModuleValue($this->options->bgColor);
190+
191+
return;
192+
}
193+
194+
$this->background = $this->getModuleValue([255, 255, 255]);
195+
}
196+
197+
/**
198+
* Sets the transparency color
199+
*/
200+
protected function setTransparencyColor():void{
201+
202+
if($this->options->outputType === QROutputInterface::GDIMAGE_JPG || !$this->options->imageTransparent){
203+
return;
204+
}
205+
206+
$transparencyColor = $this->background;
207+
208+
if($this->moduleValueIsValid($this->options->transparencyColor)){
209+
$transparencyColor = $this->getModuleValue($this->options->transparencyColor);
210+
}
211+
212+
imagecolortransparent($this->image, $transparencyColor);
213+
}
214+
215+
/**
216+
* Creates the QR image
217+
*/
218+
protected function drawImage():void{
219+
foreach($this->matrix->matrix() as $y => $row){
220+
foreach($row as $x => $M_TYPE){
221+
$this->setPixel($x, $y, $M_TYPE);
222+
}
223+
}
224+
}
225+
168226
/**
169227
* Creates a single QR pixel with the given settings
170228
*/
171229
protected function setPixel(int $x, int $y, int $M_TYPE):void{
172-
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
173-
$color = imagecolorallocate($this->image, ...$this->moduleValues[$M_TYPE]);
230+
231+
if(!$this->options->drawLightModules && !$this->matrix->check($x, $y)){
232+
return;
233+
}
174234

175235
$this->options->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->options->keepAsSquare)
176236
? imagefilledellipse(
@@ -179,15 +239,15 @@ protected function setPixel(int $x, int $y, int $M_TYPE):void{
179239
(int)(($y * $this->scale) + ($this->scale / 2)),
180240
(int)(2 * $this->options->circleRadius * $this->scale),
181241
(int)(2 * $this->options->circleRadius * $this->scale),
182-
$color
242+
$this->moduleValues[$M_TYPE]
183243
)
184244
: imagefilledrectangle(
185245
$this->image,
186246
($x * $this->scale),
187247
($y * $this->scale),
188248
(($x + 1) * $this->scale),
189249
(($y + 1) * $this->scale),
190-
$color
250+
$this->moduleValues[$M_TYPE]
191251
);
192252
}
193253

src/Output/QRImagick.php

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,21 @@
2626
*/
2727
class QRImagick extends QROutputAbstract{
2828

29+
/**
30+
* The main image instance
31+
*/
2932
protected Imagick $imagick;
33+
34+
/**
35+
* The main draw instance
36+
*/
3037
protected ImagickDraw $imagickDraw;
3138

39+
/**
40+
* The allocated background color
41+
*/
42+
protected ImagickPixel $background;
43+
3244
/**
3345
* @inheritDoc
3446
*
@@ -48,6 +60,8 @@ public function __construct(SettingsContainerInterface $options, QRMatrix $matri
4860
}
4961

5062
/**
63+
* @todo: check/validate possible values
64+
* @see https://www.php.net/manual/imagickpixel.construct.php
5165
* @inheritDoc
5266
*/
5367
protected function moduleValueIsValid($value):bool{
@@ -65,7 +79,7 @@ protected function getModuleValue($value):ImagickPixel{
6579
* @inheritDoc
6680
*/
6781
protected function getDefaultModuleValue(bool $isDark):ImagickPixel{
68-
return new ImagickPixel(($isDark) ? $this->options->markupDark : $this->options->markupLight);
82+
return $this->getModuleValue(($isDark) ? $this->options->markupDark : $this->options->markupLight);
6983
}
7084

7185
/**
@@ -76,16 +90,13 @@ protected function getDefaultModuleValue(bool $isDark):ImagickPixel{
7690
public function dump(string $file = null){
7791
$this->imagick = new Imagick;
7892

79-
$bgColor = ($this->options->imageTransparent) ? 'transparent' : 'white';
93+
$this->setBgColor();
8094

81-
// keep the imagickBG property for now (until v6)
82-
if($this->moduleValueIsValid($this->options->bgColor ?? $this->options->imagickBG)){
83-
$bgColor = ($this->options->bgColor ?? $this->options->imagickBG);
84-
}
85-
86-
$this->imagick->newImage($this->length, $this->length, new ImagickPixel($bgColor), $this->options->imagickFormat);
95+
$this->imagick->newImage($this->length, $this->length, $this->background, $this->options->imagickFormat);
8796

8897
$this->drawImage();
98+
// set transparency color after all operations
99+
$this->setTransparencyColor();
89100

90101
if($this->options->returnResource){
91102
return $this->imagick;
@@ -104,6 +115,42 @@ public function dump(string $file = null){
104115
return $imageData;
105116
}
106117

118+
/**
119+
* Sets the background color
120+
*/
121+
protected function setBgColor():void{
122+
123+
if(isset($this->background)){
124+
return;
125+
}
126+
127+
if($this->moduleValueIsValid($this->options->bgColor)){
128+
$this->background = $this->getModuleValue($this->options->bgColor);
129+
130+
return;
131+
}
132+
133+
$this->background = $this->getModuleValue('white');
134+
}
135+
136+
/**
137+
* Sets the transparency color
138+
*/
139+
protected function setTransparencyColor():void{
140+
141+
if(!$this->options->imageTransparent){
142+
return;
143+
}
144+
145+
$transparencyColor = $this->background;
146+
147+
if($this->moduleValueIsValid($this->options->transparencyColor)){
148+
$transparencyColor = $this->getModuleValue($this->options->transparencyColor);
149+
}
150+
151+
$this->imagick->transparentPaintImage($transparencyColor, 0.0, 10, false);
152+
}
153+
107154
/**
108155
* Creates the QR image via ImagickDraw
109156
*/

0 commit comments

Comments
 (0)