Thumbor image scaling for Plone — replaces in-ZODB scales with Thumbor redirect URLs.
Instead of generating and storing scaled images in ZODB annotations (via Pillow), this package intercepts Plone's image scaling and returns signed Thumbor URLs. Thumbor fetches the blob directly from PostgreSQL, scales on demand, and caches the result — no scaled image data ever enters ZODB, and nothing is decoded while a page is served.
Large or non-sRGB originals get one extra blob: a capped, colour-normalised source derivative that Thumbor reads instead of the print-resolution original. See Source derivatives below.
Plone generates signed Thumbor URLs and either embeds them directly or redirects to them — it processes no image data while serving the request.
Classic Plone / direct @@images traversal
Browser Plone Thumbor PostgreSQL
| | | |
| GET @@images/ | | |
| image/preview | | |
|------------------>| | |
| 302 to Thumbor | | |
|<------------------| | |
| | |
| GET /sig/fit-in/400x0/<zoid>/<tid> | |
|------------------------------------------------>| |
| | SELECT data |
| | FROM blob_state |
| |--------------------> |
| |<---------------------|
| 200 scaled JPEG (Thumbor cache hit) | |
|<------------------------------------------------| |
Volto / REST API (image_scales metadata)
At catalog index time, Plone pre-computes absolute Thumbor URLs for every scale and stores them in the image_scales catalog metadata. Volto reads these directly from the REST API response and renders <img src="https://thumbor/..."> — no redirect hop at all.
In both cases Thumbor retrieves blobs via zodb-pgjsonb-thumborblobloader: local disk cache first, then PostgreSQL bytea, with optional S3 fallback. Cache busting is automatic — the blob TID in the URL changes whenever the image is updated.
- zodb-pgjsonb — ZODB storage with PostgreSQL JSONB + blob_state table
- zodb-pgjsonb-thumborblobloader — Thumbor loader reading from blob_state
- Thumbor >= 7.0
- Plone 6.2+
pip install plone.pgthumborAdd to your Plone site's GenericSetup profile dependencies or install via the Add-ons control panel.
| Environment Variable | Default | Description |
|---|---|---|
PGTHUMBOR_SERVER_URL |
(required) | Public Thumbor base URL, e.g. http://thumbor:8888 |
PGTHUMBOR_SECURITY_KEY |
(required) | Shared HMAC-SHA1 signing key |
PGTHUMBOR_UNSAFE |
false |
Use /unsafe/ URLs instead of signed — dev only |
PGTHUMBOR_SOURCE_MAX_EDGE |
4000 |
Longest edge of the source derivative, in pixels. 0 disables generation; values above 8000 are clamped |
Environment variables take precedence over Plone registry settings (IThumborSettings).
| Plone mode | Thumbor behaviour |
|---|---|
scale (default) |
fit-in + smart crop (if enabled) — fits within box, no upscale |
cover |
fit-in only — fits within box, may upscale, never crops |
contain |
Crop to exact dimensions, smart crop (if enabled) — fills the box |
plone.scale's own mode names are the reverse of what they describe: its alias for cover is scale-crop-to-fill, and its docstring claims to follow CSS background-size.
scale_mode_to_thumbor swaps cover and contain behind a plone.scale < 6 gate to compensate — see plone/plone.scale#78.
The table above is the resulting live behaviour, not plone.scale's naming.
Thumbor refuses to process images above its MAX_PIXELS limit (75 MP by default) and answers HTTP 400, which is what a print original at 100x75 cm and 300 dpi does to it. It also fetches the whole original on every cache miss, so a 40 MB blob crosses the network and the decoder to produce a 3 KB listing thumbnail.
On add and modify, a subscriber stores a capped, sRGB-normalised copy of the image beside the original, as NamedBlobImage._pgthumbor_source, and Thumbor URLs address that copy instead. The original is never modified and @@download still serves it byte for byte. A derivative is generated when the longest edge exceeds PGTHUMBOR_SOURCE_MAX_EDGE, or when the image is not a clean sRGB raster (CMYK, LAB, 16-bit integer modes, palette with transparency). SVG and animated GIFs are skipped. This is the one place where the Plone process decodes pixels, and it is bounded to one decode at a time.
The default cap of 4000 is a starting point for a site nobody has measured, not an answer for yours; the cap in force is recorded with each derivative, so changing it later is an ordinary backfill run rather than a migration.
- Choose the source derivative cap — measure your own numbers and pick a value
- Backfill Thumbor source derivatives — give existing content derivatives and repair the catalog
A self-contained Docker Compose stack (Plone 6.2 + Thumbor + PostgreSQL + nginx) using PyPI releases is in tryout/.
cd tryout
docker compose up -d --build
# Plone at http://localhost:8080 (admin/admin)For development with local source installs, use development/ instead.
Rendered documentation: https://bluedynamics.github.io/plone-pgthumbor/
- Architecture -- request flow, Thumbor integration design
- Security -- three-layer access control model
- Configuration Reference -- all settings for Plone and Thumbor
- CHANGES.md -- changelog
The source code is managed in a Git repository, with its main branches hosted on GitHub. Issues can be reported there too.
We'd be happy to see many forks and pull requests to make this package even better. We welcome AI-assisted contributions, but expect every contributor to fully understand and be able to explain the code they submit. Please don't send bulk auto-generated pull requests.
Maintainers are Jens Klein and the BlueDynamics Alliance developer team. We appreciate any contribution and if a release on PyPI is needed, please just contact one of us. We also offer commercial support if any training, coaching, integration or adaptations are needed.
GPL-2.0