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.
Source types
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 WatermarkSource value 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.1 decimals (percent), integer x_10 (pixels).
  • Thumbor / Imagor — native Xp notation (10p = 10% of image width/height), integer for pixels.
  • imgproxy — relative offsets (< 1.0) for percent; absolute (>= 1.0) for pixels.
  • ImgixpaddingUnit is pixel-only and symmetric (paddingX === paddingY); emits mark-pad. Imgix mark-x / mark-y are absolute pixel offsets that override mark-align, so asymmetric padding throws UnsupportedManipulator. sizeUnit=Percent is converted to pixels using upstream output dims (set via Width / Crop / Contain etc.) — throws if no upstream dim is resolvable. Native mark-w=0..1 is 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);