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.

Maintainers

Package info

github.com/justinholtweb/craft-waterfall

Type:craft-plugin

pkg:composer/justinholtweb/craft-waterfall

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

5.0.0 2026-08-23 14:45 UTC

This package is auto-updated.

Last update: 2026-08-25 00:53:24 UTC


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.

Support

justin@justinholt.com