weldist / spatie-medialibrary-webp-downloader
Drop-in Downloader for spatie/laravel-medialibrary that converts every fetched image to WebP on the fly.
Package info
github.com/weldist/spatie-medialibrary-webp-downloader
pkg:composer/weldist/spatie-medialibrary-webp-downloader
Requires
- php: ^8.3
- illuminate/support: ^12.0|^13.0
- spatie/image: ^3.0
- spatie/laravel-medialibrary: ^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
This package is not auto-updated.
Last update: 2026-05-12 15:30:05 UTC
README
A WebP-converting Downloader for spatie/laravel-medialibrary.
A weld.ist project.
Unofficial plugin. Not affiliated with Spatie.
The Problem
spatie/laravel-medialibrary's addMediaFromUrl() (and friends) downloads a remote file and stores it byte-for-byte as it arrived:
addMediaFromUrl('https://example.com/photo.jpg') → 42/photo.jpg (the original 2 MB JPEG)
That is wasteful for the common case — user avatars, article hero images, product photos pulled from a supplier feed. WebP routinely shaves 25–35% off a JPEG and far more off a PNG at the same visual quality, but media-library exposes no hook to transcode on ingest:
- Bandwidth & storage: Every imported image is served and stored at its source format and size.
- No ingest-time hook: Conversions (
->withResponsiveImages(), registered conversions) run after the original is already on disk — the original itself is never touched. - Manual workarounds are brittle: Re-encoding in a model observer or a queued job means re-implementing media-library's path/disk resolution and racing its own pipeline.
The Solution
This package ships a drop-in Downloader that re-encodes images to WebP while they are being downloaded, before media-library ever writes the original to disk:
addMediaFromUrl('https://example.com/photo.jpg') → 42/photo.webp (re-encoded, smaller)
- Implements Spatie's
Downloaderinterface — you register it as themedia_downloaderinconfig/media-library.php. - Extends
DefaultDownloader, so it inherits Spatie's SSL & User-Agent stream context and any future change to the download logic; it only adds a post-download re-encode step. - Re-encodes via
spatie/image— the same library media-library uses for its own conversions — so there are zero new dependencies and the driver enum (gd/imagick/vips) is identical. - Reads Spatie's existing
media-library.image_driverconfig — no extra env var, no plugin config file. - Non-image payloads (PDF, video, archives) and configured skip MIME types pass through untouched. SVG and animated GIF are skipped by default.
- A bundled, auto-discovered service provider keeps the
mediarow'sfile_nameextension in sync with the re-encoded file (see Automatic File Name Correction).
Requirements
- PHP ^8.3
- Laravel ^12.0 | ^13.0
spatie/laravel-medialibrary ^11.0 | ^12.0spatie/image ^3.0(already installed transitively by media-library)- The PHP extension matching
media-library.image_driver—ext-gd,ext-imagick, or libvips forvips
Installation
composer require weldist/spatie-medialibrary-webp-downloader
The package auto-discovers a single service provider — the file-name correction listener described below. It does not bind the downloader for you; that is a one-line edit to your own config so you stay in control of whether it is active.
Setup
Edit config/media-library.php and swap the downloader binding:
use Weldist\Spatie\MediaLibrary\WebpDownloader\WebpDownloader; return [ // ... 'media_downloader' => WebpDownloader::class, // ... ];
That's it. From now on every addMediaFromUrl() call passes through the WebP converter.
Image Driver
The driver is chosen automatically from Spatie's existing config and forwarded to Spatie\Image\Image::useImageDriver():
// config/media-library.php 'image_driver' => env('IMAGE_DRIVER', 'gd'), // 'gd', 'imagick', or 'vips'
The required PHP extension (ext-gd / ext-imagick) — or libvips for vips — must be loaded.
Tuning
WebpDownloader accepts two optional constructor arguments — both auto-resolved via the container with sensible defaults:
| Argument | Default | Meaning |
|---|---|---|
quality |
85 |
WebP quality (0–100) |
skipMimes |
['image/svg+xml', 'image/gif'] |
MIME types that bypass conversion |
To override, bind a custom instance in your own service provider:
use Weldist\Spatie\MediaLibrary\WebpDownloader\WebpDownloader; $this->app->bind(WebpDownloader::class, fn () => new WebpDownloader( quality: 90, skipMimes: ['image/svg+xml'], ));
Why is SVG always skipped, and GIF skipped by default? Rasterising an SVG to WebP throws away the thing that makes it an SVG, so it is never converted. GD flattens animated GIFs to a single frame; drop
image/giffromskipMimesonly if you are on Imagick and want animated WebP output.
Automatic File Name Correction
WebpDownloaderServiceProvider is auto-discovered. It listens for Spatie's MediaHasBeenAddedEvent — fired after the downloaded bytes land on disk — and, in lock-step, renames the on-disk file to .webp and updates the row's file_name via saveQuietly() when all of the following hold:
- The configured
media-library.media_downloaderisWebpDownloader(or a subclass) - The persisted
mime_typeisimage/webp - The current
file_namedoes not already end with.webp(case-insensitive)
So obama.jpg → obama.webp (basename preserved, extension swapped). Disk uploads of unrelated formats, and media added while a different downloader is configured, are never touched. The listener is idempotent — a re-fired event finds the source already renamed and the column already .webp, so it is a no-op.
Why a listener instead of
usingFileName(...)? media-library decides the on-disk name from the URL basename (photo.jpg) insideFileAdder::processMediaItem, after the model'screatingevent. Mutatingfile_nameearlier leaves the row pointing at.webpwhile the file sits at.jpg→ 404. Renaming afterMediaHasBeenAddedEventis the first point where both the file and the row can be fixed together.
To opt out, set extra.laravel.dont-discover in your root composer.json and rename manually with ->usingFileName(...).
Converting Existing Media
For projects that already have thousands of JPEGs/PNGs on disk, the package ships an Artisan command — media-library:webp-convert — that walks the media table and re-encodes rows in place.
1. Preview first. A dry run reports which rows would be converted and touches nothing:
php artisan media-library:webp-convert --dry-run
2. Run the conversion. Sync mode for small libraries, --queue for large ones:
# Convert everything synchronously php artisan media-library:webp-convert # Dispatch one queued job per row (recommended for large libraries) php artisan media-library:webp-convert --queue php artisan media-library:webp-convert --queue --queue-connection=redis --queue-name=media
3. Regenerate conversions. Existing thumbnails and responsive images are now stale because their source changed — the command prints a reminder when it finishes:
php artisan media-library:regenerate
For each Media row the command:
- Skips
image/webp, non-image MIME types, and entries in the configuredWebpDownloaderskipMimes - Reads the file via the row's own
diskand thePathGeneratorresolved by Spatie (custom path generators are honoured automatically) - Re-encodes to WebP using
media-library.image_driverand the quality from the container-boundWebpDownloaderinstance - Writes the new
.webpfile, then updatesfile_name,mime_type, andsizeviasaveQuietly(), then deletes the original — in that order, so a crash never leaves a row pointing at a deleted file
It is safe to re-run: already-WebP rows hit the skip guard. Both modes support --dry-run (queue + dry-run just reports what would be dispatched).
Options:
| Option | Description |
|---|---|
--collection= |
Limit to one media collection |
--model= |
Limit to one owning model class |
--since= |
Only media added on or after this date |
--id-from= / --id-to= |
Restrict to an id range — designed for sharded parallel runs (two terminals split the range and dispatch independently) |
--chunk= |
chunkById() batch size for large tables |
--queue |
Dispatch one ConvertMediaToWebpJob per row instead of converting inline |
--queue-connection= / --queue-name= |
Target connection / queue for --queue |
--dry-run |
Report what would be converted (or dispatched) without changing anything |
# Scope examples php artisan media-library:webp-convert --collection=images php artisan media-library:webp-convert --model="App\Models\Post" php artisan media-library:webp-convert --since=2026-04-01 --chunk=500 # Parallel workers splitting the id range # Terminal A: php artisan media-library:webp-convert --id-from=1 --id-to=50000 --queue # Terminal B: php artisan media-library:webp-convert --id-from=50001 --id-to=100000 --queue
Testing
# Build first (once per PHP version) DOCKER_BUILDKIT=0 docker compose --profile php83 build # Run tests docker compose --profile php83 up docker compose --profile php84 up docker compose --profile php85 up
License
This package is open-sourced software licensed under the MIT license.