From e8685e2def01faaa5a6d5c63edec837dda033a44 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 31 Jul 2024 16:06:43 +0200 Subject: [PATCH] Add a new helper method to generate static thumbnails Processing animated images is an unpredictable process that is not only incredibly expensive but due to unknown resource limits very unstable. Using static thumbnails that simply pick the first frame solves this issue by offering a meaningful thumbnail without using lots of resources. --- .../file/processor/FileProcessor.class.php | 21 ++++++++++++++----- .../exception/DamagedImage.class.php | 7 +++++-- .../ISingleFrameImageAdapter.class.php | 21 +++++++++++++++++++ .../image/adapter/ImageAdapter.class.php | 19 ++++++++++++++++- .../adapter/ImagickImageAdapter.class.php | 16 +++++++++++++- .../exception/ImageNotProcessable.class.php | 21 +++++++++++++++++++ .../exception/ImageNotReadable.class.php | 20 ++++++++++++++++++ 7 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/image/adapter/ISingleFrameImageAdapter.class.php create mode 100644 wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotProcessable.class.php create mode 100644 wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotReadable.class.php diff --git a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php index 5b6459d5f9..1374d829fb 100644 --- a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php @@ -12,6 +12,8 @@ use wcf\data\object\type\ObjectTypeCache; use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\exception\SystemException; use wcf\system\file\processor\exception\DamagedImage; +use wcf\system\image\adapter\exception\ImageNotProcessable; +use wcf\system\image\adapter\exception\ImageNotReadable; use wcf\system\image\adapter\ImageAdapter; use wcf\system\image\ImageHandler; use wcf\system\SingletonFactory; @@ -20,6 +22,8 @@ use wcf\util\FileUtil; use wcf\util\JSON; use wcf\util\StringUtil; +use function wcf\functions\exception\logThrowable; + /** * @author Alexander Ebert * @copyright 2001-2024 WoltLab GmbH @@ -138,7 +142,7 @@ final class FileProcessor extends SingletonFactory $imageAdapter = ImageHandler::getInstance()->getAdapter(); try { - $imageAdapter->loadFile($file->getPathname()); + $imageAdapter->loadSingleFrameFromFile($file->getPathname()); } catch (SystemException) { throw new DamagedImage($file->fileID); } @@ -214,14 +218,21 @@ final class FileProcessor extends SingletonFactory $imageAdapter = ImageHandler::getInstance()->getAdapter(); try { - $imageAdapter->loadFile($file->getPathname()); - } catch (SystemException) { - throw new DamagedImage($file->fileID); + $imageAdapter->loadSingleFrameFromFile($file->getPathname()); + } catch (ImageNotReadable | ImageNotProcessable $e) { + throw new DamagedImage($file->fileID, $e); } } \assert($imageAdapter instanceof ImageAdapter); - $image = $imageAdapter->createThumbnail($format->width, $format->height, $format->retainDimensions); + + try { + $image = $imageAdapter->createThumbnail($format->width, $format->height, $format->retainDimensions); + } catch (\Throwable $e) { + logThrowable($e); + + continue; + } $filename = FileUtil::getTemporaryFilename(extension: 'webp'); $imageAdapter->saveImageAs($image, $filename, 'webp', 80); diff --git a/wcfsetup/install/files/lib/system/file/processor/exception/DamagedImage.class.php b/wcfsetup/install/files/lib/system/file/processor/exception/DamagedImage.class.php index 5b69754337..55ff8e1eef 100644 --- a/wcfsetup/install/files/lib/system/file/processor/exception/DamagedImage.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/exception/DamagedImage.class.php @@ -10,13 +10,16 @@ namespace wcf\system\file\processor\exception; */ final class DamagedImage extends \Exception { - public function __construct(public readonly int $fileID) - { + public function __construct( + public readonly int $fileID, + ?\Throwable $previous = null + ) { parent::__construct( \sprintf( "The file '%d' is a damaged image.", $this->fileID, ), + previous: $previous, ); } } diff --git a/wcfsetup/install/files/lib/system/image/adapter/ISingleFrameImageAdapter.class.php b/wcfsetup/install/files/lib/system/image/adapter/ISingleFrameImageAdapter.class.php new file mode 100644 index 0000000000..70b6364996 --- /dev/null +++ b/wcfsetup/install/files/lib/system/image/adapter/ISingleFrameImageAdapter.class.php @@ -0,0 +1,21 @@ + + * @since 6.1 + */ +interface ISingleFrameImageAdapter +{ + /** + * Loads the file by evaluating the first frame only. This is important for + * adapters that are aware of multiframe images and parse each of them on + * startup. + */ + public function loadSingleFrameFromFile(string $filename): void; +} diff --git a/wcfsetup/install/files/lib/system/image/adapter/ImageAdapter.class.php b/wcfsetup/install/files/lib/system/image/adapter/ImageAdapter.class.php index 9420f2d51a..b4f0dad389 100644 --- a/wcfsetup/install/files/lib/system/image/adapter/ImageAdapter.class.php +++ b/wcfsetup/install/files/lib/system/image/adapter/ImageAdapter.class.php @@ -3,6 +3,7 @@ namespace wcf\system\image\adapter; use wcf\system\exception\SystemException; +use wcf\system\image\adapter\exception\ImageNotReadable; use wcf\util\FileUtil; /** @@ -12,7 +13,7 @@ use wcf\util\FileUtil; * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License */ -class ImageAdapter implements IImageAdapter, IMemoryAwareImageAdapter +class ImageAdapter implements IImageAdapter, IMemoryAwareImageAdapter, ISingleFrameImageAdapter { /** * IImageAdapter object @@ -66,6 +67,22 @@ class ImageAdapter implements IImageAdapter, IMemoryAwareImageAdapter $this->adapter->loadFile($file); } + #[\Override] + public function loadSingleFrameFromFile(string $filename): void + { + if (!($this->adapter instanceof ISingleFrameImageAdapter)) { + $this->adapter->loadFile($filename); + + return; + } + + if (!\file_exists($filename) || !\is_readable($filename)) { + throw new ImageNotReadable($filename); + } + + $this->adapter->loadSingleFrameFromFile($filename); + } + /** * @inheritDoc */ diff --git a/wcfsetup/install/files/lib/system/image/adapter/ImagickImageAdapter.class.php b/wcfsetup/install/files/lib/system/image/adapter/ImagickImageAdapter.class.php index c5b5adc391..4c057e6bc0 100644 --- a/wcfsetup/install/files/lib/system/image/adapter/ImagickImageAdapter.class.php +++ b/wcfsetup/install/files/lib/system/image/adapter/ImagickImageAdapter.class.php @@ -4,6 +4,7 @@ namespace wcf\system\image\adapter; use wcf\system\event\EventHandler; use wcf\system\exception\SystemException; +use wcf\system\image\adapter\exception\ImageNotProcessable; use wcf\util\StringUtil; /** @@ -13,7 +14,7 @@ use wcf\util\StringUtil; * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License */ -class ImagickImageAdapter implements IImageAdapter, IWebpImageAdapter +class ImagickImageAdapter implements IImageAdapter, ISingleFrameImageAdapter, IWebpImageAdapter { /** * active color @@ -92,6 +93,19 @@ class ImagickImageAdapter implements IImageAdapter, IWebpImageAdapter $this->readImageDimensions(); } + #[\Override] + public function loadSingleFrameFromFile(string $filename): void + { + try { + $this->imagick->clear(); + $this->imagick->readImage($filename . '[0]'); + } catch (\ImagickException $e) { + throw new ImageNotProcessable($filename, $e); + } + + $this->readImageDimensions(); + } + /** * Reads width and height of the image. */ diff --git a/wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotProcessable.class.php b/wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotProcessable.class.php new file mode 100644 index 0000000000..99eb4f7569 --- /dev/null +++ b/wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotProcessable.class.php @@ -0,0 +1,21 @@ + + * @since 6.1 + */ +final class ImageNotProcessable extends \Exception +{ + #[\Override] + public function __construct(string $filename, ?\Throwable $previous = null) + { + parent::__construct("The image '{$filename}' cannot be processed.", previous: $previous); + } +} diff --git a/wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotReadable.class.php b/wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotReadable.class.php new file mode 100644 index 0000000000..1e580a4dc9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotReadable.class.php @@ -0,0 +1,20 @@ + + * @since 6.1 + */ +final class ImageNotReadable extends \Exception +{ + #[\Override] + public function __construct(string $filename, ?\Throwable $previous = null) + { + parent::__construct("The image '{$filename}' does not exist or cannot be accessed.", previous: $previous); + } +} -- 2.20.1