bitbetter / kirby-libvips
Fast, memory-efficient libvips thumbnail driver for Kirby CMS with in-process (php-vips FFI) and CLI backends
Package info
github.com/bitbetterde/kirby-libvips
Type:kirby-plugin
pkg:composer/bitbetter/kirby-libvips
Requires
- php: >=8.1
- getkirby/composer-installer: ^1.2
Suggests
- jcupitt/vips: In-process thumbnail generation via FFI - eliminates the ~60ms process-spawn cost per thumb
This package is auto-updated.
Last update: 2026-07-18 07:25:41 UTC
README
A thumbnail (Darkroom) driver for Kirby CMS that generates thumbs with libvips — in-process via php-vips (FFI) or by shelling out to the vipsthumbnail CLI. Compared to GD or ImageMagick, libvips is faster and uses a fraction of the memory — especially noticeable with large source images or bulk thumb generation. Resize, crop (center, positional, panel focus points, smartcrop), blur, grayscale and sharpen are all supported.
Requires Kirby 4 or 5, PHP ≥ 8.1 and libvips ≥ 8.15 installed on the server (apt install libvips-tools, brew install vips, …). Out of the box the plugin shells out to the vipsthumbnail and vips binaries — no PHP extension needed. If php-vips is installed (composer require jcupitt/vips), the plugin automatically switches to in-process generation via FFI, which eliminates the ~60 ms process-spawn cost per thumb and makes small thumbs faster than GD.
Installation
composer require bitbetter/kirby-libvips
Or copy this repository to /site/plugins/kirby-libvips.
For the fast in-process backend (recommended), additionally install php-vips:
composer require jcupitt/vips
The plugin picks it up automatically — no configuration needed. Without it, the plugin uses the vipsthumbnail/vips binaries.
Usage
// site/config/config.php return [ 'thumbs' => [ 'driver' => 'vips', ], ];
That's it. The plugin registers itself as a proper Darkroom driver, so Kirby's whole thumb pipeline (media jobs, srcset(), focus cropping, format conversion via thumbs.format) works unchanged.
Options
All options go into the thumbs config array:
| Option | Default | Description |
|---|---|---|
backend |
auto |
ffi (in-process php-vips), cli (binaries), or auto to pick FFI when available |
bin |
vipsthumbnail |
Path to the vipsthumbnail binary (CLI backend) |
vipsBin |
vips |
Path to the vips binary (used for exact focus crops) |
quality |
90 |
Output quality (JPEG/WebP/AVIF/…) |
interlace |
false |
Progressive JPEGs / interlaced PNGs |
strip |
true |
Strip all metadata (EXIF, ICC, XMP) from thumbs |
profile |
'srgb' |
Convert to this ICC profile before stripping (false to disable) |
smartcrop |
false |
Content-aware cropping for plain center crops: 'attention' or 'entropy' |
autoOrient |
true |
Auto-rotate according to EXIF orientation |
threads |
1 |
Number of threads vips may use per job |
return [ 'thumbs' => [ 'driver' => 'vips', 'quality' => 85, 'interlace' => true, 'smartcrop' => 'attention', 'bin' => '/usr/local/bin/vipsthumbnail', ], ];
Cropping behavior
$file->crop(300, 200)— center crop (or content-aware, ifsmartcropis configured)- Positional crops (
'top left','bottom', …) and panel focus points ('45% 30%') are honored exactly, matching the behavior of Kirby's built-in drivers. EXIF-rotated images are handled correctly (the crop is applied in display space). - With
smartcrop: 'attention', plain center crops use libvips' smartcrop to find the most interesting region — explicit focus points still win.
Effects
blur, grayscale and sharpen are fully supported:
$file->blur(10); $file->grayscale(); // also: ->bw() $file->sharpen(50); $file->thumb(['width' => 480, 'grayscale' => true, 'blur' => 4, 'sharpen' => 50]); // combined in one job
They are parameter-matched to Kirby's ImageMagick driver (gaussblur with the same sigma, colourspace b-w, sharpen --sigma amount/100), so switching drivers doesn't change the look. Note that GD's blur is much weaker than IM/vips at the same value — that inconsistency exists between Kirby's built-in drivers too.
With the FFI backend the whole pipeline (shrink, crop, effects, encode) fuses in memory — no subprocesses, no intermediate files. With the CLI backend, effects run as additional vips operations on the already-shrunk intermediate, so each adds one process spawn but little actual work.
Backends
auto(default): uses php-vips (FFI) whenjcupitt/vipsis installed and PHP's FFI extension is usable, otherwise the CLI binaries. Detection runs once per request and fails safe to CLI.ffi: forces php-vips. ~5–15 ms per thumb warm; the one-time FFI/libvips initialization (~150 ms) is paid once per PHP-FPM worker, not per thumb. If FFI is blocked on your setup, setffi.enable = truein php.ini.cli: forces thevipsthumbnail/vipsbinaries. No PHP requirements at all, but each thumb pays a fixed process-spawn cost (~10–70 ms depending on platform).
Both backends produce identically sized, visually identical output — backend only changes how fast you get it.
Performance
All numbers measured through Kirby's real thumb pipeline (Darkroom->process() on a copied file, exactly what the media route does) with Kirby 5.5.2, libvips 8.18.4, ImageMagick 7.1.2, PHP 8.5 on Apple Silicon; fastest of 3 runs, warm. Your absolute numbers will differ — the ratios are the point.
Speed
| GD | ImageMagick | vips CLI | vips FFI | |
|---|---|---|---|---|
| 1000 px source → resize 480 px | 10.5 ms | 24.1 ms | 91.4 ms | 7.8 ms |
| 1000 px source → focus crop | 19.1 ms | 25.5 ms | 158.3 ms | 3.7 ms |
| 1000 px source → gray + blur + sharpen | 35.1 ms | 30.6 ms | 313.4 ms | 12.6 ms |
| 6000 px source → resize 480 px | 204.7 ms | 154.7 ms | 121.1 ms | 37.9 ms |
| 6000 px source → srcset (4 widths) | 887.6 ms | 1320.4 ms | 735.3 ms | 219.8 ms |
What that means in practice:
- A gallery page requesting 50 thumbs of fresh 17 MP camera uploads: ~1.9 s of CPU with the FFI backend vs. ~10 s with GD — and if each image also gets a 4-width
srcset(), ~11 s vs. ~44 s. - Small, already-downscaled sources (CMS re-uploads, screenshots): FFI is still the fastest option; the CLI backend is the slowest here because every job pays a fixed ~60–80 ms process spawn regardless of image size. If you can't use FFI and your sources are small, GD is legitimately fine.
- Thumbs are generated once and cached in
/media, so these costs apply on first generation (uploads, cache busts, deploys with cleared media) — which is exactly when dozens of jobs hit PHP at once.
Memory
Peak RSS generating one 480 px thumb from the 6000 px source:
| GD | ImageMagick | vips CLI | vips FFI |
|---|---|---|---|
| 106.5 MB | 240.9 MB | 32.4 MB | ~46 MB on top of the PHP worker¹ |
¹ 74 MB total for a one-shot php process incl. the 28 MB PHP baseline; in PHP-FPM the runtime is already resident, so the marginal cost per worker is the ~46 MB libvips share, reused across all jobs of that worker.
- GD and ImageMagick decode the full bitmap first — memory scales with source megapixels, not thumb size. The 6000 × 2862 source costs GD ~68 MB for the raw bitmap before any resizing happens. libvips streams via shrink-on-load and stays near-flat regardless of source size.
- GD actually crashes on this workload:
$file->crop(240, 240)on the 6000 px source aborts withAllowed memory size of 134217728 bytes exhaustedat PHP's default 128 MBmemory_limit. The same job needs no config changes with libvips. - Concurrency is the multiplier: a panel page or gallery triggers many parallel media requests. Eight PHP-FPM workers generating simultaneously peak around ~850 MB with GD and ~1.9 GB with ImageMagick — vs. ~260 MB with the CLI backend or ~370 MB with FFI. On small servers this is the difference between finishing and OOM-killing.
Migrating from kirby3-vipsthumbnail
thumbs.driver => 'vipsthumbnail'still works as an alias for'vips'.blur,grayscaleandsharpenwere previously ignored — they are now applied.- Panel focus points and positional crops were previously flattened to smartcrop-attention — they are now honored exactly; content-aware cropping is opt-in via the
smartcropoption. - The
log/logdiroptions were removed; errors now surface as exceptions including the vips error output. autoOrientnow defaults totrue(matching libvips' own default and Kirby's other drivers) and no longer breaks on libvips ≥ 8.8.
LLM disclosure
The Kirby 4/5 rewrite of this plugin — the Darkroom driver, the FFI backend, the effects support, this README and the benchmarks — was written in large part by an LLM (Claude Opus 4.8 via Claude Code), directed and reviewed by a human maintainer.
What that means for you:
- Nothing here is unverified LLM output. Every code path was exercised against a real Kirby 5.5.2 installation with an 18-case test harness: output dimensions are checked against Kirby's GD driver, EXIF-rotated sources, panel focus points, format conversion (WebP/AVIF/PNG), metadata stripping, quality settings, and paths with spaces are all covered, for both backends.
- The benchmark numbers are real measurements from that setup (hardware and versions stated above), not estimates or vendor claims.
- The plugin still ships human-auditable, dependency-light code: two PHP files you can read in ten minutes.
If you find behavior that differs from Kirby's built-in drivers beyond what's documented here, that's a bug — please open an issue.
Credits
Originally based on kirby3-vipsthumbnail by Florian Karsten. Rewritten for Kirby 4/5's Darkroom API and current libvips.
License
MIT