The URL is the API,
with claws.
crabcrop fetches your original, applies the operations in the URL in order, and serves the result. Write the resize, crop, format, and filters into a signed URL, and skip the pre-generated thumbnails and the build step entirely.
Signed with HMAC-SHA256. Cached by canonical key. Served with Cache-Control: immutable for a year.
format(avif)
One URL. Every instruction inside it.
Every request is GET /<signature>/<operations>/<source-url>. Click a segment. Learn what it does, what it hides, and where it bites.
The signature is shortened for display. A real one is 64 hex characters, like the worked example in the panel.
signatureSignature
Grammar
Example
Watch out:
The playground. Snap a URL together.
Flip the controls. Watch a real crabcrop URL assemble, segment by segment, in the exact grammar the server parses. Runs entirely in your browser. Nothing you type leaves the page.
Local development only: the server must run with unsafe allowed, and purges still always need a real signature.
The first segment above is a placeholder that marks where the signature goes. In production it is the hex HMAC-SHA256 of everything after it, computed with your secret by a client library or the CLI. Produce a real one in the Sign it section.
What you just built
What the server will do
Before, after, and a little sideways.
Drag the crop window by its claws. Stack filters in any order and watch the order change the picture. Crop coordinates are source pixels, and filter order is meaning.
CSS preview. Your browser approximates the look with CSS filters. A running crabcrop server applies the real operations to the actual pixels, in this order. Blur strength and brighten do not map one to one.
left 300, top 150, right 900, bottom 550, size 600x400
Drag inside the window to move it, drag a claw to resize. Keyboard: arrows move by 1 px, Shift by 10 px, Alt plus arrows resize from the bottom right, Home resets.


Your chain so far:
Sign it in your stack.
One signing scheme, specified once, with a canonical set of test vectors that every implementation runs as its test suite. The server verifies with the same Rust crate your app can build with, so client and server cannot drift.
Compute HMAC-SHA256 over the remainder of the path, the operations and the source URL exactly as they appear with no leading slash, keyed with your secret. Hex encode the result. Use it as the first path segment.
# Sign the remainder. The claw signs what it serves. # The hex output is segment one of the URL. printf '%s' '300x200/https://example.com/photo.jpg' \ | openssl dgst -sha256 -hmac 'your-secret' # SHA2-256(stdin)= 3c1f6c180116661877bd4c85fd201a178087d74c1cc188cc584e819d901308c6
// Sign once, reuse the URL everywhere. // Same vectors, same signature, no drift. import { buildURL, buildSrcSet, purgeSignature } from "@crabcrop/url"; const url = buildURL({ base: "https://img.example.com", secret: "your-secret", operations: "crop:1200x630:smart/filters:format(webp):quality(80)", source: "https://example.com/photo.jpg", }); const srcset = buildSrcSet({ /* same options */ }, [400, 800, 1200]);
# Standard library only. Nothing extra to haul in. # Builds the signed URL and the srcset in one go. from crabcrop_url import build_url, build_srcset, purge_signature url = build_url( base="https://images.example.com", secret="your-secret", operations="300x200/filters:grayscale", source="https://example.com/photo.jpg", ) srcset = build_srcset(..., widths=[400, 800, 1200])
// The same crate the server verifies with. // If it signs here, it verifies there. use crabcrop_url::{build_url, sign}; let signature = sign(secret, "300x200/https://example.com/photo.jpg"); let url = build_url(base, secret, "crop:1200x630:smart/filters:format(avif):quality(auto)", "https://example.com/photo.jpg")?;
Check the scheme yourself, right now.
Secret Jefe, message what do ye want for nothing?, HMAC-SHA256 ba5396e10039ec002bf1fde117eee48df8c2f06bc47b5f2d3272ea77f6862da9.
Secret your-secret, remainder 300x200/https://example.com/photo.jpg, HMAC-SHA256 3c1f6c180116661877bd4c85fd201a178087d74c1cc188cc584e819d901308c6.
Small URL. Long reach.
The transform is only the first tide. Caching, metadata, negotiation, and purging are built into the same URL.
41 filters and encoding directives, chained inside one filters: segment in the order you write them.

Canonical keys, three-tier cache
Signed URLs are content addressed, and entries key on the canonical form of the parsed operations, so equivalent spellings share one entry. A byte-bounded in-memory LRU sits in front, an optional disk tier underneath survives restarts and promotes hits back into memory, and an optional shared S3 tier behind both lets every replica serve the same stored results. Only 200 responses are cached. Errors never are.
Cache-Control: immutablePresets that stay put
A preset names an operation string on the server, so templates stay short and the transform menu stays under operator control. Treat a preset as a stable template and never edit one in place: roll a change out under a new name, because signed responses travel with a one-year immutable cache header that no purge can reach downstream.
preset:og-cardPurge one, or purge the group
/purge invalidates every stored variant of one result across every cache tier, and /purge-group clears a whole preset output set named in the Cache-Group header. Purge signatures are HMACs under a subkey derived from your secret, a separate domain from image signatures, so an image signature can never be replayed as a purge.
Meta JSON and placeholders
Send Accept: application/json on any image URL and get dimensions, format, byte size, and source size instead of image bytes. Ask for placeholders and the same response adds the dominant color, a five-color palette, a tiny blurred JPEG preview, and a ThumbHash string your client decodes into an aspect-correct blur while the real image loads.
quality(auto), verified
quality(auto) picks a quality per image, from 50 for flat images to 95 for detailed ones. For JPEG and JPEG XL, crabcrop then encodes, decodes, and scores the result against the processed source with SSIMULACRA2, adjusting quality until it lands on target, and reports the achieved score back in meta.
Format negotiation that listens
Name no format and the Accept header decides: among AVIF, JPEG XL, and WebP, the highest q-value wins, ties break by server preference in that order, and a format refused with q=0 is never picked. An explicit format(...) in the URL always wins, and wildcards do not count as an offer.
Steer the crop: focal and corners
A focal: segment names the source rectangle the next crop must keep, and center and smart crops position their window on it, so the region the caller cares about stays in frame; a smart crop with a focal rectangle pending skips detection entirely. Or pin a corner: top-left, top-right, bottom-left, and bottom-right keep that corner's edges on whichever axis the crop cuts.
Pro colour tools: curve and convolution
filters:curve(...) remaps tones per channel, rgb, r, g, or b, through 2 to 16 control points you set: the fine tone control the additive filters only approximate. filters:convolution(...) applies your own kernel, 9 values (3x3) or 25 (5x5), with optional normalization, for the sharpen, blur, and edge styles the built-ins do not cover.
A cache lifetime per request
Responses normally ride a one-year immutable cache header. A maxage: segment overrides the max-age for that response only: it changes the header and never the cache key, so URLs differing only in maxage share one stored image. Presets stay immutable, unless a preset URL carries maxage itself, which is the caller's explicit choice for that response.
Client hints, DPR, and Save-Data
A URL that sizes nothing can take its width from client hints, quantized so nearby viewports share one cache entry, with dpr scaling the same template for retina displays. A request carrying Save-Data: on gets the default quality stepped down one notch, from 80 to 70, and stepped responses are cached separately.
Sources, where they already live
Fetch originals over HTTP and HTTPS under a source policy, from S3 or S3-compatible stores, from Google Cloud Storage, or from a local file root that refuses path escapes. Every fetch is bounded at 10 seconds and 10 MiB, and EXIF orientation is applied at decode, so phone photos come out upright.
s3://bucket/key ยท gs://bucket/key ยท file:///pathGraceful by design
An optional fallback image serves through the same operations when a source cannot be fetched or decoded, marked with X-Crabcrop-Fallback: 1. Signature and parse failures never fall back, and fallback responses are never cached, so a recovered origin is picked up on the next request.
Every major format. In and out.
Originals arrive in whatever format they arrive in, and every visitor's browser does best with a different one. crabcrop takes any of them in and serves the right one back out, so it is the only image service you run: no separate thumbnail pipeline, no format-specific tooling, no build step.
quality(auto) is verified perceptually for it.
avif-decode feature.
quality(auto) is verified perceptually for it, like JPEG.
Negotiation picks per visitor
Name no format and the Accept header decides by q-value: AVIF first, then JPEG XL, then WebP. An explicit format(...) in the URL always wins over the header.
Animation survives the trip
Animated GIF and WebP sources keep their frames, delays, and loop count when the output carries animation. Ask for a still and you get the first frame, and an animated AVIF source decodes its first frame too.
GIF in, GIF out ยท WebP in, WebP outAlpha, end to end
The radius, mask, and removebg chains end in transparency, so the output has to carry alpha: png, webp, jxl, and avif all do.
Every format, one pipeline
EXIF orientation is applied at decode, so phone photos come out upright, and ICC profiles are converted to sRGB before any operation runs. SVG originals join the same pipeline too: sanitized first, with scripts and external references stripped, then rasterized.
EXIF upright ยท ICC to sRGB ยท SVG sanitizedOne honest note: HEIC decodes in pure Rust, with no external tools. The first load of a phone photo is slower than other formats, then it caches like everything else.
Fetch, resize, crop, filter, convert, cache, purge, meta, and AI, in a single Rust binary. That is all you need for image serving.
Models you supply. Inference in process.
AI filters run ONNX models you provide as files, loaded at boot before the server reports ready. Inference runs behind a server-wide gate of 2 concurrent inferences, so a burst of AI requests cannot starve the normal transform pipeline.

Proof, not promises: Snap went to the skate park, and every after image below is the direct output of a running crabcrop server fed these exact photos, with the operations printed under each demo. Nothing was retouched in an editor. Drag the sliders.


Background removal
removebg keeps the subject and swaps the background for real alpha, ready to composite anywhere. Ask for a format that carries alpha: png, webp, jxl, or avif. The skateboard went with the background, because the mask keeps the subject, and the subject is the crab.
fit-in:1000x667/filters:removebg:format(webp)


Redact a class: pixelate
The detection model found the board (skateboard, 0.53) and pixelateclass covered its box. Redaction hides pixels. It does not erase the object or paint in what was behind it, and crabcrop does not claim otherwise.
fit-in:1100x733/filters:pixelateclass(skateboard)


Redact people: blur
Same idea, softer edge. blurclass(person) blurs every detected person and nothing else: handy for street shots, event photos, and one skater who never signed a release. Repeat class filters to cover more classes, matched case-insensitively.
fit-in:1100x733/filters:blurclass(person)



Smart crop finds the subject
The plain center crop clips Snap's raised claw at the edge. :smart scores the frame for the interesting region and keeps the whole crab in shot. Add a face model and :smart(faces) lets detected faces pick the focus instead, with :face cropping on a face outright.
crop:760x760:smart
Meta that describes the picture
Ask for JSON instead of pixels and a meta response adds CLIP tags and composed alt_text from a label vocabulary you supply, plus detection boxes, palette, dominant color, and a placeholder. It read this scene right. The detector's 80 classes hold no crab, so it called Snap a teddy bear at 0.69. Fair enough.
signed fit-in:900x600 URL, sent with Accept: application/json; placeholders=1; detections=1
Upscale
filters:upscale(2) or upscale(4) runs super-resolution by that factor, capped at 4096 px per dimension. Source transparency is carried across, even though the model only sees RGB.
Face redaction
blurfaces and pixelatefaces cover every detected face. No landmarks, no identity, no embeddings: detection in, redaction out, and the boxes go no further than the response.
The rules, with no fine print.
- Models are operator-supplied files, deployment artifacts. crabcrop never downloads a model, at startup or at request time.
- A configured model that cannot load is a startup error with a one-line diagnostic. It fails fast at boot, not on the first request.
- No model configured means a clean 501, with a body like removebg is not configured on this server.
- Nothing silently degrades. upscale never falls back to a plain resize, and a detection failure fails a redaction request rather than serving an unredacted image.
Build URLs from anywhere. Even from your agent.
One grammar and one signing scheme, specified once. Every builder below runs the same canonical test vectors as its test suite.
Rust crate
crabcrop-url: sign, verify, build URLs and srcsets, and compute purge signatures, with the same crate the server verifies with.
TypeScript
@crabcrop/url: the same sign, verify, URL, srcset, and purge helpers for TypeScript, standard library only.
Python
crabcrop_url: the same builders and purge helpers for Python, standard library only.
MCP server
A first-party MCP server for coding agents, exposing sign_url, explain_url, fetch_meta, and purge_result over the same SDK and HTTP surface.
Operator CLI
The server binary doubles as its own operator CLI, with sign, explain, meta, purge, and purge-group, and URLs are validated against the server grammar before they are signed.
Playground
A static browser page for building, signing, previewing, and explaining URLs segment by segment, with the secret never leaving the page.
Framework loaders
A Next.js custom loader, a Nuxt provider, and a Hydrogen loader, all recommending presets so client-side URL building never needs the secret.

Put a crab in front of your images.
Run one binary in front of your origin image store, sign your URLs, and let the URL do the work from there.
docker run -p 3000:3000 -e CRABCROP_SECRET=your-secret crabcrop serve