3 namespace wcf\system\image\adapter
;
5 use wcf\system\exception\SystemException
;
6 use wcf\util\StringUtil
;
9 * Image adapter for bundled GD imaging library.
11 * @author Alexander Ebert
12 * @copyright 2001-2019 WoltLab GmbH
13 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
15 class GDImageAdapter
implements IImageAdapter
, IWebpImageAdapter
24 * red, green, blue data of the active color
27 protected $colorData = [];
33 protected $height = 0;
54 * GDImageAdapter constructor.
56 public function __construct()
58 // suppress warnings like "recoverable error: Invalid SOS parameters for sequential JPEG"
59 @\
ini_set('gd.jpeg_ignore_warning', '1');
63 * Returns whether the given image is a valid GD resource / GD object
67 public function isImage($image)
69 return (\
is_resource($image) && \
get_resource_type($image) === 'gd') ||
(\
is_object($image) && $image instanceof \GdImage
);
75 public function load($image, $type = '')
77 if (!$this->isImage($image)) {
78 throw new SystemException("Image resource is invalid.");
82 throw new SystemException("Image type is missing.");
85 $this->image
= $image;
88 $this->height
= \
imagesy($this->image
);
89 $this->width
= \
imagesx($this->image
);
95 public function loadFile($file)
97 [$this->width
, $this->height
, $this->type
] = \
getimagesize($file);
99 switch ($this->type
) {
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 . "'.");
108 case \IMAGETYPE_JPEG
:
109 // suppress warnings and properly handle errors
110 $this->image
= @\
imagecreatefromjpeg($file);
111 if ($this->image
=== false) {
112 throw new SystemException("Could not read jpeg image '" . $file . "'.");
117 // suppress warnings and properly handle errors
118 $this->image
= @\
imagecreatefrompng($file);
119 if ($this->image
=== false) {
120 throw new SystemException("Could not read png image '" . $file . "'.");
124 case \IMAGETYPE_WEBP
:
125 // suppress warnings and properly handle errors
126 $this->image
= @\
imagecreatefromwebp($file);
127 if ($this->image
=== false) {
128 throw new SystemException("Could not read webp image '" . $file . "'.");
133 throw new SystemException("Could not read image '" . $file . "', format is not recognized.");
140 public function createEmptyImage($width, $height)
142 $this->image
= \
imagecreate($width, $height);
143 $this->type
= \IMAGETYPE_PNG
;
144 $this->setColor(0xFF, 0xFF, 0xFF);
147 $this->width
= $width;
148 $this->height
= $height;
154 public function createThumbnail($maxWidth, $maxHeight, $preserveAspectRatio = true)
157 $sourceWidth = $this->width
;
158 $sourceHeight = $this->height
;
160 if ($preserveAspectRatio) {
161 if ($maxWidth / $this->width
< $maxHeight / $this->height
) {
163 $height = \round
($this->height
* ($width / $this->width
));
165 $height = $maxHeight;
166 $width = \round
($this->width
* ($height / $this->height
));
170 $height = $maxHeight;
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;
177 $cut = (($sourceHeight * ($maxWidth / $this->width
)) - $maxHeight) / ($maxWidth / $this->width
);
178 $y = \
ceil($cut / 2);
179 $sourceHeight = $sourceHeight - $y * 2;
184 $image = \
imagecreatetruecolor((int)$width, (int)$height);
185 \
imagealphablending($image, false);
198 \
imagesavealpha($image, true);
206 public function clip($originX, $originY, $width, $height)
208 $image = \
imagecreatetruecolor($width, $height);
209 \
imagealphablending($image, false);
211 \
imagecopy($image, $this->image
, 0, 0, (int)$originX, (int)$originY, (int)$width, (int)$height);
212 \
imagesavealpha($image, true);
214 // reload image to update image resource, width and height
215 $this->load($image, $this->type
);
221 public function resize($originX, $originY, $originWidth, $originHeight, $targetWidth = 0, $targetHeight = 0)
223 $image = \
imagecreatetruecolor($targetWidth, $targetHeight);
224 \
imagealphablending($image, false);
238 \
imagesavealpha($image, true);
240 // reload image to update image resource, width and height
241 $this->load($image, $this->type
);
247 public function drawRectangle($startX, $startY, $endX, $endY)
249 \
imagefilledrectangle($this->image
, $startX, $startY, $endX, $endY, $this->color
);
255 public function drawText($text, $x, $y, $font, $size, $opacity = 1.0)
258 $color = \
imagecolorallocatealpha(
260 $this->colorData
['red'],
261 $this->colorData
['green'],
262 $this->colorData
['blue'],
263 \round
((1 - $opacity) * 127)
267 \
imagettftext($this->image
, $size, 0, $x, $y, $color, $font, $text);
273 public function drawTextRelative($text, $position, $margin, $offsetX, $offsetY, $font, $size, $opacity = 1.0)
275 // split text into multiple lines
276 $lines = \
explode("\n", StringUtil
::unifyNewlines($text));
278 // calc text width, height and first line height
279 $box = \
imagettfbbox($size, 0, $font, $text);
280 $firstLineBox = \
imagettfbbox($size, 0, $font, $lines[0]);
281 $textWidth = \abs
($box[0] - $box[2]);
282 $textHeight = \abs
($box[7] - $box[1]);
283 $firstLineHeight = \abs
($firstLineBox[7] - $firstLineBox[1]);
285 // calculate x coordinate
297 $x = \floor
(($this->getWidth() - $textWidth) / 2);
303 $x = $this->getWidth() - $textWidth - $margin;
307 // calculate y coordinate
313 $y = $margin +
$firstLineHeight;
319 $y = \floor
(($this->getHeight() - $textHeight) / 2) +
$firstLineHeight;
325 $y = $this->getHeight() - $textHeight +
$firstLineHeight - $margin;
329 $this->drawText($text, $x +
$offsetX, $y +
$offsetY, $font, $size, $opacity);
335 public function textFitsImage($text, $margin, $font, $size)
337 $box = \
imagettfbbox($size, 0, $font, $text);
339 $textWidth = \abs
($box[0] - $box[2]);
340 $textHeight = \abs
($box[7] - $box[1]);
342 return $textWidth +
2 * $margin <= $this->getWidth() && $textHeight +
2 * $margin <= $this->getHeight();
348 public function adjustFontSize($text, $margin, $font, $size)
356 public function setColor($red, $green, $blue)
358 $this->color
= \
imagecolorallocate($this->image
, $red, $green, $blue);
360 // save data of the color
371 public function hasColor()
373 return $this->color
!== null;
379 public function setTransparentColor($red, $green, $blue)
381 if ($this->type
== \IMAGETYPE_PNG
) {
382 $color = \
imagecolorallocate($this->image
, $red, $green, $blue);
383 \
imagecolortransparent($this->image
, $color);
390 public function writeImage($image, $filename)
392 if (!$this->isImage($image)) {
393 throw new SystemException("Given image is not a valid image resource.");
398 // fix PNG alpha channel handling
399 // see http://php.net/manual/en/function.imagecopymerge.php#92787
400 \
imagealphablending($image, false);
401 \
imagesavealpha($image, true);
403 if ($this->type
== \IMAGETYPE_GIF
) {
405 } elseif ($this->type
== \IMAGETYPE_PNG
) {
407 } elseif ($this->type
== \IMAGETYPE_WEBP
) {
408 \
imagepalettetotruecolor($image);
410 } elseif (\function_exists
('imageJPEG')) {
411 \
imagejpeg($image, null, 90);
414 $stream = \
ob_get_contents();
417 \file_put_contents
($filename, $stream);
423 public function getWidth()
431 public function getHeight()
433 return $this->height
;
439 public function getType()
447 public function getImage()
455 public function rotate($degrees)
457 // imagerotate interpretes degrees as counter-clockwise
458 return \
imagerotate($this->image
, 360.0 - $degrees, ($this->color ?
: 0));
464 public function overlayImage($file, $x, $y, $opacity)
466 $overlayImage = new self();
467 $overlayImage->loadFile($file);
469 // fix PNG alpha channel handling
470 // see http://php.net/manual/en/function.imagecopymerge.php#92787
471 $cut = \
imagecreatetruecolor($overlayImage->getWidth(), $overlayImage->getHeight());
472 \
imagealphablending($cut, false);
473 \
imagesavealpha($cut, true);
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());
478 $this->imagecopymerge_alpha(
485 $overlayImage->getWidth(),
486 $overlayImage->getHeight(),
492 * `imagecopymerge` implementation with alpha support.
494 * @see http://php.net/manual/en/function.imagecopymerge.php#88456
496 * @param \GDImage $dst_im destination image resource
497 * @param \GDImage $src_im source image resource
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
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)
514 // Get image width and height
515 $w = \
imagesx($src_im);
516 $h = \
imagesy($src_im);
517 // Turn alpha blending off
518 \
imagealphablending($src_im, false);
519 // Find the most opaque pixel in the image (the one with the smallest alpha value)
521 for ($x = 0; $x < $w; $x++
) {
522 for ($y = 0; $y < $h; $y++
) {
523 $alpha = (\
imagecolorat($src_im, $x, $y) >> 24) & 0xFF;
524 if ($alpha < $minalpha) {
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!)
533 $colorxy = \
imagecolorat($src_im, $x, $y);
534 $alpha = ($colorxy >> 24) & 0xFF;
535 // calculate new alpha
536 if ($minalpha !== 127) {
537 $alpha = 127 +
127 * $pct * ($alpha - 127) / (127 - $minalpha);
539 $alpha +
= 127 * $pct;
541 // get the color index with new alpha
542 $alphacolorxy = \
imagecolorallocatealpha(
544 ($colorxy >> 16) & 0xFF,
545 ($colorxy >> 8) & 0xFF,
549 // set pixel with the new color + opacity
550 if (!\
imagesetpixel($src_im, $x, $y, $alphacolorxy)) {
556 \
imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h);
564 public function overlayImageRelative($file, $position, $margin, $opacity)
572 public function saveImageAs($image, string $filename, string $type, int $quality = 100): void
574 if (!$this->isImage($image)) {
575 throw new \
InvalidArgumentException("Given image is not a valid image resource.");
580 // fix PNG alpha channel handling
581 // see http://php.net/manual/en/function.imagecopymerge.php#92787
582 \
imagealphablending($image, false);
583 \
imagesavealpha($image, true);
592 \
imagejpeg($image, null, $quality);
596 \
imagepng($image, null, $quality);
600 \
imagepalettetotruecolor($image);
601 \
imagewebp($image, null, $quality);
605 throw new \
LogicException("Unreachable");
608 $stream = \
ob_get_contents();
611 \file_put_contents
($filename, $stream);
617 public static function isSupported()
619 return \function_exists
('gd_info');
625 public static function supportsWebp(): bool
627 return !empty(\
gd_info()['WebP Support']);