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 Optimizer Laravel Package

ps/image-optimizer

PHP library that shrinks JPEG, PNG, GIF, SVG, and WebP images by wrapping tools like jpegoptim, optipng, pngquant, gifsicle, svgo, and cwebp. Offers a simple API, optimizer chains, and smart format-based optimization for lossless or lossy compression.

View on GitHub
Deep Wiki
Context7

Getting Started

Install. The library is framework-agnostic — there is no service provider to auto-discover, no facade, no migration. Just:

composer require ps/image-optimizer

Symfony Process is already a transitive dependency of Laravel, so there is nothing to resolve.

Where to look first. Open src/Optimizer/ and read the concrete optimizers before touching config. Each one is a thin, ~50-line wrapper showing you exactly which binary it shells out to and which flags it passes. That is the real documentation — the README will mislead you about which binaries you actually need.

Install the binaries before writing any PHP. This trips up nearly everyone on day one:

# Debian/Ubuntu base image
apt-get install -y jpegoptim optipng pngquant gifsicle

On macOS: brew install jpegoptim optipng pngquant gifsicle. On Windows: use Sail or Docker — do not attempt native binaries.

First use case — the five-line spike.

use Ps\ImageOptimizer\OptimizerChainFactory;

$chain = OptimizerChainFactory::create();
$chain->optimize(
    storage_path('app/tmp/upload.jpg'),
    storage_path('app/tmp/upload.opt.jpg')
);

That is genuinely enough to prove it works. OptimizerChainFactory::create() builds a sensible default chain (optipng → pngquant for PNG, jpegoptim → jpegtran for JPEG, gifsicle for GIF) and the result is a OptimizationResult object returning the original and optimized byte sizes.

The first day-one check is: does which optipng succeed inside the container that will run the job? Run it before your test suite, not during.

Where to go next is almost always the same place: wrap this behind your own interface rather than calling OptimizerChainFactory from a controller or job. Everything in the Implementation Patterns section assumes that seam exists.

Implementation Patterns

1. The adapter interface (do this on day one). ps/image-optimizer is an infrastructure adapter, not a domain concept. Bind it behind a contract so swapping to Spatie's image-optimizer or a CDN later is a one-line service-provider change:

namespace App\Contracts;

interface ImageOptimizer
{
    public function optimize(string $source, string $destination): OptimizationResult;
}

A PsImageOptimizer implementation builds the chain once and injects it; a NullImageOptimizer implementation for local/test is returned by the service provider when the binaries aren't present. This is the single highest-leverage structural decision and costs an hour.

2. Configuration lives in config/images.php, not in code. The library lets you pass binary paths, optimizer sets, and quality knobs — but only if you feed it the values. Keep the default factory out of AppServiceProvider::boot() and instead compose the chain from config:

// config/images.php
return [
    'enabled' => env('IMAGE_OPTIMIZATION_ENABLED', false),
    'binaries' => [
        'jpegoptim' => env('JPEGOPTIM_BIN', '/usr/bin/jpegoptim'),
        'optipng'   => env('OPTIPNG_BIN', '/usr/bin/optipng'),
        'pngquant'  => env('PNGQUANT_BIN', '/usr/bin/pngquant'),
        'gifsicle'  => env('GIFSICLE_BIN', '/usr/bin/gifsicle'),
    ],
    'timeout' => env('IMAGE_OPT_TIMEOUT', 60),
    'quality' => 85,
];

Absolute paths beat $PATH lookup every time — predictable failure modes and easier security auditing. The env-var default-off is your kill switch.

3. Per-format chains rather than one global chain. You will want different settings for different upload surfaces. The classic example is a lossy pngquant on user avatars (quality 75–85 is fine) versus lossless optipng on logos and UI assets (lossy artifacts are visible on flat colors). Build two named chains from config:

$lossy = new OptimizerChain();
$lossy->useTimeout(config('images.timeout'))
      ->addOptimizer(new Jpegoptim(['quality' => 85, 'strip' => true]))
      ->addOptimizer(new Pngquant(['quality' => '75-90']))
      ->addOptimizer(new Gifsicle(['optimizationLevel' => 3]));

$lossless = new OptimizerChain();
$lossless->addOptimizer(new Optipng(['level' => 2]))
         ->addOptimizer(new Jpegtran());

Pick the chain by upload context (avatar/profile/cms/product) or by detected MIME type after getimagesize().

4. Always run through a queued job, never inline on the request path. CPU-bound compression on the request path will surface as p95 latency incidents during launch traffic. The shape that works:

class OptimizeUploadedImage implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [30, 300];
    public $timeout = 120;
    public $queue = 'images';

    public function __construct(public int $assetId) {}

    public function handle(ImageOptimizer $optimizer): void
    {
        $asset = Asset::find($this->assetId);

        $source = Storage::disk('local')->path("tmp/{$asset->uuid}");
        $dest   = Storage::disk('local')->path("tmp/{$asset->uuid}.opt");

        $result = $optimizer->optimize($source, $dest);

        Storage::disk('s3')->put($asset->key, fopen($dest, 'r'));

        $asset->update([
            'optimized_bytes' => $result->getOptimizedSize(),
            'original_bytes'  => $result->getOriginalSize(),
        ]);

        @unlink($source); @unlink($dest);
    }
}

Two things to internalize: put it on a dedicated images queue with its own worker pool, so backfills don't starve transactional jobs; and copy a real file into a local temp path first — the library needs a filesystem path, not a stream.

5. Storage round-trip pattern. Never stream from S3 to the optimizer. The pattern is always download to Storage::disk('local')->path(...) → optimize → re-upload → unlink. If you accept the upload directly, write the incoming file to local first and dispatch, rather than trying to optimize the UploadedFile temp path (which may disappear before the job runs).

6. Original preservation. Keep the original in a cold-storage key (originals/{uuid}) for 30–90 days alongside the optimized version in the hot bucket. Lossy transforms are irreversible — this is the only way to recover from a bad quality setting without re-ingesting. When the shadow-mode pilot proves the settings, you can drop the retention window.

7. Feature flag and shadow mode. Wrap the call to the optimizer with a check on config('images.enabled') and log the savings on a sample even when disabled. That gives you real byte-savings numbers to justify turning it on, without risking the upload path during rollout. Roll out as: flag off → shadow on a sample → flag on for new uploads → backfill historical assets as a separate batched job.

8. Custom optimizers via the Optimizer interface. If you need a tool the library doesn't wrap (say cwebp for the WebP gap, or mozjpeg), implementing Ps\ImageOptimizer\Optimizer is genuinely a 40-line class — just shell out and compare sizes. Because the interface is small and stable, this is a real extension point, not a theoretical one.

Gotchas and Tips

Binaries are not optional and not bundled. The library is a wrapper. A missing pngquant surfaces as a Process exception with confusing stderr, not a clean ClassNotFoundException. Assert binary availability at boot (exec: which jpegoptim in a health check, or a one-off validation command) so a rolling deploy with a half-updated image fails loudly instead of intermittently.

OptimizerChainFactory::create() silently skips absent binaries on some versions, so a host missing pngquant produces a slightly-worse PNG instead of an error. That's worse than a hard failure. Configure the chain explicitly with absolute paths and let it throw.

Lossy flags are permanent. Jpegoptim(['strip' => true, 'quality' => 85]) and Pngquant with any quality range mutate the pixels irreversibly. There is no undo in code — only the preserved original. Establish a byte-savings target and a visual-regression check (SSIM or plain eyeballing a sample) before enabling on any user-facing surface. The --strip flag also removes EXIF, including orientation — re-orient before optimizing or you will rotate photos.

The default PHP execution timeout is 30s; a large JPEG on a tired worker can exceed it and leave a partially-written destination file. Set both the chain timeout (useTimeout()) and the queue job timeout, and make sure the job clears its temp files on failure via finally or a failed() method. The destination file is written even on partial failure in some paths.

Test environment silently skips. Developers on macOS without brew install will get a passing suite that exercises nothing, and CI on a slim php:8.3 image will do the same. Standardize: the base image contains the toolchain, CI runs the same image, and your ImageOptimizerInterface binding returns NullImageOptimizer in testing — but with an explicit assertion in a feature test that the real one is wired when the binary is present.

Malformed input and decompression bombs. You are invoking external processes on user-supplied files. Enforce a max input size and a max pixel count via getimagesize() before optimizing, and run on an isolated worker with CPU/memory limits. Never pass a user-controlled filename into the CLI as an argument — the library uses Symfony Process argument arrays (safe), but if you build your own optimizer, keep it that way.

Filename/PII leakage in logs. Symfony ProcessFailedException includes the full command line in its message. Temp paths often embed user IDs or original filenames. Catch and re-wrap into a short taxonomy — BinaryMissing, Timeout, UnsupportedInput, DiskFull — and log only metadata, not the raw exception message.

No WebP/AVIF. This is a real gap, not a footnote. If the roadmap includes format modernization, plan a second pipeline (or a cwebp/avifenc custom Optimizer in a separate chain) rather than assuming this library "will catch up." It targets PNG/JPG/GIF/SVG via the classic toolset.

Output sometimes grows. Very small PNGs or already-optimized JPEGs can come out larger after some lossy flags. OptimizationResult::getOptimizedSize() vs getOriginalSize() exists for exactly this reason — if optimized ≥ original, keep the original and discard the optimized file. Missing this check produces a regression for a fraction of your uploads and shows up as a mysterious "images got bigger after the compression release."

GPL binaries and container distribution. gifsicle, pngquant, and parts of the jpegtran chain carry GPL-family licenses. Invoking them as separate processes is the standard defense, but get a written answer from legal before baking them into a distributed image. Budget this check alongside the spike — it's the classic late-stage blocker.

Version pin the binaries in exactly one place. A base image per service means binary versions drift across services in weeks. Publish one image with the toolchain baked in, use the same tag everywhere, and bump intentionally. Track upstream ps/image-optimizer quarterly — it's in low-maintenance mode, and forking is cheap because the library is small. Keep the adapter boundary intact so that fork stays cheap.

The upstream PHP support matrix may lag. Verify the tag's composer.json against your PHP version before pinning (8.2/8.3 are the modern targets). Older tags cap at PHP 7 and would force a fork. Test the exact tag in CI, not just on your laptop.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo