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 {
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(