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.
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;
use wcf\util\JSON;
use wcf\util\StringUtil;
+use function wcf\functions\exception\logThrowable;
+
/**
* @author Alexander Ebert
* @copyright 2001-2024 WoltLab GmbH
$imageAdapter = ImageHandler::getInstance()->getAdapter();
try {
- $imageAdapter->loadFile($file->getPathname());
+ $imageAdapter->loadSingleFrameFromFile($file->getPathname());
} catch (SystemException) {
throw new DamagedImage($file->fileID);
}
$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);
*/
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,
);
}
}
--- /dev/null
+<?php
+
+namespace wcf\system\image\adapter;
+
+/**
+ * Basic interface for all image adapters.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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;
+}
namespace wcf\system\image\adapter;
use wcf\system\exception\SystemException;
+use wcf\system\image\adapter\exception\ImageNotReadable;
use wcf\util\FileUtil;
/**
* @copyright 2001-2019 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*/
-class ImageAdapter implements IImageAdapter, IMemoryAwareImageAdapter
+class ImageAdapter implements IImageAdapter, IMemoryAwareImageAdapter, ISingleFrameImageAdapter
{
/**
* IImageAdapter object
$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
*/
use wcf\system\event\EventHandler;
use wcf\system\exception\SystemException;
+use wcf\system\image\adapter\exception\ImageNotProcessable;
use wcf\util\StringUtil;
/**
* @copyright 2001-2019 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*/
-class ImagickImageAdapter implements IImageAdapter, IWebpImageAdapter
+class ImagickImageAdapter implements IImageAdapter, ISingleFrameImageAdapter, IWebpImageAdapter
{
/**
* active color
$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.
*/
--- /dev/null
+<?php
+
+namespace wcf\system\image\adapter\exception;
+
+/**
+ * The target image cannot be processed by the image adapter for an unspecified
+ * reason. This could be the cause of missing codecs or damaged files.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\image\adapter\exception;
+
+/**
+ * The target image does not exist or cannot be read.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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);
+ }
+}