Escape Hatch: query()

CDNs expose proprietary transform knobs Chainsaw deliberately does not model — background removal, AI upscale/enhance, add_shadow, opacity, and the long tail of one-off params. Unifying them across every URL grammar is not worth the maintenance, so they are out of the curated manipulator set by design.

query() is the generic escape hatch: it splices a raw, provider-native option token verbatim into the target CDN’s URL grammar. Chainsaw never parses the token — it owns the mechanism (where the text lands in each grammar), never the content.

$image->cover(300, 200)->query('background_remove=1');
$image->cover(300, 200)->query(['background_remove' => 1]); // equivalent

CDN-specific by design

A query() op couples that chain to one CDN — that is your explicit choice. Chainsaw does not guard, warn, or scope it: pointing a background_remove=1 chain at a CDN that ignores the param is a no-op on that CDN, not an error. Guarding would require the very per-CDN feature list this escape hatch exists to avoid maintaining.

The token is written in the target provider’s own grammar. The same intent looks different per CDN:

Grammar family Example token Providers
Query string (k=v) query('background_remove=1') imgix, wserv, Cloudflare, Bunny, ImageKit
Cloudinary effect segment query('e_background_removal') Cloudinary
Imagor / Thumbor filter query('round_corner(20)') Imagor, Thumbor
imgproxy processing option query('sh:0.5') imgproxy

String vs array

A string is the atomic token, spliced as-is.

An array is key=value sugar for the query-string grammars — each pair becomes its own token so every provider joins them with its own separator, and the keys are sorted for a stable emitted URL:

$image->query(['gravity' => 'face', 'blur' => 40]);
// → gravity=face and blur=40, each spliced at the provider's slot

Array values must be string, int, or float. A bool is refused — Chainsaw does not guess whether a flag renders as =1, =true, or =on (that is CDN-specific). Write the literal you want:

$image->query(['background_remove' => 1]);      // ok
$image->query(['sharpen' => 'true']);           // ok — you chose the literal
$image->query(['background_remove' => true]);   // throws InvalidManipulator

For non-k=v grammars (Cloudinary e_*, imagor/thumbor filters, imgproxy options) pass the token as a string.

Opaque to dimensions

query() is not dimension-aware. A param that secretly resizes the output is not reflected in dimensions(), srcset generation, or the resize normalizers — Chainsaw never parses the token, so it cannot know. This is inherent to any escape hatch; use the modelled resize methods (width(), cover(), …) when you need the size tracked.

Local providers

query() injects CDN-specific URL params and has no GD/Imagick rendering, so the local backends (Intervention, Imagine) refuse it with an UnsupportedManipulator — on both the build and generate paths. It is absent from the local capability matrix. Use a URL-grammar provider.

Signed URLs

On providers that sign the path or query (imgix, Cloudinary, Imagor, Thumbor, imgproxy) the token is spliced before the signature is computed, so the signature covers it automatically. The two param-bound signers fold it into the signed parameter set:

  • Bunny (token auth) folds a k=v token into the signed params. A bare flag without = has no home in a param-bound signature and is refused under token auth (it rides raw in the query when unsigned).
  • ImageKit appends the token to the tr step, which the ik-s path signature already covers.

Bunny caveat — not byte-verbatim. Bunny is the one provider that folds the token into its parameter set rather than appending it raw, so the value is URL-encoded by http_build_query() (RFC 1738: a space becomes +, not %20) — query('title=a b')title=a+b, query('ar=16:9')ar=16%3A9. This keeps the token self-consistent under token auth (the same encoded bytes are signed and emitted). A corollary: on Bunny a token whose key collides with a modelled op or an earlier token is last-write-wins (->width(100)->query('width=500') emits only width=500), and a query('v=…') token is overwritten by the automatic cache-bust v. Every other provider appends tokens verbatim and lets the CDN resolve duplicates.

Twig

query() takes a string|array, so it stays off the dedicated filter surface and is reached through manipulate() (like without):

{{ 'photo.jpg'|manipulate({ query: 'background_remove=1' }) }}