Passed
Push — main ( 6b9f6f...a1f051 )
by smiley
02:03
created

QRGdImage::getModuleValue()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 6
nop 1
dl 0
loc 20
rs 9.9666
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 array_values, count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor,
19
	imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, imagescale, intval,
20
	is_array, is_numeric, 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
	public static 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
		foreach(array_values($value) as $i => $val){
93
94
			if($i > 2){
95
				break;
96
			}
97
98
			if(!is_numeric($val)){
99
				return false;
100
			}
101
102
		}
103
104
		return true;
105
	}
106
107
	/**
108
	 * @param array $value
109
	 *
110
	 * @inheritDoc
111
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
112
	 */
113
	protected function prepareModuleValue($value):int{
114
		$values = [];
115
116
		foreach(array_values($value) as $i => $val){
117
118
			if($i > 2){
119
				break;
120
			}
121
122
			$values[] = max(0, min(255, intval($val)));
123
		}
124
125
		/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
126
		$color = imagecolorallocate($this->image, ...$values);
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

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

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...
127
128
		if($color === false){
129
			throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
130
		}
131
132
		return $color;
133
	}
134
135
	/**
136
	 * @inheritDoc
137
	 */
138
	protected function getDefaultModuleValue(bool $isDark):int{
139
		return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
140
	}
141
142
	/**
143
	 * @inheritDoc
144
	 *
145
	 * @return string|resource|\GdImage
146
	 *
147
	 * @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn
148
	 * @throws \ErrorException
149
	 */
150
	public function dump(string $file = null){
151
152
		/** @phan-suppress-next-line PhanTypeMismatchArgumentInternal */
153
		set_error_handler(function(int $severity, string $msg, string $file, int $line):void{
154
			throw new ErrorException($msg, 0, $severity, $file, $line);
155
		});
156
157
		$this->setBgColor();
158
159
		imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
160
161
		$this->drawImage();
162
163
		if($this->upscaled){
164
			// scale down to the expected size
165
			$this->image    = imagescale($this->image, ($this->length / 10), ($this->length / 10));
166
			$this->upscaled = false;
167
		}
168
169
		// set transparency after scaling, otherwise it would be undone
170
		// @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099
171
		$this->setTransparencyColor();
172
173
		if($this->options->returnResource){
174
			restore_error_handler();
175
176
			return $this->image;
177
		}
178
179
		$imageData = $this->dumpImage();
180
181
		$this->saveToFile($imageData, $file);
182
183
		if($this->options->imageBase64){
184
			$imageData = $this->toBase64DataURI($imageData, 'image/'.$this->options->outputType);
185
		}
186
187
		restore_error_handler();
188
189
		return $imageData;
190
	}
191
192
	/**
193
	 * Sets the background color
194
	 */
195
	protected function setBgColor():void{
196
197
		if(isset($this->background)){
198
			return;
199
		}
200
201
		if($this::moduleValueIsValid($this->options->bgColor)){
202
			$this->background = $this->prepareModuleValue($this->options->bgColor);
203
204
			return;
205
		}
206
207
		$this->background = $this->prepareModuleValue([255, 255, 255]);
208
	}
209
210
	/**
211
	 * Sets the transparency color
212
	 */
213
	protected function setTransparencyColor():void{
214
215
		if($this->options->outputType === QROutputInterface::GDIMAGE_JPG || !$this->options->imageTransparent){
216
			return;
217
		}
218
219
		$transparencyColor = $this->background;
220
221
		if($this::moduleValueIsValid($this->options->transparencyColor)){
222
			$transparencyColor = $this->prepareModuleValue($this->options->transparencyColor);
223
		}
224
225
		imagecolortransparent($this->image, $transparencyColor);
226
	}
227
228
	/**
229
	 * Creates the QR image
230
	 */
231
	protected function drawImage():void{
232
		for($y = 0; $y < $this->moduleCount; $y++){
233
			for($x = 0; $x < $this->moduleCount; $x++){
234
				$this->setPixel($x, $y);
235
			}
236
		}
237
	}
238
239
	/**
240
	 * Creates a single QR pixel with the given settings
241
	 */
242
	protected function setPixel(int $x, int $y):void{
243
244
		if(!$this->options->drawLightModules && !$this->matrix->check($x, $y)){
245
			return;
246
		}
247
248
		$color = $this->getModuleValueAt($x, $y);
249
250
		$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

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