Skip to content

Commit

Permalink
Images: Updated GIF handling to use native methods
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ssddanbrown committed Jun 9, 2024
1 parent bddc6ae commit 3406846
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 5 deletions.
32 changes: 27 additions & 5 deletions app/Uploads/ImageResizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
use GuzzleHttp\Psr7\Utils;
use Illuminate\Support\Facades\Cache;
use Intervention\Image\Decoders\BinaryImageDecoder;
use Intervention\Image\Drivers\Gd\Decoders\NativeObjectDecoder;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\Encoders\AutoEncoder;
use Intervention\Image\Encoders\PngEncoder;
use Intervention\Image\Interfaces\ImageInterface as InterventionImage;
use Intervention\Image\ImageManager;
use Intervention\Image\Origin;

class ImageResizer
{
Expand Down Expand Up @@ -99,7 +101,7 @@ public function resizeToThumbnailUrl(
}

// 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);
Cache::put($thumbCacheKey, $thumbFilePath, static::THUMBNAIL_CACHE_TIME);

Expand All @@ -120,7 +122,7 @@ public function resizeImageData(
?string $format = null,
): string {
try {
$thumb = $this->interventionFromImageData($imageData);
$thumb = $this->interventionFromImageData($imageData, $format);
} catch (Exception $e) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
}
Expand Down Expand Up @@ -154,11 +156,23 @@ public function resizeImageData(
* Performs some manual library usage to ensure image is specifically loaded
* 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());

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;
}

/**
Expand Down Expand Up @@ -209,7 +223,15 @@ protected function orientImageToOriginalExif(InterventionImage $image, string $o
*/
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));
}

/**
Expand Down
34 changes: 34 additions & 0 deletions tests/Uploads/ImageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,40 @@ public function test_image_manager_regen_thumbnails()
$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()
{
$imageName = 'profile.png';
Expand Down
Binary file added tests/test-data/animated.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3406846

Please sign in to comment.