raphael-gmbh / ra-watermark
Stamps a file-metadata hint text (e.g. an AI-generation disclosure) directly into the pixels of processed images.
Package info
git.raphael.zone/raphael-gmbh/watermark
Type:typo3-cms-extension
pkg:composer/raphael-gmbh/ra-watermark
Requires
- ext-gd: *
- typo3/cms-core: ^13.4
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Stamps a caption or an AI-disclosure badge directly into the pixels of processed images — not an HTML/CSS overlay, so the label survives a download, a screenshot, or the image being reused outside the page.
What it does
For every raster image TYPO3 crops/scales for frontend delivery, this extension checks the file's metadata and, if applicable, draws one of two things onto a copy of the processed image before it's served. Supported formats are PNG, GIF, WebP and JPEG — anything else (SVG, AVIF, ...) is left completely untouched, silently falling back to TYPO3's own un-stamped processed file, since there's no GD-based way to draw pixels onto it.
- Text caption — if
sys_file_metadata.file_watermarkhas text, that text (with a configurable prefix, e.g. "Bild: ...") is drawn in a filled box. - Badge — if the hint text is empty but the file has one of the three
official Art. 50 EU AI Act disclosure badges selected
(
file_watermark_badge), that badge image is composited on instead.
Text always wins over a badge; if neither is set, the image is left untouched. There is no "on by default" behavior — a stamp only ever appears because an editor explicitly set the hint text or picked a badge for that specific file.
The original upload is never modified, and TYPO3's own processed-file cache is
never touched either: this extension keeps its own content-addressed cache
under typo3temp/assets/images/hint-stamps/ and only points
ProcessedFile::updateProcessingUrl() at it. That means TYPO3's normal
resize/crop caching keeps working exactly as before, and a cache hit on TYPO3's
side can never result in stamping an already-stamped image a second time.
Backend previews and the image cropper always show the clean, un-stamped image — stamping only happens for frontend requests.
Requirements
- TYPO3 13.4
- GD with FreeType support (for the text caption; standard in TYPO3 installations since image processing needs it anyway)
- A TTF/OTF font for the text caption — defaults to the font TYPO3 core itself
ships (
EXT:core/Resources/Private/Font/nimbus.ttf), so it works out of the box; pointfontFileat a brand font if you have one
Installation
composer require raphael-gmbh/ra-watermark
Then, since it adds columns to sys_file_metadata:
vendor/bin/typo3 database:updateschema
Usage
Nothing changes in templates — any existing <f:image> / <f:uri.image>
usage picks this up automatically, since it hooks TYPO3's own
AfterFileProcessingEvent rather than a Fluid ViewHelper.
In the file metadata editing form (on any image file), a "water mark" tab holds all of these fields:
- water mark — free text. Typing something here always takes priority over a badge.
- Custom text/background color — only applies to the text caption (a badge always uses its own artwork). Each is a native color-picker field; leave either empty to inherit the site/extension default. The background color's picker has an opacity slider — drag it down for a translucent or fully transparent background, letting the text float without a box.
- water mark position —
bottom-left/bottom-right/top-left/top-right/custom, or left blank to inherit the site/extension default. - Custom horizontal/vertical position — only shown once position is set to
custom. Each accepts a percentage (e.g.50%) or a pixel value (e.g.120px), measured inward from the edge within the same padding inset the corner presets use — so0%/0%(or0px/0px) lands exactly where "Top left" would, and100%/100%lands exactly where "Bottom right" would, on any crop size, with the same breathing room from the edge. The hint can never end up partially outside the image. - Custom rotation — only shown once position is set to
custom. Degrees from -180 to 180, applied to whichever of the text caption or badge ends up being stamped. - water mark badge — one of the three official badges
(
ki-generiert/mit-ki-erstellt/mit-ki-veraendert), or "No badge". There is deliberately no "inherit the site default" option here: whether a legal disclosure badge appears on an image is always an explicit per-file choice, never an incidental side effect of a global setting.
Configuration
Every visual setting (font, colors, sizes, padding, default position) is configurable at two levels, resolved in this order — most specific wins:
- Per file —
file_watermark_position(plus the custom position/rotation fields when it'scustom), and the custom text/background color fields (text caption only). Everything else falls through to the next level. Per site — a Site Set (
raphael-gmbh/ra-watermark) exposes every setting under theraWatermark.*namespace. A site opts in by adding it to itsdependenciesinconfig/sites/<identifier>/config.yaml:dependencies: - raphael-gmbh/ra-watermark settings: raWatermark: position: bottom-right badgeWidth: 140Only the keys a site actually overrides need to appear under
settings:; anything left out falls through to the extension configuration default.- Extension configuration (Admin Tools → Settings → Extension
Configuration →
ra_watermark) — the global fallback for every site, whether or not it has opted into the Site Set.
Available keys: fontFile, labelPrefix, fontSize, textColor,
backgroundColor, padding, margin, position, badgeWidth.
padding and margin are two distinct, deliberately separate settings:
padding is the space between the caption text and its background box's own
edge (text captions only — a badge has no box); margin is the space between
the hint (box or badge) and the edge of the image itself. margin is also
what the custom position fields use as their 0%/100% inset (see above).
A caveat on textColor / backgroundColor
TYPO3's Site Settings require a syntactically valid color as a setting's
default value, so unlike the other keys these two can't use an empty "inherit"
sentinel. Their Site Set default is a real color (#000000 / #ffffff,
matching the extension configuration default at the time of writing). If you
later change these colors in the extension configuration, sites that included
the Set without setting their own color will keep showing the old value until
it's also updated in Configuration/Sets/Watermark/settings.definitions.yaml.
Every other setting doesn't have this problem — changing them in the extension
configuration immediately affects every site that hasn't explicitly overridden
that specific key.
Translation
All the per-file fields are ordinary translatable sys_file_metadata columns.
file_watermark and the two custom-position fields deliberately don't use
l10n_mode = prefixLangTitle (unlike core's own title/alternative):
TYPO3 only honors that setting for input/text/email fields, which these
are, and for those it prepends a literal [Translate to <language>:] to a
newly-created translation's copied value — text that would otherwise get
stamped straight into the image, or (for the position fields) parsed as 0
by resolveCustomCoordinate()'s numeric cast, silently collapsing the
position to the margin. A new translation instead starts as a plain,
unprefixed copy of the default-language value, safe to render as-is until an
editor gets around to translating it.
If an editor translates a file's metadata (via the file list's "Translate"
action) and fills in a translated file_watermark, the stamping listener
resolves the current frontend language itself — via
PageRepository::getLanguageOverlay() against an explicitly-fetched
sys_file_metadata row — and uses the translated value whenever one exists,
falling back to the default-language value otherwise. This is done explicitly
rather than relying on TYPO3 core's implicit frontend overlay
(FileMetadataOverlayAspect), which silently skips eID requests and isn't
guaranteed to run outside a full frontend render.
This project currently has only one site language configured everywhere, so there's nothing to translate into yet — but the mechanism is exercised and verified, ready for any other multi-language installation of this extension.
Caching notes
Changing an extension configuration value takes effect immediately — no cache
flush needed. Changing anything under Configuration/Sets/ (i.e. the Site
Settings definitions) does require a cache flush, since TYPO3 compiles the
Set registry into its core cache. Stamped images themselves are cached by
content hash (source file + all resolved settings, including the current
frontend language and the resolved site), so a settings change produces a
new file automatically.
Old variants are not deleted automatically as part of a request — the same source image legitimately has several simultaneously valid stamped variants (different languages, different sites in a multi-site setup), and an already-cached HTML page can still reference an older variant's URL, so nothing gets deleted just because a different variant became current. Instead, run this periodically (manually, or via a TYPO3 Scheduler "Execute console command" task) to delete cache files that haven't been served in a while:
vendor/bin/typo3 watermark:cleanup-cache
It deletes files under typo3temp/assets/images/hint-stamps/ whose content
hasn't been confirmed still current for more than cacheMaxAgeDays days
(default 30; configurable in the extension configuration, or per-run via
--max-age=<days>, which must be a positive integer — anything else, e.g. a
typo or a negative value, is rejected rather than silently deleting
everything). Keep this comfortably longer than any page cache lifetime in
your installation. Add --dry-run to preview what would be deleted without
deleting anything, or --purge-all to delete every cache file regardless of
age (dangerous — pages TYPO3 has already cached may still reference these
URLs; run with --dry-run first).
If this runs as a TYPO3 Scheduler "Execute console command" task, a failure
only shows as a generic "task failed" in the Scheduler task list — TYPO3
core runs every Scheduler command with its output discarded, so the
specific reason (an invalid --max-age, a permissions error preventing
deletion, ...) never reaches that screen. Check Admin Tools → Log (or
wherever this installation's PSR-3 log is configured to write) for the
actual message.
Badge artwork
The three badge PNGs/SVGs under Resources/Private/Images/ are the official
Art. 50 EU AI Act Kennzeichnung artwork (KI-generiert / Mit-KI-erstellt /
Mit-KI-veraendert), shipped at 1x/2x/3x. Stamping always composites from the
3x PNG and resizes to badgeWidth, since GD can't rasterize the SVG source
directly.
Known limitations
- The text caption and a badge are mutually exclusive per file (text always wins); there's no way to show both at once.
textColor/backgroundColorsite-setting overrides don't fully cascade from extension configuration changes — see the caveat above.- Only PNG, GIF, WebP and JPEG are stamped (see Requirements above). Setting a watermark or badge on an SVG or other unsupported format has no effect and produces no error — TYPO3's clean, un-stamped file is served as-is.