3 namespace BookStack\Uploads;
5 use BookStack\Exceptions\ImageUploadException;
7 use GuzzleHttp\Psr7\Utils;
8 use Illuminate\Support\Facades\Cache;
9 use Intervention\Image\Image as InterventionImage;
10 use Intervention\Image\ImageManager;
14 public function __construct(
15 protected ImageManager $intervention,
16 protected ImageStorage $storage,
21 * Get the thumbnail for an image.
22 * If $keepRatio is true only the width will be used.
23 * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
27 public function resizeToThumbnailUrl(
31 bool $keepRatio = false,
32 bool $shouldCreate = false,
33 bool $canCreate = false,
35 // Do not resize GIF images where we're not cropping
36 if ($keepRatio && $this->isGif($image)) {
37 return $this->storage->getPublicUrl($image->path);
40 $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
41 $imagePath = $image->path;
42 $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
44 $thumbCacheKey = 'images::' . $image->id . '::' . $thumbFilePath;
46 // Return path if in cache
47 $cachedThumbPath = Cache::get($thumbCacheKey);
48 if ($cachedThumbPath && !$shouldCreate) {
49 return $this->storage->getPublicUrl($cachedThumbPath);
52 // If thumbnail has already been generated, serve that and cache path
53 $disk = $this->storage->getDisk($image->type);
54 if (!$shouldCreate && $disk->exists($thumbFilePath)) {
55 Cache::put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
57 return $this->storage->getPublicUrl($thumbFilePath);
60 $imageData = $disk->get($imagePath);
62 // Do not resize apng images where we're not cropping
63 if ($keepRatio && $this->isApngData($image, $imageData)) {
64 Cache::put($thumbCacheKey, $image->path, 60 * 60 * 72);
66 return $this->storage->getPublicUrl($image->path);
69 if (!$shouldCreate && !$canCreate) {
73 // If not in cache and thumbnail does not exist, generate thumb and cache path
74 $thumbData = $this->resizeImageData($imageData, $width, $height, $keepRatio);
75 $disk->put($thumbFilePath, $thumbData, true);
76 Cache::put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
78 return $this->storage->getPublicUrl($thumbFilePath);
82 * Resize the image of given data to the specified size, and return the new image data.
84 * @throws ImageUploadException
86 public function resizeImageData(string $imageData, ?int $width, ?int $height, bool $keepRatio): string
89 $thumb = $this->intervention->make($imageData);
90 } catch (Exception $e) {
91 throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
94 $this->orientImageToOriginalExif($thumb, $imageData);
97 $thumb->resize($width, $height, function ($constraint) {
98 $constraint->aspectRatio();
99 $constraint->upsize();
102 $thumb->fit($width, $height);
105 $thumbData = (string) $thumb->encode();
107 // Use original image data if we're keeping the ratio
108 // and the resizing does not save any space.
109 if ($keepRatio && strlen($thumbData) > strlen($imageData)) {
117 * Orientate the given intervention image based upon the given original image data.
118 * Intervention does have an `orientate` method but the exif data it needs is lost before it
119 * can be used (At least when created using binary string data) so we need to do some
120 * implementation on our side to use the original image data.
121 * Bulk of logic taken from: https://github.com/Intervention/image/blob/b734a4988b2148e7d10364b0609978a88d277536/src/Intervention/Image/Commands/OrientateCommand.php
122 * Copyright (c) Oliver Vogel, MIT License.
124 protected function orientImageToOriginalExif(InterventionImage $image, string $originalData): void
126 if (!extension_loaded('exif')) {
130 $stream = Utils::streamFor($originalData)->detach();
131 $exif = @exif_read_data($stream);
132 $orientation = $exif ? ($exif['Orientation'] ?? null) : null;
134 switch ($orientation) {
142 $image->rotate(180)->flip();
145 $image->rotate(270)->flip();
151 $image->rotate(90)->flip();
160 * Checks if the image is a gif. Returns true if it is, else false.
162 protected function isGif(Image $image): bool
164 return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
168 * Check if the given image and image data is apng.
170 protected function isApngData(Image $image, string &$imageData): bool
172 $isPng = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'png';
177 $initialHeader = substr($imageData, 0, strpos($imageData, 'IDAT'));
179 return str_contains($initialHeader, 'acTL');