Watermark
Composite a watermark image over the source.
watermark(WatermarkSource|string $source, Position $position = Position::BottomRight, float $paddingX = 0.0, float $paddingY = 0.0, Unit $paddingUnit = Unit::Pixel, float $width = 0.0, float $height = 0.0, Unit $sizeUnit = Unit::Pixel, WatermarkFit $fit = WatermarkFit::Contain, int $alpha = 100)
Widely available
Unsupported on Cloudflare and Wserv.
CFCNIMIMIMIXIPIVTHWS
- All providers
- Unit and positioning semantics vary per provider — see watermark sub-matrix for source-type and unit detail.
WatermarkFromUrl
CFCNIMIMIMIXIPIVTHWS
8/10
- Imgproxy
- Pro mode only (free tier uses `IMGPROXY_WATERMARK_URL` env var).
WatermarkFromServerDefault
CFCNIMIMIMIXIPIVTHWS
2/10
- Imgproxy
- Free tier: only `key === 'default'` (matches `IMGPROXY_WATERMARK_URL`); custom keys require Pro.
use Timber\Chainsaw\Enum\Position;
use Timber\Chainsaw\Enum\Unit;
$image->watermark(
'logo.png',
Position::BottomRight,
paddingX: 10,
paddingY: 10,
width: 25.0,
sizeUnit: Unit::Percent,
);
The first argument can be:
- A plain string — sugared into
new WatermarkFromUrl($source)for you. - A
WatermarkSourcevalue object — explicit, required for server-side defaults.
Source types
A watermark source is either a per-request URL or a reference to something the provider already knows about.
| Source | Class | Used for |
|---|---|---|
| Per-request URL | WatermarkFromUrl('https://…/logo.png') |
Most providers: Cloudinary, Imgix, Imagor, Thumbor, Intervention, imgproxy Pro |
| Server-side default | WatermarkFromServerDefault('key') |
imgproxy free (key === 'default' only, matches IMGPROXY_WATERMARK_URL); imgproxy Pro; Cloudinary (named public ID) |
Providers without a matching source type throw UnsupportedManipulator with an actionable reason (e.g. “Imgix has no server-default registry; use WatermarkFromUrl”).
use Timber\Chainsaw\Watermark\WatermarkFromServerDefault;
$image->watermark(
new WatermarkFromServerDefault(), // uses the server's configured default
Position::BottomRight,
width: 20.0,
sizeUnit: Unit::Percent,
);
Units
Two Unit parameters govern interpretation of the four numeric inputs:
| Unit | Applies to | Meaning |
|---|---|---|
$paddingUnit (default Unit::Pixel) |
$paddingX, $paddingY |
Pixel = absolute offset; Percent = fraction of base image width/height |
$sizeUnit (default Unit::Pixel) |
$width, $height |
Pixel = absolute target size; Percent = fraction of base image dims |
Both units are applied independently — you can mix them (e.g. percent padding with pixel size, or vice versa).
Provider mapping at URL-build time:
- Cloudinary — native
x_0.1decimals (percent), integerx_10(pixels). - Thumbor / Imagor — native
Xpnotation (10p= 10% of image width/height), integer for pixels. - imgproxy — relative offsets (
< 1.0) for percent; absolute (>= 1.0) for pixels. - Imgix —
paddingUnitis pixel-only and symmetric (paddingX === paddingY); emitsmark-pad. Imgixmark-x/mark-yare absolute pixel offsets that overridemark-align, so asymmetric padding throwsUnsupportedManipulator.sizeUnit=Percentis converted to pixels using upstream output dims (set viaWidth/Crop/Containetc.) — throws if no upstream dim is resolvable. Nativemark-w=0..1is a fraction of the watermark source, so direct emission would diverge from other providers. - Intervention — percent computed locally from base image dims.
See the provider support matrix for the per-cell story.
Proportional logo placement (percent padding + percent size)
Position a logo 5% from the bottom-right corner, sized to 20% of the base width — scales naturally with the output image:
$image->watermark(
'logo.png',
Position::BottomRight,
paddingX: 5.0,
paddingY: 5.0,
paddingUnit: Unit::Percent,
width: 20.0,
sizeUnit: Unit::Percent,
);
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
$source |
WatermarkSource | string |
required | Plain string sugars to WatermarkFromUrl; otherwise pass the explicit VO |
$position |
Position |
BottomRight |
Placement (9-point grid) |
$paddingX |
float |
0.0 |
Horizontal offset — interpretation depends on $paddingUnit |
$paddingY |
float |
0.0 |
Vertical offset — same |
$paddingUnit |
Unit |
Pixel |
Unit applied to $paddingX and $paddingY |
$width |
float |
0.0 |
Target width (0 = original) — interpretation depends on $sizeUnit |
$height |
float |
0.0 |
Target height (0 = original) — same |
$sizeUnit |
Unit |
Pixel |
Unit applied to $width and $height |
$fit |
WatermarkFit |
Contain |
Scaling strategy |
$alpha |
int |
100 |
Opacity (0 = transparent, 100 = opaque) |
Positions
TopLeft, Top, TopRight, Left, Center, Right, BottomLeft, Bottom, BottomRight
Fit modes
| Mode | Behavior |
|---|---|
Contain |
Scale to fit inside the target box |
Fill |
Fill the box (may crop) |
Stretch |
Stretch to exact dimensions |
Crop |
Crop to fit the box |
Factory defaults
Register a watermark as a factory default so all images get it:
use Timber\Chainsaw\Enum\Position;
use Timber\Chainsaw\Enum\Unit;
use Timber\Chainsaw\ImageFactory;
use Timber\Chainsaw\Manipulator\Watermark;
use Timber\Chainsaw\Watermark\WatermarkFromUrl;
$factory = new ImageFactory(
provider: $provider,
defaults: [
new Watermark(
source: new WatermarkFromUrl('logo.png'),
position: Position::BottomRight,
paddingX: 10,
paddingY: 10,
width: 20.0,
sizeUnit: Unit::Percent,
),
],
);
Skip the watermark for specific images:
$image->without(Watermark::class);