Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Image Laravel Package

intervention/image

Intervention Image is a PHP image manipulation library with a simple API for resizing, cropping, filters, and format conversion using GD or Imagick. It integrates smoothly with Laravel and is ideal for handling uploads and dynamic image processing.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and pick a driver

composer require intervention/image

You also need an image extension compiled in: ext-gd (everywhere, weaker) or ext-imagick (ImageMagick + more formats/colorspace control). Version 4 requires PHP 8.3+, version 3 requires PHP 8.1+, and v4 adds libvips as a third driver. Verify what you actually have before writing any code:

php -m | grep -Ei 'gd|imagick|vips'
php -r "print_r(gd_info());"

2. Laravel projects: install the framework bridge instead of using the library raw.

composer require intervention/image-laravel
php artisan vendor:publish --tag=image   # config/image.php -> pick 'gd' or 'imagick'

That gives you the Image facade and container bindings; the config file is the first place to look when behaviour differs between local and production.

3. Where to look first

Class Why you care
Intervention\Image\ImageManager Entry point. ImageManager::gd(), ::imagick(), ::vips(). Stateless — create once.
Intervention\Image\Image The fluent API you'll spend 95% of your time in.
Intervention\Image\Drivers\{Gd,Imagick}\* Concrete modifiers/encoders/decoders. This is where driver-specific gaps live.
Intervention\Image\Encoders\* JpegEncoder, WebpEncoder, PngEncoder, AvifEncoder, JxlEncoder, …

4. First real use case — normalise an upload

use Intervention\Image\ImageManager;

$manager = ImageManager::gd();          // or ::imagick(), ::vips()

$image = $manager->read($request->file('photo'));   // path, string, UploadedFile, GDImage, Imagick, PSR-7 stream/response (4.4.0+)

$image->scaleDown(width: 1600)          // never upscale
      ->toWebp(quality: 82)
      ->save(storage_path('app/public/uploads/'.$name.'.webp'));

That one chain — read, constrain, re-encode, save — is the heart of ~80% of production usage. Re-encoding also strips EXIF and neutralises malformed/polyglot files, so treat it as a security step, not just a size optimisation.

Implementation Patterns

Bind the manager once

ImageManager carries no request state but does cache driver instances and config. Don't new it in every controller.

// AppServiceProvider
$this->app->singleton(ImageManager::class, fn () => ImageManager::imagick(
    autoOrientation: true,   // v3.7+ config API
    decodeAnimation: false,  // skip GIF frame decoding unless you need it
));

Disabling animation decoding is a meaningful memory win if you only ever serve static thumbnails.

Reading from every source you'll meet

$manager->read('storage/foo.jpg');                // path
$manager->read($binaryString);                    // raw bytes
$manager->read($request->file('avatar'));         // UploadedFile
$manager->read($gdImageResourceOrObject);         // GDImage (v3.6+)
$manager->read($psr7StreamOrResponse);            // PSR-7 (v4.4+)
$manager->read($encodedImage);                    // EncodedImage (v3.8+)

The EncodedImage case matters for queued pipelines: encode once on the web node, hand the EncodedImage to the job, decode it there.

Understand the four resize verbs

They are not interchangeable, and picking wrong is the #1 source of "the image looks stretched" tickets.

$image->resize(300, 200);      // exact box, will distort aspect ratio
$image->scale(width: 300);     // proportional, one dimension
$image->scaleDown(height: 300);// proportional AND never enlarges
$image->cover(300, 200);       // fill box, crop overflow  (thumbnails/grids)
$image->contain(300, 200);     // fit inside box, letterbox (product shots)
$image->pad(300, 200);         // fit inside box, pad to exact size

Reach for cover() for avatar/thumbnail grids, contain() or pad() when nothing may be cropped. Use scaleDown()/coverDown() anywhere user input determines the target size, or you'll be upscaling 200px avatars to blurry 4000px hero images.

Chain order is a design decision

Modifiers apply sequentially to the canvas, so order changes output and cost.

$image->trim()                     // drop borders first
      ->cover(1200, 630)
      ->sharpen(8)                 // sharpen after downscale, never before
      ->place($logo, 'bottom-right', 24, 24, 70)
      ->text('© ACME', 24, 24, fn ($font) => $font
            ->file(resource_path('fonts/Inter-Regular.ttf'))
            ->size(28)
            ->color('#ffffff'));

Crop before resize (cheaper on fewer pixels), sharpen last, overlays after all geometry. Text drawn after a resize keeps the font size you specified — it does not scale with the image, which is usually what you want for watermarks.

Encoders: use encode() when you need options

The shorthands (toJpeg(80), toWebp(82), toPng(), toAvif(), toGif(), toJxl()) cover the common case. For anything else, construct the encoder — this is also the clearest way to express intent in review:

use Intervention\Image\Encoders\JpegEncoder;
use Intervention\Image\Encoders\PngEncoder;

$image->encode(new JpegEncoder(quality: 82, progressive: true));
$image->encode(new PngEncoder(strip: true));           // remove metadata
$image->toWebp(quality: 78);

strip on the encoder is the correct place to drop EXIF/ICC — not a post-processing step. Note that unknown options passed to encode() are filtered out per format (v3.6.1+), so a shared options array won't blow up on WebP.

A responsive-variant helper worth copying

final class ResponsiveImage
{
    public function __construct(private ImageManager $manager) {}

    /** @return array<string,string> width => relative path */
    public function generate(string $sourcePath, string $slug, array $widths = [400, 800, 1600]): array
    {
        $out = [];
        foreach ($widths as $w) {
            $target = "derived/{$slug}-{$w}.webp";
            $absolute = storage_path("app/public/{$target}");

            if (! is_file($absolute)) {
                $this->manager->read($sourcePath)
                    ->scaleDown(width: $w)
                    ->toWebp(quality: 80)
                    ->save($absolute);
            }
            $out[$w] = $target;
        }
        return $out;
    }
}

Filename derived from content hash, existence check as cache, scaleDown so small originals don't get upscaled.

Push heavy work into queues

Decoding a 12MP JPEG costs ~50 MB in GD and considerably more in Imagick. Never do a batch of them in a web request.

dispatch(new GenerateThumbnails($mediaId))->onQueue('images');

In the job, raise the limit explicitly and let the worker die rather than the whole node:

ini_set('memory_limit', '512M');

Run image workers on a separate queue so a memory spike doesn't starve mail/notification jobs.

Custom modifiers and encoders are the extension point

If a transformation repeats across the codebase, encapsulate it:

use Intervention\Image\Interfaces\ModifierInterface;
use Intervention\Image\Interfaces\ImageInterface;

final class SquareThumb implements ModifierInterface
{
    public function __construct(private int $size = 256) {}

    public function apply(ImageInterface $image): ImageInterface
    {
        return $image->cover($this->size, $this->size);
    }
}

$image->modify(new SquareThumb(320));

Custom EncoderInterface/DecoderInterface implementations slot in the same way, and v3.11.4+ relaxes the namespace constraint, so your drivers can live in App\Image\....

Test with tiny fixtures

Don't assert on pixel-perfect output across GD and Imagick — they genuinely differ. Assert on dimensions, MIME, and byte non-emptiness:

$image = ImageManager::gd()->read(__DIR__.'/fixtures/landscape.jpg');
$result = $image->cover(200, 200);

$this->assertSame(200, $result->width());
$this->assertSame(200, $result->height());
$this->assertSame('image/webp', $result->toWebp()->mimeType());

Keep 3–4 fixture files under 20 KB (one landscape, one portrait, one with alpha, one animated GIF) and use var_dump() freely — __debugInfo() (v3.10+) prints {width, height} for Image and the channel values for color objects, which beats dumping a resource.

Gotchas and Tips

Memory is the failure mode, not CPU. GD holds an uncompressed bitmap: roughly width × height × 4 bytes, plus overhead. A 6000×4000 input is ~96 MB before any modifier allocates a second canvas. Reject oversized uploads before read() using the cheap header read, and set an explicit ceiling:

[$w, $h] = getimagesize($tmpPath) ?: [0, 0];
abort_if($w * $h > 40_000_000, 422, 'Image too large');

This is also your decompression-bomb guard — a 300 KB PNG can decode to gigabytes.

save() uses filesystem paths, not disks. In Laravel, $image->save('uploads/x.jpg') writes relative to the process CWD, which will not be what you expect. Always pass an absolute path — Storage::path($relative) — or use Storage::put($path, $image->toJpeg(80)->toString()).

Transparency dies on JPEG. Flatten explicitly or you'll get black backgrounds:

$image->fill('#ffffff')       // or place on a canvas
      ->toJpeg(85);

Same trap when contain()/pad() letterbox with a default background on an alpha image.

EXIF orientation. autoOrientation: true is the default, but if you've disabled it — or you're decoding raw GDImage/Imagick objects, which carry no EXIF — you must call ->orientate() yourself. Phone uploads appearing sideways in production is almost always this.

Driver parity is not a thing. GD has no real colour-profile handling, weaker filters, and limited font rendering; Imagick handles CMYK/ICC, YCbCr (converted to sRGB since 4.1.5), and multi-frame animation properly. If you accept print-ready or camera-original files, use Imagick and say so in your composer/platform requirements. Check capability at boot rather than discovering it in production:

$manager->driver()->version();               // v3.11+
$manager->driver()->supports('image/avif');  // format detection (v3.6+)

Animated GIF/WebP will bite you. Decoding all frames is expensive and coalescing is subtle (there were several fixes across 4.2.x/4.3.x). If you only need a still thumbnail, disable animation decoding in the manager config and take the first frame.

Don't re-encode to re-save. EncodedImage holds a temp stream (v3.9+), and saving the same instance twice was broken before v3.11.2. If you need two destinations, either stay on a current release or encode twice — it's cheap relative to a bug.

Font paths must be absolute. Relative font paths resolved differently across GD versions (patched repeatedly in 3.11.5/3.11.6). Always base_path()/resource_path() the TTF.

resize() with two dimensions distorts. If you're computing dimensions yourself and only need a cap, use scaleDown(). If you need an exact box with the subject intact, that's cover().

Chained ->text() does not wrap by default in older 3.x and non-Latin wrapping had fixes (3.11.3). For long captions, measure and break lines yourself rather than trusting automatic wrapping.

SVG is not supported. Intervention is raster-only. Reject SVGs at validation rather than letting read() throw a DecoderException and surfacing a 500.

Catch the right exception. v4 introduced a proper hierarchy (decoder/encoder/analyzer exceptions, file-not-found, etc.). Catch Intervention\Image\Exceptions\... specifically — a bare catch (\Throwable) around image code hides genuine driver misconfiguration.

read() is lazy-ish, but not free. Validation like "is this actually an image" still costs a decode. Validate MIME/size at the request layer, and re-encode — not re-validate — at the storage layer.

Upgrade note: v4 is a deliberate API cleanup (PHP 8.3+, revised method signatures, new exception tree) and v3 is still widely deployed. Pin the major in composer.json and read image.intervention.io/v4 before a jump — the differences are mostly naming and error handling, but they will surface in tests that asserted on exception messages.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource