gumlet/php-image-resize
Resize, scale, and crop images in PHP with a simple, typed API. Requires PHP 8.1+ with gd and fileinfo, supports files, data URLs, and binary strings, and handles enlargement limits and multiple crop modes.
Install and verify the runtime first. The library is a plain Composer package with no Laravel service provider, config file, or facade — so there is nothing to publish and nothing to register.
composer require gumlet/php-image-resize:^3.0
php -m | grep -E 'gd|fileinfo'
php -r 'var_dump(gd_info()["WebP Support"]);'
Version selection is tied to your PHP runtime, not Laravel: 3.x requires PHP 8.1+, 2.0.x covers PHP 7.2–8.0, 2.1.x covers 8.0. Pin accordingly if you maintain a matrix of apps.
Where to look first. Skip the source and read the method list in the README — the entire public surface is roughly resize, resizeToWidth, resizeToHeight, resizeToBestFit, scale, crop, freecrop, save, output, plus the getters and the ImageResize::createFromString() static constructor. If your need isn't in that list (smart crop, EXIF rotation, SVG, HEIC, animated GIF preservation), this package will not do it and you should reach for Intervention Image instead.
First use case — a thumbnail on upload. Everything is chainable and operates on a filename on the local filesystem:
use Gumlet\ImageResize;
$image = new ImageResize($request->file('photo')->getRealPath());
$image->crop(400, 400)
->save(storage_path('app/public/avatars/'.$name.'.jpg'), IMAGETYPE_JPEG, 82);
Before spreading that around, decide two things: which disk the source lives on (the constructor wants a local path, not an S3 key) and whether this runs inline or in a queue. For anything user-facing, assume a queue.
Wrap it in a single action class. The library's API is procedural and mutates internal state, so having one StoreResizedImage action (or a small App\Images\Resizer service) keeps call sites honest and gives you one place to swap to Intervention later.
final class ResizeImage
{
public function __invoke(string $sourcePath, string $targetPath, int $width, int $height, string $disk = 'public'): void
{
$filesystem = Storage::disk($disk);
$image = new ImageResize($filesystem->path($sourcePath));
$image->crop($width, $height);
$image->save($filesystem->path($targetPath), IMAGETYPE_JPEG, 82);
}
}
Storage::path() is only meaningful on local disks (local, public). For S3, see the temp-file pattern below.
Generate each derivative from a fresh instance. Chained calls mutate the working GD resource, so a second resize computes from the first result, not the original. Derive every size from the source:
foreach (['thumb' => [200, 200], 'card' => [600, 400], 'hero' => [1600, 900]] as $name => [$w, $h]) {
(new ImageResize($source))->crop($w, $h)->save($filesystem->path("derivatives/{$name}.jpg"), IMAGETYPE_JPEG, 82);
}
Push it to a job. Uploads should return fast; resize work belongs behind ShouldQueue, usually dispatched ->afterCommit() so the job never reads a file from a rolled-back transaction.
ProcessImage::dispatch($media->id)->afterCommit();
S3 / remote disks: go through a temp file. The library writes to paths, and it has no in-memory "give me the bytes" method (only save() and an echoing output()). The reliable bridge is:
$tmp = tempnam(sys_get_temp_dir(), 'img');
file_put_contents($tmp, Storage::disk('s3')->get($key));
(new ImageResize($tmp))->resizeToWidth(1600)->save($tmp, IMAGETYPE_JPEG, 82);
Storage::disk('s3')->put($target, fopen($tmp, 'r'));
unlink($tmp);
Reading an upload without touching the disk is clean via createFromString() — useful in feature tests and for images arriving in a request body:
$image = ImageResize::createFromString($request->file('photo')->get());
Validate before you hand it over. The library trusts you; it will happily be fed a 9000×9000 decompression bomb. Guard at the edge:
$request->validate([
'photo' => ['required', 'file', 'mimes:jpg,jpeg,png,webp', 'max:10240',
'dimensions:max_width=6000,max_height=6000'],
]);
Testing dimensions is trivial with Storage::fake() and PHP's own getimagesize():
Storage::fake('public');
// ... exercise the controller or job ...
$this->assertSame([200, 200], array_slice(
getimagesize(Storage::disk('public')->path('avatars/a.jpg')), 0, 2
));
Serving the result. Don't call output() inside a controller — it echoes directly and sets its own headers, which fights Laravel's response lifecycle. Either return a normal file response, or stream:
return response()->file(Storage::disk('public')->path($key));
quality argument means different things per format. For JPEG and WebP it's 0–100 (higher = better). For PNG it's the zlib compression level 0–9, passed straight to imagepng(). Passing 90 for a PNG produces a needlessly huge file. Branch on type in your wrapper.$image_type, save() falls back to the source type, not the extension you gave it — so a JPEG saved as thumb.png is a PNG extension with JPEG bytes inside. Always pass IMAGETYPE_JPEG / IMAGETYPE_PNG / IMAGETYPE_WEBP explicitly.resizeToWidth(100)->resizeToWidth(200) gives you 200px, not a re-derive from the original. This is the single most common bug with this package.exif_read_data() and rotate the GD resource yourself before saving. This alone often justifies Intervention.allow_enlarge defaults to false on most resize methods. A 60×60 upload silently stays 60×60 when you asked for 400×400 — which reads like a bug in testing. Pass true when you genuinely want upscaling; do it deliberately, since it just blurs.scale() takes a percentage, not a ratio: scale(50) halves. scale(0.5) gives you a one-pixel image.memory_limit. Cap input dimensions (see validation above) and consider ini_set('memory_limit', ...) inside the job.save() will not create directories. Missing parent directories produce a confusing failure — Storage::makeDirectory() first.Gumlet\ImageResizeException. Corrupt files, unsupported types, and unreadable sources all throw it. Letting it bubble turns a bad upload into a 500 with a stack trace.gd_info()['WebP Support'] and fall back to JPEG.fileinfo and reject those types explicitly rather than relying on extension checks.output() sets Content-Type and echoes. It's designed for standalone scripts. In Laravel, use response()->file() or a temp file.ImageResize and add methods that write through imagejpeg()/imagewebp() on the internal resource — but at that point, evaluate whether Intervention Image's driver abstraction is the better long-term bet.How can I help you explore Laravel packages today?