schliesser / imaginator
Imaginator - Zero-config responsive images for TYPO3: signed srcset ladders, local or external processing.
Package info
github.com/schliesser/imaginator
Type:typo3-cms-extension
pkg:composer/schliesser/imaginator
Requires
- php: >=8.3
- srwiez/thumbhash: ^1.5
- typo3/cms-backend: ^13.4 || ^14.3
- typo3/cms-core: ^13.4 || ^14.3
- typo3/cms-fluid: ^13.4 || ^14.3
- typo3/cms-frontend: ^13.4 || ^14.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.51
- friendsoftypo3/content-blocks: ^1.6 || ^2.4
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.0
- saschaegerer/phpstan-typo3: ^3.0
- typo3/cms-belog: ^13.4 || ^14.3
- typo3/cms-beuser: ^13.4 || ^14.3
- typo3/cms-extensionmanager: ^13.4 || ^14.3
- typo3/cms-filelist: ^13.4 || ^14.3
- typo3/cms-fluid-styled-content: ^13.4 || ^14.3
- typo3/cms-info: ^13.4 || ^14.3
- typo3/cms-install: ^13.4 || ^14.3
- typo3/cms-lowlevel: ^13.4 || ^14.3
- typo3/cms-setup: ^13.4 || ^14.3
- typo3/cms-tstemplate: ^13.4 || ^14.3
- typo3/testing-framework: ^8.2 || ^9.0
Conflicts
README
Zero-config responsive images for TYPO3. The integrator writes no sizes and no per-image
breakpoints: at render time the extension emits a real <picture>/<img> with a quantized
width-ladder srcset and sizes="auto", so the browser's preload scanner fetches the
correctly-sized image in a single request.
For editors. A backend aspect-ratio field per content element lets editors pick the framing once — per breakpoint — and get uniform, consistent images across every element of that type, no more ragged grids from mismatched upload dimensions. Crop and focus area are honored, so the chosen subject stays in frame at every size.
For developers. Drop one <i:image> and get sharp, perfectly-sized images on every device — no
hand-tuned sizes, no breakpoint lists, no layout shift (CLS). Stop shipping oversized images:
the width ladder bounds processing to a fixed set of sizes, so you serve fewer, smaller bytes and
score better Core Web Vitals (LCP/CLS) out of the box.
Features. Quantized width-ladder srcset + sizes="auto" · a single uniform output format
(AVIF by default, or WebP) · per-breakpoint art direction (crop variants + focus area honored) · low-quality
placeholders (ThumbHash, dominant-color, or none) · priority images get fetchpriority="high"
and skip lazy-loading · pluggable processing — classic sync on first request, async via a
signed middleware endpoint (default), or an external processor (e.g. imgproxy).
JavaScript is never required for sharpness — it's only a polyfill for Safari < 27.
Requirements
- PHP 8.3+ (tested 8.3 / 8.4 / 8.5)
- TYPO3 13.4 LTS or 14.3 LTS
- A working image processor (GraphicsMagick or ImageMagick — TYPO3's standard
GFXsettings) for local processing; alternatively an external processor (e.g. imgproxy) - A non-empty
encryptionKey(standard on every TYPO3 install — see Signing below)
Installation
composer require schliesser/imaginator
A Composer install activates the extension automatically. Installing it is enough to use the
<i:image> ViewHelper — no Site Set or TypoScript is required. Configure it under
Settings → Extension Configuration (see below).
Usage
Declare the ViewHelper namespace once per template and call <i:image>:
<html xmlns:i="http://typo3.org/ns/Schliesser/Imaginator/ViewHelpers" data-namespace-typo3-fluid="true"> {# Simplest case: one ratio, renders an <img> with the full ladder #} <i:image image="{fileReference}" aspectRatio="16:9" alt="{fileReference.description}"/> {# By file UID or path #} <i:image src="{file.uid}" treatIdAsReference="0" aspectRatio="4:3" alt="A product"/> {# Art direction: per-breakpoint ratios render a <picture> with one <source media> each. The map is {breakpoint: ratio}; keys are preconfigured breakpoint aliases (see extension config) or a px min-width, with 0 / xs as the base. Tiers are mobile-first (min-width only); the base (min-width 0) tier becomes the <img> fallback, larger tiers become <source>s. #} <i:image image="{image}" aspectRatio="{0: '1:1', 'lg': '16:9'}" alt="This is an image"/> {# LCP / above-the-fold image: drops lazy-loading, adds fetchpriority="high" and an explicit sizes="100vw" #} <i:image image="{hero}" aspectRatio="16:9" alt="Hero" priority="1"/> </html>
Arguments
| Argument | Type | Default | Description |
|---|---|---|---|
image |
File/FileReference |
– | FAL object (use this or src) |
src |
string | '' |
File UID or path (e.g. fileadmin/img/x.jpg) |
treatIdAsReference |
bool | false |
Treat src as a sys_file_reference UID |
aspectRatio |
string | map | 16:9 |
"W:H", or a {breakpoint: "W:H"} map for art direction |
cropVariant |
string | default |
FAL crop variant to use |
alt |
string | '' |
Alternative text |
class |
string | – | CSS class on the <img> |
priority |
bool | false |
Mark as the LCP image (see below) |
width/height are always emitted from the largest rung, so there is zero layout shift.
Priority / LCP images
Set priority="1" on the above-the-fold (LCP) image. Imaginator then:
- drops
loading="lazy"and addsfetchpriority="high"on the<img>; - renders an explicit
sizes="100vw"instead ofsizes="auto"; - adds a
<link rel="preload" as="image" imagesrcset="…" imagesizes="100vw" type="image/…" fetchpriority="high">to the<head>, so the request is discoverable in the initial document before the body is parsed.
Only the most-preferred format is preloaded (gated by type, so other browsers don't
double-download), and its imagesrcset/imagesizes mirror the rendered tier exactly so the
browser reuses the preload. This satisfies Lighthouse's "LCP request is discoverable",
"not lazily loaded" and "fetchpriority should be applied" audits. Use it on one image per page.
sizes="auto" and the Safari polyfill
Zero-config sizing relies on sizes="auto", which lets the browser pick the right srcset
candidate from the laid-out width — no hand-written sizes, no per-image breakpoints. Chrome/Edge
(126+) and Firefox (150+) support it natively; Safari and iOS Safari only from version 27
(WebKit landed it in March 2026; Safari 27 ships in the 2026 cycle). On older Safari/iOS an
unsupported sizes="auto" falls back to 100vw, so they fetch an oversized (but still sharp)
candidate. (sizes="auto" is spec-valid only on loading="lazy" images, which is exactly where
Imaginator emits it — priority/eager images get an explicit sizes="100vw" instead.)
To close that gap, whenever a processed <i:image> renders, Imaginator queues a tiny
progressive-enhancement script via the AssetCollector (head, async, registered once per page):
the vendored Shopify/autosizes polyfill (MIT,
Resources/Public/JavaScript/frontend/autosizes.js). It self-detects native support and backs off;
on Safari it measures the rendered width and writes a concrete pixel sizes, so the browser picks a
right-sized candidate. It is never load-bearing — sharpness comes from the server-rendered
ladder, so the page works fully with JavaScript disabled — and it only touches
<img loading="lazy" sizes="auto">, so priority/LCP images are untouched (they carry an explicit
sizes="100vw" and loading="eager"). Pages without any <i:image> ship no extra JS.
Crop & focus areas
When an image (a FAL FileReference) is rendered, the editor's crop variant is resolved server-side:
it reads the reference's crop JSON and fits the requested ratio inside the crop area, centered on the
focus area — so the framing the editor chose is honored and no crop geometry is exposed in the URL.
The ladder is bounded by the crop area, so a tightly-cropped region is never upscaled. A src given as
a path or plain file uid (f…) has no crop data and is center-cropped to the ratio.
Configuration
Configuration is instance-wide Extension Configuration. Edit it under Settings → Extension
Configuration → imaginator in the backend, or set it in config/system/settings.php:
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['imaginator'] = [ 'lqip' => 'dominant-color', 'ladder' => '320,640,960,1280,1920', ];
| Setting | Default | Description |
|---|---|---|
processor |
local:async |
Image processor: local:async (signed endpoint + 302), local:sync (static srcset, no middleware) or imgproxy (offloaded) |
maxDimension |
3840 |
Largest image dimension in px; the ladder is capped to it |
ladder |
320,420,560,740,980,1300,1720,2000,2560,3200,3840 |
Width-ladder rungs (comma-separated), covering up to 4K (UHD) displays |
format |
avif |
Single uniform output format: avif or webp |
quality.avif |
50 |
AVIF quality (AVIF's scale sits lower than JPEG/WebP for the same perceived quality) |
quality.webp |
72 |
WebP quality |
lqip |
thumbhash |
Low-quality placeholder: thumbhash, dominant-color or none |
The signing secret is derived from the global encryptionKey; when it changes, flush the frontend
caches so re-rendered HTML carries freshly signed URLs. Settings are global by design, so the render path
and the signed-endpoint verify path always agree (a per-site model would risk mismatched ladders →
spurious 403s).
Content Security Policy
The placeholder is not rendered as an inline style="" attribute (those cannot carry a CSP
nonce). The <img> only gets a CSS class; the actual rule is registered through TYPO3's
AssetCollector and emitted as a <style> element, which TYPO3 nonces automatically when frontend
CSP is enabled. Identical placeholders are deduplicated to a single rule.
CSP directives you may need:
thumbhash(default): the blurred preview is adata:background-image, so allowimg-src data:(TYPO3's default frontend CSP already does).dominant-color: a plain background color — needs nothing beyond the nonced<style>.none: no placeholder, no extra directive.
The local:async image endpoint
Each async candidate URL has one of these forms:
reference: /_imaginator/{16-hex-signature}/r{referenceUid}/{cropVariant}/{width}x{height}.{ext}
file: /_imaginator/{16-hex-signature}/f{fileUid}/{cropVariant}/{width}x{height}.{ext}
The uid is site-unique, so no storage segment is needed. A PSR-15 middleware verifies the signature,
re-checks the width against the ladder, processes the image and 302-redirects to the processed
file with Cache-Control: public, max-age=31536000, immutable. A forged or tampered URL returns
403, and only ladder-quantized widths are ever processed — so the endpoint cannot be abused to
exhaust the server with arbitrary sizes.
If the derivative already exists at render time, the static processed-file URL is written straight into the markup — so the web server serves it directly, with no middleware/PHP roundtrip.
The signing secret is derived automatically from
$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']. No configuration is needed, but note that
changing encryptionKey invalidates previously generated URLs (they will re-sign on the next
render).
Contributing
A reproducible DDEV demo instance, the full test workflow, and the coding/PR conventions are documented in CONTRIBUTING.md. In short:
ddev start && ddev setup && ddev demo # demo at https://imaginator.ddev.site/ (admin / Password.1) ddev test all && ddev lint
License
GPL-2.0-or-later.
