Commit | Line | Data |
---|---|---|
b86ca09c | 1 | <?php |
64a820cf | 2 | namespace wcf\system\image\adapter; |
a3399fc5 | 3 | use wcf\system\exception\SystemException; |
a17de04e | 4 | use wcf\util\StringUtil; |
64a820cf AE |
5 | |
6 | /** | |
7 | * Image adapter for bundled GD imaging library. | |
8 | * | |
9f959ced | 9 | * @author Alexander Ebert |
7b7b9764 | 10 | * @copyright 2001-2019 WoltLab GmbH |
64a820cf | 11 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
e71525e4 | 12 | * @package WoltLabSuite\Core\System\Image\Adapter |
64a820cf AE |
13 | */ |
14 | class GDImageAdapter implements IImageAdapter { | |
15 | /** | |
16 | * active color | |
9f959ced MS |
17 | * @var integer |
18 | */ | |
64a820cf AE |
19 | protected $color = null; |
20 | ||
cfdb4fa4 MS |
21 | /** |
22 | * red, green, blue data of the active color | |
23 | * @var array | |
24 | */ | |
058cbd6a | 25 | protected $colorData = []; |
cfdb4fa4 | 26 | |
b86ca09c AE |
27 | /** |
28 | * image height | |
29 | * @var integer | |
a17de04e | 30 | */ |
b86ca09c AE |
31 | protected $height = 0; |
32 | ||
33 | /** | |
34 | * loaded image | |
35 | * @var resource | |
a17de04e | 36 | */ |
b86ca09c AE |
37 | protected $image = null; |
38 | ||
39 | /** | |
40 | * image type | |
41 | * @var integer | |
42 | */ | |
43 | protected $type = 0; | |
44 | ||
45 | /** | |
46 | * image width | |
47 | * @var integer | |
a17de04e | 48 | */ |
b86ca09c AE |
49 | protected $width = 0; |
50 | ||
5cf84a99 MS |
51 | /** |
52 | * GDImageAdapter constructor. | |
53 | */ | |
54 | public function __construct() { | |
55 | // suppress warnings like "recoverable error: Invalid SOS parameters for sequential JPEG" | |
ac81e92d | 56 | @ini_set('gd.jpeg_ignore_warning', '1'); |
5cf84a99 MS |
57 | } |
58 | ||
26f418a4 SG |
59 | /** |
60 | * Returns whether the given image is a valid GD resource / GD object | |
61 | * | |
62 | * @return boolean | |
63 | */ | |
64 | public function isImage($image) { | |
65 | return (is_resource($image) && get_resource_type($image) === 'gd') || (is_object($image) && $image instanceof \GdImage); | |
66 | } | |
67 | ||
b86ca09c | 68 | /** |
0fcfe5f6 | 69 | * @inheritDoc |
b86ca09c AE |
70 | */ |
71 | public function load($image, $type = '') { | |
26f418a4 | 72 | if (!$this->isImage($image)) { |
b86ca09c AE |
73 | throw new SystemException("Image resource is invalid."); |
74 | } | |
75 | ||
76 | if (empty($type)) { | |
77 | throw new SystemException("Image type is missing."); | |
78 | } | |
79 | ||
80 | $this->image = $image; | |
81 | $this->type = $type; | |
82 | ||
379875ee MS |
83 | $this->height = imagesy($this->image); |
84 | $this->width = imagesx($this->image); | |
b86ca09c AE |
85 | } |
86 | ||
87 | /** | |
0fcfe5f6 | 88 | * @inheritDoc |
a17de04e | 89 | */ |
b86ca09c | 90 | public function loadFile($file) { |
379875ee | 91 | list($this->width, $this->height, $this->type) = getimagesize($file); |
b86ca09c AE |
92 | |
93 | switch ($this->type) { | |
94 | case IMAGETYPE_GIF: | |
379875ee | 95 | $this->image = imagecreatefromgif($file); |
b86ca09c AE |
96 | break; |
97 | ||
98 | case IMAGETYPE_JPEG: | |
5cf84a99 | 99 | // suppress warnings and properly handle errors |
93395679 | 100 | $this->image = @imagecreatefromjpeg($file); |
5cf84a99 MS |
101 | if ($this->image === false) { |
102 | throw new SystemException("Could not read jpeg image '".$file."'."); | |
103 | } | |
b86ca09c AE |
104 | break; |
105 | ||
106 | case IMAGETYPE_PNG: | |
c5c9e424 AE |
107 | // suppress warnings and properly handle errors |
108 | $this->image = @imagecreatefrompng($file); | |
109 | if ($this->image === false) { | |
110 | throw new SystemException("Could not read png image '".$file."'."); | |
111 | } | |
b86ca09c AE |
112 | break; |
113 | ||
114 | default: | |
115 | throw new SystemException("Could not read image '".$file."', format is not recognized."); | |
116 | break; | |
117 | } | |
118 | } | |
119 | ||
45140a75 | 120 | /** |
0fcfe5f6 | 121 | * @inheritDoc |
45140a75 TD |
122 | */ |
123 | public function createEmptyImage($width, $height) { | |
379875ee | 124 | $this->image = imagecreate($width, $height); |
45140a75 TD |
125 | $this->type = IMAGETYPE_PNG; |
126 | $this->setColor(0xFF, 0xFF, 0xFF); | |
127 | $this->color = null; | |
6e019f83 AE |
128 | |
129 | $this->width = $width; | |
130 | $this->height = $height; | |
45140a75 TD |
131 | } |
132 | ||
b86ca09c | 133 | /** |
0fcfe5f6 | 134 | * @inheritDoc |
a17de04e | 135 | */ |
34e42c28 | 136 | public function createThumbnail($maxWidth, $maxHeight, $preserveAspectRatio = true) { |
13b7bb1f | 137 | $x = $y = 0; |
32f6cd95 MW |
138 | $sourceWidth = $this->width; |
139 | $sourceHeight = $this->height; | |
b86ca09c | 140 | |
34e42c28 | 141 | if ($preserveAspectRatio) { |
a3399fc5 AE |
142 | if ($maxWidth / $this->width < $maxHeight / $this->height) { |
143 | $width = $maxWidth; | |
144 | $height = round($this->height * ($width / $this->width)); | |
b86ca09c AE |
145 | } |
146 | else { | |
a3399fc5 AE |
147 | $height = $maxHeight; |
148 | $width = round($this->width * ($height / $this->height)); | |
b86ca09c AE |
149 | } |
150 | } | |
151 | else { | |
f7ac2ad1 MW |
152 | $width = $maxWidth; |
153 | $height = $maxHeight; | |
a3399fc5 | 154 | |
f7ac2ad1 MW |
155 | if ($maxWidth / $this->width < $maxHeight / $this->height) { |
156 | $cut = (($sourceWidth * ($maxHeight / $this->height)) - $maxWidth) / ($maxHeight / $this->height); | |
157 | $x = ceil($cut / 2); | |
158 | $sourceWidth = $sourceWidth - $x * 2; | |
b86ca09c AE |
159 | } |
160 | else { | |
f7ac2ad1 MW |
161 | $cut = (($sourceHeight * ($maxWidth / $this->width)) - $maxHeight) / ($maxWidth / $this->width); |
162 | $y = ceil($cut / 2); | |
163 | $sourceHeight = $sourceHeight - $y * 2; | |
b86ca09c AE |
164 | } |
165 | } | |
166 | ||
167 | // resize image | |
29eac4dd | 168 | $image = imagecreatetruecolor((int) $width, (int) $height); |
379875ee | 169 | imagealphablending($image, false); |
29eac4dd | 170 | imagecopyresampled($image, $this->image, 0, 0, (int) $x, (int) $y, (int) $width, (int) $height, (int) $sourceWidth, (int) $sourceHeight); |
379875ee | 171 | imagesavealpha($image, true); |
b86ca09c AE |
172 | |
173 | return $image; | |
174 | } | |
175 | ||
176 | /** | |
0fcfe5f6 | 177 | * @inheritDoc |
b86ca09c AE |
178 | */ |
179 | public function clip($originX, $originY, $width, $height) { | |
379875ee MS |
180 | $image = imagecreatetruecolor($width, $height); |
181 | imagealphablending($image, false); | |
b86ca09c | 182 | |
29eac4dd | 183 | imagecopy($image, $this->image, 0, 0, (int) $originX, (int) $originY, (int) $width, (int) $height); |
379875ee | 184 | imagesavealpha($image, true); |
b86ca09c | 185 | |
c3844f55 | 186 | // reload image to update image resource, width and height |
08c8e6f8 | 187 | $this->load($image, $this->type); |
b86ca09c AE |
188 | } |
189 | ||
190 | /** | |
0fcfe5f6 | 191 | * @inheritDoc |
b86ca09c | 192 | */ |
6eb4648f | 193 | public function resize($originX, $originY, $originWidth, $originHeight, $targetWidth = 0, $targetHeight = 0) { |
379875ee MS |
194 | $image = imagecreatetruecolor($targetWidth, $targetHeight); |
195 | imagealphablending($image, false); | |
b86ca09c | 196 | |
29eac4dd | 197 | imagecopyresampled($image, $this->image, 0, 0, (int) $originX, (int) $originY, (int) $targetWidth, (int) $targetHeight, (int) $originWidth, (int) $originHeight); |
379875ee | 198 | imagesavealpha($image, true); |
b86ca09c | 199 | |
c3844f55 | 200 | // reload image to update image resource, width and height |
08c8e6f8 | 201 | $this->load($image, $this->type); |
b86ca09c AE |
202 | } |
203 | ||
204 | /** | |
0fcfe5f6 | 205 | * @inheritDoc |
b86ca09c | 206 | */ |
64a820cf | 207 | public function drawRectangle($startX, $startY, $endX, $endY) { |
379875ee | 208 | imagefilledrectangle($this->image, $startX, $startY, $endX, $endY, $this->color); |
b86ca09c AE |
209 | } |
210 | ||
211 | /** | |
0fcfe5f6 | 212 | * @inheritDoc |
b86ca09c | 213 | */ |
6840f856 | 214 | public function drawText($text, $x, $y, $font, $size, $opacity = 1.0) { |
cfdb4fa4 MS |
215 | // set opacity |
216 | $color = imagecolorallocatealpha($this->image, $this->colorData['red'], $this->colorData['green'], $this->colorData['blue'], (1 - $opacity) * 127); | |
d3e0ca88 | 217 | |
46ce632b MW |
218 | // draw text |
219 | imagettftext($this->image, $size, 0, $x, $y, $color, $font, $text); | |
cfdb4fa4 MS |
220 | } |
221 | ||
222 | /** | |
0fcfe5f6 | 223 | * @inheritDoc |
cfdb4fa4 | 224 | */ |
6840f856 | 225 | public function drawTextRelative($text, $position, $margin, $offsetX, $offsetY, $font, $size, $opacity = 1.0) { |
46ce632b MW |
226 | // split text into multiple lines |
227 | $lines = explode("\n", StringUtil::unifyNewlines($text)); | |
cfdb4fa4 | 228 | |
46ce632b MW |
229 | // calc text width, height and first line height |
230 | $box = imagettfbbox($size, 0, $font, $text); | |
231 | $firstLineBox = imagettfbbox($size, 0, $font, $lines[0]); | |
232 | $textWidth = abs($box[0] - $box[2]); | |
233 | $textHeight = abs($box[7] - $box[1]); | |
234 | $firstLineHeight = abs($firstLineBox[7] - $firstLineBox[1]); | |
cfdb4fa4 | 235 | |
46ce632b MW |
236 | // calculate x coordinate |
237 | $x = 0; | |
238 | switch ($position) { | |
239 | case 'topLeft': | |
240 | case 'middleLeft': | |
241 | case 'bottomLeft': | |
242 | $x = $margin; | |
8c56908c MS |
243 | break; |
244 | ||
46ce632b MW |
245 | case 'topCenter': |
246 | case 'middleCenter': | |
247 | case 'bottomCenter': | |
248 | $x = floor(($this->getWidth() - $textWidth) / 2); | |
8c56908c MS |
249 | break; |
250 | ||
46ce632b MW |
251 | case 'topRight': |
252 | case 'middleRight': | |
253 | case 'bottomRight': | |
254 | $x = $this->getWidth() - $textWidth - $margin; | |
8c56908c | 255 | break; |
46ce632b | 256 | } |
8c56908c | 257 | |
46ce632b MW |
258 | // calculate y coordinate |
259 | $y = 0; | |
260 | switch ($position) { | |
261 | case 'topLeft': | |
262 | case 'topCenter': | |
263 | case 'topRight': | |
264 | $y = $margin + $firstLineHeight; | |
8c56908c MS |
265 | break; |
266 | ||
46ce632b MW |
267 | case 'middleLeft': |
268 | case 'middleCenter': | |
269 | case 'middleRight': | |
270 | $y = floor(($this->getHeight() - $textHeight) / 2) + $firstLineHeight; | |
8c56908c MS |
271 | break; |
272 | ||
46ce632b MW |
273 | case 'bottomLeft': |
274 | case 'bottomCenter': | |
275 | case 'bottomRight': | |
276 | $y = $this->getHeight() - $textHeight + $firstLineHeight - $margin; | |
8c56908c | 277 | break; |
cfdb4fa4 | 278 | } |
46ce632b MW |
279 | |
280 | $this->drawText($text, $x + $offsetX, $y + $offsetY, $font, $size, $opacity); | |
b86ca09c AE |
281 | } |
282 | ||
8c56908c | 283 | /** |
0fcfe5f6 | 284 | * @inheritDoc |
8c56908c MS |
285 | */ |
286 | public function textFitsImage($text, $margin, $font, $size) { | |
287 | $box = imagettfbbox($size, 0, $font, $text); | |
288 | ||
289 | $textWidth = abs($box[0] - $box[2]); | |
290 | $textHeight = abs($box[7] - $box[1]); | |
291 | ||
292 | return ($textWidth + 2 * $margin <= $this->getWidth() && $textHeight + 2 * $margin <= $this->getHeight()); | |
293 | } | |
294 | ||
295 | /** | |
0fcfe5f6 | 296 | * @inheritDoc |
8c56908c MS |
297 | */ |
298 | public function adjustFontSize($text, $margin, $font, $size) { | |
299 | // does nothing | |
300 | } | |
301 | ||
b86ca09c | 302 | /** |
0fcfe5f6 | 303 | * @inheritDoc |
d726f13d | 304 | */ |
64a820cf | 305 | public function setColor($red, $green, $blue) { |
379875ee | 306 | $this->color = imagecolorallocate($this->image, $red, $green, $blue); |
cfdb4fa4 MS |
307 | |
308 | // save data of the color | |
058cbd6a | 309 | $this->colorData = [ |
cfdb4fa4 MS |
310 | 'red' => $red, |
311 | 'green' => $green, | |
312 | 'blue' => $blue | |
058cbd6a | 313 | ]; |
b86ca09c AE |
314 | } |
315 | ||
316 | /** | |
0fcfe5f6 | 317 | * @inheritDoc |
64a820cf AE |
318 | */ |
319 | public function hasColor() { | |
320 | return ($this->color !== null); | |
321 | } | |
322 | ||
04e3288d | 323 | /** |
0fcfe5f6 | 324 | * @inheritDoc |
04e3288d TS |
325 | */ |
326 | public function setTransparentColor($red, $green, $blue) { | |
327 | if ($this->type == IMAGETYPE_PNG) { | |
328 | $color = imagecolorallocate($this->image, $red, $green, $blue); | |
379875ee | 329 | imagecolortransparent($this->image, $color); |
04e3288d TS |
330 | } |
331 | } | |
332 | ||
64a820cf | 333 | /** |
0fcfe5f6 | 334 | * @inheritDoc |
64a820cf | 335 | */ |
b86ca09c | 336 | public function writeImage($image, $filename) { |
26f418a4 | 337 | if (!$this->isImage($image)) { |
a3399fc5 AE |
338 | throw new SystemException("Given image is not a valid image resource."); |
339 | } | |
340 | ||
b86ca09c AE |
341 | ob_start(); |
342 | ||
d7e1222e MS |
343 | imagealphablending($image, false); |
344 | imagesavealpha($image, true); | |
64a820cf | 345 | if ($this->type == IMAGETYPE_GIF) { |
379875ee | 346 | imagegif($image); |
b86ca09c | 347 | } |
64a820cf | 348 | else if ($this->type == IMAGETYPE_PNG) { |
379875ee | 349 | imagepng($image); |
b86ca09c AE |
350 | } |
351 | else if (function_exists('imageJPEG')) { | |
379875ee | 352 | imagejpeg($image, null, 90); |
b86ca09c AE |
353 | } |
354 | ||
45140a75 | 355 | $stream = ob_get_contents(); |
b86ca09c AE |
356 | ob_end_clean(); |
357 | ||
45140a75 | 358 | file_put_contents($filename, $stream); |
b86ca09c AE |
359 | } |
360 | ||
a3399fc5 | 361 | /** |
0fcfe5f6 | 362 | * @inheritDoc |
d726f13d | 363 | */ |
a3399fc5 AE |
364 | public function getWidth() { |
365 | return $this->width; | |
366 | } | |
367 | ||
368 | /** | |
0fcfe5f6 | 369 | * @inheritDoc |
a3399fc5 AE |
370 | */ |
371 | public function getHeight() { | |
372 | return $this->height; | |
373 | } | |
374 | ||
aa6ceb28 | 375 | /** |
0fcfe5f6 | 376 | * @inheritDoc |
aa6ceb28 MW |
377 | */ |
378 | public function getType() { | |
379 | return $this->type; | |
380 | } | |
381 | ||
b86ca09c | 382 | /** |
0fcfe5f6 | 383 | * @inheritDoc |
b86ca09c AE |
384 | */ |
385 | public function getImage() { | |
386 | return $this->image; | |
387 | } | |
87646f44 | 388 | |
f268d97a | 389 | /** |
0fcfe5f6 | 390 | * @inheritDoc |
f268d97a MW |
391 | */ |
392 | public function rotate($degrees) { | |
a8e8435c | 393 | // imagerotate interpretes degrees as counter-clockwise |
63b9817b | 394 | return imagerotate($this->image, 360.0 - $degrees, ($this->color ?: 0)); |
f268d97a MW |
395 | } |
396 | ||
a7c0248c | 397 | /** |
0fcfe5f6 | 398 | * @inheritDoc |
a7c0248c MS |
399 | */ |
400 | public function overlayImage($file, $x, $y, $opacity) { | |
401 | $overlayImage = new self(); | |
402 | $overlayImage->loadFile($file); | |
403 | ||
404 | // fix PNG alpha channel handling | |
405 | // see http://php.net/manual/en/function.imagecopymerge.php#92787 | |
406 | $cut = imagecreatetruecolor($overlayImage->getWidth(), $overlayImage->getHeight()); | |
a4556a32 MS |
407 | imagealphablending($cut, false); |
408 | imagesavealpha($cut, true); | |
409 | ||
a7c0248c MS |
410 | imagecopy($cut, $this->image, 0, 0, $x, $y, $overlayImage->getWidth(), $overlayImage->getHeight()); |
411 | imagecopy($cut, $overlayImage->image, 0, 0, 0, 0, $overlayImage->getWidth(), $overlayImage->getHeight()); | |
a4556a32 MS |
412 | |
413 | $this->imagecopymerge_alpha($this->image, $cut, $x, $y, 0, 0, $overlayImage->getWidth(), $overlayImage->getHeight(), $opacity * 100); | |
414 | } | |
415 | ||
416 | /** | |
417 | * `imagecopymerge` implementation with alpha support. | |
418 | * | |
419 | * @see http://php.net/manual/en/function.imagecopymerge.php#88456 | |
420 | * | |
421 | * @param resource $dst_im destination image resource | |
422 | * @param resource $src_im source image resource | |
423 | * @param integer $dst_x x-coordinate of destination point | |
424 | * @param integer $dst_y y-coordinate of destination point | |
425 | * @param integer $src_x x-coordinate of source point | |
426 | * @param integer $src_y y-coordinate of source point | |
427 | * @param integer $src_w source width | |
428 | * @param integer $src_h source height | |
429 | * @param integer $pct opacity percent | |
430 | * @return boolean | |
431 | */ | |
82d72850 | 432 | private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) { // phpcs:ignore |
a4556a32 MS |
433 | if (!isset($pct)) { |
434 | return false; | |
435 | } | |
436 | $pct /= 100; | |
437 | // Get image width and height | |
438 | $w = imagesx($src_im); | |
439 | $h = imagesy($src_im); | |
440 | // Turn alpha blending off | |
441 | imagealphablending($src_im, false); | |
442 | // Find the most opaque pixel in the image (the one with the smallest alpha value) | |
443 | $minalpha = 127; | |
444 | for ($x = 0; $x < $w; $x++) { | |
445 | for ($y = 0; $y < $h; $y++) { | |
446 | $alpha = (imagecolorat($src_im, $x, $y) >> 24) & 0xFF; | |
447 | if ($alpha < $minalpha) { | |
448 | $minalpha = $alpha; | |
449 | } | |
450 | } | |
451 | } | |
452 | // loop through image pixels and modify alpha for each | |
453 | for ($x = 0; $x < $w; $x++) { | |
454 | for ($y = 0; $y < $h; $y++) { | |
455 | // get current alpha value (represents the TANSPARENCY!) | |
456 | $colorxy = imagecolorat($src_im, $x, $y); | |
457 | $alpha = ($colorxy >> 24) & 0xFF; | |
458 | // calculate new alpha | |
459 | if ($minalpha !== 127) { | |
460 | $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minalpha); | |
461 | } | |
462 | else { | |
463 | $alpha += 127 * $pct; | |
464 | } | |
465 | // get the color index with new alpha | |
466 | $alphacolorxy = imagecolorallocatealpha($src_im, ($colorxy >> 16) & 0xFF, ($colorxy >> 8) & 0xFF, $colorxy & 0xFF, $alpha); | |
467 | // set pixel with the new color + opacity | |
468 | if (!imagesetpixel($src_im, $x, $y, $alphacolorxy)) { | |
469 | return false; | |
470 | } | |
471 | } | |
472 | } | |
473 | // The image copy | |
474 | imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); | |
475 | ||
476 | return true; | |
a7c0248c MS |
477 | } |
478 | ||
479 | /** | |
0fcfe5f6 | 480 | * @inheritDoc |
a7c0248c MS |
481 | */ |
482 | public function overlayImageRelative($file, $position, $margin, $opacity) { | |
483 | // does nothing | |
484 | } | |
485 | ||
87646f44 | 486 | /** |
0fcfe5f6 | 487 | * @inheritDoc |
d726f13d | 488 | */ |
87646f44 | 489 | public static function isSupported() { |
5e3aefb5 | 490 | return function_exists('gd_info'); |
87646f44 | 491 | } |
b86ca09c | 492 | } |