A self-hosted image transformation service.
Put your original images in a directory, start one Docker container, then resize, crop and convert them by changing the URL.
/image/upload/c_fill,w_640,h_360/demo.jpg
No upload API. No database. No configuration file.
EvaThumber keeps your originals untouched and generates everything else on demand.
The demo image is 5184×3456. EvaThumber never emits anything larger than 4096 pixels on a side, so the picture below is the file as it sits in your images directory. It is not something you can fetch back through a transformation URL.
Resize to 600px wide:
/image/upload/w_600/demo.jpg
Crop it to a 600×300 image:
/image/upload/c_fill,w_600,h_300/demo.jpg
Turn it into a square:
/image/upload/c_fill,w_400,h_400/demo.jpg
Keep the whole image and fit it inside 600×300:
/image/upload/c_fit,w_600,h_300/demo.jpg
Add a white canvas instead of cropping:
/image/upload/c_pad,w_600,h_300,b_white/demo.jpg
Convert to grayscale:
/image/upload/w_600/e_grayscale/demo.jpg
Convert to WebP with automatic quality:
/image/upload/w_600/q_auto/f_webp/demo.jpg
Transformations can be chained:
/image/upload/c_fill,w_600,h_300/e_grayscale/q_80/f_webp/demo.jpg
That is basically how EvaThumber works.
Mount the directory containing your original images:
docker run \
-p 8080:8080 \
-v /path/to/your/images:/data/images:ro \
docker.io/allovince/evathumberIf /path/to/your/images/demo.jpg exists, open:
http://localhost:8080/image/upload/w_600/demo.jpg
The image is generated on the first request and cached afterwards.
The original directory is mounted read-only. EvaThumber never modifies your source images.
Generated images are cached in /data/cache.
You do not have to configure it.
With the command above, the cache lives inside the container. Removing the container effectively clears all generated images. They will simply be generated again when requested.
If you want the cache to survive container replacement, mount it:
docker run \
-p 8080:8080 \
-v /path/to/your/images:/data/images:ro \
-v evathumber-cache:/data/cache \
docker.io/allovince/evathumberThe cache contains derived data only. It does not need to be backed up.
EvaThumber uses Cloudinary-style transformation URLs:
/image/upload/{transformations}/{public_id}.{ext}
It also accepts the Cloudinary form with a cloud name:
/{cloud_name}/image/upload/{transformations}/{public_id}.{ext}
Parameters inside one transformation are separated by commas:
c_fill,w_600,h_400
Multiple transformations are separated by /:
c_fill,w_600,h_400/e_grayscale/q_80/f_webp
So:
/image/upload/c_fill,w_600,h_400/e_grayscale/q_80/f_webp/demo.jpg
means:
demo.jpg
↓
fill 600×400
↓
grayscale
↓
quality 80
↓
WebP
Resize by width:
/image/upload/w_600/demo.jpg
Resize by height:
/image/upload/h_400/demo.jpg
Relative size:
/image/upload/w_0.5/demo.jpg
Scale:
/image/upload/c_scale,w_600/demo.jpg
Fit inside a box without cropping:
/image/upload/c_fit,w_600,h_300/demo.jpg
Fill a box and crop overflow:
/image/upload/c_fill,w_600,h_300/demo.jpg
Crop:
/image/upload/c_crop,w_600,h_400,g_center/demo.jpg
Thumbnail:
/image/upload/c_thumb,w_400,h_400,g_center/demo.jpg
Pad:
/image/upload/c_pad,w_600,h_300,b_white/demo.jpg
Limit an image without enlarging it:
/image/upload/c_limit,w_1600,h_1600/demo.jpg
/image/upload/c_fill,w_400,h_400,g_north/demo.jpg
/image/upload/c_fill,w_400,h_400,g_south/demo.jpg
/image/upload/c_fill,w_400,h_400,g_east/demo.jpg
/image/upload/c_fill,w_400,h_400,g_west/demo.jpg
Compass gravity is supported:
center
north
north_east
east
south_east
south
south_west
west
north_west
/image/upload/c_fill,w_600,ar_16:9/demo.jpg
/image/upload/c_fill,w_300,h_200,dpr_2/demo.jpg
The resulting image is rendered at 600×400 pixels.
/image/upload/w_600/a_90/demo.jpg
/image/upload/w_600/a_180/demo.jpg
/image/upload/w_600/a_hflip/demo.jpg
/image/upload/w_600/a_vflip/demo.jpg
/image/upload/w_600/e_grayscale/demo.jpg
/image/upload/w_600/e_negate/demo.jpg
Explicit quality:
/image/upload/w_600/q_80/demo.jpg
Automatic quality:
/image/upload/w_600/q_auto/demo.jpg
/image/upload/w_600/q_auto:best/demo.jpg
/image/upload/w_600/q_auto:good/demo.jpg
/image/upload/w_600/q_auto:eco/demo.jpg
/image/upload/w_600/q_auto:low/demo.jpg
q_auto is EvaThumber's local content-adaptive heuristic. It is not Cloudinary's proprietary perceptual algorithm.
The result of every request must fit within 4096 pixels on each side and 16 megapixels in total. Rotation, flipping, effects and quality settings do not change the pixel count, so applying them on their own to a large original is rejected with 413 image_too_large. Resize in the same request, as in every example above.
The URL extension can choose the output format:
/image/upload/w_600/demo.webp
Or use f_:
/image/upload/w_600/f_webp/demo.jpg
/image/upload/w_600/f_avif/demo.jpg
/image/upload/w_600/f_png/demo.jpg
Automatic format negotiation is also supported:
/image/upload/w_600/f_auto/demo.jpg
EvaThumber uses the request's Accept header to choose the output format.
Input:
JPEG
PNG
WebP
AVIF
GIF (static only)
Output:
JPEG
PNG
WebP
AVIF
GIF
EvaThumber 2 deliberately keeps its scope small.
It does not currently implement:
- remote or S3 image sources
- animated image processing
- SVG or PDF
- text and image overlays
- face detection
- automatic AI gravity
- rounded corners
- blur, sharpen and other stylized effects
- video transformation
- upload or administration APIs
Unsupported transformations fail explicitly instead of being silently approximated.
EvaThumber 2 is built on PHP 8.5, FrankenPHP and libvips.
The Docker image:
- supports
linux/amd64andlinux/arm64 - runs as a non-root user
- keeps source images read-only
- publishes cached files atomically
- deduplicates concurrent generation of the same image
- uses bounded work queues under load
- returns
503instead of allowing overload to corrupt images or exhaust the process - supports
ETag,Last-Modifiedand conditional304responses - exposes
/healthzand/readyz
The default configuration is intended to work without tuning.
If you need to know exactly how it behaves under load or process failure, see bench/ and docs/. The repository contains the benchmark and crash-recovery evidence used for the 2.0 release.
The transformation engine can also be used without the HTTP service:
use EvaThumber\Image\Pipeline;
use EvaThumber\Source\LocalSource;
use EvaThumber\Transformation\Parser;
$source = (new LocalSource('/path/to/images'))->resolve('demo');
(new Pipeline())->write(
$source,
(new Parser())->parse('c_fill,w_600,h_400'),
'/tmp/demo.webp',
'webp'
);The library core does not depend on the HTTP cache or server layer.
composer install
composer test
composer analyseSee docs/ for architecture, testing and release documentation.
EvaThumber started in 2012 as a small PHP image thumbnail library.
Version 2 is a complete rewrite. The basic idea is still the same:
change the URL, get the image you need.
The URL syntax, processing engine and deployment model have changed.
See docs/migration-v1.md.
BSD-3-Clause. See LICENSE.
demo.jpg uses Lake Mountain Landscape by Bonnie Moreland, released under CC0 / public domain.
Source: Wikimedia Commons.