diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php
index 5e197e750..1e42f414b 100644
--- a/app/Uploads/Image.php
+++ b/app/Uploads/Image.php
@@ -52,7 +52,7 @@ class Image extends Model
      */
     public function getThumb(?int $width, ?int $height, bool $keepRatio = false): ?string
     {
-        return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio, false, true);
+        return app()->make(ImageResizer::class)->resizeToThumbnailUrl($this, $width, $height, $keepRatio, false, true);
     }
 
     /**
diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php
index 8a770da78..4aa36bab9 100644
--- a/app/Uploads/ImageRepo.php
+++ b/app/Uploads/ImageRepo.php
@@ -13,7 +13,8 @@ class ImageRepo
 {
     public function __construct(
         protected ImageService $imageService,
-        protected PermissionApplicator $permissions
+        protected PermissionApplicator $permissions,
+        protected ImageResizer $imageResizer,
     ) {
     }
 
@@ -225,14 +226,12 @@ class ImageRepo
     }
 
     /**
-     * Get the thumbnail for an image.
-     * If $keepRatio is true only the width will be used.
-     * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
+     * Get a thumbnail URL for the given image.
      */
     protected function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio, bool $shouldCreate): ?string
     {
         try {
-            return $this->imageService->getThumbnail($image, $width, $height, $keepRatio, $shouldCreate);
+            return $this->imageResizer->resizeToThumbnailUrl($image, $width, $height, $keepRatio, $shouldCreate);
         } catch (Exception $exception) {
             return null;
         }
diff --git a/app/Uploads/ImageResizer.php b/app/Uploads/ImageResizer.php
index 7a89b9d35..5fe8a8954 100644
--- a/app/Uploads/ImageResizer.php
+++ b/app/Uploads/ImageResizer.php
@@ -3,28 +3,91 @@
 namespace BookStack\Uploads;
 
 use BookStack\Exceptions\ImageUploadException;
+use Exception;
 use GuzzleHttp\Psr7\Utils;
-use Intervention\Image\Exception\NotSupportedException;
+use Illuminate\Support\Facades\Cache;
 use Intervention\Image\Image as InterventionImage;
 use Intervention\Image\ImageManager;
 
 class ImageResizer
 {
     public function __construct(
-        protected ImageManager $intervention
+        protected ImageManager $intervention,
+        protected ImageStorage $storage,
     ) {
     }
 
+    /**
+     * Get the thumbnail for an image.
+     * If $keepRatio is true only the width will be used.
+     * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
+     *
+     * @throws Exception
+     */
+    public function resizeToThumbnailUrl(
+        Image $image,
+        ?int $width,
+        ?int $height,
+        bool $keepRatio = false,
+        bool $shouldCreate = false,
+        bool $canCreate = false,
+    ): ?string {
+        // Do not resize GIF images where we're not cropping
+        if ($keepRatio && $this->isGif($image)) {
+            return $this->storage->getPublicUrl($image->path);
+        }
+
+        $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
+        $imagePath = $image->path;
+        $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
+
+        $thumbCacheKey = 'images::' . $image->id . '::' . $thumbFilePath;
+
+        // Return path if in cache
+        $cachedThumbPath = Cache::get($thumbCacheKey);
+        if ($cachedThumbPath && !$shouldCreate) {
+            return $this->storage->getPublicUrl($cachedThumbPath);
+        }
+
+        // If thumbnail has already been generated, serve that and cache path
+        $disk = $this->storage->getDisk($image->type);
+        if (!$shouldCreate && $disk->exists($thumbFilePath)) {
+            Cache::put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
+
+            return $this->storage->getPublicUrl($thumbFilePath);
+        }
+
+        $imageData = $disk->get($imagePath);
+
+        // Do not resize apng images where we're not cropping
+        if ($keepRatio && $this->isApngData($image, $imageData)) {
+            Cache::put($thumbCacheKey, $image->path, 60 * 60 * 72);
+
+            return $this->storage->getPublicUrl($image->path);
+        }
+
+        if (!$shouldCreate && !$canCreate) {
+            return null;
+        }
+
+        // If not in cache and thumbnail does not exist, generate thumb and cache path
+        $thumbData = $this->resizeImageData($imageData, $width, $height, $keepRatio);
+        $disk->put($thumbFilePath, $thumbData, true);
+        Cache::put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
+
+        return $this->storage->getPublicUrl($thumbFilePath);
+    }
+
     /**
      * Resize the image of given data to the specified size, and return the new image data.
      *
      * @throws ImageUploadException
      */
-    protected function resizeImageData(string $imageData, ?int $width, ?int $height, bool $keepRatio): string
+    public function resizeImageData(string $imageData, ?int $width, ?int $height, bool $keepRatio): string
     {
         try {
             $thumb = $this->intervention->make($imageData);
-        } catch (NotSupportedException $e) {
+        } catch (Exception $e) {
             throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
         }
 
@@ -92,4 +155,27 @@ class ImageResizer
                 break;
         }
     }
+
+    /**
+     * Checks if the image is a gif. Returns true if it is, else false.
+     */
+    protected function isGif(Image $image): bool
+    {
+        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 str_contains($initialHeader, 'acTL');
+    }
 }
diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php
index f8567c3e5..1655a4cc3 100644
--- a/app/Uploads/ImageService.php
+++ b/app/Uploads/ImageService.php
@@ -6,15 +6,10 @@ use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Page;
 use BookStack\Exceptions\ImageUploadException;
-use ErrorException;
 use Exception;
-use Illuminate\Contracts\Cache\Repository as Cache;
-use Illuminate\Filesystem\FilesystemManager;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
-use Intervention\Image\Exception\NotSupportedException;
-use Intervention\Image\ImageManager;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 use Symfony\Component\HttpFoundation\StreamedResponse;
 
@@ -23,10 +18,8 @@ class ImageService
     protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
 
     public function __construct(
-        protected ImageManager $imageTool,
-        protected FilesystemManager $fileSystem,
-        protected Cache $cache,
         protected ImageStorage $storage,
+        protected ImageResizer $resizer,
     ) {
     }
 
@@ -47,7 +40,7 @@ class ImageService
         $imageData = file_get_contents($uploadedFile->getRealPath());
 
         if ($resizeWidth !== null || $resizeHeight !== null) {
-            $imageData = $this->resizeImage($imageData, $resizeWidth, $resizeHeight, $keepRatio);
+            $imageData = $this->resizer->resizeImageData($imageData, $resizeWidth, $resizeHeight, $keepRatio);
         }
 
         return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
@@ -129,125 +122,6 @@ class ImageService
         $disk->put($path, $imageData);
     }
 
-    /**
-     * Checks if the image is a gif. Returns true if it is, else false.
-     */
-    protected function isGif(Image $image): bool
-    {
-        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 str_contains($initialHeader, 'acTL');
-    }
-
-    /**
-     * Get the thumbnail for an image.
-     * If $keepRatio is true only the width will be used.
-     * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
-     *
-     * @throws Exception
-     */
-    public function getThumbnail(
-        Image $image,
-        ?int $width,
-        ?int $height,
-        bool $keepRatio = false,
-        bool $shouldCreate = false,
-        bool $canCreate = false,
-    ): ?string {
-        // Do not resize GIF images where we're not cropping
-        if ($keepRatio && $this->isGif($image)) {
-            return $this->storage->getPublicUrl($image->path);
-        }
-
-        $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
-        $imagePath = $image->path;
-        $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
-
-        $thumbCacheKey = 'images::' . $image->id . '::' . $thumbFilePath;
-
-        // Return path if in cache
-        $cachedThumbPath = $this->cache->get($thumbCacheKey);
-        if ($cachedThumbPath && !$shouldCreate) {
-            return $this->storage->getPublicUrl($cachedThumbPath);
-        }
-
-        // If thumbnail has already been generated, serve that and cache path
-        $disk = $this->storage->getDisk($image->type);
-        if (!$shouldCreate && $disk->exists($thumbFilePath)) {
-            $this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
-
-            return $this->storage->getPublicUrl($thumbFilePath);
-        }
-
-        $imageData = $disk->get($imagePath);
-
-        // 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->storage->getPublicUrl($image->path);
-        }
-
-        if (!$shouldCreate && !$canCreate) {
-            return null;
-        }
-
-        // If not in cache and thumbnail does not exist, generate thumb and cache path
-        $thumbData = $this->resizeImage($imageData, $width, $height, $keepRatio);
-        $disk->put($thumbFilePath, $thumbData, true);
-        $this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
-
-        return $this->storage->getPublicUrl($thumbFilePath);
-    }
-
-    /**
-     * Resize the image of given data to the specified size, and return the new image data.
-     *
-     * @throws ImageUploadException
-     */
-    protected function resizeImage(string $imageData, ?int $width, ?int $height, bool $keepRatio): string
-    {
-        try {
-            $thumb = $this->imageTool->make($imageData);
-        } catch (ErrorException | NotSupportedException $e) {
-            throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
-        }
-
-        $this->orientImageToOriginalExif($thumb, $imageData);
-
-        if ($keepRatio) {
-            $thumb->resize($width, $height, function ($constraint) {
-                $constraint->aspectRatio();
-                $constraint->upsize();
-            });
-        } else {
-            $thumb->fit($width, $height);
-        }
-
-        $thumbData = (string) $thumb->encode();
-
-        // Use original image data if we're keeping the ratio
-        // and the resizing does not save any space.
-        if ($keepRatio && strlen($thumbData) > strlen($imageData)) {
-            return $imageData;
-        }
-
-        return $thumbData;
-    }
-
     /**
      * Get the raw data content from an image.
      *
@@ -375,7 +249,7 @@ class ImageService
      */
     protected function checkUserHasAccessToRelationOfImageAtPath(string $path): bool
     {
-        if (str_starts_with($path, '/uploads/images/')) {
+        if (str_starts_with($path, 'uploads/images/')) {
             $path = substr($path, 15);
         }
 
diff --git a/app/Uploads/ImageStorageDisk.php b/app/Uploads/ImageStorageDisk.php
index 3a95661ca..798b72abd 100644
--- a/app/Uploads/ImageStorageDisk.php
+++ b/app/Uploads/ImageStorageDisk.php
@@ -50,7 +50,7 @@ class ImageStorageDisk
     /**
      * Get the file at the given path.
      */
-    public function get(string $path): bool
+    public function get(string $path): ?string
     {
         return $this->filesystem->get($this->adjustPathForDisk($path));
     }
@@ -106,6 +106,7 @@ class ImageStorageDisk
      */
     public function mimeType(string $path): string
     {
+        $path = $this->adjustPathForDisk($path);
         return $this->filesystem instanceof FilesystemAdapter ? $this->filesystem->mimeType($path) : '';
     }
 
@@ -114,7 +115,7 @@ class ImageStorageDisk
      */
     public function response(string $path): StreamedResponse
     {
-        return $this->filesystem->response($path);
+        return $this->filesystem->response($this->adjustPathForDisk($path));
     }
 
     /**
diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php
index 9943302d3..4da964d48 100644
--- a/tests/Uploads/ImageTest.php
+++ b/tests/Uploads/ImageTest.php
@@ -557,6 +557,7 @@ class ImageTest extends TestCase
         $this->asEditor();
         $imageName = 'first-image.png';
         $relPath = $this->files->expectedImagePath('gallery', $imageName);
+        $this->files->deleteAtRelativePath($relPath);
 
         $this->files->uploadGalleryImage($this, $imageName, $this->entities->page()->id);
         $image = Image::first();