intervention/image-laravel
Laravel integration for Intervention Image that provides a service provider, facade, and publishable configuration file, letting you set the image driver and options once centrally instead of repeating them on every image manager call.
This package is a thin but genuinely useful glue layer: it registers a singleton ImageManager, a facade, a publishable config file, and a response()->image() macro. Almost all real work is done by intervention/image itself, so treat this repo as "wiring + config" and the Intervention Image v4 docs as your actual API reference.
composer require intervention/image-laravel
php artisan vendor:publish --provider="Intervention\Image\Laravel\ServiceProvider"
That drops config/intervention-image.php. Do not edit a config/image.php unless it already exists from an older install — v4.1.0 renamed the file to avoid a collision with Laravel 13.20's own image integration (the old name is still detected via a fallback, but it's deprecated behaviour).
Open the published config first. It tells you everything about how the manager will behave before you write a single line of image code:
return [
'driver' => env('IMAGE_DRIVER', \Intervention\Image\Drivers\Gd\Driver::class),
'options' => [
'autoOrientation' => true, // honour EXIF rotation on decode
'decodeAnimation' => true, // keep GIF/WebP animation frames
'backgroundColor' => 'ffffff',
'strip' => false, // drop EXIF/metadata on encode
],
];
Then verify the driver actually exists in your runtime — GD is bundled with most PHP builds, Imagick needs ext-imagick, and Vips needs ext-vips. A missing extension is the #1 cause of "it works locally, dies in production".
The canonical "day one" task, using the facade:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Laravel\Facades\Image;
Route::post('/avatar', function (Request $request) {
$upload = $request->file('avatar');
$image = Image::decode($upload)->cover(400, 400);
Storage::disk('public')->put(
'avatars/' . Str::uuid() . '.' . $upload->getClientOriginalExtension(),
$image->encodeUsingFileExtension($upload->getClientOriginalExtension(), quality: 85)
);
});
For an UploadedFile specifically, decodeSplFileInfo($request->file('image')) is the more explicit form and avoids any ambiguity about what decode() will sniff.
There are three equivalent entry points; mixing them in one codebase causes confusion during review:
| Style | Use when |
|---|---|
Image::decode(...) facade |
Controllers, routes, one-off scripts. Zero ceremony. |
ImageManagerInterface injected |
Services, actions, jobs — testable, swappable, no static coupling. |
app('intervention.image') |
Legacy/container access only. |
Dependency injection is the cleanest for anything you intend to unit test:
use Intervention\Image\Interfaces\ImageManagerInterface;
class StoreThumbnail
{
public function __construct(
private ImageManagerInterface $manager,
private Filesystem $storage,
) {}
public function handle(string $path): string
{
$image = $this->manager->decode($this->storage->get($path))->scale(width: 800);
$out = 'thumbs/' . basename($path);
$this->storage->put($out, $image->encodeUsingFileExtension('jpg', quality: 75));
return $out;
}
}
Both ImageManager::class and ImageManagerInterface::class are bound as configured singletons (since 4.0.1), so DI gives you the same instance the facade uses — config changes are picked up identically either way.
Rather than sprinkling resize() calls everywhere, centralise the variant matrix. This is the pattern that pays for itself once you have more than one image size:
class ImagePipeline
{
public function __construct(private ImageManagerInterface $manager) {}
public function variant(string $source, string $variant): string
{
$image = $this->manager->decode($source);
$image = match ($variant) {
'thumb' => $image->cover(200, 200),
'card' => $image->cover(640, 360),
'hero' => $image->scale(width: 1920),
default => throw new InvalidArgumentException("Unknown variant [$variant]"),
};
return (string) $image->encodeUsingFileExtension('webp', quality: 80);
}
}
Because backgroundColor and strip come from config, a cover() on a transparent PNG will get the configured background colour automatically — set it once, not per call.
The response macro removes the encode-then-response()->make() dance:
use Intervention\Image\Format;
use Intervention\Image\Laravel\Facades\Image;
use Illuminate\Support\Facades\Storage;
Route::get('/og/{slug}', function (string $slug) {
$image = Image::decode(Storage::get("og/{$slug}.jpg"))->scale(width: 1200);
return response()->image($image, Format::WEBP, quality: 65);
});
Only the first argument is required — skip Format:: and you get the image's own format. Add cache headers with a normal ->header() chain; the macro returns a regular response instance.
Decoding a 12 MP JPEG costs tens of megabytes of RAM. In a request cycle that's a timeout waiting to happen; in a jobs table row it's fine:
class GenerateVariants implements ShouldQueue
{
use Queueable;
public int $timeout = 120;
public function __construct(public string $path) {}
public function handle(ImageManagerInterface $manager, ImagePipeline $pipeline): void
{
$raw = Storage::disk('s3')->get($this->path);
foreach (['thumb', 'card', 'hero'] as $variant) {
Storage::disk('s3')->put(
"derived/{$variant}/" . basename($this->path),
$pipeline->variant($raw, $variant)
);
}
}
}
Note: read the file into memory once and reuse it ($raw) rather than re-fetching per variant.
put(); encodeUsingFileExtension('png') returns an encoded image object, not a string — cast with (string) or let Storage handle it. Never mix the extension argument with a mismatched destination filename.image|mimes:jpg,png,webp|max:5120 rules. This package does not validate uploads; decode() will happily throw on a corrupt file.Storage::fake('public') + the facade works fine in tests; the manager singleton is resolved from the container so config overrides via config(['intervention-image.driver' => ...]) in setUp() are honoured.The README itself is inconsistent across versions because the underlying library renamed methods. If you're on intervention/image v4 (this package's 4.x line), use:
Image::decode($data) — not Image::read() (that was v1.x of this package / v3 of the core)encodeUsingFileExtension() / encodeUsingFormat() — check whichever your core version exposesFormat::WEBP enum — not the string 'webp'Copying snippets from blog posts written before mid-2024 is the most common source of BadMethodCallException.
Laravel shipped its own Intervention-based image integration in 13.20, and it registers a facade alias image and a config/image.php. If you're on 13.20+:
app('intervention.image'), not app('image')config/intervention-image.php exists; the old config/image.php is read as a fallback but will silently lose to Laravel's own config in confusing ways>=4.1.0 — 4.1.1 additionally fixed wrong config keys, so pin "intervention/image-laravel": "^4.1.1"This is a young, low-star package with a small surface area; pin minor versions and read release notes before composer update.
Because the driver is constructed from the options array at container-resolution time, malformed keys are ignored rather than throwing. If autoOrientation appears to have no effect:
autoOrientation, decodeAnimation, backgroundColor, strip)php artisan config:clear — an opcache'd config cache is the usual culpritOrientation tag; GD strips it on some decode pathsAlso remember 'strip' => false is the default: if you care about privacy (GPS coordinates in phone photos), set it to true — this is a one-line, high-impact change.
It's registered lazily when ResponseFactory is resolved, and is deliberately disabled in console context (since 1.5.3, refined in 1.5.6). Calling response()->image() from a queued job, an artisan command, or a phpunit assertion that hits the console kernel will fail. Encode manually in those contexts:
$bytes = (string) $image->encodeUsingFileExtension('jpg', quality: 70);
Imagick for CMYK source files, ICC profiles, or serious print work.decodeAnimation defaults to true — decoding a 200-frame GIF into memory is a denial-of-service vector on user uploads. Set IMAGE_DRIVER-adjacent config or override per call for untrusted input, and cap dimensions before processing.memory_limit and consider ini_set('memory_limit', ...) scoped around the job; GD needs roughly width × height × 4 bytes plus headroom.new ImageManager(new Driver(...)) for occasional one-off drivers rather than mutating config mid-request.There is very little to extend here — no custom drivers, no middleware, no events. Your extension surface is:
image.intervention container binding — rebind it in a service provider if you need a differently-configured manager (e.g. a strip: true instance for public uploads alongside the default).If you find yourself wanting more than this, you want the features in intervention/image itself, not the Laravel wrapper.
How can I help you explore Laravel packages today?