Add a new helper method to generate static thumbnails
authorAlexander Ebert <ebert@woltlab.com>
Wed, 31 Jul 2024 14:06:43 +0000 (16:06 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 31 Jul 2024 14:06:43 +0000 (16:06 +0200)
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.

wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php
wcfsetup/install/files/lib/system/file/processor/exception/DamagedImage.class.php
wcfsetup/install/files/lib/system/image/adapter/ISingleFrameImageAdapter.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/image/adapter/ImageAdapter.class.php
wcfsetup/install/files/lib/system/image/adapter/ImagickImageAdapter.class.php
wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotProcessable.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/image/adapter/exception/ImageNotReadable.class.php [new file with mode: 0644]

index 5b6459d5f98f0cd0ea22945d1d00a4059179fea0..1374d829fb561baae02e3ac5b5947c11d8ab18f7 100644 (file)
@@ -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);
index 5b69754337bba5ff475b918016343327cc6427b3..55ff8e1eef8b2db0978a5d40b0ee4ecdc62a88de 100644 (file)
@@ -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 (file)
index 0000000..70b6364
--- /dev/null
@@ -0,0 +1,21 @@
+<?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;
+}
index 9420f2d51a7afcbbef95522dc6842998d150f946..b4f0dad3891319aa60f6ea8d678d93a5ad4101bd 100644 (file)
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
  */
-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
      */
index c5b5adc39185f79d9b36b6d624e0736bb8fe65d6..4c057e6bc0a6083090a5588b13c9f9710c0b81f5 100644 (file)
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
  */
-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 (file)
index 0000000..99eb4f7
--- /dev/null
@@ -0,0 +1,21 @@
+<?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);
+    }
+}
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 (file)
index 0000000..1e580a4
--- /dev/null
@@ -0,0 +1,20 @@
+<?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);
+    }
+}