pecinaon / processwire-modern-images
Lets ProcessWire use WebP, AVIF and HEIC images as sources, not just as output. Adds an ImageSizer engine so modern originals can be resized, cropped and thumbnailed like a JPEG.
Package info
github.com/pecinaon/processwire-modern-images
Type:processwire-module
pkg:composer/pecinaon/processwire-modern-images
Requires
- php: >=8.1
- ext-gd: *
- composer/installers: ^2.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5
Suggests
- ext-imagick: Required to read HEIC/HEIF originals; GD cannot decode them.
Provides
None
Conflicts
None
Replaces
None
README
Lets ProcessWire use WebP, AVIF and HEIC images as sources — resized, cropped and thumbnailed like a JPEG — instead of only writing them as output.
The problem
Upload a .webp to an image field and ProcessWire accepts the file, then fails
at everything afterwards:
blog-cover.0x260.webp - not a supported image type
No variation is produced. No crop. Not even the thumbnail in the page editor, which renders as a broken image with that message beside it.
This is not a bug in your site. ImageSizerEngine lists exactly four readable
types — gif, jpg, jpeg, png (wire/core/ImageSizerEngine.php:274) — and WebP
support in the core is output only: webpAdd and webpOnly write a .webp
next to a JPEG, and $image->webp->url asks for one. Nothing reads one back.
The core Imagick engine does not help either. Its validSourceImageFormats() is
the same four types.
So today a site that wants to work in modern formats has to keep a JPEG or PNG of every image purely so ProcessWire can resize it. That is the constraint this module removes.
What it does
| Format | Read | Written | Result |
|---|---|---|---|
| WebP | GD or Imagick | GD or Imagick | variations stay WebP |
| AVIF | GD or Imagick | GD or Imagick | variations stay AVIF |
| HEIC / HEIF | Imagick only | never | original kept, variations written as WebP or AVIF |
A WebP original produces WebP variations. An AVIF original produces AVIF. The format the author chose is the format that gets served, which is the whole point.
HEIC is the deliberate exception. Every iPhone shoots it by default, so it turns up in real uploads whether a site asks for it or not — but outside Safari no browser renders it. The module reads it so the upload just works, keeps the original at full quality as the archive copy, and writes the variations in a format the visitor's browser can actually display. It never writes HEIC.
It also adds the readable extensions to every image field on install, because a
field checks uploads against its own list and would otherwise still refuse a
.webp however capable the server is.
Install
composer require pecinaon/processwire-modern-images
Then Modules → Refresh → Install Modern Images.
Until the package is on Packagist, add it as a VCS repository first:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/pecinaon/processwire-modern-images.git" }
],
"require": { "pecinaon/processwire-modern-images": "^1.0" },
"config": { "allow-plugins": { "composer/installers": true } }
}
Two things that bite people:
config.allow-pluginsis mandatory from Composer 2.2 on. Without it the installer plugin never runs, the package lands invendor/instead ofsite/modules/, and ProcessWire never sees it.- Composer resolves this from git tags. Without one you need
dev-mainand a matchingminimum-stability.
No template changes. Nothing to call. Existing code that does
$image->size(800, 0)->url starts working on modern originals.
Requirements
- ProcessWire 3.0.138 or newer
- PHP 8.1 or newer
- WebP and AVIF: GD built with libwebp / libavif, which is the common case,
or the
imagickextension - HEIC: the
imagickextension, with ImageMagick built against libheif. GD has no HEIC decoder and there is no sign of one coming
Nothing is assumed from the PHP version. The module asks gd_info() and
Imagick::queryFormats() at runtime, and a format whose library is missing is
simply not offered — the site behaves exactly as it does without the module
rather than failing on the first upload.
The module's configuration screen shows what the server can do, format by format. That table is the first thing to check when something is not working.
Settings
| Setting | Default | What it is |
|---|---|---|
| WebP quality | 90 | used when the caller did not pass webpQuality to size() |
| AVIF quality | 45 | AVIF holds up at lower numbers than WebP; 45 is roughly WebP 80 by eye |
| AVIF encoder speed | 6 | 0 is slowest and smallest, 10 fastest and largest |
| HEIC delivered as | WebP | what variations of a HEIC upload are written in |
| Update image fields | on | add the readable extensions to image fields when installing |
A webpQuality passed to size() always beats the module setting. That is the
caller being explicit, and an explicit request should never lose to a default.
How it works
The engine wraps ProcessWire's own pipeline rather than replacing it:
- decode the original into a temporary lossless PNG
- re-inspect, so the engine believes it is working on that PNG
- hand off to
ImageSizerEngineGD::processResize()— unmodified - encode the result back into the target format
The alternative was to copy processResize() and add two switch branches.
That method is over 300 lines and carries gamma correction, sharpening, the
memory guard, Exif rotation and IPTC handling. A copy forks all of it and rots
at the next core release. The wrapper costs one extra decode and encode per
variation, paid once because variations are cached, and cannot be broken by a
core change short of a signature change.
PNG is the intermediate because it is lossless. A JPEG intermediate would
quantise every variation a second time, and the point of accepting a WebP
original is not to degrade it on the way through. Alpha is carried explicitly
through both hops — without imagesavealpha() GD drops it, and a logo on a
transparent ground comes back on black.
The engine only claims formats the core cannot read. A JPEG never enters the
wrapper; supported() returns false and ProcessWire hands it to the default GD
engine as before.
What it deliberately does not do
- Convert your images behind your back. A WebP original stays WebP. The one exception is HEIC, which no browser would render, and the module says so on its settings screen rather than doing it silently.
- Write HEIC. Reading it is a convenience for uploads. Writing it would produce files most visitors cannot open.
- Replace
$config->webpOptions. Core's->webpderivative behaviour is untouched; this module is about what can be used as a source. - Add a
<picture>helper or format negotiation. That is a template concern, and there are modules for it.
Contributing
See CONTRIBUTING.md. In short: composer install, then
composer test, composer analyse and composer lint.
Licence
MIT. See LICENSE.