From 096a6f04f1fbe79d12f8211a08cdf7969aaecd6b Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 28 Mar 2025 14:52:33 +0100 Subject: [PATCH] LibMagickMediaHandler.php: add support for animated webp thumbnails --- inc/Service/Media/LibMagickMediaHandler.php | 188 +++++++++++++------- 1 file changed, 126 insertions(+), 62 deletions(-) diff --git a/inc/Service/Media/LibMagickMediaHandler.php b/inc/Service/Media/LibMagickMediaHandler.php index 3514783b..51cd3b1f 100644 --- a/inc/Service/Media/LibMagickMediaHandler.php +++ b/inc/Service/Media/LibMagickMediaHandler.php @@ -8,7 +8,7 @@ use Vichan\Functions\{Fs, Metadata}; class LibMagickMediaHandler implements MediaHandler { use MediaHandlerTrait; - // getImageAlphaChannel requires Imagick >= 2.3.0 and ImageMagick >= 6.4.0 + // getImageAlphaChannel and writeImages requires Imagick >= 2.3.0 and ImageMagick >= 6.4.0 private const MIN_IMAGICK_VERSION = '2.3.0'; private const MIN_IMAGEMAGICK_VERSION = '6.4.0'; @@ -17,10 +17,11 @@ class LibMagickMediaHandler implements MediaHandler { private bool $strip_metadata; - private bool $frames_for_gif_thumbs; + private bool $frames_for_animated_thumbs; private int $image_max_width; private int $image_max_height; private string $static_thumb_mime; + private ?string $animated_thumb_mime; private static function degreesFromOrientation(int $orientation): int { @@ -65,6 +66,13 @@ class LibMagickMediaHandler implements MediaHandler { } } + /** + * If the mime type supports animations. + */ + private static function mimeSupportAnimation(string $mime) { + return $mime === 'image/gif' || $mime === 'image/webp'; + } + /** * @return bool Returns if width and height were swapped. */ @@ -83,6 +91,40 @@ class LibMagickMediaHandler implements MediaHandler { } } + private function generateThumbImplSingleFrame( + \Imagick $imagick, + string $preferred_out_file_basepath, + int $width, + int $height, + int $max_width, + int $max_height + ) { + if ($width > $max_width || $height > $max_height) { + $thumb_width = $max_width; + $thumb_height = $max_height; + + // Unreliable behavior on some versions if the target width/height are under the limit? + $imagick->thumbnailImage($max_width, $max_height, true); + } else { + $thumb_width = $width; + $thumb_height = $height; + } + + $out_ext = Metadata\mime_to_ext($this->static_thumb_mime); + $out_path = "$preferred_out_file_basepath.$out_ext"; + + $imagick->stripImage(); + $imagick->setImageCompressionQuality(70); + $imagick->writeImage("$out_ext:$out_path"); + + return new ThumbGenerationResult( + $out_path, + $this->static_thumb_mime, + $thumb_width, + $thumb_height + ); + } + private function generateThumbImpl( \Imagick $imagick, string $source_file_mime, @@ -92,13 +134,12 @@ class LibMagickMediaHandler implements MediaHandler { int $max_width, int $max_height ) { - // Special handling for gifs with multiple frames. - if ( - $source_file_mime === 'image/gif' - && $this->frames_for_gif_thumbs !== self::THUMB_KEEP_FRAMES_NO - && $imagick->getNumberImages() > 1 - ) { - $out_path = $preferred_out_file_basepath . '.gif'; + $source_is_animated = self::mimeSupportAnimation($source_file_mime) && $imagick->getNumberImages() > 1; + + // Special handling for animated images with multiple frames. + if ($this->frames_for_animated_thumbs !== self::THUMB_KEEP_FRAMES_NO && $source_is_animated) { + $animated_thumb_ext = Metadata\mime_to_ext($this->animated_thumb_mime); + $out_path = "$preferred_out_file_basepath.$animated_thumb_ext"; if ($width > $max_width || $height > $max_height) { $thumb_width = $max_width; @@ -109,72 +150,71 @@ class LibMagickMediaHandler implements MediaHandler { } // By now $this->frames_for_gif_thumbs !== 0. - $step = \floor($imagick->getNumberImages() / $this->frames_for_gif_thumbs); + $step = \floor($imagick->getNumberImages() / $this->frames_for_animated_thumbs); - if ($this->frames_for_gif_thumbs !== self::THUMB_KEEP_FRAMES_ALL && $step > 1) { - // Reduce the number of frames. + if ($this->frames_for_animated_thumbs !== self::THUMB_KEEP_FRAMES_ALL) { + if ($step > 1) { + // Reduce the number of frames. - $other = new \Imagick(); - try { - $other->setFormat('gif'); + $other = new \Imagick(); + try { + $other->setFormat($animated_thumb_ext); - for ($i = 0, $j = 0; $i < $imagick->getNumberImages(); $i += $step, $j++) { - $imagick->setIteratorIndex($i); - $delay = $imagick->getImageDelay(); + for ($i = 0, $j = 0; $i < $imagick->getNumberImages(); $i += $step, $j++) { + $imagick->setIteratorIndex($i); + $delay = $imagick->getImageDelay(); - $imagick->sampleImage($thumb_width, $thumb_height); - $imagick->setImagePage($thumb_width, $thumb_height, 0, 0); - $imagick->setImageDelay($delay); + $imagick->sampleImage($thumb_width, $thumb_height); + $imagick->setImagePage($thumb_width, $thumb_height, 0, 0); + $imagick->setImageDelay($delay); - $other->addImage($imagick->getImage()); + $other->addImage($imagick->getImage()); + } + + $other->optimizeImageLayers(); + + $other->setImageCompressionQuality(70); + $other->writeImages("$animated_thumb_ext:$out_path", true); + } finally { + $other->clear(); } + } else { + // Only a single frame would be left, save it as a single image. + $imagick->setIteratorIndex(0); - $other->optimizeImageLayers(); - - $other->setImageCompressionQuality(70); - $other->writeImage("gif:$out_path"); - } finally { - $other->clear(); + return $this->generateThumbImplSingleFrame( + $imagick, + $preferred_out_file_basepath, + $width, + $height, + $max_width, + $max_height + ); } } else { // Just try to optimize it a little. $imagick->stripImage(); $imagick->optimizeImageLayers(); + $imagick->setFormat($animated_thumb_ext); $imagick->setImageCompressionQuality(70); - $imagick->writeImage("gif:$out_path"); + $imagick->writeImages("$animated_thumb_ext:$out_path", true); } return new ThumbGenerationResult( $out_path, - 'image/gif', + $this->animated_thumb_mime, $thumb_width, $thumb_height ); } else { - if ($width > $max_width || $height > $max_height) { - $thumb_width = $max_width; - $thumb_height = $max_height; - - // Unreliable behavior on some versions if the target width/height are under the limit? - $imagick->thumbnailImage($max_width, $max_height, true); - } else { - $thumb_width = $width; - $thumb_height = $height; - } - - $out_ext = Metadata\mime_to_ext($this->static_thumb_mime); - $out_path = $preferred_out_file_basepath . '.' . $out_ext; - - $imagick->stripImage(); - $imagick->setImageCompressionQuality(70); - $imagick->writeImage("$out_ext:$out_path"); - - return new ThumbGenerationResult( - $out_path, - $this->static_thumb_mime, - $thumb_width, - $thumb_height + return $this->generateThumbImplSingleFrame( + $imagick, + $preferred_out_file_basepath, + $width, + $height, + $max_width, + $max_height ); } } @@ -184,22 +224,46 @@ class LibMagickMediaHandler implements MediaHandler { * @return bool */ public static function checkImagickVersion(): bool { - $imagick_ver = \phpversion('imagick'); - if ($imagick_ver !== false && \version_compare($imagick_ver, self::MIN_IMAGICK_VERSION, '>=')) { - $str = \Imagick::getVersion()['versionString']; - if (\preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $str, $matches)) { - return \version_compare($matches[1], self::MIN_IMAGEMAGICK_VERSION, '>='); + static $version_ok = null; + + if ($version_ok === null) { + $version_ok = false; + $imagick_ver = \phpversion('imagick'); + if ($imagick_ver !== false && \version_compare($imagick_ver, self::MIN_IMAGICK_VERSION, '>=')) { + $str = \Imagick::getVersion()['versionString']; + if (\preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $str, $matches)) { + $version_ok = \version_compare($matches[1], self::MIN_IMAGEMAGICK_VERSION, '>='); + } } } - return false; + + return $version_ok; } - public function __construct(bool $strip_metadata, int $frames_for_gif_thumbs, int $max_width, int $max_height, string $static_thumb_mime) { + public function __construct( + bool $strip_metadata, + int $frames_for_animated_thumbs, + int $max_width, + int $max_height, + string $static_thumb_mime, + ?string $animated_thumb_mime + ) { + if (!self::checkImagickVersion()) { + throw new MediaException('Imagick extension or ImageMagick are not available or too old', MediaException::ERR_BAD_HANDLER); + } + if ($frames_for_animated_thumbs !== self::THUMB_KEEP_FRAMES_NO && $animated_thumb_mime === null) { + throw new MediaException('Missing mime type for animated thumbnails', MediaException::ERR_BAD_MEDIA_TYPE); + } + if ($animated_thumb_mime !== null && !self::mimeSupportAnimation($animated_thumb_mime)) { + throw new MediaException("$animated_thumb_mime does not support animations", MediaException::ERR_BAD_MEDIA_TYPE); + } + $this->strip_metadata = $strip_metadata; - $this->frames_for_gif_thumbs = $frames_for_gif_thumbs; + $this->frames_for_animated_thumbs = $frames_for_animated_thumbs; $this->image_max_width = $max_width; $this->image_max_height = $max_height; $this->static_thumb_mime = $static_thumb_mime; + $this->animated_thumb_mime = $animated_thumb_mime; } public function supportsMime(string $mime): bool { @@ -324,7 +388,7 @@ class LibMagickMediaHandler implements MediaHandler { } $out_ext = Metadata\mime_to_ext($media_file_mime); - $out_path = $media_preferred_out_file_basepath . '.' . $out_ext; + $out_path = "$media_preferred_out_file_basepath.$out_ext"; $imagick->writeImage("$out_ext:$out_path"); $thumb = self::generateThumbImpl(