From e8f55b5102ae41b5b2c9af21a4172e4c1ff03a66 Mon Sep 17 00:00:00 2001 From: Tresor kasenda Date: Thu, 9 Jul 2026 12:24:42 +0200 Subject: [PATCH 1/2] [13.x] Add PNG, GIF, AVIF, and BMP output formats to the Image component The `Image` component only supported converting to webp/jpg/jpeg via `toFormat()`, even though the underlying Intervention Image encoders for png, gif, avif, and bmp were already available. Adds `toPng()`, `toGif()`, `toAvif()`, and `toBmp()` following the same pattern as the existing format methods. --- .../Image/Drivers/InterventionDriver.php | 8 ++ src/Illuminate/Image/Image.php | 35 +++++- src/Illuminate/Image/ImageOutputOptions.php | 2 +- tests/Image/Drivers/GdDriverTest.php | 48 +++++++++ tests/Image/Drivers/ImagickDriverTest.php | 44 ++++++++ tests/Image/ImageTest.php | 100 +++++++++++++++--- 6 files changed, 223 insertions(+), 14 deletions(-) diff --git a/src/Illuminate/Image/Drivers/InterventionDriver.php b/src/Illuminate/Image/Drivers/InterventionDriver.php index 8bd152f1afa2..7a69690b5886 100644 --- a/src/Illuminate/Image/Drivers/InterventionDriver.php +++ b/src/Illuminate/Image/Drivers/InterventionDriver.php @@ -20,8 +20,12 @@ use Illuminate\Image\Transformations\Scale; use Illuminate\Image\Transformations\Sharpen; use Intervention\Image\Direction; +use Intervention\Image\Encoders\AvifEncoder; +use Intervention\Image\Encoders\BmpEncoder; +use Intervention\Image\Encoders\GifEncoder; use Intervention\Image\Encoders\JpegEncoder; use Intervention\Image\Encoders\MediaTypeEncoder; +use Intervention\Image\Encoders\PngEncoder; use Intervention\Image\Encoders\WebpEncoder; use Intervention\Image\ImageManager; @@ -111,6 +115,10 @@ public function process(string $contents, ImagePipeline $pipeline): string return $image->encode(match ($pipeline->output->format) { 'webp' => new WebpEncoder($quality), 'jpg', 'jpeg' => new JpegEncoder($quality), + 'png' => new PngEncoder, + 'gif' => new GifEncoder, + 'avif' => new AvifEncoder($quality), + 'bmp' => new BmpEncoder, })->toString(); } diff --git a/src/Illuminate/Image/Image.php b/src/Illuminate/Image/Image.php index e415b814d14b..0528dbab27ec 100644 --- a/src/Illuminate/Image/Image.php +++ b/src/Illuminate/Image/Image.php @@ -258,6 +258,38 @@ public function toJpeg(): static return $this->toJpg(); } + /** + * Convert the image to PNG format. + */ + public function toPng(): static + { + return $this->toFormat('png'); + } + + /** + * Convert the image to GIF format. + */ + public function toGif(): static + { + return $this->toFormat('gif'); + } + + /** + * Convert the image to AVIF format. + */ + public function toAvif(): static + { + return $this->toFormat('avif'); + } + + /** + * Convert the image to BMP format. + */ + public function toBmp(): static + { + return $this->toFormat('bmp'); + } + /** * Set the output format. * @@ -265,7 +297,7 @@ public function toJpeg(): static */ protected function toFormat(string $format): static { - if (! in_array($format, ['webp', 'jpg', 'jpeg'])) { + if (! in_array($format, ['webp', 'jpg', 'jpeg', 'png', 'gif', 'avif', 'bmp'])) { throw new ImageException("The [{$format}] format is not supported."); } @@ -390,6 +422,7 @@ public function extension(): string 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', + 'image/avif' => 'avif', 'image/bmp' => 'bmp', 'image/svg+xml' => 'svg', 'image/tiff' => 'tiff', diff --git a/src/Illuminate/Image/ImageOutputOptions.php b/src/Illuminate/Image/ImageOutputOptions.php index bc006256e087..9f1c87c34d21 100644 --- a/src/Illuminate/Image/ImageOutputOptions.php +++ b/src/Illuminate/Image/ImageOutputOptions.php @@ -12,7 +12,7 @@ class ImageOutputOptions /** * The output format. * - * @var 'webp'|'jpg'|'jpeg'|null + * @var 'webp'|'jpg'|'jpeg'|'png'|'gif'|'avif'|'bmp'|null */ public ?string $format = null; diff --git a/tests/Image/Drivers/GdDriverTest.php b/tests/Image/Drivers/GdDriverTest.php index ff262f69e710..63ba0fe21a86 100644 --- a/tests/Image/Drivers/GdDriverTest.php +++ b/tests/Image/Drivers/GdDriverTest.php @@ -63,6 +63,54 @@ public function test_processes_optimize_to_jpeg() $this->assertSame(IMAGETYPE_JPEG, getimagesizefromstring($result)[2]); } + public function test_processes_optimize_to_png() + { + $driver = new GdDriver; + + $pipeline = $this->pipeline(format: 'png'); + + $result = $driver->process($this->fakeImageContents(), $pipeline); + + $this->assertSame(IMAGETYPE_PNG, getimagesizefromstring($result)[2]); + } + + public function test_processes_optimize_to_gif() + { + $driver = new GdDriver; + + $pipeline = $this->pipeline(format: 'gif'); + + $result = $driver->process($this->fakeImageContents(), $pipeline); + + $this->assertSame(IMAGETYPE_GIF, getimagesizefromstring($result)[2]); + } + + public function test_processes_optimize_to_avif() + { + if (! function_exists('imageavif')) { + $this->markTestSkipped('The GD extension was not compiled with AVIF support.'); + } + + $driver = new GdDriver; + + $pipeline = $this->pipeline(format: 'avif'); + + $result = $driver->process($this->fakeImageContents(), $pipeline); + + $this->assertSame(IMAGETYPE_AVIF, getimagesizefromstring($result)[2]); + } + + public function test_processes_optimize_to_bmp() + { + $driver = new GdDriver; + + $pipeline = $this->pipeline(format: 'bmp'); + + $result = $driver->process($this->fakeImageContents(), $pipeline); + + $this->assertSame(IMAGETYPE_BMP, getimagesizefromstring($result)[2]); + } + public function test_processes_cover_and_optimize_together() { $driver = new GdDriver; diff --git a/tests/Image/Drivers/ImagickDriverTest.php b/tests/Image/Drivers/ImagickDriverTest.php index b236197ddb6f..548f5bca1a17 100644 --- a/tests/Image/Drivers/ImagickDriverTest.php +++ b/tests/Image/Drivers/ImagickDriverTest.php @@ -61,6 +61,50 @@ public function test_processes_optimize_to_jpeg() $this->assertSame(IMAGETYPE_JPEG, getimagesizefromstring($result)[2]); } + public function test_processes_optimize_to_png() + { + $driver = new ImagickDriver; + + $pipeline = $this->pipeline(format: 'png'); + + $result = $driver->process($this->fakeImageContents(), $pipeline); + + $this->assertSame(IMAGETYPE_PNG, getimagesizefromstring($result)[2]); + } + + public function test_processes_optimize_to_gif() + { + $driver = new ImagickDriver; + + $pipeline = $this->pipeline(format: 'gif'); + + $result = $driver->process($this->fakeImageContents(), $pipeline); + + $this->assertSame(IMAGETYPE_GIF, getimagesizefromstring($result)[2]); + } + + public function test_processes_optimize_to_avif() + { + $driver = new ImagickDriver; + + $pipeline = $this->pipeline(format: 'avif'); + + $result = $driver->process($this->fakeImageContents(), $pipeline); + + $this->assertSame(IMAGETYPE_AVIF, getimagesizefromstring($result)[2]); + } + + public function test_processes_optimize_to_bmp() + { + $driver = new ImagickDriver; + + $pipeline = $this->pipeline(format: 'bmp'); + + $result = $driver->process($this->fakeImageContents(), $pipeline); + + $this->assertSame(IMAGETYPE_BMP, getimagesizefromstring($result)[2]); + } + public function test_processes_cover_and_optimize_together() { $driver = new ImagickDriver; diff --git a/tests/Image/ImageTest.php b/tests/Image/ImageTest.php index a523c5bf165a..e961828e6867 100644 --- a/tests/Image/ImageTest.php +++ b/tests/Image/ImageTest.php @@ -125,6 +125,34 @@ public function test_to_jpg_returns_new_instance() $this->assertNotSame($image, $image->toJpg()); } + public function test_to_png_returns_new_instance() + { + $image = $this->makeImage(); + + $this->assertNotSame($image, $image->toPng()); + } + + public function test_to_gif_returns_new_instance() + { + $image = $this->makeImage(); + + $this->assertNotSame($image, $image->toGif()); + } + + public function test_to_avif_returns_new_instance() + { + $image = $this->makeImage(); + + $this->assertNotSame($image, $image->toAvif()); + } + + public function test_to_bmp_returns_new_instance() + { + $image = $this->makeImage(); + + $this->assertNotSame($image, $image->toBmp()); + } + public function test_using_returns_new_instance() { $image = $this->makeImage(); @@ -221,6 +249,21 @@ public function test_extension_returns_jpg_for_jpeg() $this->assertSame('jpg', $image->extension()); } + public function test_extension_returns_avif_for_avif() + { + if (! extension_loaded('imagick')) { + $this->markTestSkipped('The Imagick extension is not available.'); + } + + $imagick = new \Imagick; + $imagick->newImage(10, 10, 'red'); + $imagick->setImageFormat('avif'); + + $image = new Image($imagick->getImageBlob()); + + $this->assertSame('avif', $image->extension()); + } + public function test_dimensions_returns_width_and_height() { $image = new Image($this->fakeImageContents(300, 200)); @@ -298,9 +341,9 @@ public function test_optimize_throws_for_unsupported_format() $image = $this->makeImage(); $this->expectException(ImageException::class); - $this->expectExceptionMessage('The [bmp] format is not supported.'); + $this->expectExceptionMessage('The [tiff] format is not supported.'); - $image->optimize('bmp'); + $image->optimize('tiff'); } public function test_quality_sets_option() @@ -332,6 +375,34 @@ public function test_to_jpeg_is_alias_for_to_jpg() $this->assertSame('jpg', $this->getOptions($image->toJpeg())->format); } + public function test_to_png_sets_format() + { + $image = $this->makeImage(); + + $this->assertSame('png', $this->getOptions($image->toPng())->format); + } + + public function test_to_gif_sets_format() + { + $image = $this->makeImage(); + + $this->assertSame('gif', $this->getOptions($image->toGif())->format); + } + + public function test_to_avif_sets_format() + { + $image = $this->makeImage(); + + $this->assertSame('avif', $this->getOptions($image->toAvif())->format); + } + + public function test_to_bmp_sets_format() + { + $image = $this->makeImage(); + + $this->assertSame('bmp', $this->getOptions($image->toBmp())->format); + } + public function test_quality_survives_format_conversion() { $image = $this->makeImage(); @@ -620,9 +691,16 @@ public function test_optimize_throws_for_jpg_with_wrong_spelling() $image = $this->makeImage(); $this->expectException(ImageException::class); - $this->expectExceptionMessage('The [png] format is not supported.'); + $this->expectExceptionMessage('The [jpge] format is not supported.'); - $image->optimize('png'); + $image->optimize('jpge'); + } + + public function test_optimize_allows_png() + { + $result = $this->makeImage()->optimize('png'); + + $this->assertSame('png', $this->getOptions($result)->format); } public function test_serialization_throws_exception() @@ -819,20 +897,18 @@ public function test_optimize_sets_both_format_and_quality() $this->assertSame(90, $options->quality); } - public function test_optimize_throws_for_gif() + public function test_optimize_allows_gif() { - $this->expectException(ImageException::class); - $this->expectExceptionMessage('The [gif] format is not supported.'); + $result = $this->makeImage()->optimize('gif'); - $this->makeImage()->optimize('gif'); + $this->assertSame('gif', $this->getOptions($result)->format); } - public function test_optimize_throws_for_avif() + public function test_optimize_allows_avif() { - $this->expectException(ImageException::class); - $this->expectExceptionMessage('The [avif] format is not supported.'); + $result = $this->makeImage()->optimize('avif'); - $this->makeImage()->optimize('avif'); + $this->assertSame('avif', $this->getOptions($result)->format); } public function test_optimize_allows_jpeg_spelling() From d3c46d5b28db2e6fb2951c7388bbb1c41c6e5f2a Mon Sep 17 00:00:00 2001 From: Tresor kasenda Date: Thu, 9 Jul 2026 12:50:19 +0200 Subject: [PATCH 2/2] Make AVIF tests resilient to environment-dependent format detection CI failed because getimagesizefromstring() and finfo's AVIF detection depend on the libavif/libmagic versions installed on the runner, which differ from the local dev environment. The driver test now inspects the raw ISOBMFF ftyp box/brand instead of relying on getimagesizefromstring(), and the extension() regression test skips itself when the local fileinfo database doesn't recognize AVIF. --- tests/Image/Drivers/ImagickDriverTest.php | 6 +++++- tests/Image/ImageTest.php | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/Image/Drivers/ImagickDriverTest.php b/tests/Image/Drivers/ImagickDriverTest.php index 548f5bca1a17..77af996c2344 100644 --- a/tests/Image/Drivers/ImagickDriverTest.php +++ b/tests/Image/Drivers/ImagickDriverTest.php @@ -91,7 +91,11 @@ public function test_processes_optimize_to_avif() $result = $driver->process($this->fakeImageContents(), $pipeline); - $this->assertSame(IMAGETYPE_AVIF, getimagesizefromstring($result)[2]); + // PHP's getimagesizefromstring()/finfo AVIF detection varies by the + // libavif/libmagic versions installed, so we assert on the ISOBMFF + // "ftyp" box and brand directly instead of relying on either. + $this->assertStringContainsString('ftyp', substr($result, 0, 16)); + $this->assertMatchesRegularExpression('/avif|avis|mif1/', substr($result, 0, 32)); } public function test_processes_optimize_to_bmp() diff --git a/tests/Image/ImageTest.php b/tests/Image/ImageTest.php index e961828e6867..08ab3d8f0794 100644 --- a/tests/Image/ImageTest.php +++ b/tests/Image/ImageTest.php @@ -258,8 +258,13 @@ public function test_extension_returns_avif_for_avif() $imagick = new \Imagick; $imagick->newImage(10, 10, 'red'); $imagick->setImageFormat('avif'); + $contents = $imagick->getImageBlob(); - $image = new Image($imagick->getImageBlob()); + if ((new \finfo(FILEINFO_MIME_TYPE))->buffer($contents) !== 'image/avif') { + $this->markTestSkipped('The installed fileinfo database does not recognize AVIF.'); + } + + $image = new Image($contents); $this->assertSame('avif', $image->extension()); }