From 54d9ee8cff00dd9d39d99570466c64889b48f9dd Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 10:41:51 +0100 Subject: [PATCH 01/12] Implement ImageResponse::class and macro --- README.md | 5 +- composer.json | 1 + src/ImageResponse.php | 99 +++++++++++++++++++++++++++++++++++++ src/ServiceProvider.php | 18 +++++++ tests/ImageResponseTest.php | 56 +++++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/ImageResponse.php create mode 100644 tests/ImageResponseTest.php diff --git a/README.md b/README.md index 1a4892d..985b6b5 100644 --- a/README.md +++ b/README.md @@ -92,13 +92,16 @@ You can read more about the different options for ## Getting started The integration is now complete and it is possible to access the [ImageManager](https://image.intervention.io/v3/basics/instantiation) -via Laravel's facade system. +via Laravel's facade system. The package also includes a response macro that can be used to elegantly convert an image resource into an HTTP response. ```php use Intervention\Image\Laravel\Facades\Image; +use Intervention\Image\Format; Route::get('/', function () { $image = Image::read('images/example.jpg'); + + return response()->image($image, Format::WEBP, quality: 65); }); ``` diff --git a/composer.json b/composer.json index f6bb16f..23b04a8 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "intervention/image": "^3.11" }, "require-dev": { + "ext-fileinfo": "*", "phpunit/phpunit": "^10.0 || ^11.0", "orchestra/testbench": "^8.18" }, diff --git a/src/ImageResponse.php b/src/ImageResponse.php new file mode 100644 index 0000000..a2db8d1 --- /dev/null +++ b/src/ImageResponse.php @@ -0,0 +1,99 @@ + + */ + protected array $options = []; + + /** + * Create new ImageResponse instance + * + * @param Image|EncodedImage $image + * @param null|Format $format + * @param mixed ...$options + * @return void + */ + public function __construct( + protected Image|EncodedImage $image, + protected ?Format $format = null, + mixed ...$options + ) { + $this->options = $options; + } + + /** + * Static factory method + * + * @param Image $image + * @param null|Format $format + * @param mixed ...$options + * @throws NotSupportedException + * @throws DriverException + * @throws RuntimeException + * @return Response + */ + public static function make(Image $image, ?Format $format = null, mixed ...$options): Response + { + $generator = new self($image, $format, ...$options); + + return ResponseFactory::make( + content: $generator->content(), + headers: $generator->headers() + ); + } + + /** + * Read image contents + * + * @throws NotSupportedException + * @throws DriverException + * @throws RuntimeException + * @return string + */ + private function content(): string + { + return (string) $this->image->encodeByMediaType( + $this->format()->mediaType(), + ...$this->options + ); + } + + /** + * Return HTTP response headers to be attached in the image response + * + * @return array + */ + private function headers(): array + { + return [ + 'Content-Type' => $this->format()->mediaType()->value + ]; + } + + /** + * Determine the format in the image response + * + * @return Format + */ + private function format(): Format + { + return $this->format ?? Format::JPEG; + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index cf552a5..29d4eb3 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,8 +4,12 @@ namespace Intervention\Image\Laravel; +use Illuminate\Support\Facades\Response as ResponseFacade; use Illuminate\Support\ServiceProvider as BaseServiceProvider; use Intervention\Image\ImageManager; +use Intervention\Image\Image; +use Illuminate\Http\Response; +use Intervention\Image\Format; class ServiceProvider extends BaseServiceProvider { @@ -21,9 +25,23 @@ class ServiceProvider extends BaseServiceProvider */ public function boot() { + // define config files for publishing $this->publishes([ __DIR__ . '/../config/image.php' => config_path($this::BINDING . '.php') ]); + + // register response macro "image" + if (!ResponseFacade::hasMacro('image')) { + Response::macro( + 'image', + fn( + Image $image, + ?Format $format = null, + mixed ...$options, + ): Response + => ImageResponse::make($image, $format, ...$options) + ); + } } /** diff --git a/tests/ImageResponseTest.php b/tests/ImageResponseTest.php new file mode 100644 index 0000000..e255af1 --- /dev/null +++ b/tests/ImageResponseTest.php @@ -0,0 +1,56 @@ +testImage()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); + } + + public function testNonDefaultFormat(): void + { + $response = ImageResponse::make($this->testImage(), Format::GIF); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/gif', $response->headers->get('content-type')); + $this->assertMimeType('image/gif', $response->content()); + } + + public function testWithEncoderOptions(): void + { + $response = ImageResponse::make($this->testImage(), Format::JPEG, quality: 10); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); + } + + private function assertMimeType(string $mimeType, string $contents): void + { + $detected = (new finfo(FILEINFO_MIME))->buffer($contents); + $this->assertTrue( + str_starts_with($detected, $mimeType), + 'The detected type ' . $detected . ' does not correspond to ' . $mimeType . '.' + ); + } + + private function testImage(): Image + { + return ImageManager::gd()->create(3, 2); + } +} From a45f809c4b058e4545b359b0640c310fdcec7736 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 11:30:09 +0100 Subject: [PATCH 02/12] Edit readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 985b6b5..6279721 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,8 @@ use Intervention\Image\Laravel\Facades\Image; use Intervention\Image\Format; Route::get('/', function () { - $image = Image::read('images/example.jpg'); + $image = Image::read(Storage::get('example.jpg')) + ->place(resource_path('images/watermark.png')); return response()->image($image, Format::WEBP, quality: 65); }); From bf29594ed33fe1a1fdf5cfd8a93c70326d360c41 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 12:32:18 +0100 Subject: [PATCH 03/12] Take origin into account in the encoding process --- src/ImageResponse.php | 8 ++++++-- tests/ImageResponseTest.php | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/ImageResponse.php b/src/ImageResponse.php index a2db8d1..bfa2fac 100644 --- a/src/ImageResponse.php +++ b/src/ImageResponse.php @@ -88,12 +88,16 @@ private function headers(): array } /** - * Determine the format in the image response + * Determine the target format of the image in the HTTP response * * @return Format */ private function format(): Format { - return $this->format ?? Format::JPEG; + if ($this->format) { + return $this->format; + } + + return Format::tryCreate($this->image->origin()->mediaType()) ?? Format::JPEG; } } diff --git a/tests/ImageResponseTest.php b/tests/ImageResponseTest.php index e255af1..6ed001a 100644 --- a/tests/ImageResponseTest.php +++ b/tests/ImageResponseTest.php @@ -22,6 +22,11 @@ public function testDefaultFormat(): void $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); + + $response = ImageResponse::make(ImageManager::gd()->read($this->testImage()->toGif())); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/gif', $response->headers->get('content-type')); + $this->assertMimeType('image/gif', $response->content()); } public function testNonDefaultFormat(): void @@ -30,6 +35,11 @@ public function testNonDefaultFormat(): void $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/gif', $response->headers->get('content-type')); $this->assertMimeType('image/gif', $response->content()); + + $response = ImageResponse::make(ImageManager::gd()->read($this->testImage()->toGif()), Format::JPEG); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); } public function testWithEncoderOptions(): void From 090f20b36b6767a9bb91017612f01703befc2354 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 12:43:00 +0100 Subject: [PATCH 04/12] Implement target format definition via strings --- src/ImageResponse.php | 14 +++++++++----- src/ServiceProvider.php | 2 +- tests/ImageResponseTest.php | 25 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/ImageResponse.php b/src/ImageResponse.php index bfa2fac..f35b198 100644 --- a/src/ImageResponse.php +++ b/src/ImageResponse.php @@ -26,13 +26,13 @@ class ImageResponse * Create new ImageResponse instance * * @param Image|EncodedImage $image - * @param null|Format $format + * @param null|string|Format $format * @param mixed ...$options * @return void */ public function __construct( protected Image|EncodedImage $image, - protected ?Format $format = null, + protected null|string|Format $format = null, mixed ...$options ) { $this->options = $options; @@ -42,14 +42,14 @@ public function __construct( * Static factory method * * @param Image $image - * @param null|Format $format + * @param null|string|Format $format * @param mixed ...$options * @throws NotSupportedException * @throws DriverException * @throws RuntimeException * @return Response */ - public static function make(Image $image, ?Format $format = null, mixed ...$options): Response + public static function make(Image $image, null|string|Format $format = null, mixed ...$options): Response { $generator = new self($image, $format, ...$options); @@ -94,10 +94,14 @@ private function headers(): array */ private function format(): Format { - if ($this->format) { + if ($this->format instanceof Format) { return $this->format; } + if (is_string($this->format)) { + return Format::create($this->format); + } + return Format::tryCreate($this->image->origin()->mediaType()) ?? Format::JPEG; } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 29d4eb3..94bcfa6 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -36,7 +36,7 @@ public function boot() 'image', fn( Image $image, - ?Format $format = null, + null|string|Format $format = null, mixed ...$options, ): Response => ImageResponse::make($image, $format, ...$options) diff --git a/tests/ImageResponseTest.php b/tests/ImageResponseTest.php index 6ed001a..8df5ad1 100644 --- a/tests/ImageResponseTest.php +++ b/tests/ImageResponseTest.php @@ -5,6 +5,7 @@ namespace Intervention\Image\Laravel\Tests; use finfo; +use Intervention\Image\Exceptions\NotSupportedException; use Intervention\Image\Format; use Intervention\Image\Image; use Intervention\Image\ImageManager; @@ -42,6 +43,30 @@ public function testNonDefaultFormat(): void $this->assertMimeType('image/jpeg', $response->content()); } + public function testStringFormat(): void + { + $response = ImageResponse::make($this->testImage(), 'gif'); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/gif', $response->headers->get('content-type')); + $this->assertMimeType('image/gif', $response->content()); + + $response = ImageResponse::make(ImageManager::gd()->read($this->testImage()->toGif()), 'jpg'); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); + + $response = ImageResponse::make(ImageManager::gd()->read($this->testImage()->toGif()), 'image/jpeg'); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); + } + + public function testUnknownFormat(): void + { + $this->expectException(NotSupportedException::class); + ImageResponse::make($this->testImage(), 'unknown'); + } + public function testWithEncoderOptions(): void { $response = ImageResponse::make($this->testImage(), Format::JPEG, quality: 10); From 1a0d2dc63da4330724997f4361093d985dd077a3 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 12:45:41 +0100 Subject: [PATCH 05/12] Register macro by BINDING constant --- src/ServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 94bcfa6..66b9438 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -33,7 +33,7 @@ public function boot() // register response macro "image" if (!ResponseFacade::hasMacro('image')) { Response::macro( - 'image', + $this::BINDING, fn( Image $image, null|string|Format $format = null, From b924bec71797a129e5d3b5a7a00c43494c075ddf Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 12:51:37 +0100 Subject: [PATCH 06/12] Rename ImageResponse::class to ImageResponseFactory::class --- ...eResponse.php => ImageResponseFactory.php} | 23 ++++++++++++++----- src/ServiceProvider.php | 2 +- ...eTest.php => ImageResponseFactoryTest.php} | 22 +++++++++--------- 3 files changed, 29 insertions(+), 18 deletions(-) rename src/{ImageResponse.php => ImageResponseFactory.php} (82%) rename tests/{ImageResponseTest.php => ImageResponseFactoryTest.php} (75%) diff --git a/src/ImageResponse.php b/src/ImageResponseFactory.php similarity index 82% rename from src/ImageResponse.php rename to src/ImageResponseFactory.php index f35b198..9331b29 100644 --- a/src/ImageResponse.php +++ b/src/ImageResponseFactory.php @@ -13,7 +13,7 @@ use Intervention\Image\Format; use Intervention\Image\Image; -class ImageResponse +class ImageResponseFactory { /** * Image encoder options @@ -23,7 +23,7 @@ class ImageResponse protected array $options = []; /** - * Create new ImageResponse instance + * Create new ImageResponseFactory instance * * @param Image|EncodedImage $image * @param null|string|Format $format @@ -39,7 +39,7 @@ public function __construct( } /** - * Static factory method + * Static factory method to create HTTP response directly * * @param Image $image * @param null|string|Format $format @@ -51,11 +51,22 @@ public function __construct( */ public static function make(Image $image, null|string|Format $format = null, mixed ...$options): Response { - $generator = new self($image, $format, ...$options); + return (new self($image, $format, ...$options))->response(); + } + /** + * Create HTTP response + * + * @throws NotSupportedException + * @throws DriverException + * @throws RuntimeException + * @return Response + */ + public function response(): Response + { return ResponseFactory::make( - content: $generator->content(), - headers: $generator->headers() + content: $this->content(), + headers: $this->headers() ); } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 66b9438..d408591 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -39,7 +39,7 @@ public function boot() null|string|Format $format = null, mixed ...$options, ): Response - => ImageResponse::make($image, $format, ...$options) + => ImageResponseFactory::make($image, $format, ...$options) ); } } diff --git a/tests/ImageResponseTest.php b/tests/ImageResponseFactoryTest.php similarity index 75% rename from tests/ImageResponseTest.php rename to tests/ImageResponseFactoryTest.php index 8df5ad1..0b2d32a 100644 --- a/tests/ImageResponseTest.php +++ b/tests/ImageResponseFactoryTest.php @@ -9,22 +9,22 @@ use Intervention\Image\Format; use Intervention\Image\Image; use Intervention\Image\ImageManager; -use Intervention\Image\Laravel\ImageResponse; +use Intervention\Image\Laravel\ImageResponseFactory; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as TestBenchTestCase; -class ImageResponseTest extends TestBenchTestCase +class ImageResponseFactoryTest extends TestBenchTestCase { use WithWorkbench; public function testDefaultFormat(): void { - $response = ImageResponse::make($this->testImage()); + $response = ImageResponseFactory::make($this->testImage()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); - $response = ImageResponse::make(ImageManager::gd()->read($this->testImage()->toGif())); + $response = ImageResponseFactory::make(ImageManager::gd()->read($this->testImage()->toGif())); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/gif', $response->headers->get('content-type')); $this->assertMimeType('image/gif', $response->content()); @@ -32,12 +32,12 @@ public function testDefaultFormat(): void public function testNonDefaultFormat(): void { - $response = ImageResponse::make($this->testImage(), Format::GIF); + $response = ImageResponseFactory::make($this->testImage(), Format::GIF); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/gif', $response->headers->get('content-type')); $this->assertMimeType('image/gif', $response->content()); - $response = ImageResponse::make(ImageManager::gd()->read($this->testImage()->toGif()), Format::JPEG); + $response = ImageResponseFactory::make(ImageManager::gd()->read($this->testImage()->toGif()), Format::JPEG); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); @@ -45,17 +45,17 @@ public function testNonDefaultFormat(): void public function testStringFormat(): void { - $response = ImageResponse::make($this->testImage(), 'gif'); + $response = ImageResponseFactory::make($this->testImage(), 'gif'); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/gif', $response->headers->get('content-type')); $this->assertMimeType('image/gif', $response->content()); - $response = ImageResponse::make(ImageManager::gd()->read($this->testImage()->toGif()), 'jpg'); + $response = ImageResponseFactory::make(ImageManager::gd()->read($this->testImage()->toGif()), 'jpg'); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); - $response = ImageResponse::make(ImageManager::gd()->read($this->testImage()->toGif()), 'image/jpeg'); + $response = ImageResponseFactory::make(ImageManager::gd()->read($this->testImage()->toGif()), 'image/jpeg'); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); @@ -64,12 +64,12 @@ public function testStringFormat(): void public function testUnknownFormat(): void { $this->expectException(NotSupportedException::class); - ImageResponse::make($this->testImage(), 'unknown'); + ImageResponseFactory::make($this->testImage(), 'unknown'); } public function testWithEncoderOptions(): void { - $response = ImageResponse::make($this->testImage(), Format::JPEG, quality: 10); + $response = ImageResponseFactory::make($this->testImage(), Format::JPEG, quality: 10); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); From 6fe9483f6153e4a0cf20716e1bbd4cc08a1d9d83 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 16:07:29 +0100 Subject: [PATCH 07/12] Improve readme --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6279721..537320b 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,29 @@ In your existing Laravel application you can install this package using [Compose composer require intervention/image-laravel ``` -Next, add the configuration files to your application using the `vendor:publish` command: +## Features + +Although Intervention Image can be used with Laravel without this extension, +this intergration package includes the following features that make image +interaction with the framework much easier. + +### Application-wide configuration + +The extension comes with a global configuration file that is recognized by +Laravel. It is therefore possible to store the settings for Intervention Image +once centrally and not have to define them individually each time you call the +image manager. + +The configuration file can be copied to the application with the following command. ```bash php artisan vendor:publish --provider="Intervention\Image\Laravel\ServiceProvider" ``` -This command will publish the configuration file `image.php` for the image -integration to your `app/config` directory. In this file you can set the -desired driver and its configuration options for Intervention Image. By default -the library is configured to use GD library for image processing. +This command will publish the configuration file `config/image.php`. Here you +can set the desired driver and its configuration options for Intervention +Image. By default the library is configured to use GD library for image +processing. The configuration files looks like this. @@ -89,24 +102,50 @@ You can read more about the different options for [decoding animations](https://image.intervention.io/v3/modifying/animations) and [blending color](https://image.intervention.io/v3/basics/colors#transparency). -## Getting started +### Static Facade Interface -The integration is now complete and it is possible to access the [ImageManager](https://image.intervention.io/v3/basics/instantiation) -via Laravel's facade system. The package also includes a response macro that can be used to elegantly convert an image resource into an HTTP response. +This package also integrates access to Intervention Image's central entry +point, the `ImageManager::class`, via a static [facade](https://laravel.com/docs/11.x/facades). The call provides access to the +centrally configured [image manager](https://image.intervention.io/v3/basics/instantiation) via singleton pattern. + +The following code example shows how to read an image with the image facade in a Laravel route. ```php +use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Storage; use Intervention\Image\Laravel\Facades\Image; -use Intervention\Image\Format; Route::get('/', function () { $image = Image::read(Storage::get('example.jpg')) - ->place(resource_path('images/watermark.png')); + ->resize(300, 200); +}); +``` + +### Image Response Macro + +Furthermore, the package also includes a response macro that can be used to +elegantly convert an image resource into an HTTP response. + +The following code example shows how to read an image from a upload request +place and use the image response macro to encode it and send the image back to +the user in one call. Only the first parameter is required. + +```php +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Storage; +use Intervention\Image\Laravel\Facades\Image; + +Route::get('/', function (Request $request) { + $upload = $request->file('image'); + $image = Image::read($request) + ->scale(300, 200); return response()->image($image, Format::WEBP, quality: 65); }); ``` -Read the [official documentation of Intervention Image](https://image.intervention.io) for more information. +You can read more about intervention image in general in the [official documentation of Intervention Image](https://image.intervention.io). ## Authors From fc9f6e2f06268893193a5ac36ff0fde729c3f781 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 16:33:05 +0100 Subject: [PATCH 08/12] Improve documentation --- README.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 537320b..1c52c2a 100644 --- a/README.md +++ b/README.md @@ -108,16 +108,26 @@ This package also integrates access to Intervention Image's central entry point, the `ImageManager::class`, via a static [facade](https://laravel.com/docs/11.x/facades). The call provides access to the centrally configured [image manager](https://image.intervention.io/v3/basics/instantiation) via singleton pattern. -The following code example shows how to read an image with the image facade in a Laravel route. +The following code example shows how to read an image from an upload request +the image facade in a Laravel route and save it on disk with a random file +name. ```php +use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; use Intervention\Image\Laravel\Facades\Image; -Route::get('/', function () { - $image = Image::read(Storage::get('example.jpg')) +Route::get('/', function (Request $request) { + $upload = $request->file('image'); + $image = Image::read($upload) ->resize(300, 200); + + Storage::put( + Str::random() . '.' . $upload->getClientOriginalExtension(), + $image->encodeByExtension($upload->getClientOriginalExtension(), quality: 70) + ); }); ``` @@ -126,19 +136,18 @@ Route::get('/', function () { Furthermore, the package also includes a response macro that can be used to elegantly convert an image resource into an HTTP response. -The following code example shows how to read an image from a upload request -place and use the image response macro to encode it and send the image back to +The following code example shows how to read an image from disk +apply modifications and use the image response macro to encode it and send the image back to the user in one call. Only the first parameter is required. ```php -use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; +use Intervention\Image\Format; use Intervention\Image\Laravel\Facades\Image; -Route::get('/', function (Request $request) { - $upload = $request->file('image'); - $image = Image::read($request) +Route::get('/', function () { + $image = Image::read(Storage::get('example.jpg')) ->scale(300, 200); return response()->image($image, Format::WEBP, quality: 65); From a8375507be252f424330c8fb342eb1676d212727 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 16:56:33 +0100 Subject: [PATCH 09/12] Fix bug --- src/ServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index d408591..b2bf72e 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -31,7 +31,7 @@ public function boot() ]); // register response macro "image" - if (!ResponseFacade::hasMacro('image')) { + if (!ResponseFacade::hasMacro($this::BINDING)) { Response::macro( $this::BINDING, fn( From e9357c20345a3dd02bcfa8d8e69fbc9f8e84a01a Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 17:09:23 +0100 Subject: [PATCH 10/12] Enable format definition via enum values of MediaType and FileExtension --- src/ImageResponseFactory.php | 23 +++++-- src/ServiceProvider.php | 4 +- tests/ImageResponseFactoryTest.php | 101 +++++++++++++++++++++++++---- 3 files changed, 108 insertions(+), 20 deletions(-) diff --git a/src/ImageResponseFactory.php b/src/ImageResponseFactory.php index 9331b29..35b98a0 100644 --- a/src/ImageResponseFactory.php +++ b/src/ImageResponseFactory.php @@ -10,8 +10,10 @@ use Intervention\Image\Exceptions\DriverException; use Intervention\Image\Exceptions\NotSupportedException; use Intervention\Image\Exceptions\RuntimeException; +use Intervention\Image\FileExtension; use Intervention\Image\Format; use Intervention\Image\Image; +use Intervention\Image\MediaType; class ImageResponseFactory { @@ -26,13 +28,13 @@ class ImageResponseFactory * Create new ImageResponseFactory instance * * @param Image|EncodedImage $image - * @param null|string|Format $format + * @param null|string|Format|MediaType|FileExtension $format * @param mixed ...$options * @return void */ public function __construct( protected Image|EncodedImage $image, - protected null|string|Format $format = null, + protected null|string|Format|MediaType|FileExtension $format = null, mixed ...$options ) { $this->options = $options; @@ -42,15 +44,18 @@ public function __construct( * Static factory method to create HTTP response directly * * @param Image $image - * @param null|string|Format $format + * @param null|string|Format|MediaType|FileExtension $format * @param mixed ...$options * @throws NotSupportedException * @throws DriverException * @throws RuntimeException * @return Response */ - public static function make(Image $image, null|string|Format $format = null, mixed ...$options): Response - { + public static function make( + Image $image, + null|string|Format|MediaType|FileExtension $format = null, + mixed ...$options, + ): Response { return (new self($image, $format, ...$options))->response(); } @@ -109,6 +114,14 @@ private function format(): Format return $this->format; } + if (($this->format instanceof MediaType)) { + return $this->format->format(); + } + + if ($this->format instanceof FileExtension) { + return $this->format->format(); + } + if (is_string($this->format)) { return Format::create($this->format); } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index b2bf72e..e90f525 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -9,7 +9,9 @@ use Intervention\Image\ImageManager; use Intervention\Image\Image; use Illuminate\Http\Response; +use Intervention\Image\FileExtension; use Intervention\Image\Format; +use Intervention\Image\MediaType; class ServiceProvider extends BaseServiceProvider { @@ -36,7 +38,7 @@ public function boot() $this::BINDING, fn( Image $image, - null|string|Format $format = null, + null|string|Format|MediaType|FileExtension $format = null, mixed ...$options, ): Response => ImageResponseFactory::make($image, $format, ...$options) diff --git a/tests/ImageResponseFactoryTest.php b/tests/ImageResponseFactoryTest.php index 0b2d32a..61b5980 100644 --- a/tests/ImageResponseFactoryTest.php +++ b/tests/ImageResponseFactoryTest.php @@ -6,10 +6,12 @@ use finfo; use Intervention\Image\Exceptions\NotSupportedException; +use Intervention\Image\FileExtension; use Intervention\Image\Format; use Intervention\Image\Image; use Intervention\Image\ImageManager; use Intervention\Image\Laravel\ImageResponseFactory; +use Intervention\Image\MediaType; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as TestBenchTestCase; @@ -17,14 +19,21 @@ class ImageResponseFactoryTest extends TestBenchTestCase { use WithWorkbench; + protected Image $image; + + protected function setUp(): void + { + $this->image = ImageManager::gd()->create(3, 2)->fill('f50'); + } + public function testDefaultFormat(): void { - $response = ImageResponseFactory::make($this->testImage()); + $response = ImageResponseFactory::make($this->image); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); - $response = ImageResponseFactory::make(ImageManager::gd()->read($this->testImage()->toGif())); + $response = ImageResponseFactory::make(ImageManager::gd()->read($this->image->toGif())); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/gif', $response->headers->get('content-type')); $this->assertMimeType('image/gif', $response->content()); @@ -32,12 +41,18 @@ public function testDefaultFormat(): void public function testNonDefaultFormat(): void { - $response = ImageResponseFactory::make($this->testImage(), Format::GIF); + $response = ImageResponseFactory::make( + $this->image, + Format::GIF, + ); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/gif', $response->headers->get('content-type')); $this->assertMimeType('image/gif', $response->content()); - $response = ImageResponseFactory::make(ImageManager::gd()->read($this->testImage()->toGif()), Format::JPEG); + $response = ImageResponseFactory::make( + ImageManager::gd()->read($this->image->toGif()), + Format::JPEG, + ); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); @@ -45,17 +60,80 @@ public function testNonDefaultFormat(): void public function testStringFormat(): void { - $response = ImageResponseFactory::make($this->testImage(), 'gif'); + $response = ImageResponseFactory::make( + $this->image, + 'gif', + ); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/gif', $response->headers->get('content-type')); + $this->assertMimeType('image/gif', $response->content()); + + $response = ImageResponseFactory::make( + ImageManager::gd()->read($this->image->toGif()), + 'jpg', + ); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); + + $response = ImageResponseFactory::make( + ImageManager::gd()->read($this->image->toGif()), + 'image/jpeg', + ); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); + } + + public function testMediaTypeFormat(): void + { + $response = ImageResponseFactory::make( + $this->image, + MediaType::IMAGE_GIF, + ); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/gif', $response->headers->get('content-type')); $this->assertMimeType('image/gif', $response->content()); - $response = ImageResponseFactory::make(ImageManager::gd()->read($this->testImage()->toGif()), 'jpg'); + $response = ImageResponseFactory::make( + ImageManager::gd()->read($this->image->toGif()), + MediaType::IMAGE_JPEG, + ); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); - $response = ImageResponseFactory::make(ImageManager::gd()->read($this->testImage()->toGif()), 'image/jpeg'); + $response = ImageResponseFactory::make( + ImageManager::gd()->read($this->image->toGif()), + MediaType::IMAGE_JPEG, + ); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); + } + + public function testFileExtensionFormat(): void + { + $response = ImageResponseFactory::make( + $this->image, + FileExtension::GIF + ); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/gif', $response->headers->get('content-type')); + $this->assertMimeType('image/gif', $response->content()); + + $response = ImageResponseFactory::make( + ImageManager::gd()->read($this->image->toGif()), + FileExtension::JPG + ); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('image/jpeg', $response->headers->get('content-type')); + $this->assertMimeType('image/jpeg', $response->content()); + + $response = ImageResponseFactory::make( + ImageManager::gd()->read($this->image->toGif()), + FileExtension::JPEG + ); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); @@ -64,12 +142,12 @@ public function testStringFormat(): void public function testUnknownFormat(): void { $this->expectException(NotSupportedException::class); - ImageResponseFactory::make($this->testImage(), 'unknown'); + ImageResponseFactory::make($this->image, 'unknown'); } public function testWithEncoderOptions(): void { - $response = ImageResponseFactory::make($this->testImage(), Format::JPEG, quality: 10); + $response = ImageResponseFactory::make($this->image, Format::JPEG, quality: 10); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('image/jpeg', $response->headers->get('content-type')); $this->assertMimeType('image/jpeg', $response->content()); @@ -83,9 +161,4 @@ private function assertMimeType(string $mimeType, string $contents): void 'The detected type ' . $detected . ' does not correspond to ' . $mimeType . '.' ); } - - private function testImage(): Image - { - return ImageManager::gd()->create(3, 2); - } } From cf612a36e5234b8b86f801cf6e4174aebd9b75a4 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 17:12:56 +0100 Subject: [PATCH 11/12] Refactor code --- src/ServiceProvider.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index e90f525..6d73456 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -34,15 +34,13 @@ public function boot() // register response macro "image" if (!ResponseFacade::hasMacro($this::BINDING)) { - Response::macro( - $this::BINDING, - fn( - Image $image, - null|string|Format|MediaType|FileExtension $format = null, - mixed ...$options, - ): Response - => ImageResponseFactory::make($image, $format, ...$options) - ); + Response::macro($this::BINDING, function ( + Image $image, + null|string|Format|MediaType|FileExtension $format = null, + mixed ...$options, + ): Response { + return ImageResponseFactory::make($image, $format, ...$options); + }); } } @@ -58,7 +56,7 @@ public function register() $this::BINDING ); - $this->app->singleton($this::BINDING, function ($app) { + $this->app->singleton($this::BINDING, function () { return new ImageManager( driver: config('image.driver'), autoOrientation: config('image.options.autoOrientation', true), From c0981843b4284580f584799aa41135e61143718c Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 2 Feb 2025 17:16:16 +0100 Subject: [PATCH 12/12] Move BINDING constant --- src/Facades/Image.php | 7 ++++++- src/ServiceProvider.php | 15 +++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Facades/Image.php b/src/Facades/Image.php index 095a488..5181122 100644 --- a/src/Facades/Image.php +++ b/src/Facades/Image.php @@ -13,8 +13,13 @@ */ class Image extends Facade { + /** + * Binding name of the service container + */ + public const BINDING = 'image'; + protected static function getFacadeAccessor() { - return 'image'; + return self::BINDING; } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 6d73456..7e7ab70 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -15,11 +15,6 @@ class ServiceProvider extends BaseServiceProvider { - /** - * Binding name of the service container - */ - protected const BINDING = 'image'; - /** * Bootstrap application events * @@ -29,12 +24,12 @@ public function boot() { // define config files for publishing $this->publishes([ - __DIR__ . '/../config/image.php' => config_path($this::BINDING . '.php') + __DIR__ . '/../config/image.php' => config_path(Facades\Image::BINDING . '.php') ]); // register response macro "image" - if (!ResponseFacade::hasMacro($this::BINDING)) { - Response::macro($this::BINDING, function ( + if (!ResponseFacade::hasMacro(Facades\Image::BINDING)) { + Response::macro(Facades\Image::BINDING, function ( Image $image, null|string|Format|MediaType|FileExtension $format = null, mixed ...$options, @@ -53,10 +48,10 @@ public function register() { $this->mergeConfigFrom( __DIR__ . '/../config/image.php', - $this::BINDING + Facades\Image::BINDING ); - $this->app->singleton($this::BINDING, function () { + $this->app->singleton(Facades\Image::BINDING, function () { return new ImageManager( driver: config('image.driver'), autoOrientation: config('image.options.autoOrientation', true),