Encoding
Encoding is the serialize-to-bytes step: which container/codec the output uses (format()) and how hard it’s compressed (quality()). It changes the delivered bytes, not the pixel composition — that’s what the manipulators do.
format(Format $format)
Set the output format.
use Timber\Chainsaw\Enum\Format;
$image->format(Format::Webp);
$image->format(Format::Avif);
$image->format(Format::Png);
$image->format(Format::Jpg);
$image->format(Format::Gif);
No format set (the default)
If you never call format(), Chainsaw preserves the source format: a .avif source is delivered as AVIF, a .png as PNG, and so on. The local backends (InterventionProvider, ImagineProvider) re-encode to the source format. On the URL providers, an untransformed request is served from the origin verbatim (already the source format); a transforming request (a resize, crop, quality, …) emits the source format as an explicit token so the CDN re-encodes to it rather than substituting another.
This matters because, left to their own devices, the CDNs disagree. Given an unset format some serve the source unchanged, but others apply their own default: imgix and imgproxy transcode an AVIF source to JPEG (quality loss — and a transparent AVIF loses its alpha), while ImageKit and Gumlet negotiate a format from the request’s Accept header. Emitting the source format explicitly makes the delivered bytes the same whichever provider is wired.
Two cases fall back to the CDN’s own default instead of the source format: a format the CDN cannot emit (Cloudflare produces no PNG or GIF, ImageKit no GIF), and a source with no file extension to read a format from.
To opt into per-visitor negotiation instead of preserving the source, set Format::Auto — see Format negotiation.
The three format questions
“Does it support format X?” hides three separate questions. They’re easy to tangle, but they’re independent — a provider can answer yes to one and no to another.
| Question | Can …? | Answered by | How Chainsaw surfaces it | If the answer is no |
|---|---|---|---|---|
| Display | the visitor’s browser render it? | the browser, via its Accept header |
Format::Auto (CDN f_auto, or an AutoFormatStrategyInterface you inject) and a <picture> type fallback |
the wrong or a broken image is shown — no exception |
| Decode (input) | this provider read the source format? | the CDN, or the local backend build | URL providers declare supportedSourceFormats(); local backends probe it (DecoderCapabilities) |
UnsupportedSourceFormat at URL-build time |
| Encode (output) | this provider produce the requested format? | the CDN, or the local backend build | URL providers declare an emittable set (supportedOutputFormats()); local backends probe it (EncoderCapabilities) |
UnsupportedOperation at URL-build time |
The three don’t imply each other:
- Decode ≠ encode. A backend can read a format it can’t write, or the reverse — a libvips build with JPEG XL load but not save, an ImageMagick with a read-only delegate. Chainsaw declares the two sides separately, per provider.
- Encode ≠ display. A provider can produce a format no mainstream browser shows yet. Producing it is only useful behind a
<picture>fallback, or for a known audience (e.g. Safari). JPEG XL is today’s example: several providers emit it, a few decode it, but only Safari displays it by default. Format::Autoonly answers display. It picks which format to serve this visitor; it never widens what a provider can decode or encode. A provider that can’t encode the formatAutoresolves to still throws.
Where each is documented: display in Format negotiation below; decode and encode, per provider, in the provider support matrix.
Auto format
Format::Auto defers the concrete format to URL-build time. CDN providers that support it (Cloudflare, Cloudinary, ImageKit, imgix) negotiate at the edge. The local providers (InterventionProvider, ImagineProvider) delegate to an AutoFormatStrategyInterface that you inject — the library ships no Accept-header parser of its own, so without a strategy Format::Auto throws:
$image->format(Format::Auto); // resolved by the strategy you inject
See Format negotiation below for a copy-paste strategy and the full-page-cache caveat.
quality(int $quality)
Set JPEG/WebP/AVIF quality. Range: 0–100.
$image->quality(80);
Format negotiation
Format::Auto defers format selection until URL build time. How it’s resolved depends on the provider:
| Provider | Mode | Behavior |
|---|---|---|
InterventionProvider, ImagineProvider |
Origin-side | Delegate to an AutoFormatStrategyInterface you inject; throw if none is set. |
CloudflareProvider |
CDN-side | Emits format=auto; Cloudflare negotiates per request. |
CloudinaryProvider |
CDN-side | Emits f_auto; Cloudinary negotiates per request. |
ImagekitProvider |
CDN-side | Emits f-auto; ImageKit negotiates per request. |
ImgixProvider |
CDN-side | Emits auto=format; imgix negotiates per request. |
The remaining URL-grammar providers (imgproxy, Imagor, Thumbor, wsrv) ignore Format::Auto — set format() explicitly, or compose a <picture> with multiple <source type="..."> elements for client-side selection.
Origin-side (local providers)
The library ships no Accept-header parser: there is no PSR for content negotiation, and a general-purpose library can’t assume how you model the request (PSR-7, $_SERVER, a framework Request…), so negotiation is the app’s job. Implement AutoFormatStrategyInterface and inject it — without one, the local providers (InterventionProvider, ImagineProvider) throw on Format::Auto.
use Timber\Chainsaw\Enum\Format;
use Timber\Chainsaw\Format\AutoFormatStrategyInterface;
use Timber\Chainsaw\Format\FormatSupportInterface;
use Timber\Chainsaw\Image;
final class AcceptAutoFormatStrategy implements AutoFormatStrategyInterface
{
public function resolve(Image $image, FormatSupportInterface $support): Format
{
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
// Most modern format the client accepts AND the backend can encode.
return match (true) {
$support->supports(Format::Avif) && str_contains($accept, 'image/avif') => Format::Avif,
$support->supports(Format::Webp) && str_contains($accept, 'image/webp') => Format::Webp,
default => Format::Jpg,
};
}
}
$provider = new InterventionProvider(
// ... source, cache, cachePublicUrl, manager
autoFormat: new AcceptAutoFormatStrategy(),
);
The $support argument (FormatSupportInterface) is the backend’s encode-capability detector, supplied by the provider. Consult it so you never negotiate a format the GD/Imagick/Vips build can’t actually produce. The provider probes capability lazily by default (EncoderCapabilities on Intervention, ImagineFormatSupport on Imagine); pass your own formatSupport: to override or to skip probing. Whatever the strategy returns is guarded anyway — an unencodable format throws UnsupportedOperation rather than failing deep in the encoder.
ImagineProvider accepts the same autoFormat: constructor argument — one strategy implementation serves both local providers.
A PSR-7 / Symfony app reads the header from its request object instead of $_SERVER — the contract is identical:
$accept = $this->request->getHeaderLine('Accept'); // PSR-7 ServerRequestInterface
The resolved format is part of the variant cache path, so AVIF and JPEG variants never collide on disk. Whether it is safe to bake one format into cached HTML is a separate question — see When is Auto safe? below.
CDN-side (Cloudflare, Cloudinary, ImageKit, imgix)
$image->format(Format::Auto); // emits format=auto / f_auto / f-auto / auto=format
The URL is identical for every visitor; the CDN performs the negotiation per request based on the Accept header it sees.
When is Auto safe?
Format::Auto hands different bytes to different visitors from one call site. That is only correct if nothing between the format decision and the browser collapses those variants into a single cached response. Two situations are safe:
- Fully dynamic delivery — the format decision reaches the browser per request. For origin-side
Autothe chosen format is baked into the<img src>, so the HTML must be rendered per request (no full-page cache). For CDN-sideAutothe CDN itself negotiates per request. Accept-aware caching — every cache layer in the path keys on theAcceptheader (Vary: Accept, or a cache key that includes it). That means your full-page / HTML cache, any reverse proxy, and any CDN placed in front of the image origin. The CDN-side providers already emitVary: Acceptat their own edge; the risk is a cache you put in front of them that drops it.
If neither holds — a shared full-page cache with no Vary: Accept, a proxy that strips the header — the first visitor’s format is served to everyone, and clients that can’t decode it get a broken image. Then don’t use Auto: compose a <picture> with typed <source> elements (->img(alt: '…')->formats(...)). The markup is identical for every visitor, so it caches safely, and the browser picks the source it supports.