Add the new variable `FileProcessorFormField::$bigPreview` with getter and setter.
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / image / adapter / GDImageAdapter.class.php
CommitLineData
b86ca09c 1<?php
a9229942 2
64a820cf 3namespace wcf\system\image\adapter;
a9229942 4
a3399fc5 5use wcf\system\exception\SystemException;
a17de04e 6use wcf\util\StringUtil;
64a820cf
AE
7
8/**
9 * Image adapter for bundled GD imaging library.
a9229942
TD
10 *
11 * @author Alexander Ebert
12 * @copyright 2001-2019 WoltLab GmbH
13 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
64a820cf 14 */
cecb02de 15class GDImageAdapter implements IImageAdapter, IWebpImageAdapter
a9229942
TD
16{
17 /**
18 * active color
19 * @var int
20 */
21 protected $color;
22
23 /**
24 * red, green, blue data of the active color
25 * @var array
26 */
27 protected $colorData = [];
28
29 /**
30 * image height
31 * @var int
32 */
33 protected $height = 0;
34
35 /**
36 * loaded image
85823f15 37 * @var \GDImage
a9229942
TD
38 */
39 protected $image;
40
41 /**
42 * image type
43 * @var int
44 */
45 protected $type = 0;
46
47 /**
48 * image width
49 * @var int
50 */
51 protected $width = 0;
52
53 /**
54 * GDImageAdapter constructor.
55 */
56 public function __construct()
57 {
58 // suppress warnings like "recoverable error: Invalid SOS parameters for sequential JPEG"
59 @\ini_set('gd.jpeg_ignore_warning', '1');
60 }
61
62 /**
63 * Returns whether the given image is a valid GD resource / GD object
64 *
65 * @return bool
66 */
67 public function isImage($image)
68 {
69 return (\is_resource($image) && \get_resource_type($image) === 'gd') || (\is_object($image) && $image instanceof \GdImage);
70 }
71
72 /**
73 * @inheritDoc
74 */
75 public function load($image, $type = '')
76 {
77 if (!$this->isImage($image)) {
78 throw new SystemException("Image resource is invalid.");
79 }
80
81 if (empty($type)) {
82 throw new SystemException("Image type is missing.");
83 }
84
85 $this->image = $image;
86 $this->type = $type;
87
9e4fe52b
MS
88 $this->height = \imagesy($this->image);
89 $this->width = \imagesx($this->image);
a9229942
TD
90 }
91
92 /**
93 * @inheritDoc
94 */
95 public function loadFile($file)
96 {
97 [$this->width, $this->height, $this->type] = \getimagesize($file);
98
99 switch ($this->type) {
100 case \IMAGETYPE_GIF:
28ab4fee
MW
101 // suppress warnings and properly handle errors
102 $this->image = @\imagecreatefromgif($file);
103 if ($this->image === false) {
104 throw new SystemException("Could not read gif image '" . $file . "'.");
105 }
a9229942
TD
106 break;
107
108 case \IMAGETYPE_JPEG:
109 // suppress warnings and properly handle errors
9e4fe52b 110 $this->image = @\imagecreatefromjpeg($file);
a9229942
TD
111 if ($this->image === false) {
112 throw new SystemException("Could not read jpeg image '" . $file . "'.");
113 }
114 break;
115
116 case \IMAGETYPE_PNG:
117 // suppress warnings and properly handle errors
9e4fe52b 118 $this->image = @\imagecreatefrompng($file);
a9229942
TD
119 if ($this->image === false) {
120 throw new SystemException("Could not read png image '" . $file . "'.");
121 }
122 break;
123
124 case \IMAGETYPE_WEBP:
125 // suppress warnings and properly handle errors
9e4fe52b 126 $this->image = @\imagecreatefromwebp($file);
a9229942
TD
127 if ($this->image === false) {
128 throw new SystemException("Could not read webp image '" . $file . "'.");
129 }
130 break;
131
132 default:
133 throw new SystemException("Could not read image '" . $file . "', format is not recognized.");
134 }
135 }
136
137 /**
138 * @inheritDoc
139 */
140 public function createEmptyImage($width, $height)
141 {
9e4fe52b 142 $this->image = \imagecreate($width, $height);
a9229942
TD
143 $this->type = \IMAGETYPE_PNG;
144 $this->setColor(0xFF, 0xFF, 0xFF);
145 $this->color = null;
146
147 $this->width = $width;
148 $this->height = $height;
149 }
150
151 /**
152 * @inheritDoc
153 */
154 public function createThumbnail($maxWidth, $maxHeight, $preserveAspectRatio = true)
155 {
156 $x = $y = 0;
157 $sourceWidth = $this->width;
158 $sourceHeight = $this->height;
159
160 if ($preserveAspectRatio) {
161 if ($maxWidth / $this->width < $maxHeight / $this->height) {
162 $width = $maxWidth;
163 $height = \round($this->height * ($width / $this->width));
164 } else {
165 $height = $maxHeight;
166 $width = \round($this->width * ($height / $this->height));
167 }
168 } else {
169 $width = $maxWidth;
170 $height = $maxHeight;
171
172 if ($maxWidth / $this->width < $maxHeight / $this->height) {
173 $cut = (($sourceWidth * ($maxHeight / $this->height)) - $maxWidth) / ($maxHeight / $this->height);
174 $x = \ceil($cut / 2);
175 $sourceWidth = $sourceWidth - $x * 2;
176 } else {
177 $cut = (($sourceHeight * ($maxWidth / $this->width)) - $maxHeight) / ($maxWidth / $this->width);
178 $y = \ceil($cut / 2);
179 $sourceHeight = $sourceHeight - $y * 2;
180 }
181 }
182
183 // resize image
9e4fe52b
MS
184 $image = \imagecreatetruecolor((int)$width, (int)$height);
185 \imagealphablending($image, false);
186 \imagecopyresampled(
a9229942
TD
187 $image,
188 $this->image,
189 0,
190 0,
191 (int)$x,
192 (int)$y,
193 (int)$width,
194 (int)$height,
195 (int)$sourceWidth,
196 (int)$sourceHeight
197 );
9e4fe52b 198 \imagesavealpha($image, true);
a9229942
TD
199
200 return $image;
201 }
202
203 /**
204 * @inheritDoc
205 */
206 public function clip($originX, $originY, $width, $height)
207 {
9e4fe52b
MS
208 $image = \imagecreatetruecolor($width, $height);
209 \imagealphablending($image, false);
a9229942 210
9e4fe52b
MS
211 \imagecopy($image, $this->image, 0, 0, (int)$originX, (int)$originY, (int)$width, (int)$height);
212 \imagesavealpha($image, true);
a9229942
TD
213
214 // reload image to update image resource, width and height
215 $this->load($image, $this->type);
216 }
217
218 /**
219 * @inheritDoc
220 */
221 public function resize($originX, $originY, $originWidth, $originHeight, $targetWidth = 0, $targetHeight = 0)
222 {
9e4fe52b
MS
223 $image = \imagecreatetruecolor($targetWidth, $targetHeight);
224 \imagealphablending($image, false);
a9229942 225
9e4fe52b 226 \imagecopyresampled(
a9229942
TD
227 $image,
228 $this->image,
229 0,
230 0,
231 (int)$originX,
232 (int)$originY,
233 (int)$targetWidth,
234 (int)$targetHeight,
235 (int)$originWidth,
236 (int)$originHeight
237 );
9e4fe52b 238 \imagesavealpha($image, true);
a9229942
TD
239
240 // reload image to update image resource, width and height
241 $this->load($image, $this->type);
242 }
243
244 /**
245 * @inheritDoc
246 */
247 public function drawRectangle($startX, $startY, $endX, $endY)
248 {
9e4fe52b 249 \imagefilledrectangle($this->image, $startX, $startY, $endX, $endY, $this->color);
a9229942
TD
250 }
251
252 /**
253 * @inheritDoc
254 */
255 public function drawText($text, $x, $y, $font, $size, $opacity = 1.0)
256 {
257 // set opacity
9e4fe52b 258 $color = \imagecolorallocatealpha(
a9229942
TD
259 $this->image,
260 $this->colorData['red'],
261 $this->colorData['green'],
262 $this->colorData['blue'],
a4908bde 263 \round((1 - $opacity) * 127)
a9229942
TD
264 );
265
266 // draw text
9e4fe52b 267 \imagettftext($this->image, $size, 0, $x, $y, $color, $font, $text);
a9229942
TD
268 }
269
270 /**
271 * @inheritDoc
272 */
273 public function drawTextRelative($text, $position, $margin, $offsetX, $offsetY, $font, $size, $opacity = 1.0)
274 {
275 // split text into multiple lines
276 $lines = \explode("\n", StringUtil::unifyNewlines($text));
277
278 // calc text width, height and first line height
9e4fe52b
MS
279 $box = \imagettfbbox($size, 0, $font, $text);
280 $firstLineBox = \imagettfbbox($size, 0, $font, $lines[0]);
a9229942
TD
281 $textWidth = \abs($box[0] - $box[2]);
282 $textHeight = \abs($box[7] - $box[1]);
283 $firstLineHeight = \abs($firstLineBox[7] - $firstLineBox[1]);
284
285 // calculate x coordinate
286 $x = 0;
287 switch ($position) {
288 case 'topLeft':
289 case 'middleLeft':
290 case 'bottomLeft':
291 $x = $margin;
292 break;
293
294 case 'topCenter':
295 case 'middleCenter':
296 case 'bottomCenter':
297 $x = \floor(($this->getWidth() - $textWidth) / 2);
298 break;
299
300 case 'topRight':
301 case 'middleRight':
302 case 'bottomRight':
303 $x = $this->getWidth() - $textWidth - $margin;
304 break;
305 }
306
307 // calculate y coordinate
308 $y = 0;
309 switch ($position) {
310 case 'topLeft':
311 case 'topCenter':
312 case 'topRight':
313 $y = $margin + $firstLineHeight;
314 break;
315
316 case 'middleLeft':
317 case 'middleCenter':
318 case 'middleRight':
319 $y = \floor(($this->getHeight() - $textHeight) / 2) + $firstLineHeight;
320 break;
321
322 case 'bottomLeft':
323 case 'bottomCenter':
324 case 'bottomRight':
325 $y = $this->getHeight() - $textHeight + $firstLineHeight - $margin;
326 break;
327 }
328
329 $this->drawText($text, $x + $offsetX, $y + $offsetY, $font, $size, $opacity);
330 }
331
332 /**
333 * @inheritDoc
334 */
335 public function textFitsImage($text, $margin, $font, $size)
336 {
9e4fe52b 337 $box = \imagettfbbox($size, 0, $font, $text);
a9229942
TD
338
339 $textWidth = \abs($box[0] - $box[2]);
340 $textHeight = \abs($box[7] - $box[1]);
341
342 return $textWidth + 2 * $margin <= $this->getWidth() && $textHeight + 2 * $margin <= $this->getHeight();
343 }
344
345 /**
346 * @inheritDoc
347 */
348 public function adjustFontSize($text, $margin, $font, $size)
349 {
350 // does nothing
351 }
352
353 /**
354 * @inheritDoc
355 */
356 public function setColor($red, $green, $blue)
357 {
9e4fe52b 358 $this->color = \imagecolorallocate($this->image, $red, $green, $blue);
a9229942
TD
359
360 // save data of the color
361 $this->colorData = [
362 'red' => $red,
363 'green' => $green,
364 'blue' => $blue,
365 ];
366 }
367
368 /**
369 * @inheritDoc
370 */
371 public function hasColor()
372 {
373 return $this->color !== null;
374 }
375
376 /**
377 * @inheritDoc
378 */
379 public function setTransparentColor($red, $green, $blue)
380 {
381 if ($this->type == \IMAGETYPE_PNG) {
9e4fe52b
MS
382 $color = \imagecolorallocate($this->image, $red, $green, $blue);
383 \imagecolortransparent($this->image, $color);
a9229942
TD
384 }
385 }
386
387 /**
388 * @inheritDoc
389 */
390 public function writeImage($image, $filename)
391 {
392 if (!$this->isImage($image)) {
393 throw new SystemException("Given image is not a valid image resource.");
394 }
395
396 \ob_start();
397
398 // fix PNG alpha channel handling
399 // see http://php.net/manual/en/function.imagecopymerge.php#92787
9e4fe52b
MS
400 \imagealphablending($image, false);
401 \imagesavealpha($image, true);
a9229942
TD
402
403 if ($this->type == \IMAGETYPE_GIF) {
9e4fe52b 404 \imagegif($image);
a9229942 405 } elseif ($this->type == \IMAGETYPE_PNG) {
9e4fe52b 406 \imagepng($image);
a9229942 407 } elseif ($this->type == \IMAGETYPE_WEBP) {
db206eb0 408 \imagepalettetotruecolor($image);
9e4fe52b 409 \imagewebp($image);
a9229942 410 } elseif (\function_exists('imageJPEG')) {
9e4fe52b 411 \imagejpeg($image, null, 90);
a9229942
TD
412 }
413
414 $stream = \ob_get_contents();
415 \ob_end_clean();
416
417 \file_put_contents($filename, $stream);
418 }
419
420 /**
421 * @inheritDoc
422 */
423 public function getWidth()
424 {
425 return $this->width;
426 }
427
428 /**
429 * @inheritDoc
430 */
431 public function getHeight()
432 {
433 return $this->height;
434 }
435
436 /**
437 * @inheritDoc
438 */
439 public function getType()
440 {
441 return $this->type;
442 }
443
444 /**
445 * @inheritDoc
446 */
447 public function getImage()
448 {
449 return $this->image;
450 }
451
452 /**
453 * @inheritDoc
454 */
455 public function rotate($degrees)
456 {
457 // imagerotate interpretes degrees as counter-clockwise
9e4fe52b 458 return \imagerotate($this->image, 360.0 - $degrees, ($this->color ?: 0));
a9229942
TD
459 }
460
461 /**
462 * @inheritDoc
463 */
464 public function overlayImage($file, $x, $y, $opacity)
465 {
466 $overlayImage = new self();
467 $overlayImage->loadFile($file);
468
469 // fix PNG alpha channel handling
470 // see http://php.net/manual/en/function.imagecopymerge.php#92787
9e4fe52b
MS
471 $cut = \imagecreatetruecolor($overlayImage->getWidth(), $overlayImage->getHeight());
472 \imagealphablending($cut, false);
473 \imagesavealpha($cut, true);
a9229942 474
9e4fe52b
MS
475 \imagecopy($cut, $this->image, 0, 0, $x, $y, $overlayImage->getWidth(), $overlayImage->getHeight());
476 \imagecopy($cut, $overlayImage->image, 0, 0, 0, 0, $overlayImage->getWidth(), $overlayImage->getHeight());
a9229942
TD
477
478 $this->imagecopymerge_alpha(
479 $this->image,
480 $cut,
481 $x,
482 $y,
483 0,
484 0,
485 $overlayImage->getWidth(),
486 $overlayImage->getHeight(),
487 $opacity * 100
488 );
489 }
490
491 /**
492 * `imagecopymerge` implementation with alpha support.
493 *
494 * @see http://php.net/manual/en/function.imagecopymerge.php#88456
495 *
85823f15
MW
496 * @param \GDImage $dst_im destination image resource
497 * @param \GDImage $src_im source image resource
a9229942
TD
498 * @param int $dst_x x-coordinate of destination point
499 * @param int $dst_y y-coordinate of destination point
500 * @param int $src_x x-coordinate of source point
501 * @param int $src_y y-coordinate of source point
502 * @param int $src_w source width
503 * @param int $src_h source height
504 * @param int $pct opacity percent
505 * @return bool
506 */
507 // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
508 private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct)
509 {
510 if (!isset($pct)) {
511 return false;
512 }
513 $pct /= 100;
514 // Get image width and height
9e4fe52b
MS
515 $w = \imagesx($src_im);
516 $h = \imagesy($src_im);
a9229942 517 // Turn alpha blending off
9e4fe52b 518 \imagealphablending($src_im, false);
a9229942
TD
519 // Find the most opaque pixel in the image (the one with the smallest alpha value)
520 $minalpha = 127;
521 for ($x = 0; $x < $w; $x++) {
522 for ($y = 0; $y < $h; $y++) {
9e4fe52b 523 $alpha = (\imagecolorat($src_im, $x, $y) >> 24) & 0xFF;
a9229942
TD
524 if ($alpha < $minalpha) {
525 $minalpha = $alpha;
526 }
527 }
528 }
529 // loop through image pixels and modify alpha for each
530 for ($x = 0; $x < $w; $x++) {
531 for ($y = 0; $y < $h; $y++) {
532 // get current alpha value (represents the TANSPARENCY!)
9e4fe52b 533 $colorxy = \imagecolorat($src_im, $x, $y);
a9229942
TD
534 $alpha = ($colorxy >> 24) & 0xFF;
535 // calculate new alpha
536 if ($minalpha !== 127) {
537 $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minalpha);
538 } else {
539 $alpha += 127 * $pct;
540 }
541 // get the color index with new alpha
9e4fe52b 542 $alphacolorxy = \imagecolorallocatealpha(
a9229942
TD
543 $src_im,
544 ($colorxy >> 16) & 0xFF,
545 ($colorxy >> 8) & 0xFF,
546 $colorxy & 0xFF,
a4908bde 547 \round($alpha)
a9229942
TD
548 );
549 // set pixel with the new color + opacity
9e4fe52b 550 if (!\imagesetpixel($src_im, $x, $y, $alphacolorxy)) {
a9229942
TD
551 return false;
552 }
553 }
554 }
555 // The image copy
9e4fe52b 556 \imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h);
a9229942
TD
557
558 return true;
559 }
560
561 /**
562 * @inheritDoc
563 */
564 public function overlayImageRelative($file, $position, $margin, $opacity)
565 {
566 // does nothing
567 }
568
569 /**
570 * @inheritDoc
571 */
572 public function saveImageAs($image, string $filename, string $type, int $quality = 100): void
573 {
574 if (!$this->isImage($image)) {
575 throw new \InvalidArgumentException("Given image is not a valid image resource.");
576 }
577
578 \ob_start();
579
580 // fix PNG alpha channel handling
581 // see http://php.net/manual/en/function.imagecopymerge.php#92787
9e4fe52b
MS
582 \imagealphablending($image, false);
583 \imagesavealpha($image, true);
a9229942
TD
584
585 switch ($type) {
586 case "gif":
9e4fe52b 587 \imagegif($image);
a9229942
TD
588 break;
589
590 case "jpg":
591 case "jpeg":
9e4fe52b 592 \imagejpeg($image, null, $quality);
a9229942
TD
593 break;
594
595 case "png":
9e4fe52b 596 \imagepng($image, null, $quality);
a9229942
TD
597 break;
598
599 case "webp":
db206eb0 600 \imagepalettetotruecolor($image);
9e4fe52b 601 \imagewebp($image, null, $quality);
a9229942
TD
602 break;
603
604 default:
605 throw new \LogicException("Unreachable");
606 }
607
608 $stream = \ob_get_contents();
609 \ob_end_clean();
610
611 \file_put_contents($filename, $stream);
612 }
613
614 /**
615 * @inheritDoc
616 */
617 public static function isSupported()
618 {
619 return \function_exists('gd_info');
620 }
cecb02de
AE
621
622 /**
623 * @inheritDoc
624 */
625 public static function supportsWebp(): bool
626 {
627 return !empty(\gd_info()['WebP Support']);
628 }
b86ca09c 629}