namespace BookStack\Uploads;
use BookStack\Exceptions\ImageUploadException;
+use BookStack\Util\WebSafeMimeSniffer;
use ErrorException;
use Exception;
use Illuminate\Contracts\Cache\Repository as Cache;
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.
*/
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);
}
$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);
}
$this->assertEquals($originalFileSize, $displayFileSize, 'Display thumbnail generation should not increase image size');
}
+ public function test_image_display_thumbnail_generation_for_apng_images_uses_original_file()
+ {
+ $page = Page::query()->first();
+ $admin = $this->getAdmin();
+ $this->actingAs($admin);
+
+ $imgDetails = $this->uploadGalleryImage($page, 'animated.png');
+ $this->deleteImage($imgDetails['path']);
+
+ $this->assertStringContainsString('thumbs-', $imgDetails['response']->thumbs->gallery);
+ $this->assertStringNotContainsString('thumbs-', $imgDetails['response']->thumbs->display);
+ }
+
public function test_image_edit()
{
$editor = $this->getEditor();