diff --git a/app/Uploads/ImageResizer.php b/app/Uploads/ImageResizer.php index d09177fff..fa6b1cac2 100644 --- a/app/Uploads/ImageResizer.php +++ b/app/Uploads/ImageResizer.php @@ -7,11 +7,13 @@ use Exception; 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 { @@ -99,7 +101,7 @@ class ImageResizer } // 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); @@ -120,7 +122,7 @@ class ImageResizer ?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')); } @@ -154,11 +156,23 @@ class ImageResizer * 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; } /** @@ -209,7 +223,15 @@ class ImageResizer */ 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)); } /** diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index d24b6202b..db500f606 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -599,6 +599,40 @@ class ImageTest extends TestCase $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'; diff --git a/tests/test-data/animated.gif b/tests/test-data/animated.gif new file mode 100644 index 000000000..13c9d0524 Binary files /dev/null and b/tests/test-data/animated.gif differ