mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Images: Updated GIF handling to use native methods
Changes GIF image thumbnail handling to direcly load via gd instead of going through interventions own handling (which supports frames) since we don't need animation for our thumbnails, and since performance issues could arise with GIFs that have large frame counts. For #5029
This commit is contained in:
parent
bddc6ae66b
commit
3406846c82
@ -7,11 +7,13 @@ use Exception;
|
|||||||
use GuzzleHttp\Psr7\Utils;
|
use GuzzleHttp\Psr7\Utils;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Intervention\Image\Decoders\BinaryImageDecoder;
|
use Intervention\Image\Decoders\BinaryImageDecoder;
|
||||||
|
use Intervention\Image\Drivers\Gd\Decoders\NativeObjectDecoder;
|
||||||
use Intervention\Image\Drivers\Gd\Driver;
|
use Intervention\Image\Drivers\Gd\Driver;
|
||||||
use Intervention\Image\Encoders\AutoEncoder;
|
use Intervention\Image\Encoders\AutoEncoder;
|
||||||
use Intervention\Image\Encoders\PngEncoder;
|
use Intervention\Image\Encoders\PngEncoder;
|
||||||
use Intervention\Image\Interfaces\ImageInterface as InterventionImage;
|
use Intervention\Image\Interfaces\ImageInterface as InterventionImage;
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
|
use Intervention\Image\Origin;
|
||||||
|
|
||||||
class ImageResizer
|
class ImageResizer
|
||||||
{
|
{
|
||||||
@ -99,7 +101,7 @@ class ImageResizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If not in cache and thumbnail does not exist, generate thumb and cache path
|
// If not in cache and thumbnail does not exist, generate thumb and cache path
|
||||||
$thumbData = $this->resizeImageData($imageData, $width, $height, $keepRatio);
|
$thumbData = $this->resizeImageData($imageData, $width, $height, $keepRatio, $this->getExtension($image));
|
||||||
$disk->put($thumbFilePath, $thumbData, true);
|
$disk->put($thumbFilePath, $thumbData, true);
|
||||||
Cache::put($thumbCacheKey, $thumbFilePath, static::THUMBNAIL_CACHE_TIME);
|
Cache::put($thumbCacheKey, $thumbFilePath, static::THUMBNAIL_CACHE_TIME);
|
||||||
|
|
||||||
@ -120,7 +122,7 @@ class ImageResizer
|
|||||||
?string $format = null,
|
?string $format = null,
|
||||||
): string {
|
): string {
|
||||||
try {
|
try {
|
||||||
$thumb = $this->interventionFromImageData($imageData);
|
$thumb = $this->interventionFromImageData($imageData, $format);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||||
}
|
}
|
||||||
@ -154,11 +156,23 @@ class ImageResizer
|
|||||||
* Performs some manual library usage to ensure image is specifically loaded
|
* Performs some manual library usage to ensure image is specifically loaded
|
||||||
* from given binary data instead of data being misinterpreted.
|
* from given binary data instead of data being misinterpreted.
|
||||||
*/
|
*/
|
||||||
protected function interventionFromImageData(string $imageData): InterventionImage
|
protected function interventionFromImageData(string $imageData, ?string $fileType): InterventionImage
|
||||||
{
|
{
|
||||||
$manager = new ImageManager(new Driver());
|
$manager = new ImageManager(new Driver());
|
||||||
|
|
||||||
return $manager->read($imageData, BinaryImageDecoder::class);
|
// Ensure gif images are decoded natively instead of deferring to intervention GIF
|
||||||
|
// handling since we don't need the added animation support.
|
||||||
|
$isGif = $fileType === 'gif';
|
||||||
|
$decoder = $isGif ? NativeObjectDecoder::class : BinaryImageDecoder::class;
|
||||||
|
$input = $isGif ? @imagecreatefromstring($imageData) : $imageData;
|
||||||
|
|
||||||
|
$image = $manager->read($input, $decoder);
|
||||||
|
|
||||||
|
if ($isGif) {
|
||||||
|
$image->setOrigin(new Origin('image/gif'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $image;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -209,7 +223,15 @@ class ImageResizer
|
|||||||
*/
|
*/
|
||||||
protected function isGif(Image $image): bool
|
protected function isGif(Image $image): bool
|
||||||
{
|
{
|
||||||
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
|
return $this->getExtension($image) === 'gif';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the extension for the given image, normalised to lower-case.
|
||||||
|
*/
|
||||||
|
protected function getExtension(Image $image): string
|
||||||
|
{
|
||||||
|
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -599,6 +599,40 @@ class ImageTest extends TestCase
|
|||||||
$this->files->deleteAtRelativePath($relPath);
|
$this->files->deleteAtRelativePath($relPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_gif_thumbnail_generation()
|
||||||
|
{
|
||||||
|
$this->asAdmin();
|
||||||
|
$originalFile = $this->files->testFilePath('animated.gif');
|
||||||
|
$originalFileSize = filesize($originalFile);
|
||||||
|
|
||||||
|
$imgDetails = $this->files->uploadGalleryImageToPage($this, $this->entities->page(), 'animated.gif');
|
||||||
|
$relPath = $imgDetails['path'];
|
||||||
|
|
||||||
|
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));
|
||||||
|
$galleryThumb = $imgDetails['response']->thumbs->gallery;
|
||||||
|
$displayThumb = $imgDetails['response']->thumbs->display;
|
||||||
|
|
||||||
|
// Ensure display thumbnail is original image
|
||||||
|
$this->assertStringEndsWith($imgDetails['path'], $displayThumb);
|
||||||
|
$this->assertStringNotContainsString('thumbs', $displayThumb);
|
||||||
|
|
||||||
|
// Ensure gallery thumbnail is reduced image (single frame)
|
||||||
|
$galleryThumbRelPath = implode('/', array_slice(explode('/', $galleryThumb), 3));
|
||||||
|
$galleryThumbPath = public_path($galleryThumbRelPath);
|
||||||
|
$galleryFileSize = filesize($galleryThumbPath);
|
||||||
|
|
||||||
|
// Basic scan of GIF content to check frame count
|
||||||
|
$originalFrameCount = count(explode("\x00\x21\xF9", file_get_contents($originalFile)));
|
||||||
|
$galleryFrameCount = count(explode("\x00\x21\xF9", file_get_contents($galleryThumbPath)));
|
||||||
|
|
||||||
|
$this->files->deleteAtRelativePath($relPath);
|
||||||
|
$this->files->deleteAtRelativePath($galleryThumbRelPath);
|
||||||
|
|
||||||
|
$this->assertNotEquals($originalFileSize, $galleryFileSize);
|
||||||
|
$this->assertEquals(3, $originalFrameCount);
|
||||||
|
$this->assertEquals(1, $galleryFrameCount);
|
||||||
|
}
|
||||||
|
|
||||||
protected function getTestProfileImage()
|
protected function getTestProfileImage()
|
||||||
{
|
{
|
||||||
$imageName = 'profile.png';
|
$imageName = 'profile.png';
|
||||||
|
BIN
tests/test-data/animated.gif
Normal file
BIN
tests/test-data/animated.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 B |
Loading…
Reference in New Issue
Block a user