mercator / wn-queuedresize-plugin
Queued image and PDF resizing plugin for Winter CMS, providing a drop-in replacement for the resize filter with background processing and modern formats.
Installs: 3
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:winter-plugin
pkg:composer/mercator/wn-queuedresize-plugin
Requires
- php: >=8.0
- composer/installers: ~1.0
- intervention/image: ^3.0
This package is auto-updated.
Last update: 2025-12-14 01:33:12 UTC
README
Asynchronous image (and PDF) resizing for WinterCMS, with automatic WebP support and media-library integration.
This plugin adds a qresize Twig filter and function that behaves like Winter’s built-in resize, but:
- Resizing is done via the queue (no heavy work in the HTTP request).
- Results are cached on disk and reused.
- It can generate thumbnails from PDF files (first page).
- It can output WebP when the browser supports it.
- It works with different filesystem disks.
Features
qresizeTwig filter and function- Drop-in replacement for
resizein templates - Works with:
- Media paths (
media/foo/bar.jpg) - URLs from
| media(e.g./storage/app/media/...) - External URLs (
https://example.com/image.jpg)
- Media paths (
- PDF → image thumbnail (first page)
- WebP output when the client sends
Accept: image/webp - Multi-disk support via
diskoption - Caching on disk (resized files + JSON metadata)
- Avoids repeated image processing under load
Requirements
- WinterCMS (PHP 8.2+)
- PHP GD or Imagick (via Intervention Image)
- A working queue setup in WinterCMS (e.g.
php artisan queue:work) - For PDF thumbnails:
- PHP Imagick extension
- Ghostscript / ImageMagick properly installed on the server
Note: If PDF support is missing, PDF inputs will fail with a runtime error until Imagick is available.
Configuration
Plugin Config File
The plugin reads settings via config('mercator.queuedresize::config.*'). These are defined in:
plugins/mercator/queuedresize/config/config.php
(or in an app-level override at config/mercator/queuedresize/config.php).
Example config.php:
<?php return [ /* |-------------------------------------------------------------------------- | Queue name |-------------------------------------------------------------------------- | | The queue on which resize jobs will be dispatched. | */ 'queue' => env('IMAGE_RESIZE_QUEUE', 'imaging'), /* |-------------------------------------------------------------------------- | Image driver |-------------------------------------------------------------------------- | | The Intervention Image driver to use: "gd" or "imagick". | */ 'driver' => env('IMAGE_RESIZE_DRIVER', 'gd'), /* |-------------------------------------------------------------------------- | Default storage disk |-------------------------------------------------------------------------- | | Default filesystem disk used when no per-call "disk" option is provided. | It is used for: | - reading originals | - writing resized images | - writing resize metadata (JSON) | - default lookup by /queuedresize/{hash} | */ 'disk' => env('IMAGE_RESIZE_DISK', 'local'), /* |-------------------------------------------------------------------------- | Additional lookup disks |-------------------------------------------------------------------------- | | Extra disks that /queuedresize/{hash} will also search (in addition to | the default disk) when resolving metadata/images. | | Needed when you use per-call disk overrides, especially if the disk is | part of the cache hash (recommended). | */ 'disks' => array_values(array_filter(array_map( 'trim', explode(',', (string) env('IMAGE_RESIZE_DISKS', '')) ))), /* |-------------------------------------------------------------------------- | Default quality |-------------------------------------------------------------------------- | | Fallback JPEG / WebP quality when none is provided in the options. | */ 'quality' => (int) env('IMAGE_RESIZE_QUALITY', 60), ];
.env Variables
The plugin does not read .env directly; it uses config(). The mapping to .env happens in the config file shown above.
Supported environment variables:
# Queue name used for resize jobs IMAGE_RESIZE_QUEUE=imaging # Image driver: gd or imagick IMAGE_RESIZE_DRIVER=gd # Default filesystem disk for originals + resized images IMAGE_RESIZE_DISK=media # Additional disks searched by /queuedresize/{hash} # (comma-separated, no spaces recommended) IMAGE_RESIZE_DISKS=portfolio,s3 # Default image quality (0–100) IMAGE_RESIZE_QUALITY=60
Installation
Install via Composer
composer require mercator/wn-queuedresize-plugin
Start the Queue Worker
php artisan queue:work
Or run workers via systemd / Supervisor / your process manager.
Run Updates and Clear Caches
php artisan winter:up php artisan cache:clear php artisan config:clear
Usage in Twig
The plugin registers qresize as a filter and as a function.
As a Filter (Drop-in style)
{# qresize as a filter: src | qresize(width, height, options) #} <img src="{{ 'media/example.jpg' | qresize(800, 600) }}" alt=""> {# With options #} <img src="{{ 'media/example.jpg' | qresize(800, 600, { mode: 'crop', quality: 80 }) }}" alt="">
** Drop-in replacement for built-in resize:**
{# Before #} <img src="{{ 'media/example.jpg' | resize(800, 600) }}" alt=""> {# After (queued) #} <img src="{{ 'media/example.jpg' | qresize(800, 600) }}" alt="">
** Works with | media:**
<img src="{{ record.image | media | qresize(800, 600) }}" alt="">
As a Function
{# qresize(src, width, height, options) #} <img src="{{ qresize('media/example.jpg', 800, 600) }}" alt=""> {# With options #} <img src="{{ qresize('media/example.jpg', 800, 600, { mode: 'crop', quality: 80 }) }}" alt="">
Useful with variables:
{% set src = record.image | media %}
{% set width = 800 %}
{% set opts = { mode: 'crop', quality: 75 } %}
<img src="{{ qresize(src, width, null, opts) }}" alt="">
Arguments and Options
Source (src)src can be:
- A media path:
media/example.jpg - A URL created by
| media, e.g./storage/app/media/example.jpg - A full external URL:
https://example.com/image.jpg
Width and Height
nullor0means “no constraint” on that dimension (aspect ratio preserved).
{{ 'media/example.jpg' | qresize(800, 600) }} {# target box #}
{{ 'media/example.jpg' | qresize(800, null) }} {# fixed width, auto height #}
{{ 'media/example.jpg' | qresize(null, 400) }} {# fixed height, auto width #}
Options ArrayThe 4th parameter is the options array:
{{ 'media/example.jpg' | qresize(800, 600, {
mode: 'crop', // 'auto' (default) or 'crop'
quality: 70, // JPEG/WebP quality 0–100
format: 'best', // 'best', 'jpg', 'png', 'gif', 'webp', 'jpeg', 'avif'
disk: 'media' // override default disk
}) }}
mode
auto(default): scale down to fit within width/height.crop: crop to exact width/height (centered) when both are given.
quality
- Output quality for JPEG and WebP. Defaults to
IMAGE_RESIZE_QUALITY.
format
best(default): serve WebP if the client supports it, otherwise JPEG.jpg,png,gif,webp,jpeg,avif: explicit formats.
disk
- Override the default disk from config for this call.
Note (multi-disk): If you use
{ disk: 'portfolio' }while your default disk is local, make sureIMAGE_RESIZE_DISKSincludesportfolio, so/queuedresize/{hash}can resolve the cached files.
PDF Thumbnails
If Imagick is available, the plugin can treat PDF files like images by rendering the first page.
<img src="{{ 'media/docs/report.pdf' | qresize(null, 200) }}" alt="Report">
Requirements:
- PHP Imagick extension.
- Ghostscript / ImageMagick installed and working.
Multi-disk Usage
If your originals live on a non-default disk, pass disk:
<img src="{{ 'uploads/gallery/image1.jpg' | qresize(1200, 800, { disk: 's3' }) }}" alt="">
If you have a public URL from that disk:
{% set urlForS3 = someModel.s3_image_url %}
<img src="{{ qresize(urlForS3, 800, 600, { disk: 's3' }) }}" alt="">
The plugin uses the disk’s configured url to map URLs back to storage paths. If mapping fails, fix the disk’s url in config/filesystems.php.
WebP “Best Format” Mode
If format is omitted or set to 'best':
- If the client’s
Acceptheader includesimage/webp, output is WebP. - Otherwise, it falls back to JPEG.
<img src="{{ 'media/example.jpg' | qresize(800, 600, { format: 'best' }) }}" alt="">
Caching and Storage Layout
Resized images are stored on the configured disk under a nested directory structure, based on a hash:
resized/ab/cd/ef/abcdef1234567890...webp
resized/ab/cd/ef/abcdef1234567890...json
The hash is derived from:
- Source (path or URL)
- Requested width / height
- Options (including disk, if enabled in your hash implementation)
- mtime and size snapshot (where available)
The .json file contains metadata such as:
src(original source)w,h(dimensions)opts(options)diskmtime,size
If an identical resize is requested again, the existing file is reused and no new job is queued. To clear cached resized images, delete the resized directory on the relevant disk.
Queue Behaviour and Concurrency
When you call qresize with a new combination of source + options:
- The plugin writes the JSON metadata next to where the image will live.
- It dispatches a
ProcessImageResizejob on the configured queue. - It immediately returns the URL for the resized image.
Rendering happens asynchronously in the queue worker.
Notes:
- A single
php artisan queue:workprocess handles jobs one at a time. - Concurrency comes from running multiple workers:
php artisan queue:work --queue=imaging php artisan queue:work --queue=imaging php artisan queue:work --queue=imaging
Troubleshooting
“Source not found on disk …”
-
Verify what you pass into
qresize(media/foo.jpg,file.path,someField | media, etc.). -
Confirm the file exists on the disk used (
IMAGE_RESIZE_DISKordiskoption). -
PDFs not rendering
-
Confirm Imagick is installed and enabled in PHP.
-
Confirm ImageMagick + Ghostscript are installed and working.
-
Check queue and PHP logs for Imagick exceptions.
-
Nothing is being resized
-
Verify at least one queue worker is running.
-
Ensure the worker listens on the correct queue (
IMAGE_RESIZE_QUEUE). -
Run
php artisan queue:work --verboseand inspectstorage/logs/.
LicenseThe MIT License (MIT)
Copyright (C) 2025 Helmut Kaufmann, https://mercator.li, software@mercator.li
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.