Formats
Sources are identified by magic bytes (extensions are never trusted). By default the output format is the source’s own; any decode column combines with any encode column:
| Format | Decode | Encode |
|---|---|---|
| JPEG | baseline & progressive, grayscale; streaming, full-size decode (shrink-on-load opt-in) | jpegli progressive (default), mozjpeg profiles via PRESET |
| PNG | palette / grayscale / 16-bit, normalized to RGB(A)8 | lossless RGB(A); opt-in palette quantization (OXIMG_PNG_QUANTIZE) |
| WebP | lossy & lossless, alpha | lossy (OXIMG_WEBP_QUALITY, 75), alpha; output is scaled to fit WebP’s 16383 px limit |
AVIF (--features avif) |
dav1d: 8/10/12-bit, all subsamplings, alpha | SVT-AV1: 10-bit 4:2:0, tune=ssim, alpha as auxiliary image |
| GIF | GIF87a/89a, every frame composited onto the logical screen (frame sub-rectangle, transparent index, all four disposal methods) | none — see below |
GIF is the one decode-only format, so it is also the one source whose
output format is not its own: with no @{fmt} and no negotiation, a GIF
becomes WebP, and an animated GIF becomes an animated WebP
(see Animation for the budgets that decide whether the
animation is kept). That
is a deliberate choice, not a missing encoder —
on a 15-file real-world corpus, lossless GIF→GIF saved nothing on 9 of
them (median 100% of the source bytes, which is also what imgproxy’s
GIF→GIF measured), and at native size the smallest GIF variant measured
still landed at 80.7% where WebP reached 25.2% at the same visual score.
@gif and format=gif are rejected with a 400 instead of quietly
answering with different bytes under the name the client asked for. See
docs/gif-evaluation.md for the measurements.
Cross-format output: append an imgproxy-style @{fmt} token to the
filename — /resize/300/200/photo.jpg@webp (jpg/jpeg, png,
webp, avif; jxl is reserved, gif permanently so). Only exact
tokens count, so photo@2x.jpg is still a filename. Precedence:
explicit @{fmt} > Accept negotiation > source format. Negotiation is
opt-in: set OXIMG_AUTO_FORMAT to a preference list (e.g. avif,webp) and
bare-URL responses follow the request’s Accept header; every response
then carries Vary: Accept (make sure your CDN honors it or normalizes
Accept into the cache key — explicit @{fmt} URLs avoid the issue
entirely, which is what signed deployments should prefer since headers
are outside the signature). Alpha sources encoded to JPEG are flattened
in linear light onto OXIMG_FLATTEN_BG (hex RRGGBB, default white).
Encode settings are keyed by the output format, using the same knobs
as same-format requests.
Choose the preference order by your goal: the AVIF defaults target
fidelity, not minimum bytes — at default quality settings AVIF output
measures 10–28% larger than WebP on photographic sources, and costs
the more expensive encode. If the deployment’s goal is byte reduction,
prefer webp,avif (or webp alone), or lower OXIMG_AVIF_QUALITY
until AVIF earns its slot; put avif first only after comparing sizes
on your own corpus at your own settings. Also note what negotiation
does not cover: when it doesn’t fire (client sends Accept: */* —
link-preview scrapers, social-card fetchers, curl integrations), the
source format is kept, and PNG output defaults to lossless RGB(A) — a
large photographic PNG stays large unless OXIMG_PNG_QUANTIZE=1 is
set. Deployments that care about those clients should enable
quantization or prefer explicit @{fmt} URLs over relying on
negotiation. On flat graphics (charts, screenshots, text-heavy
panels), a quantized PNG is often both smaller and truer to the
source than any WebP quality setting — worth remembering when tuning
OXIMG_AUTO_FORMAT for mixed content.
Orientation: every source format auto-rotates — JPEG EXIF, PNG
eXIf, WebP EXIF chunks, and AVIF irot/imir transforms. The
target box applies to the displayed frame and the pixels come out
upright in every output format (the metadata itself is not forwarded,
so nothing double-rotates). OXIMG_AUTO_ROTATE=0 restores the raw
stored orientation.
ICC profiles: a source’s color profile (JPEG APP2 chain, PNG
iCCP, WebP ICCP, AVIF colr) passes through byte-for-byte into
any output format, across format conversion included. RGB pixels are
never color-converted. This matters for wide-gamut sources: the
common proxy default is to normalize pixels to sRGB and strip the
profile, which permanently clips every color outside the sRGB gamut —
a Display P3 phone photo loses exactly the saturated reds and greens
that made it worth shooting in P3. oximg keeps the pixels and the
profile as they were, so wide-gamut images render on a wide-gamut
display the way the original did (and identically everywhere else).
OXIMG_ICC=0 opts into stripping instead.
CMYK/YCCK JPEG sources (print-workflow assets) are the one
exception, since no browser renders CMYK pixels: they are converted
to sRGB — through the embedded CMYK profile (moxcms, relative
colorimetric, like imgproxy/libvips) when one is present, with the
naive composite browsers use otherwise — and the CMYK profile is
consumed, never passed through. OXIMG_ICC=0 skips profile
extraction entirely, so it also selects the naive conversion.