LibMagickMediaHandler.php: add support for animated webp thumbnails

This commit is contained in:
Zankaria 2025-03-28 14:52:33 +01:00
parent 37fbb35f8b
commit 096a6f04f1

View file

@ -8,7 +8,7 @@ use Vichan\Functions\{Fs, Metadata};
class LibMagickMediaHandler implements MediaHandler { class LibMagickMediaHandler implements MediaHandler {
use MediaHandlerTrait; 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_IMAGICK_VERSION = '2.3.0';
private const MIN_IMAGEMAGICK_VERSION = '6.4.0'; private const MIN_IMAGEMAGICK_VERSION = '6.4.0';
@ -17,10 +17,11 @@ class LibMagickMediaHandler implements MediaHandler {
private bool $strip_metadata; 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_width;
private int $image_max_height; private int $image_max_height;
private string $static_thumb_mime; private string $static_thumb_mime;
private ?string $animated_thumb_mime;
private static function degreesFromOrientation(int $orientation): int { 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. * @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( private function generateThumbImpl(
\Imagick $imagick, \Imagick $imagick,
string $source_file_mime, string $source_file_mime,
@ -92,13 +134,12 @@ class LibMagickMediaHandler implements MediaHandler {
int $max_width, int $max_width,
int $max_height int $max_height
) { ) {
// Special handling for gifs with multiple frames. $source_is_animated = self::mimeSupportAnimation($source_file_mime) && $imagick->getNumberImages() > 1;
if (
$source_file_mime === 'image/gif' // Special handling for animated images with multiple frames.
&& $this->frames_for_gif_thumbs !== self::THUMB_KEEP_FRAMES_NO if ($this->frames_for_animated_thumbs !== self::THUMB_KEEP_FRAMES_NO && $source_is_animated) {
&& $imagick->getNumberImages() > 1 $animated_thumb_ext = Metadata\mime_to_ext($this->animated_thumb_mime);
) { $out_path = "$preferred_out_file_basepath.$animated_thumb_ext";
$out_path = $preferred_out_file_basepath . '.gif';
if ($width > $max_width || $height > $max_height) { if ($width > $max_width || $height > $max_height) {
$thumb_width = $max_width; $thumb_width = $max_width;
@ -109,14 +150,15 @@ class LibMagickMediaHandler implements MediaHandler {
} }
// By now $this->frames_for_gif_thumbs !== 0. // 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) { if ($this->frames_for_animated_thumbs !== self::THUMB_KEEP_FRAMES_ALL) {
if ($step > 1) {
// Reduce the number of frames. // Reduce the number of frames.
$other = new \Imagick(); $other = new \Imagick();
try { try {
$other->setFormat('gif'); $other->setFormat($animated_thumb_ext);
for ($i = 0, $j = 0; $i < $imagick->getNumberImages(); $i += $step, $j++) { for ($i = 0, $j = 0; $i < $imagick->getNumberImages(); $i += $step, $j++) {
$imagick->setIteratorIndex($i); $imagick->setIteratorIndex($i);
@ -132,49 +174,47 @@ class LibMagickMediaHandler implements MediaHandler {
$other->optimizeImageLayers(); $other->optimizeImageLayers();
$other->setImageCompressionQuality(70); $other->setImageCompressionQuality(70);
$other->writeImage("gif:$out_path"); $other->writeImages("$animated_thumb_ext:$out_path", true);
} finally { } finally {
$other->clear(); $other->clear();
} }
} else {
// Only a single frame would be left, save it as a single image.
$imagick->setIteratorIndex(0);
return $this->generateThumbImplSingleFrame(
$imagick,
$preferred_out_file_basepath,
$width,
$height,
$max_width,
$max_height
);
}
} else { } else {
// Just try to optimize it a little. // Just try to optimize it a little.
$imagick->stripImage(); $imagick->stripImage();
$imagick->optimizeImageLayers(); $imagick->optimizeImageLayers();
$imagick->setFormat($animated_thumb_ext);
$imagick->setImageCompressionQuality(70); $imagick->setImageCompressionQuality(70);
$imagick->writeImage("gif:$out_path"); $imagick->writeImages("$animated_thumb_ext:$out_path", true);
} }
return new ThumbGenerationResult( return new ThumbGenerationResult(
$out_path, $out_path,
'image/gif', $this->animated_thumb_mime,
$thumb_width, $thumb_width,
$thumb_height $thumb_height
); );
} else { } else {
if ($width > $max_width || $height > $max_height) { return $this->generateThumbImplSingleFrame(
$thumb_width = $max_width; $imagick,
$thumb_height = $max_height; $preferred_out_file_basepath,
$width,
// Unreliable behavior on some versions if the target width/height are under the limit? $height,
$imagick->thumbnailImage($max_width, $max_height, true); $max_width,
} else { $max_height
$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
); );
} }
} }
@ -184,22 +224,46 @@ class LibMagickMediaHandler implements MediaHandler {
* @return bool * @return bool
*/ */
public static function checkImagickVersion(): bool { public static function checkImagickVersion(): bool {
static $version_ok = null;
if ($version_ok === null) {
$version_ok = false;
$imagick_ver = \phpversion('imagick'); $imagick_ver = \phpversion('imagick');
if ($imagick_ver !== false && \version_compare($imagick_ver, self::MIN_IMAGICK_VERSION, '>=')) { if ($imagick_ver !== false && \version_compare($imagick_ver, self::MIN_IMAGICK_VERSION, '>=')) {
$str = \Imagick::getVersion()['versionString']; $str = \Imagick::getVersion()['versionString'];
if (\preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $str, $matches)) { if (\preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $str, $matches)) {
return \version_compare($matches[1], self::MIN_IMAGEMAGICK_VERSION, '>='); $version_ok = \version_compare($matches[1], self::MIN_IMAGEMAGICK_VERSION, '>=');
} }
} }
return false;
} }
public function __construct(bool $strip_metadata, int $frames_for_gif_thumbs, int $max_width, int $max_height, string $static_thumb_mime) { return $version_ok;
}
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->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_width = $max_width;
$this->image_max_height = $max_height; $this->image_max_height = $max_height;
$this->static_thumb_mime = $static_thumb_mime; $this->static_thumb_mime = $static_thumb_mime;
$this->animated_thumb_mime = $animated_thumb_mime;
} }
public function supportsMime(string $mime): bool { public function supportsMime(string $mime): bool {
@ -324,7 +388,7 @@ class LibMagickMediaHandler implements MediaHandler {
} }
$out_ext = Metadata\mime_to_ext($media_file_mime); $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"); $imagick->writeImage("$out_ext:$out_path");
$thumb = self::generateThumbImpl( $thumb = self::generateThumbImpl(