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 gravity —
crop_gravityuses compass names (north,south,east,west,northwest,northeast,southwest,southeast,center). Thetop/left/top_leftforms are silently ignored (the crop falls back to centre), so Chainsaw maps eachCropPositionto its compass equivalent. Focal-point-as-percent and content-aware gravities (smart, entropy, attention, face, object-class) are not available and throwUnsupportedManipulator. - Resize never upscales — Bunny’s
crop,widthandheightall 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_ratioslot, so they cannot be combined (the second is refused). A manual crop can be composed withcontain()/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 awidth()/cover()/etc. to actually transcode. - Sharpen — Bunny’s
sharpenis a boolean (sharpen=true); a numeric value is silently ignored, so any positive amount maps to on and0emits nothing. - Gamma — Bunny’s
gammais a non-inverted-100..100midtone adjustment where a positive value brightens. Chainsaw’s multiplier is translated asround((gamma - 1) * 100)and clamped:gamma(1.0)→gamma=0,gamma(2.0)→gamma=100(saturated). Format::Auto— emits noformatparameter. Bunny negotiates WebP/AVIF at the zone level via theAcceptheader when the Optimizer is on;auto_optimizeis 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 absoluteUrlSourcethrowsUnsupportedOperation. - SVG — SVG sources are refused; use a raster source.