BunnyProvider

Generates Bunny.net Optimizer transformation URLs. Bunny is a query-string grammar on a pull-zone host that resolves relative paths against the zone’s configured origin.

use Timber\Chainsaw\Provider\Bunny\BunnyProvider;

$provider = new BunnyProvider(
    host: 'https://zone.b-cdn.net',
);

Output: {host}/{source}?width=800&crop=800,600&crop_gravity=center

Prerequisites

The pull zone must have the Optimizer enabled (Bunny dashboard → Pull Zone → Optimizer), with the Dynamic Image API turned on. Without it the query parameters are ignored and the origin image is served unchanged.

Because a Bunny pull zone resolves the request path against its own configured origin, the source is always a storage-relative path (PathSource). An absolute UrlSource cannot be expressed and throws UnsupportedOperation.

Token authentication (signed URLs)

Pass an HMAC-SHA256 signer keyed with the zone’s Token Authentication Key (Bunny dashboard → Pull Zone → Security) to sign every URL. Bunny’s V2 tokens are HS256-<base64url(HMAC-SHA256(path + expires + params))>, so the transform parameters are cryptographically bound — a tampered width/crop invalidates the token.

use Timber\Chainsaw\Provider\Bunny\BunnyProvider;
use Timber\Chainsaw\Signer\Algorithm;
use Timber\Chainsaw\Signer\SignatureEncoding;
use Timber\Chainsaw\Signer\HmacUrlSigner;

$provider = new BunnyProvider(
    host: 'https://zone.b-cdn.net',
    signer: new HmacUrlSigner(key: $tokenAuthenticationKey, algorithm: Algorithm::Sha256, encoding: SignatureEncoding::Base64Url),
    // expirySeconds: 3600,  // optional TTL; omit for a far-future, cacheable token
);

Output: {host}/{source}?width=800&token=HS256-…&expires=…

By default the URL carries a fixed far-future expires, keeping signed URLs deterministic and CDN-cacheable. Set expirySeconds to enforce a real TTL (expires = now + N), at the cost of cache reuse. The expiry stamp comes from an optional PSR-20 clock (clock: constructor parameter, system time by default) — inject a fixed clock such as symfony/clock’s MockClock to make signed-TTL URLs deterministic in tests. Because V2 binds parameters into the token itself, no “Token Vary Parameters” configuration is needed on the zone.

Manipulation support

Legend: ✅ = implemented in Chainsaw, ⚠️ = supported with caveats, 🟦 = available on the CDN, not exposed by Chainsaw, ❌ = not available on this CDN.

Resize & crop

Manipulation Chainsaw Availability
Width width
Height height
Scale No scale-by-factor primitive; use width()/height()
Cover crop=w,h + crop_gravity (compass anchors)
ManualCrop crop=w,h,x,y
Contain width + height (fits, preserves aspect)
CropToRatio aspect_ratio + crop_gravity
Pad No letterbox/pad primitive
Stretch No distort/stretch mode
PadToRatio No aspect-ratio pad

Filters & effects

Manipulation Chainsaw Availability
Blur blur (0…100, direct)
Sharpen ⚠️ sharpen=true — boolean, so 0…100 collapses to on/off
Brightness brightness
Contrast contrast
Saturation saturation
Greyscale via saturation=-100
Hue shift hue (ours 0…360 scaled to Bunny 0…100)
Gamma gamma (non-inverted; positive brightens)
Sepia sepia=100
Negate / Invert No invert/negate primitive
Pixelate No pixelate primitive
Trim No trim primitive

Decoration & orientation

Manipulation Chainsaw Availability
Background No canvas background fill
Border No border primitive
Flip flip (vertical) / flop (horizontal)
Rotate ⚠️ rotate; 90/180/270 only — arbitrary angles throw UnsupportedManipulator
AutoOrient EXIF orientation applied implicitly by default
Watermark Configured per-zone in the dashboard, not per request

Encoding

Format Supported
JPEG
PNG
WebP
AVIF
GIF
Auto format
Quality

Notes

  • Crop gravitycrop_gravity uses compass names (north, south, east, west, northwest, northeast, southwest, southeast, center). The top/left/top_left forms are silently ignored (the crop falls back to centre), so Chainsaw maps each CropPosition to its compass equivalent. Focal-point-as-percent and content-aware gravities (smart, entropy, attention, face, object-class) are not available and throw UnsupportedManipulator.
  • Resize never upscales — Bunny’s crop, width and height all clamp to the source size per axis, so cover and contain into a box larger than the source come back at the source size, not the box (a built-in no-upscale). Into a smaller-or-equal box they behave normally.
  • One crop per URL — cover, manual crop and crop-to-ratio all use the single crop/aspect_ratio slot, so they cannot be combined (the second is refused). A manual crop can be composed with contain()/width()/height() (distinct keys) to extract a region and then resize it.
  • Transcode needs a resize — Bunny only re-encodes when a dimension op is present. A bare format=/quality= on a source with no resize passes the original bytes through unchanged; pair a format change with a width()/cover()/etc. to actually transcode.
  • Sharpen — Bunny’s sharpen is a boolean (sharpen=true); a numeric value is silently ignored, so any positive amount maps to on and 0 emits nothing.
  • Gamma — Bunny’s gamma is a non-inverted -100..100 midtone adjustment where a positive value brightens. Chainsaw’s multiplier is translated as round((gamma - 1) * 100) and clamped: gamma(1.0)gamma=0, gamma(2.0)gamma=100 (saturated).
  • Format::Auto — emits no format parameter. Bunny negotiates WebP/AVIF at the zone level via the Accept header when the Optimizer is on; auto_optimize is a compression-aggressiveness knob, not format negotiation.
  • Source — a Bunny pull zone resolves the request path against its own origin, so the source is always a relative PathSource. An absolute UrlSource throws UnsupportedOperation.
  • SVG — SVG sources are refused; use a raster source.