Waterfall for Craft CMS

Usage

The three modes

Each profile chooses any combination of three, because they answer different questions.

Into transforms

The mark is composited while Craft generates a transform. The stored original is never touched, every template that renders a transform is covered with no template change, and the whole thing is undone by clearing the transform cache.

<img src="{{ asset.getUrl({ width: 1200, height: 800, mode: 'crop' }) }}">

That is watermarked. So is the srcset Craft builds, and so are ImageOptimize's .webp and .avif variants.

What this mode does not cover is the file itself: anyone who reaches the asset's own URL, or who was sent the file, gets the unmarked original. If that matters, add permanent.

Permanently, into the file

The stored file is rewritten. The original is copied to the backup store first, and a row is written to Waterfall's ledger recording which profile did it, when, and where the backup went.

It happens in four places:

  • On upload, for any image matching a permanent profile — switchable in the settings
  • Assets → select images → Apply watermark
  • The Watermark panel on an asset's own edit screen
  • php craft waterfall/apply/run

Applying twice does nothing the second time: the ledger, not the pixels, decides what has already been done. Editing the profile changes its fingerprint, and then the same asset is marked again — on top of the first mark, because the file is the file. Restore first if that is not what you want.

On demand

Nothing automatic. A template, an element action or a console command asks for a watermarked derivative and Waterfall writes it into its own store, keyed on the profile and its current settings.

<img src="{{ craft.waterfall.url(asset, { width: 1200 }, 'copyright') }}">

This mode exists because a transform's filename is derived from its parameters alone: the same transform of the same asset, once with a watermark and once without, would otherwise collide on one file. Waterfall's own store has the profile in the path, so both can exist.

Undoing it

php craft waterfall/apply/restore --volume=photos

or Assets → select → Restore original, or the button on the asset's edit screen. The backup is written back through Craft, so dimensions, metadata and transforms are all refreshed, and the ledger rows for that asset are dropped.

A restore that finds no backup says so and changes nothing.

Bulk runs

Waterfall → Apply. Choose a volume, press Count first, and you get the number that would be marked, the number that would be skipped, and the five commonest reasons for the skips — before anything is written.

Runs larger than the Queue above setting are handed to the queue rather than run in the request, on the ids the count was taken from.

From the console:

php craft waterfall/apply/run --volume=photos --dry-run
php craft waterfall/apply/run --volume=photos
php craft waterfall/apply/run --volume=photos --force     # re-apply where the profile changed
php craft waterfall/apply/restore --volume=photos
php craft waterfall/apply/status                          # what carries a permanent watermark
php craft waterfall/doctor                                # what this server can do

Twig

Most sites need none of this. It is for the cases that have to ask.

{# A watermarked URL — from the profile's provider, or Waterfall's own store #}
{{ craft.waterfall.url(asset, { width: 1200 }, 'copyright') }}

{# Would this asset be watermarked? #}
{% if craft.waterfall.appliesTo(asset, 'copyright') %}…{% endif %}

{# Has this asset's own file been marked? #}
{% 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 %}

{# Every enabled profile #}
{% for profile in craft.waterfall.profiles %}{{ profile.name }}{% endfor %}

url() falls back to the asset's plain URL rather than returning null when a profile does not apply, so a template asking for a watermark on an image that cannot take one still renders.

ImageOptimize

Nothing to configure, and nothing to install in a particular order.

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. Waterfall registers its handler prepended, so the mark is on the image before ImageOptimize ever sees it and every file it produces carries the mark.

If ImageOptimize is set to transform through a service rather than on your server, Craft's transformer never runs, so a file-based watermark never happens. Waterfall says so on its settings screen and in waterfall/doctor — use a delivery provider for those images instead.

Imager-X

Imager-X does not use Craft's transform pipeline, so it is wired separately.

Automatically. Waterfall registers a transformer that folds matching profiles into every transform, and each profile gets its own cache entry. This needs Imager-X Pro — the Lite edition does not fire the event that lets a plugin register a transformer.

By hand, on any Imager-X edition:

{% set image = craft.imagerx.transformImage(asset, {
    width: 1200,
    effects: { waterfall: 'copyright' }
}) %}

The waterfall 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. Naming a profile in a transform always wins over the automatic rules.

If you would rather configure Imager-X directly, Waterfall will translate a profile into its native parameter:

{% set params = craft.waterfall.imagerParams('copyright', 1200) %}
{% set image = craft.imagerx.transformImage(asset, { width: 1200 }|merge(params)) %}

It is a lossy translation and it says so: Imager-X's watermark takes an image, a corner and an opacity, so a text, tiled or rotated profile has no equivalent and returns nothing.

Delivery services (Pro)

Set a profile's Drawn by to imgix, Cloudinary, ImageKit or your own URL template and craft.waterfall.url() returns a URL built for that service. Nothing is composited on your server and no file is written.

Image marksText marksTilingRotation
imgix
Cloudinary
ImageKit
URL template

Each provider says on the profile screen what it cannot do for that profile, rather than quietly delivering something different. imgix URLs are signed when a secure token is set.

The URL template driver takes {src}, {srcEncoded}, {path}, {mark}, {markEncoded}, {position}, {opacity}, {scale}, {width}, {height} and {format} — enough for Bunny, Cloudflare Images, Thumbor or anything else that watermarks from a URL.