*/ class GDImageAdapter implements IImageAdapter, IWebpImageAdapter { /** * active color * @var int */ protected $color; /** * red, green, blue data of the active color * @var array */ protected $colorData = []; /** * image height * @var int */ protected $height = 0; /** * loaded image * @var \GDImage */ protected $image; /** * image type * @var int */ protected $type = 0; /** * image width * @var int */ protected $width = 0; /** * GDImageAdapter constructor. */ public function __construct() { // suppress warnings like "recoverable error: Invalid SOS parameters for sequential JPEG" @\ini_set('gd.jpeg_ignore_warning', '1'); } /** * Returns whether the given image is a valid GD resource / GD object * * @return bool */ public function isImage($image) { return (\is_resource($image) && \get_resource_type($image) === 'gd') || (\is_object($image) && $image instanceof \GdImage); } /** * @inheritDoc */ public function load($image, $type = '') { if (!$this->isImage($image)) { throw new SystemException("Image resource is invalid."); } if (empty($type)) { throw new SystemException("Image type is missing."); } $this->image = $image; $this->type = $type; $this->height = \imagesy($this->image); $this->width = \imagesx($this->image); } /** * @inheritDoc */ public function loadFile($file) { [$this->width, $this->height, $this->type] = \getimagesize($file); switch ($this->type) { case \IMAGETYPE_GIF: // suppress warnings and properly handle errors $this->image = @\imagecreatefromgif($file); if ($this->image === false) { throw new SystemException("Could not read gif image '" . $file . "'."); } break; case \IMAGETYPE_JPEG: // suppress warnings and properly handle errors $this->image = @\imagecreatefromjpeg($file); if ($this->image === false) { throw new SystemException("Could not read jpeg image '" . $file . "'."); } break; case \IMAGETYPE_PNG: // suppress warnings and properly handle errors $this->image = @\imagecreatefrompng($file); if ($this->image === false) { throw new SystemException("Could not read png image '" . $file . "'."); } break; case \IMAGETYPE_WEBP: // suppress warnings and properly handle errors $this->image = @\imagecreatefromwebp($file); if ($this->image === false) { throw new SystemException("Could not read webp image '" . $file . "'."); } break; default: throw new SystemException("Could not read image '" . $file . "', format is not recognized."); } } /** * @inheritDoc */ public function createEmptyImage($width, $height) { $this->image = \imagecreate($width, $height); $this->type = \IMAGETYPE_PNG; $this->setColor(0xFF, 0xFF, 0xFF); $this->color = null; $this->width = $width; $this->height = $height; } /** * @inheritDoc */ public function createThumbnail($maxWidth, $maxHeight, $preserveAspectRatio = true) { $x = $y = 0; $sourceWidth = $this->width; $sourceHeight = $this->height; if ($preserveAspectRatio) { if ($maxWidth / $this->width < $maxHeight / $this->height) { $width = $maxWidth; $height = \round($this->height * ($width / $this->width)); } else { $height = $maxHeight; $width = \round($this->width * ($height / $this->height)); } } else { $width = $maxWidth; $height = $maxHeight; if ($maxWidth / $this->width < $maxHeight / $this->height) { $cut = (($sourceWidth * ($maxHeight / $this->height)) - $maxWidth) / ($maxHeight / $this->height); $x = \ceil($cut / 2); $sourceWidth = $sourceWidth - $x * 2; } else { $cut = (($sourceHeight * ($maxWidth / $this->width)) - $maxHeight) / ($maxWidth / $this->width); $y = \ceil($cut / 2); $sourceHeight = $sourceHeight - $y * 2; } } // resize image $image = \imagecreatetruecolor((int)$width, (int)$height); \imagealphablending($image, false); \imagecopyresampled( $image, $this->image, 0, 0, (int)$x, (int)$y, (int)$width, (int)$height, (int)$sourceWidth, (int)$sourceHeight ); \imagesavealpha($image, true); return $image; } /** * @inheritDoc */ public function clip($originX, $originY, $width, $height) { $image = \imagecreatetruecolor($width, $height); \imagealphablending($image, false); \imagecopy($image, $this->image, 0, 0, (int)$originX, (int)$originY, (int)$width, (int)$height); \imagesavealpha($image, true); // reload image to update image resource, width and height $this->load($image, $this->type); } /** * @inheritDoc */ public function resize($originX, $originY, $originWidth, $originHeight, $targetWidth = 0, $targetHeight = 0) { $image = \imagecreatetruecolor($targetWidth, $targetHeight); \imagealphablending($image, false); \imagecopyresampled( $image, $this->image, 0, 0, (int)$originX, (int)$originY, (int)$targetWidth, (int)$targetHeight, (int)$originWidth, (int)$originHeight ); \imagesavealpha($image, true); // reload image to update image resource, width and height $this->load($image, $this->type); } /** * @inheritDoc */ public function drawRectangle($startX, $startY, $endX, $endY) { \imagefilledrectangle($this->image, $startX, $startY, $endX, $endY, $this->color); } /** * @inheritDoc */ public function drawText($text, $x, $y, $font, $size, $opacity = 1.0) { // set opacity $color = \imagecolorallocatealpha( $this->image, $this->colorData['red'], $this->colorData['green'], $this->colorData['blue'], \round((1 - $opacity) * 127) ); // draw text \imagettftext($this->image, $size, 0, $x, $y, $color, $font, $text); } /** * @inheritDoc */ public function drawTextRelative($text, $position, $margin, $offsetX, $offsetY, $font, $size, $opacity = 1.0) { // split text into multiple lines $lines = \explode("\n", StringUtil::unifyNewlines($text)); // calc text width, height and first line height $box = \imagettfbbox($size, 0, $font, $text); $firstLineBox = \imagettfbbox($size, 0, $font, $lines[0]); $textWidth = \abs($box[0] - $box[2]); $textHeight = \abs($box[7] - $box[1]); $firstLineHeight = \abs($firstLineBox[7] - $firstLineBox[1]); // calculate x coordinate $x = 0; switch ($position) { case 'topLeft': case 'middleLeft': case 'bottomLeft': $x = $margin; break; case 'topCenter': case 'middleCenter': case 'bottomCenter': $x = \floor(($this->getWidth() - $textWidth) / 2); break; case 'topRight': case 'middleRight': case 'bottomRight': $x = $this->getWidth() - $textWidth - $margin; break; } // calculate y coordinate $y = 0; switch ($position) { case 'topLeft': case 'topCenter': case 'topRight': $y = $margin + $firstLineHeight; break; case 'middleLeft': case 'middleCenter': case 'middleRight': $y = \floor(($this->getHeight() - $textHeight) / 2) + $firstLineHeight; break; case 'bottomLeft': case 'bottomCenter': case 'bottomRight': $y = $this->getHeight() - $textHeight + $firstLineHeight - $margin; break; } $this->drawText($text, $x + $offsetX, $y + $offsetY, $font, $size, $opacity); } /** * @inheritDoc */ public function textFitsImage($text, $margin, $font, $size) { $box = \imagettfbbox($size, 0, $font, $text); $textWidth = \abs($box[0] - $box[2]); $textHeight = \abs($box[7] - $box[1]); return $textWidth + 2 * $margin <= $this->getWidth() && $textHeight + 2 * $margin <= $this->getHeight(); } /** * @inheritDoc */ public function adjustFontSize($text, $margin, $font, $size) { // does nothing } /** * @inheritDoc */ public function setColor($red, $green, $blue) { $this->color = \imagecolorallocate($this->image, $red, $green, $blue); // save data of the color $this->colorData = [ 'red' => $red, 'green' => $green, 'blue' => $blue, ]; } /** * @inheritDoc */ public function hasColor() { return $this->color !== null; } /** * @inheritDoc */ public function setTransparentColor($red, $green, $blue) { if ($this->type == \IMAGETYPE_PNG) { $color = \imagecolorallocate($this->image, $red, $green, $blue); \imagecolortransparent($this->image, $color); } } /** * @inheritDoc */ public function writeImage($image, $filename) { if (!$this->isImage($image)) { throw new SystemException("Given image is not a valid image resource."); } \ob_start(); // fix PNG alpha channel handling // see http://php.net/manual/en/function.imagecopymerge.php#92787 \imagealphablending($image, false); \imagesavealpha($image, true); if ($this->type == \IMAGETYPE_GIF) { \imagegif($image); } elseif ($this->type == \IMAGETYPE_PNG) { \imagepng($image); } elseif ($this->type == \IMAGETYPE_WEBP) { \imagepalettetotruecolor($image); \imagewebp($image); } elseif (\function_exists('imageJPEG')) { \imagejpeg($image, null, 90); } $stream = \ob_get_contents(); \ob_end_clean(); \file_put_contents($filename, $stream); } /** * @inheritDoc */ public function getWidth() { return $this->width; } /** * @inheritDoc */ public function getHeight() { return $this->height; } /** * @inheritDoc */ public function getType() { return $this->type; } /** * @inheritDoc */ public function getImage() { return $this->image; } /** * @inheritDoc */ public function rotate($degrees) { // imagerotate interpretes degrees as counter-clockwise return \imagerotate($this->image, 360.0 - $degrees, ($this->color ?: 0)); } /** * @inheritDoc */ public function overlayImage($file, $x, $y, $opacity) { $overlayImage = new self(); $overlayImage->loadFile($file); // fix PNG alpha channel handling // see http://php.net/manual/en/function.imagecopymerge.php#92787 $cut = \imagecreatetruecolor($overlayImage->getWidth(), $overlayImage->getHeight()); \imagealphablending($cut, false); \imagesavealpha($cut, true); \imagecopy($cut, $this->image, 0, 0, $x, $y, $overlayImage->getWidth(), $overlayImage->getHeight()); \imagecopy($cut, $overlayImage->image, 0, 0, 0, 0, $overlayImage->getWidth(), $overlayImage->getHeight()); $this->imagecopymerge_alpha( $this->image, $cut, $x, $y, 0, 0, $overlayImage->getWidth(), $overlayImage->getHeight(), $opacity * 100 ); } /** * `imagecopymerge` implementation with alpha support. * * @see http://php.net/manual/en/function.imagecopymerge.php#88456 * * @param \GDImage $dst_im destination image resource * @param \GDImage $src_im source image resource * @param int $dst_x x-coordinate of destination point * @param int $dst_y y-coordinate of destination point * @param int $src_x x-coordinate of source point * @param int $src_y y-coordinate of source point * @param int $src_w source width * @param int $src_h source height * @param int $pct opacity percent * @return bool */ // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) { if (!isset($pct)) { return false; } $pct /= 100; // Get image width and height $w = \imagesx($src_im); $h = \imagesy($src_im); // Turn alpha blending off \imagealphablending($src_im, false); // Find the most opaque pixel in the image (the one with the smallest alpha value) $minalpha = 127; for ($x = 0; $x < $w; $x++) { for ($y = 0; $y < $h; $y++) { $alpha = (\imagecolorat($src_im, $x, $y) >> 24) & 0xFF; if ($alpha < $minalpha) { $minalpha = $alpha; } } } // loop through image pixels and modify alpha for each for ($x = 0; $x < $w; $x++) { for ($y = 0; $y < $h; $y++) { // get current alpha value (represents the TANSPARENCY!) $colorxy = \imagecolorat($src_im, $x, $y); $alpha = ($colorxy >> 24) & 0xFF; // calculate new alpha if ($minalpha !== 127) { $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minalpha); } else { $alpha += 127 * $pct; } // get the color index with new alpha $alphacolorxy = \imagecolorallocatealpha( $src_im, ($colorxy >> 16) & 0xFF, ($colorxy >> 8) & 0xFF, $colorxy & 0xFF, \round($alpha) ); // set pixel with the new color + opacity if (!\imagesetpixel($src_im, $x, $y, $alphacolorxy)) { return false; } } } // The image copy \imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); return true; } /** * @inheritDoc */ public function overlayImageRelative($file, $position, $margin, $opacity) { // does nothing } /** * @inheritDoc */ public function saveImageAs($image, string $filename, string $type, int $quality = 100): void { if (!$this->isImage($image)) { throw new \InvalidArgumentException("Given image is not a valid image resource."); } \ob_start(); // fix PNG alpha channel handling // see http://php.net/manual/en/function.imagecopymerge.php#92787 \imagealphablending($image, false); \imagesavealpha($image, true); switch ($type) { case "gif": \imagegif($image); break; case "jpg": case "jpeg": \imagejpeg($image, null, $quality); break; case "png": \imagepng($image, null, $quality); break; case "webp": \imagepalettetotruecolor($image); \imagewebp($image, null, $quality); break; default: throw new \LogicException("Unreachable"); } $stream = \ob_get_contents(); \ob_end_clean(); \file_put_contents($filename, $stream); } /** * @inheritDoc */ public static function isSupported() { return \function_exists('gd_info'); } /** * @inheritDoc */ public static function supportsWebp(): bool { return !empty(\gd_info()['WebP Support']); } }