]> BookStack Code Mirror - bookstack/blobdiff - app/Uploads/ImageService.php
Updated minimum php version from 7.3 to 7.4
[bookstack] / app / Uploads / ImageService.php
index 644269731a1b0bc1be8c28278ada4af5198d551a..e755be7e6e7c261adfd8e49ef1d344957983013f 100644 (file)
@@ -6,10 +6,10 @@ use BookStack\Exceptions\ImageUploadException;
 use ErrorException;
 use Exception;
 use Illuminate\Contracts\Cache\Repository as Cache;
-use Illuminate\Contracts\Filesystem\Factory as FileSystem;
 use Illuminate\Contracts\Filesystem\FileNotFoundException;
-use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
 use Illuminate\Contracts\Filesystem\Filesystem as Storage;
+use Illuminate\Filesystem\FilesystemAdapter;
+use Illuminate\Filesystem\FilesystemManager;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
@@ -33,7 +33,7 @@ class ImageService
     /**
      * ImageService constructor.
      */
-    public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
+    public function __construct(Image $image, ImageManager $imageTool, FilesystemManager $fileSystem, Cache $cache)
     {
         $this->image = $image;
         $this->imageTool = $imageTool;
@@ -44,7 +44,7 @@ class ImageService
     /**
      * Get the storage that will be used for storing images.
      */
-    protected function getStorageDisk(string $imageType = ''): FileSystemInstance
+    protected function getStorageDisk(string $imageType = ''): Storage
     {
         return $this->fileSystem->disk($this->getStorageDiskName($imageType));
     }
@@ -228,6 +228,21 @@ class ImageService
         return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
     }
 
+    /**
+     * Check if the given image and image data is apng.
+     */
+    protected function isApngData(Image $image, string &$imageData): bool
+    {
+        $isPng = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'png';
+        if (!$isPng) {
+            return false;
+        }
+
+        $initialHeader = substr($imageData, 0, strpos($imageData, 'IDAT'));
+
+        return strpos($initialHeader, 'acTL') !== false;
+    }
+
     /**
      * Get the thumbnail for an image.
      * If $keepRatio is true only the width will be used.
@@ -238,6 +253,7 @@ class ImageService
      */
     public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
     {
+        // Do not resize GIF images where we're not cropping
         if ($keepRatio && $this->isGif($image)) {
             return $this->getPublicUrl($image->path);
         }
@@ -246,19 +262,35 @@ class ImageService
         $imagePath = $image->path;
         $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
 
-        if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
-            return $this->getPublicUrl($thumbFilePath);
+        $thumbCacheKey = 'images::' . $image->id . '::' . $thumbFilePath;
+
+        // Return path if in cache
+        $cachedThumbPath = $this->cache->get($thumbCacheKey);
+        if ($cachedThumbPath) {
+            return $this->getPublicUrl($cachedThumbPath);
         }
 
+        // If thumbnail has already been generated, serve that and cache path
         $storage = $this->getStorageDisk($image->type);
         if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
+            $this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
+
             return $this->getPublicUrl($thumbFilePath);
         }
 
-        $thumbData = $this->resizeImage($storage->get($this->adjustPathForStorageDisk($imagePath, $image->type)), $width, $height, $keepRatio);
+        $imageData = $storage->get($this->adjustPathForStorageDisk($imagePath, $image->type));
+
+        // Do not resize apng images where we're not cropping
+        if ($keepRatio && $this->isApngData($image, $imageData)) {
+            $this->cache->put($thumbCacheKey, $image->path, 60 * 60 * 72);
+
+            return $this->getPublicUrl($image->path);
+        }
 
+        // If not in cache and thumbnail does not exist, generate thumb and cache path
+        $thumbData = $this->resizeImage($imageData, $width, $height, $keepRatio);
         $this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($thumbFilePath, $image->type), $thumbData);
-        $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
+        $this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
 
         return $this->getPublicUrl($thumbFilePath);
     }
@@ -352,7 +384,7 @@ class ImageService
     /**
      * Check whether a folder is empty.
      */
-    protected function isFolderEmpty(FileSystemInstance $storage, string $path): bool
+    protected function isFolderEmpty(Storage $storage, string $path): bool
     {
         $files = $storage->files($path);
         $folders = $storage->directories($path);
@@ -437,10 +469,12 @@ class ImageService
      */
     public function pathExistsInLocalSecure(string $imagePath): bool
     {
+        /** @var FilesystemAdapter $disk */
         $disk = $this->getStorageDisk('gallery');
 
         // Check local_secure is active
         return $this->usingSecureImages()
+            && $disk instanceof FilesystemAdapter
             // Check the image file exists
             && $disk->exists($imagePath)
             // Check the file is likely an image file