Passed
Push — main ( de27a9...f13300 )
by smiley
02:02
created

QRGdImage::dumpImage()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 8
nop 0
dl 0
loc 32
rs 9.3554
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class QRGdImage
4
 *
5
 * @created      05.12.2015
6
 * @author       Smiley <[email protected]>
7
 * @copyright    2015 Smiley
8
 * @license      MIT
9
 *
10
 * @noinspection PhpComposerExtensionStubsInspection
11
 */
12
13
namespace chillerlan\QRCode\Output;
14
15
use chillerlan\QRCode\Data\QRMatrix;
16
use chillerlan\Settings\SettingsContainerInterface;
17
use ErrorException, Throwable;
18
use function count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor,
19
	imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, imagescale, is_array, is_numeric,
20
	max, min, ob_end_clean, ob_get_contents, ob_start, restore_error_handler, set_error_handler;
21
22
/**
23
 * Converts the matrix into GD images, raw or base64 output (requires ext-gd)
24
 *
25
 * @see http://php.net/manual/book.image.php
26
 */
27
class QRGdImage extends QROutputAbstract{
28
29
	/**
30
	 * The GD image resource
31
	 *
32
	 * @see imagecreatetruecolor()
33
	 * @var resource|\GdImage
34
	 */
35
	protected $image;
36
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
51
	/**
52
	 * @inheritDoc
53
	 *
54
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
55
	 * @noinspection PhpMissingParentConstructorInspection
56
	 */
57
	public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
58
59
		if(!extension_loaded('gd')){
60
			throw new QRCodeOutputException('ext-gd not loaded'); // @codeCoverageIgnore
61
		}
62
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();
80
	}
81
82
	/**
83
	 * @inheritDoc
84
	 */
85
	protected function moduleValueIsValid($value):bool{
86
87
		if(!is_array($value) || count($value) < 3){
88
			return false;
89
		}
90
91
		// check the first 3 values of the array
92
		for($i = 0; $i < 3; $i++){
93
			if(!is_numeric($value[$i])){
94
				return false;
95
			}
96
		}
97
98
		return true;
99
	}
100
101
	/**
102
	 * @inheritDoc
103
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
104
	 */
105
	protected function getModuleValue($value):int{
106
		$v = [];
107
108
		for($i = 0; $i < 3; $i++){
109
			// clamp value
110
			$v[] = (int)max(0, min(255, $value[$i]));
111
		}
112
113
		/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
114
		$color = imagecolorallocate($this->image, ...$v);
0 ignored issues
show
Bug introduced by
The call to imagecolorallocate() has too few arguments starting with green. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

114
		$color = /** @scrutinizer ignore-call */ imagecolorallocate($this->image, ...$v);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
115
116
		if($color === false){
117
			throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
118
		}
119
120
		return $color;
121
	}
122
123
	/**
124
	 * @inheritDoc
125
	 */
126
	protected function getDefaultModuleValue(bool $isDark):int{
127
		return $this->getModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
128
	}
129
130
	/**
131
	 * @inheritDoc
132
	 *
133
	 * @return string|resource|\GdImage
134
	 *
135
	 * @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn
136
	 * @throws \ErrorException
137
	 */
138
	public function dump(string $file = null){
139
140
		/** @phan-suppress-next-line PhanTypeMismatchArgumentInternal */
141
		set_error_handler(function(int $severity, string $msg, string $file, int $line):void{
142
			throw new ErrorException($msg, 0, $severity, $file, $line);
143
		});
144
145
		$this->setBgColor();
146
147
		imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
148
149
		$this->drawImage();
150
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;
155
		}
156
157
		// set transparency after scaling, otherwise it would be undone
158
		$this->setTransparencyColor();
159
160
		if($this->options->returnResource){
161
			restore_error_handler();
162
163
			return $this->image;
164
		}
165
166
		$imageData = $this->dumpImage();
167
168
		$this->saveToFile($imageData, $file);
169
170
		if($this->options->imageBase64){
171
			$imageData = $this->toBase64DataURI($imageData, 'image/'.$this->options->outputType);
172
		}
173
174
		restore_error_handler();
175
176
		return $imageData;
177
	}
178
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
226
	/**
227
	 * Creates a single QR pixel with the given settings
228
	 */
229
	protected function setPixel(int $x, int $y, int $M_TYPE):void{
230
231
		if(!$this->options->drawLightModules && !$this->matrix->check($x, $y)){
232
			return;
233
		}
234
235
		$this->options->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->options->keepAsSquare)
0 ignored issues
show
Bug introduced by
It seems like $this->options->keepAsSquare can also be of type null; however, parameter $M_TYPES of chillerlan\QRCode\Data\QRMatrix::checkTypeIn() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

235
		$this->options->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, /** @scrutinizer ignore-type */ $this->options->keepAsSquare)
Loading history...
236
			? imagefilledellipse(
237
				$this->image,
238
				(int)(($x * $this->scale) + ($this->scale / 2)),
239
				(int)(($y * $this->scale) + ($this->scale / 2)),
240
				(int)(2 * $this->options->circleRadius * $this->scale),
241
				(int)(2 * $this->options->circleRadius * $this->scale),
242
				$this->moduleValues[$M_TYPE]
243
			)
244
			: imagefilledrectangle(
245
				$this->image,
246
				($x * $this->scale),
247
				($y * $this->scale),
248
				(($x + 1) * $this->scale),
249
				(($y + 1) * $this->scale),
250
				$this->moduleValues[$M_TYPE]
251
			);
252
	}
253
254
	/**
255
	 * Creates the final image by calling the desired GD output function
256
	 *
257
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
258
	 */
259
	protected function dumpImage():string{
260
		ob_start();
261
262
		try{
263
264
			switch($this->options->outputType){
265
				case QROutputInterface::GDIMAGE_GIF:
266
					imagegif($this->image);
267
					break;
268
				case QROutputInterface::GDIMAGE_JPG:
269
					imagejpeg($this->image, null, max(0, min(100, $this->options->jpegQuality)));
270
					break;
271
				// silently default to png output
272
				case QROutputInterface::GDIMAGE_PNG:
273
				default:
274
					imagepng($this->image, null, max(-1, min(9, $this->options->pngCompression)));
275
			}
276
277
		}
278
		// not going to cover edge cases
279
		// @codeCoverageIgnoreStart
280
		catch(Throwable $e){
281
			throw new QRCodeOutputException($e->getMessage());
282
		}
283
		// @codeCoverageIgnoreEnd
284
285
		$imageData = ob_get_contents();
286
		imagedestroy($this->image);
287
288
		ob_end_clean();
289
290
		return $imageData;
291
	}
292
293
}
294