justinholtweb / craft-waterfall
Watermarking for Craft CMS — image and text watermarks applied to transforms, to originals with backups, or at your CDN, with Imager-X and ImageOptimize integration.
Package info
github.com/justinholtweb/craft-waterfall
Type:craft-plugin
pkg:composer/justinholtweb/craft-waterfall
Requires
- php: ^8.2
- ext-json: *
- craftcms/cms: ^5.3.0
README
Watermarking for Craft CMS 5.
Put a mark on your images — automatically, everywhere, and reversibly. Waterfall is the Craft answer to the WordPress plugin Easy Watermark: image and text watermarks applied to generated transforms, burned into the original file with a restorable backup, produced on demand from Twig, or built at your CDN by imgix, Cloudinary or ImageKit.
- Craft CMS 5.3+, PHP 8.2+
- Lite $59 · Pro $79
Why
Four problems, and Waterfall exists for all four.
A watermark applied by hand is applied inconsistently. Somebody forgets, somebody uses the wrong corner, somebody uploads the unmarked original next Tuesday.
A watermark burned into the original cannot be taken back. Waterfall backs the original up before it writes anything, and will not write if the backup fails. Restoring is one click, one element action, or one console command — in Lite as well as Pro.
The mark has to survive the image pipeline. Craft's transforms, ImageOptimize's .webp and
.avif variants and Imager-X's output are all new files made from the source, so a mark that
lives only in the source file disappears from every derivative that gets regenerated, and one that
lives only in a transform is missing from the file people download. Waterfall covers both.
Some sites do not process images at all — the CDN does. So the CDN is a driver, not a different plugin.
Install
composer require justinholtweb/craft-waterfall php craft plugin/install waterfall
A profile
A profile is one watermark: what it is, how big, where it goes, how see-through, and which images get it. Profiles live in project config, so they deploy with the site.
| Type | An image (any asset — a PNG with transparency is best) or, on Pro, text |
| Size | A percentage of the base image's width or height, a box to fit inside or cover, or the mark's own size |
| Position | Nine anchors, an exact offset, or tiled across the whole image (Pro) |
| Offsets | In pixels or as a percentage of the image |
| Opacity | 0–100 |
| Rotation and blend mode | Pro; blend modes need Imagick |
| Applies to | Volumes, file types, named transforms, a minimum size, and any Craft asset condition (Pro) |
| Drawn by | This server (GD or Imagick), or one of the delivery services (Pro) |
Text watermarks are rendered as object templates, so they can carry the asset and the site with them:
© {{ now|date('Y') }} {{ siteName }} — {{ asset.title }}
Four fonts are bundled (Lato, Lato Bold, IBM Plex Mono, EB Garamond, all OFL). A profile can also
point at an uploaded .ttf or .otf, which wins.
Where a watermark can be applied
Three modes, switchable per profile, because they answer different questions.
Transforms. The mark is composited into generated transforms. Originals are never touched,
everything is undone by clearing the transform cache, and the mark reaches ImageOptimize's variants
and Imager-X's output. What most sites want, and it needs no template changes: asset.getUrl()
already returns a watermarked image.
Permanent. The stored file itself is rewritten — on upload, from the asset index, from the asset's own edit screen, or from the console. The original is copied to the backup store first. This is the only mode that protects the file somebody downloads straight from the volume.
On demand. Nothing automatic; a Twig call, an element action or a console command asks for a watermarked derivative, which Waterfall stores itself.
Twig
Most sites need none of this — a profile in transforms mode watermarks asset.getUrl() with no
template changes at all. This is for the cases that need to ask.
{# A watermarked URL, from the profile's provider or Waterfall's own derivative store #} <img src="{{ craft.waterfall.url(asset, { width: 1200 }, 'copyright') }}"> {# Would this asset get a watermark? #} {% if craft.waterfall.appliesTo(asset, 'copyright') %}…{% endif %} {# Has this asset's own file been watermarked? #} {% if craft.waterfall.isWatermarked(asset) %}…{% endif %} {# Why not? One verdict per profile, each with a sentence #} {% for verdict in craft.waterfall.explain(asset) %} {{ verdict.profile.name }}: {{ verdict.applies ? 'yes' : verdict.message }} {% endfor %} {# A profile as Imager-X's own watermark parameter, to merge into a transform you write yourself #} {% set params = craft.waterfall.imagerParams('copyright', 1200) %}
ImageOptimize
Nothing to configure. ImageOptimize listens to the same Craft event Waterfall does, and re-saves,
optimizes and derives its .webp/.avif variants from the image on that event — so the only thing
that matters is running first. Waterfall registers its handler prepended, composites onto the
image before ImageOptimize ever sees it, and every file ImageOptimize produces carries the mark.
An integration check asserts the ordering, because if it ever regressed the symptom would be
"the .webp has no watermark and nothing is in the log".
If ImageOptimize is set to transform through a service rather than on this server, Waterfall says
so on its settings screen and in waterfall/doctor — mark those images with a provider instead.
Imager-X
Imager-X does not use Craft's transform pipeline, so it gets its own wiring.
Automatic. Waterfall registers a transformer that adds matching profiles to every transform, so the watermark appears with no template changes and each profile gets its own cache entry. Requires Imager-X Pro, which is the edition where transformers can be registered.
By hand, on any Imager-X edition:
{% set image = craft.imagerx.transformImage(asset, {
width: 1200,
effects: { waterfall: 'copyright' }
}) %}
The effect runs Waterfall's own compositor, so text, tiling and rotation all work — none of which
Imager-X's built-in watermark parameter can do.
Delivery services (Pro)
| Provider | Image marks | Text marks | Tiling | Notes |
|---|---|---|---|---|
| imgix | ✓ | — | ✓ | mark, mark-align, mark-alpha, mark-tile; signs URLs when a secure token is set |
| Cloudinary | ✓ | ✓ | ✓ | Fetch or upload delivery; l_text: overlays |
| ImageKit | ✓ | ✓ | — | tr=…l-image/l-text…l-end |
| URL template | ✓ | — | — | Your own pattern, for Bunny, Cloudflare Images, Thumbor, anything |
A provider that cannot do what a profile asks for says so on the profile screen rather than
quietly delivering something else. The URL template takes {src}, {srcEncoded}, {path},
{mark}, {markEncoded}, {position}, {opacity}, {scale}, {width}, {height}, {format}.
Console
php craft waterfall/apply/run --volume=photos --dry-run # count and explain, write nothing php craft waterfall/apply/run --volume=photos # write watermarks into the files php craft waterfall/apply/restore --volume=photos # put the originals back php craft waterfall/apply/status # what carries a permanent watermark php craft waterfall/doctor # what this server can actually do
waterfall/doctor is worth running once on any new server. Watermarking fails quietly more often
than it fails loudly — GD compiled without FreeType draws no text, a server without Imagick has no
blend modes, a provider with no credentials returns no URL — and it prints all of that in one
place.
Editions
Lite ($59) — one profile, image watermarks, nine positions and offsets, scaling, opacity, GD and Imagick, all three modes, backups and restore, volume/file-type/size rules.
Pro ($79) — unlimited and stackable profiles, text watermarks, tiling, rotation, blend modes, asset conditions, the four delivery services, the Imager-X and ImageOptimize integrations, the per-asset Waterfall field, and the console tooling.
Backups and restore are in Lite deliberately. Charging for the undo of a destructive operation is not a business model.
What Waterfall refuses, and says so
Vector files, animated GIFs (unless you turn that off — re-encoding loses the animation), images
below a profile's minimum size, files Craft cannot manipulate, assets in volumes a profile does not
cover, and assets whose Waterfall field says never. Each refusal comes with the sentence that
explains it, on the asset's edit screen, in the dry run, and in craft.waterfall.explain().
Licence
The Craft License. See LICENSE.md. Waterfall is commercial — Lite $59, Pro $79 — and is
licensed through the Craft Plugin Store.