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

zenstruck/image

Zenstruck Image is a lightweight PHP library for object-oriented image manipulation. It offers a clean, fluent API for common tasks like resizing, cropping, rotating, and converting images with minimal setup and readable code.

Deep Wiki
Context7

Getting Started

zenstruck/image is a tiny, framework-agnostic wrapper around an image file that defers actual manipulation until you ask for the result. It ships no resizing engine of its own — you bring GD, Imagick, Intervention Image, or Imagine and plug it in through a transformer. Install with:

composer require zenstruck/image

Where to look first. Read src/Image.php and the Transformer contract before anything else. The public surface is intentionally small: a static factory (Image::from(...)), a transform() method that returns a new lazy Image, and accessors to materialise the result (save(), getPathname(), getSplFileInfo()). The bundled transformers live under src/Transformer/ and show the expected return shape (an SplFileInfo or path pointing at the transformed file).

First use case — avatar thumbnailing in a controller:

use Zenstruck\Image;
use Zenstruck\Image\Transformer\InterventionImageTransformer;

// UploadedFile extends Symfony's UploadedFile, which extends SplFileInfo
$image = Image::from($request->file('avatar'))
    ->withTransformer(new InterventionImageTransformer()); // or set once globally

$thumb = $image->transform(
    fn (\SplFileInfo $file) => $file, // your resize/crop call here
    ['width' => 200, 'height' => 200]
);

// Still lazy — do the work now and persist it:
$thumb->save(storage_path('app/public/avatars/'.$filename));

If you only remember one thing: constructing an Image does nothing; the work happens when you save or read the path. That is the whole design.

Implementation Patterns

Wrap once, transform many times. Keep the source Image as the entry point and derive variants from it. Because transformations are lazy, you can build a small pipeline in a service and only pay the cost for the variants the request actually needs (e.g. skip the 1920px hero on mobile).

$source   = Image::from($path);
$thumb    = $source->transform($resize, ['w' => 200]);
$preview  = $source->transform($resize, ['w' => 800]);

Implement the transformer to centralise your image library. The package's real value is the seam it gives you. Write one transformer per library (Intervention, Imagine, raw GD) and reuse it everywhere; swapping libraries later is a one-line change.

final class GdTransformer implements \Zenstruck\Image\Transformer
{
    public function transform(\SplFileInfo $image, callable $transformation, array $options = []): \SplFileInfo
    {
        // load $image, apply $transformation/options, write to a temp file, return it
    }
}

Compose with Laravel storage. Resolve the transformed file, then hand the path to the Storage facade. This keeps the library out of your storage layer and keeps Storage out of your transformation logic.

Storage::disk('s3')->putFileAs(
    'avatars',
    $image->transform($resize, ['w' => 200])->getSplFileInfo(),
    $name
);

In queued jobs, pass paths — not Image instances. An Image is a wrapper over a live filesystem path (often a PHP upload temp file). Serialising it is a foot-gun; serialise the source path, rebuild the Image inside the job, transform, store, and let the wrapper's temp output be cleaned up.

Cache by source hash + transformation key. The package does not cache for you. A cheap pattern is sha1_file($source).'_'.md5(serialize($options)).'.jpg' as the output filename; if it exists, skip the transform entirely.

Testing with fakes. In Laravel feature tests, UploadedFile::fake()->image('x.jpg', 800, 600) produces a real GD image on disk and slots straight into Image::from() — no fixtures needed.

Use temporary() for request-scoped outputs. When you don't want to pick a destination (e.g. streaming a response), ask the wrapper for a temp-backed Image and point the response at its pathname; this is the idiomatic "I just need a throwaway file" flow.

Gotchas and Tips

  • Uploaded-file lifetime is the #1 trap. On Symfony/Laravel, UploadedFile::getPathname() points at a temp file PHP deletes at end of request. Materialise or copy the image within the request; a queued job that receives the raw temp path will fail. Store the original first, then transform from the stored file.
  • Lazy means errors are late. A missing GD extension, corrupt JPEG, or 500 MB TIFF will only blow up at save()/getPathname(). Wrap the consumption point in try/catch, not the transform() call, and log the source path — the stack trace alone won't tell you which file died.
  • Memory limits hit harder than you think. GD decodes the whole image into RAM; a 12 MP JPEG can cost ~50 MB. Raise memory_limit for the job/request or downscale in two passes. Imagick is gentler but can hit policy.xml limits on shared hosts.
  • EXIF orientation is not fixed for you. Phone photos will come out sideways. Read the orientation tag (or run -auto-orient/Imagick::setImageOrientation) before your resize; do it explicitly in a transformer step.
  • Trust MIME, not the extension. SplFileInfo::getExtension() is attacker-controlled. Validate with finfo/getimagesize() before handing the file to GD, or you're feeding arbitrary bytes to a decoder.
  • Clear the stat cache after in-place transforms. If your transformer rewrites the same path, PHP's stat cache can report the old size/mtime. Call clearstatcache(true, $path) before reading metadata back.
  • Return shape matters. A transformer must return a file (SplFileInfo/path) — returning true, null, or a resource will surface as a confusing type error deep in the wrapper. Validate in your own transformer.
  • Keep transformers stateless and closure-light. They are often reused across many images in one process; capturing a request, a user, or a huge options array in the callback invites memory leaks and surprising serialisation errors if the pipeline ever gets queued.
  • Don't rely on the package for cleanup. Temp files produced by transformations are yours to delete. Schedule a sweeper or use temporary() into a directory you prune.
  • Extension point worth knowing: implement the transformer contract and register your implementation once (constructor/DI or a global default if the version supports it). That single class becomes your project-wide image policy — format, quality, strip-metadata, orientation — instead of scattering options across call sites.
  • Reality check: 31 stars and a single-maintainer ecosystem means docs are thin and the API may shift between minors. It's a well-scoped seam, not a batteries-included image toolkit — budget an afternoon to read the source before leaning on it in production.
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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager