gregwar/image-bundle
Symfony bundle wrapping the Gregwar Image library for easy image manipulation: resize, crop, rotate, watermark, and apply filters via services, Twig helpers, or a simple API, with built-in caching to keep generated images fast.
Know what you're actually installing. gregwar/image-bundle is a Symfony bundle — it wires the gregwar/image library into Symfony's container, Twig, and routing. In a Laravel app you do not install the bundle; you install the underlying library and use it directly:
composer require gregwar/image
That requires PHP with the GD extension (Imagick optional). If you're on Symfony, the bundle itself gives you the gregwar_image service, the image() Twig function, and a cache-serving controller out of the box.
Where to look first:
Gregwar\Image\Image class in the library — that's 90% of the API surface.GregwarImageExtension (Twig function signature) and ImageController (cache-serving route) if you're porting the behavior to Laravel.config/packages/gregwar_image.yaml; in Laravel, your own config/images.php.First use case — a cached thumbnail. The canonical entry point:
use Gregwar\Image\Image;
Image::open(storage_path('app/public/uploads/photo.jpg'))
->resize(300, 300)
->save(storage_path('app/public/thumbs/photo.jpg'));
That's it. If you're used to Intervention Image, the mental model is the same, just with a fluent object and no facade.
Chaining operations. The Image object is immutable-ish by convention — each call returns $this, so chain and terminate with a save/output:
Image::open($source)
->crop(0, 0, 1200, 1200)
->resize(400, 400)
->grayscale()
->rotate(90)
->save($dest, 'jpg', 85);
Terminal methods to remember: save($path, $type, $quality), output($type, $quality) (streams to the browser, useful in a controller), write($path) (no format change), and shortcuts jpeg(), png(), guess().
Resize vs. scale vs. forceResize. These trip people up:
resize($w, $h) — shrinks within the box, preserving aspect ratio, padding with background if needed.scale($w, $h) — resizes and crops to exactly fill the box. This is your "cover" behavior.forceResize($w, $h) — stretches to exactly $w × $h, aspect ratio be damned. Almost never what you want.Layered composition — watermark and effects stack naturally:
Image::open($source)
->scale(1200, 800)
->watermark(public_path('logo.png'), 'bottom-right', 20, 20, 60)
->brightness(-5)
->save($dest);
Dynamic, URL-driven serving (the bundle pattern). The Symfony bundle's best idea is that image transforms are described by a URL and cached on disk, so you never regenerate on every request. You can replicate this in Laravel with a route:
Route::get('/img/{path}', function (string $path) {
$src = storage_path("app/public/{$path}");
$cache = storage_path('app/image-cache/'.md5($src.request()->query()));
if (! file_exists($cache)) {
Image::open($src)
->setCache(storage_path('app/image-cache'))
->resize(request('w', 800), request('h', 800))
->save($cache);
}
return response()->file($cache);
})->where('path', '.*');
Cache configuration. In the library, setCache($dir, $gcPeriod) enables a built-in garbage collector and a "pretty name" cache key so URLs like /cache/thumbnails/photo_200x200.jpg map to deterministic files. In the bundle you set this via gregwar_image.cache_dir and web_dir. The web_dir matters: the cache must sit somewhere the web server can serve statically, otherwise every request proxies through PHP.
Wrap it in a Laravel service. Since there's no first-party integration, the day-to-day pattern is a small dedicated class:
class Thumbnailer
{
public function make(string $diskPath, int $w, int $h): string
{
$src = Storage::disk('public')->path($diskPath);
$out = "thumbs/".md5($diskPath.$w.$h).".jpg";
Image::open($src)->scale($w, $h)->save(Storage::disk('public')->path($out), 'jpg', 85);
return $out;
}
}
Return the relative path and let Storage::url() handle the rest — don't hardcode public_path() everywhere.
Queue the work for real apps. Generation is synchronous and blocking. For user uploads, dispatch a job after the upload completes; keep the HTTP request path free of resize calls.
The bundle/package identity mismatch is the #1 trap. Composer package name, GitHub repo, and namespace all say "Symfony bundle," but the useful code is the gregwar/image dependency. Anyone copy-pasting bundle docs into Laravel will get nowhere — the image() Twig function and gregwar_image service simply don't exist outside Symfony.
Extensions are optional at runtime. GD is required; Imagick gives you better quality and more formats but isn't guaranteed. If you call webp() or process a TIFF and get silent failures, check the adapter before blaming the code. There's no adapter class to configure explicitly — it auto-detects.
Cache directory permissions and web exposure. The cache dir must be writable by PHP and readable by the web server. The classic failure is a writable cache under storage/ that the web server can't reach — every "cached" image still routes through PHP. Also, setCache() will happily write files derived from user-supplied paths; never pass raw request input as the source path without validating against a whitelist, or you've built a path-traversal file reader.
Memory limits on large source images. A 6000×4000 JPEG can blow past PHP's default memory_limit during decode. The library has setMemoryLimit() but it only raises the limit up to a cap — better to reject oversized uploads at validation time (dimensions:max_width=4000) than to discover this in production under load.
Format guessing follows the extension. save('foo.jpg') writes JPEG, save('foo') guesses from the source. If you rename a PNG to .jpg upstream, you'll get unexpected mime types served with the wrong Content-Type. Be explicit: save($path, 'jpg', 85).
Quality defaults are lossy. JPEG default quality is around 80. For thumbnails that's fine; for hero images bump to 90+ or switch to PNG/WebP deliberately.
Stale cache has no invalidation. File-based caching keys on the path (and, with pretty names, the transform parameters — but not the file mtime). Overwrite an image and the old cached derivative lingers. Either version your filenames (photo-v2.jpg) or clear the cache dir in the same job that replaces the source.
Project health is a real consideration. Low recent activity and no Laravel shims mean you're on your own for framework glue and PHP-version compatibility on bump. Pin the version and keep the wrapper class thin so swapping to Intervention Image later is a one-file change.
Extension points worth knowing: the Image object is mutable-by-chaining, so you can write helpers that accept an Image and apply a house style — a BrandStyle::apply(Image $img) that watermarks and applies contrast is a clean seam and keeps controllers trivial. In Symfony, the equivalent seam is a Twig filter or a tagged service; in Laravel, a Blade directive or a small facade.
How can I help you explore Laravel packages today?