The
netresearch/nr-image-optimize
extension provides
on-demand image optimization for TYPO3. Images are processed
lazily via middleware when first requested, with support for
modern formats (WebP, AVIF), responsive srcset generation,
and automatic format negotiation.
Introduction
Learn what the extension does, its features, and
system requirements.
Installation
Install via Composer or the Extension Manager.
Configuration
Configure the SourceSetViewHelper, render modes,
responsive srcset, and lazy loading.
Usage
Integrate responsive images into your Fluid
templates with practical examples.
Maintenance
Manage processed images and check system
requirements in the backend module.
Developer reference
Architecture overview and PHP API reference.
Introduction
What does it do?
The |extension_name| extension (|extension_key|) optimizes images
in TYPO3. Three operating modes activate automatically after
installation:
On-demand frontend processing. Images are converted and
resized lazily when first requested through the
/processed/ URL path, so only images actually viewed by
visitors are processed.
On-upload compression. Newly uploaded or replaced files
are losslessly compressed in place (via optipng,
gifsicle, or jpegoptim, whichever is installed) by
an event listener, without changing the file's dimensions
or format.
Bulk CLI. The nr:image:optimize and
nr:image:analyze console commands scan existing FAL
storages. See Usage.
Features
Lazy image processing. Images are optimized only when
a visitor first requests them.
Modern format support. Automatic WebP and AVIF
conversion with fallback to original formats.
Responsive images. Built-in SourceSetViewHelper for
srcset and sizes generation.
Render modes. Choose between cover and fit
resize strategies.
Width-based srcset. Optional responsive srcset with
configurable width variants and sizes attribute.
Fetch priority. Native fetchpriority attribute
support for Core Web Vitals optimization.
Backend maintenance module. View statistics, check
system requirements, and clear processed images.
On-upload compression. Uploaded and replaced files are
losslessly compressed in place via optipng,
gifsicle, or jpegoptim.
Bulk CLI tools.nr:image:optimize and
nr:image:analyze process or report on existing FAL
storages.
Powered by Intervention Image. Uses the
Intervention Image
library for reliable image manipulation.
Requirements
PHP 8.2, 8.3, or 8.4.
TYPO3 12.4.
Intervention Image library, version 3.7.2 or 3.11.1
(installed automatically via Composer).
Optional, for on-upload/CLI compression: optipng,
gifsicle, and/or jpegoptim on the $PATH (or
pointed to via the OPTIPNG_BIN/GIFSICLE_BIN/
JPEGOPTIM_BIN environment variables). Missing binaries
degrade gracefully -- that format is simply skipped.
The extension works out of the box after installation. No
additional configuration is required. Images accessed through
the /processed/ path are automatically optimized by the
frontend middleware.
Tip
For best results, ensure that your server has the
Imagick or GD PHP extension installed. The backend
maintenance module can verify all prerequisites for you
(see System requirements check).
Configuration
The extension works out of the box with sensible defaults.
Images are automatically optimized when accessed via the
/processed/ path.
SourceSetViewHelper
The SourceSetViewHelper generates responsive <img> tags
with srcset attributes.
Public path to the source image (for example
/fileadmin/foo.jpg), typically generated via
f:uri.image().
width
width
Type
int|float
Default
0
Base width in pixels for the rendered <img>. 0
resolves automatically from the source file.
height
height
Type
int|float
Default
0
Base height in pixels. 0 preserves aspect ratio
relative to width.
set
set
Type
array
Default
[]
Responsive set in the form
{maxWidth: {width: int, height: int}}. Each entry
becomes a <source media="(max-width: <maxWidth>px)">
tag.
alt
alt
Type
string
Default
empty string
Alternative text (accessibility). HTML-escaped.
title
title
Type
string
Default
empty string
Title attribute for the image. HTML-escaped.
class
class
Type
string
Default
empty string
CSS classes for the <img> tag; include lazyload
to use JS lazy load.
attributes
attributes
Type
array
Default
[]
Extra HTML attributes merged into the rendered tag.
lazyload
lazyload
Type
boolean
Default
false
Add loading="lazy" (native lazy loading).
sizes
sizes
Type
string
Default
auto, (min-width: 992px) 991px, 100vw
Responsive sizes attribute for the generated
<img> tag.
mode
mode
Type
string
Default
cover
Render mode. cover resizes images to fully cover
the given dimensions. fit resizes images to fit
within the given dimensions.
responsiveSrcset
responsiveSrcset
Type
boolean
Default
false
Enable width-based responsive srcset instead of
density-based 2x srcset.
widthVariants
widthVariants
Type
string|array
Default
480, 576, 640, 768, 992, 1200, 1800
Width variants for responsive srcset
(comma-separated string or array).
fetchpriority
fetchpriority
Type
string
Default
empty string
Native HTML fetchpriority attribute. Allowed
values: high, low, auto. Omitted when
empty.
Encoding quality
The encoding quality is not configurable per ViewHelper call --
there is no quality argument. Generated URLs always carry the
default quality of 75 (for example
/processed/fileadmin/image.w1200h800m0q75.jpg), and variant
requests that carry no q value are processed with the same
default.
Note
As of this version the default variant quality is 75. Earlier
versions encoded WebP and AVIF variants at quality 100, so
those variants are now generated with stronger compression and
accordingly lower fidelity. The quality value is part of the
generated file name, so all previously generated variants
become stale and are recreated on demand under their new name.
Use Clear processed images to remove
the obsolete files.
Source set configuration
Define source sets per media breakpoint via the set
attribute:
Both modes support lazy loading via the native
loading="lazy" attribute. When using JS-based lazy
loading (class="lazyload"), the data-srcset
attribute is added automatically.
Backward compatibility
By default responsiveSrcset
is false, preserving the existing 2x density-based
srcset behavior. All existing templates continue to work
without modifications.
Trusted storage symlinks
New in version 1.2.0
The additionalTrustedStorageSymlinks extension configuration
setting.
The processor validates that both the source image and the
target variant resolve (via realpath()) to a location
inside an allowed root -- the public webroot, or a Local FAL
storage's own base path -- before reading or writing anything.
This rejects requests for images reached through a symlink that
escapes those roots (for example a symlink accidentally or
maliciously pointing at /etc).
Some deployments relocate TYPO3 core's own
_processed_ image cache -- a subdirectory inside a FAL
storage's own directory, e.g. fileadmin/_processed_ --
onto local/ephemeral storage, to keep frequently-rewritten
derivative images off shared/NFS storage. That leaves only a
symlink behind inside the storage, which the FAL-storage
basePath lookup above does not see (it only resolves the
storage's own base path, never looks inside it), so variant
requests for such images are rejected even though the original
file is legitimately part of the deployed application.
The additionalTrustedStorageSymlinks extension configuration
setting closes this gap on an explicit, per-instance, opt-in
basis: a comma-separated list of directory names that, when
found as a symlink directly inside a Local FAL storage's base
path, are resolved and added to the allow-list too.
The setting can also be edited via the backend:
Admin Tools > Settings > Extension Configuration >
nr_image_optimize.
Attention
This widens the set of filesystem locations the processor
will read from and publicly serve variants of. Only add
well-known, infrastructure-managed names here -- never a
name an untrusted party (e.g. an FTP-only content account)
could create on their own. The default is empty, which
keeps today's behavior unchanged for every installation that
does not configure it.
Additional trusted roots
New in version 1.3.0
The additionalTrustedRoots extension configuration setting.
TYPO3's var/ directory is now trusted automatically.
Some deployments need to serve variants of images that live under
an absolute filesystem path that is neither the public webroot,
a Local FAL storage's own base path, nor one of the hardcoded
TYPO3-internal locations (var/, symlinked
processed/uploads, or extension-published
_assets/<hash> directories) -- for example a custom mount
managed outside of FAL.
The additionalTrustedRoots extension configuration setting
closes this gap on an explicit, per-instance, opt-in basis: a
comma-separated list of absolute filesystem paths that are
realpath-resolved and added to the allow-list directly.
The setting can also be edited via the backend:
Admin Tools > Settings > Extension Configuration >
nr_image_optimize.
Attention
This widens the set of filesystem locations the processor
will read from and publicly serve variants of. Only add paths
you fully trust. Relative paths are rejected outright (never
resolved against the PHP process's working directory). The
default is empty, which keeps today's behavior unchanged for
every installation that does not configure it.
WebP/AVIF output quality
New in version 1.3.0
The qualityWebp and qualityAvif extension configuration
settings.
The primary variant's quality is controlled per-request via the
q<n> URL segment. The .webp and .avif sidecars previously
reused that same numeric quality, but AVIF's quality scale is steeper
than WebP's or JPEG's -- at matching numbers an AVIF file comes out
larger than the WebP sidecar, defeating the point of serving AVIF at
all.
Two extension configuration settings control sidecar quality
independently of the primary variant:
qualityWebp (default 75)
Output quality for the generated WebP variant.
qualityAvif (default 60)
Output quality for the generated AVIF variant. The lower default
keeps AVIF variants genuinely smaller than WebP while staying
visually comparable.
The settings can also be edited via the backend:
Admin Tools > Settings > Extension Configuration >
nr_image_optimize.
Attention
Per-format quality is not part of the processed-variant cache
filename. Changing either setting only affects newly generated
variants -- clear already-processed images (see
Clear processed images) to apply the new quality to existing
ones.
Usage
This chapter shows practical examples for integrating
responsive images into your Fluid templates.
Register the namespace
Add the ViewHelper namespace at the top of your Fluid
template or register it globally:
Public images only: absolute URLs are passed through
New in version 1.1.3
Absolute URLs, data: URIs, and URLs with a query string are
passed through unchanged and rendered as a plain <img> tag.
The /processed/ endpoint is designed for public files only.
It resolves the given path below the public web root and writes the
generated variants as static files into public/processed/,
where the web server delivers them directly — without any access
check.
Files in non-public FAL storages (is_public = 0) can therefore
not be processed. Extensions such as
fal_securedownload
resolve such files to tokenized eID URLs
(/index.php?eID=dumpFile&...) whose delivery runs through TYPO3
and performs a permission check on every request.
The ViewHelper detects absolute URLs (http://, https://,
//), data: URIs, and URLs containing a query string and
passes them through unchanged, rendering a plain <img> tag with
the URL as src:
No srcset/sizes attributes and no per-breakpoint
<source> elements are generated — the browser always
loads the image in its original dimensions.
No WebP/AVIF variants and no quality optimization are
applied.
In return, the access control of the generating extension
(e.g. fal_securedownload) stays fully intact, because the
URL — including its access token — is emitted unchanged.
If you need optimized variants of images in protected storages,
generate them with TYPO3's own image processing (for example
f:image or the ImageService). Processed files are then
created inside the protected storage's processing folder and are
delivered through the same secure-download mechanism, keeping the
permission check intact.
Fetch priority for Core Web Vitals
Use the fetchpriority attribute to hint the browser
about resource prioritization, improving Largest Contentful
Paint (LCP) scores:
Both commands read the FAL index directly and process eligible
image files (image/jpeg, image/gif, image/png) on
online storages. Per-file storage permission evaluation is
temporarily disabled and restored in a finally block so
long-running CLI runs don't leak state across iterations or
require a BE user context.
Bulk optimize images
The nr:image:optimize command compresses every eligible
PNG, GIF, and JPEG file across all storages (or a restricted
subset) using the installed optimizer binaries. The original
file is replaced in place only when the tool produces a
smaller result.
Restrict to specific storage UIDs. Accepts repeated
occurrences or a comma-separated list.
--jpeg-quality
Lossy JPEG quality 0--100. Omit for lossless JPEG
optimization.
--strip-metadata
Remove EXIF and comments when the tool supports it.
Analyze optimization potential
The nr:image:analyze command estimates how much disk
space could be saved by running nr:image:optimize or by
downscaling oversized originals. It is purely heuristic --
no external binaries are invoked, so it runs quickly even on
large installations.
Report potential for storage 1
vendor/bin/typo3 nr:image:analyze --storages=1
Copied!
Options:
--storages
Restrict to specific storage UIDs.
--max-width / --max-height
Target display box (default 2560 x 1440). Images larger
than this box are assumed to be downscaled and the
estimate factors in the area reduction.
--min-size
Skip files smaller than this many bytes (default
512000). Prevents noise from already-tiny images.
Maintenance
The extension provides a backend module accessible via
Admin Tools > Processed Images Maintenance.
Overview
View statistics about processed images:
File count and total size.
Directory count.
Largest files.
File type distribution.
System requirements check
Verify all technical prerequisites and tool availability:
Remove all on-demand generated images. Images are regenerated
automatically when first accessed again.
Warning
After clearing processed images, expect temporarily
increased loading times on the frontend until images are
regenerated on demand.
Developer reference
Architecture
The extension has three entry points that share the same
ImageOptimizer service where applicable:
ProcessingMiddleware intercepts frontend requests
matching the /processed/ path and delegates to
Processor, which handles image optimization, format
conversion, and caching.
OptimizeOnUploadListener reacts to PSR-14 file events
(AfterFileAddedEvent and AfterFileReplacedEvent)
and delegates to ImageOptimizer for in-place lossless
compression via optipng/gifsicle/jpegoptim.
OptimizeImagesCommand and AnalyzeImagesCommand
iterate the FAL index from the CLI, extending
AbstractImageCommand for shared iteration and option
parsing. See Usage.
SourceSetViewHelper generates responsive <img> markup
in Fluid templates. MaintenanceController provides the
backend module for statistics and cleanup.
Frontend middleware registered before
typo3/cms-frontend/site. Intercepts requests whose
path starts with /processed/ and delegates image
processing to the Processor class.
Processor
classProcessor
Fully qualified name
\Netresearch\NrImageOptimize\Processor
Core image processing engine. Parses the requested URL
to extract dimensions, quality, mode, and format
parameters. Uses the Intervention Image library for
actual image manipulation. Processed images are cached
on disk to avoid repeated processing.
Fluid ViewHelper that generates <img> tags with
srcset attributes for responsive image delivery.
Supports both density-based (2x) and width-based
responsive srcset modes.
Shared service that shells out to optipng,
gifsicle, or jpegoptim (whichever is resolvable
on the $PATH, or via the
OPTIPNG_BIN/GIFSICLE_BIN/JPEGOPTIM_BIN
environment variables) via proc_open with argument
arrays. Used by both OptimizeOnUploadListener and
OptimizeImagesCommand. A missing binary degrades
gracefully -- that format is simply skipped.
PSR-14 listener for AfterFileAddedEvent and
AfterFileReplacedEvent. Compresses newly
uploaded/replaced files in place via
ImageOptimizer,
guarded against re-entrancy by storage UID + file
identifier.
Fixed: every README/Documentation SourceSetViewHelper example
used a non-existent file argument -- only path is
registered, so these examples failed at Fluid render time. Also
removed a fictitious format confval, added the real arguments
(alt, title, class, attributes, set,
lazyload) that were missing from both READMEs' parameter
lists, and documented the nr:image:optimize/nr:image:analyze
CLI commands and the on-upload compression feature,
both previously undocumented anywhere.
Fixed: this changelog and CHANGELOG.md had fallen behind --
backfilled the missing 1.3.1 and 1.3.2 entries.
1.4.0
Added: on-upload compression. OptimizeOnUploadListener
subscribes to AfterFileAddedEvent and
AfterFileReplacedEvent and runs optipng,
gifsicle, and jpegoptim inline as a file lands,
instead of relying on a separate cron/CLI pass. Port of the
main-branch feature released in 2.2.2.
Added: nr:image:optimize and nr:image:analyze console
commands to cover existing files -- bulk optimization with
filter/dry-run support, and a heuristic per-image savings
report without modifying files. See
Usage.
Fixed: bulk nr:image:optimize/nr:image:analyze no
longer abort the whole run when a single file fails. A stale
FAL identifier (e.g. after an out-of-band folder rename)
raised an uncaught RuntimeException mid-run, discarding
progress already made. Both commands now catch per-file
failures, report them individually, and continue with the
rest of the queue.
Attention
OptimizeOnUploadListener is registered by default and
runs inline on every upload/replace. If your workflow depends
on uploads being stored byte-for-byte as they arrived,
disable the listener in your site package's Services.yaml
before upgrading -- see the CHANGELOG's "Upgrading" note for
1.4.0.
1.3.2
Fixed: animated GIFs are passed through unprocessed instead
of being collapsed to their first frame. GIFs with more than
one frame are excluded from variant processing; the original
file is copied to the variant path and served as-is. WebP/AVIF
sidecar generation is skipped for animated GIFs too. Port of
the main-branch fix (2.x PR #143).
1.3.1
Fixed: URL dimensions of 0 (meaning "derive this side from
the aspect ratio", as SourceSetViewHelper always writes
both dimensions into the variant URL) were floored to 1
by dimension clamping, turning height-based or width-based
variants into 1x1 pixel images. 0 now derives the missing
side from the aspect ratio, as it already did for an absent
dimension.
1.3.0
Added: additionalTrustedRoots extension configuration --
per-instance, opt-in, comma-separated list of absolute
filesystem paths, outside FAL and TYPO3-internal locations,
that are realpath-resolved and added to the path-validation
allow-list. TYPO3's var/ directory is now trusted
automatically. Port of the fix on main (2.4.0). See
Additional trusted roots.
Added: qualityWebp (default 75) and qualityAvif
(default 60) extension configuration settings, so the
WebP and AVIF sidecar variants can be tuned independently of
the primary variant's quality. AVIF's steeper quality scale
previously meant AVIF variants came out larger than WebP at
the same numeric quality. Port of the fix on main (2.4.0).
See WebP/AVIF output quality.
Fixed: the Maintenance module's "clear processed images"
action failed whenever processed was a symlink to a
shared volume -- a common Deployer/CI deployment layout. It
validated the target with realpath()-equality, which
resolves the symlink and never matches, so clearing failed on
every symlinked deployment. The directory is now emptied in
place instead of recreated, so the symlink survives. Port of
the fix on main (2.4.0).
1.2.0
Added: additionalTrustedStorageSymlinks extension
configuration -- per-instance, opt-in, comma-separated list of
directory names that, when found as a symlink directly inside a
Local FAL storage's own base path (e.g.
fileadmin/_processed_), are resolved and added to the
path-validation allow-list. Closes the gap where deployments
relocate TYPO3 core's own _processed_ image cache onto
local/ephemeral storage to keep it off shared/NFS storage,
leaving a symlink behind that the FAL-storage basePath lookup
cannot see. Default empty; keeps today's behaviour for every
installation that doesn't opt in. See Trusted storage symlinks.
Fixed: images published via public/_assets/<hash>
symlinks (extension Resources/Public/ assets) were
rejected with HTTP 400. TYPO3 core publishes each extension's
Resources/Public/ directory by symlinking
public/_assets/<hash>/ to a location outside the public
webroot. getAllowedRoots() did not resolve these symlinks, so
variant requests for e.g. an extension's default/fallback image
failed even though the file is a legitimate part of the deployed
application. Every immediate child of _assets is now
resolved individually.
1.1.3
Fixed: the sourceSet ViewHelper passes absolute URLs
(http://, https://, //), data: URIs, and URLs
carrying a query string through unchanged and renders them as a
plain <img> tag. Previously such paths — e.g. the tokenized
eID=dumpFile URLs fal_securedownload
generates for files in non-public storages — were mangled into
broken /processed/... variant paths. The access control
of the generating extension stays intact; see
Public images only: absolute URLs are passed through for the trade-off. Port of the
fix on main (2.2.4).
1.1.2
Fixed: silent HTTP 400 responses now log their rejection reason
via error_log() (URL-pattern mismatch and
path-outside-allowed-roots branches).
Fixed: a transient StorageRepository failure during early
TYPO3 bootstrap no longer poisons the per-process allowed-roots
cache; the degraded fallback is kept only for the current
request.
Fixed: getAllowedRoots() is memoized per request, avoiding
redundant lookups and repeated log lines.
Fixed: a filesystem-root public path (/) no longer rejects
every valid path.
1.1.1
Fixed: processed image requests no longer return
HTTP 400 when fileadmin (or any other Local
FAL storage) is a symlink to an external location
such as an NFS/EFS mount. isPathWithinAllowedRoots
now accepts any realpath-resolved path that lies
within the TYPO3 public root or the realpath of any
configured Local storage's basePath. Symlinks
placed inside a storage that escape every allowed
root -- e.g. fileadmin/evil -> /etc
-- continue to be rejected. Backport of the fix on
main, reported in
issue #70.
Hardened: paths containing NUL bytes are rejected
outright, closing a minor realpath-bypass via the
not-yet-existing-path parent-walk branch.
Changed (BC for subclasses and manual instantiators):
Netresearch\\NrImageOptimize\\Processor
gains a
new required StorageRepository constructor
parameter. Consumers that autowire the service (the
default in TYPO3 12+) are unaffected; any code that
extends the class or constructs it by hand must
forward the new dependency.
Changed (BC): dropped PHP 8.1 support. The TYPO3_12
maintenance branch now requires PHP 8.2 or newer
(TYPO3 v12 itself still supports PHP 8.1, but this
extension aligns with the netresearch/typo3-ci-workflows
tooling which requires PHP 8.2+).
1.1.0
New in version 1.1.0
Comprehensive quality review: security hardening, performance
improvements, backend maintenance module, responsive srcset,
and expanded test coverage.
Added backend maintenance module with directory statistics,
system requirements check, and clear processed images action.
Added responsive width-based srcset generation as
opt-in feature.
Added widthVariants parameter for custom breakpoints.
Added sizes parameter for responsive image sizing.
Added fetchpriority attribute for resource hints.
Added path traversal hardening and XSS prevention.
Added DoS prevention via dimension and quality clamping.