smottt/wideimage
WideImage is a PHP image manipulation library built on GD. It provides an easy API for resizing, cropping, rotating, flipping, watermarking, and applying filters to images, with support for common formats. The smottt/wideimage package is a maintained fork for modern PHP projects.
composer require smottt/wideimage
php -m | grep -i gd # WideImage requires ext-gd, no Imagick path
There is no service provider, config file, or facade — it's a plain PHP library. Look first at vendor/smottt/wideimage/lib/WideImage.php for the entry point and lib/operations/ for the available verbs (resize, crop, rotate, merge, applyFilter, drawText, …). The README examples are enough to get productive in minutes.
Smallest useful flow — thumbnail an upload in a controller:
use WideImage\WideImage; // some forks expose a global `WideImage` class instead
public function store(Request $request)
{
$path = $request->file('photo')->store('originals', 'public');
$image = WideImage::load(Storage::disk('public')->path($path));
$thumb = $image->resize(320, 240, 'inside'); // keeps aspect ratio
$thumb->save(Storage::disk('public')->path('thumbs/'.basename($path)), 85);
return back()->with('ok', true);
}
First real use case: upload → normalize → generate thumbnail/hero variants → persist to the public disk.
Wrap it in one service, not scattered calls. WideImage is procedural in spirit; keep the fluent chains inside a small ImageProcessor class so controllers and jobs don't grow image logic.
class ImageProcessor
{
public function variant(string $absSource, string $absTarget, int $w, int $h): void
{
WideImage::load($absSource)
->resize($w, $h, 'inside')
->crop('center', 'center', $w, $h) // square-ish hero
->save($absTarget, 82);
}
}
Chaining returns new objects. resize(), crop(), rotate(), merge(), applyFilter() all return a new WideImage instance. Nothing is written until save(), output(), or asString(), which makes it easy to build a pipeline and only persist the final image.
Queue the heavy work. Image decoding is CPU- and memory-bound; dispatch a GenerateImageVariants job after upload and let the request return immediately. The job receives the stored path, resolves it with Storage::disk(...)->path(), and writes variants back.
Use asString() for non-local disks. S3/R2 have no local path, so load from a temp file or string and push the bytes back:
$bytes = WideImage::load($tmpPath)->resize(1200, null)->asString('jpg', 85);
Storage::disk('s3')->put("photos/{$id}.jpg", $bytes);
Streaming responses. ->output('png') writes directly to the browser; good for a signed “download resized” route, but set Content-Type yourself since the library won't.
Overlays and branding. merge($watermark, 'bottom-right', 10, 10, 40) for logos (opacity is a percent), and drawText($text, $x, $y, $size, $color, $fontPath) for captions. Fonts must be absolute paths to .ttf files — copy them into resources/fonts and resolve with resource_path().
Smart thumbnails. crop('center', 'center', $w, $h) after a resize(..., 'inside') is the standard grid-thumbnail recipe; resize($w, $h, 'fill') stretches and is almost never what you want.
Dynamic resize endpoint. A route like /img/{path}?w=800 that caches the resized file on disk is a common Laravel integration; guard it with a signed URL or an allow-list of source paths.
gd_info() before relying on imagewebp.memory_limit and fail with a fatal error rather than an exception. Raise memory_limit in the job/command, not globally.->asTrueColor() (and save as PNG) when transparency matters.exif_read_data() and call ->rotate(90 * $orientation) yourself before resizing, then strip EXIF on save.(x, y) with origin top-left, and crop() has several overloads (crop($left, $top, $w, $h) vs. crop($posX, $posY, $w, $h) with string anchors). Pin down which signature your version uses; off-by-one crops are the usual bug.save('x.jpg', 85) sets JPEG quality; save('x.png') ignores the quality argument and uses PNG compression. asString('jpg', 85) needs the format passed explicitly.WideImage\Exception\... or WideImage_Exception depending on the fork). Catch that, not \Exception, and wrap it into a domain error so a corrupt upload doesn't 500.Storage::path() only works for local disks. For remote disks, download to tempnam(sys_get_temp_dir(), 'img'), process, upload, and unlink() in a finally.image, mimes:jpg,png,webp, max:10240) and the cleanup.NOASSERTION (originally LGPL-3.0), which is worth a legal check before shipping. Pin the Composer version, run composer audit, and consider wrapping it behind your ImageProcessor interface so swapping to Intervention Image later is a one-class change.WideImage or registering a custom operation is possible but undocumented; for anything beyond the built-ins, doing the raw GD work in your own method and returning a new image is safer than fighting the operation registry.How can I help you explore Laravel packages today?