Positional
Fit within w×h, never enlarge. 0 frees an axis — what srcset and Next.js loaders emit.
GET /resize/750/0/photos/2026/a.jpg
GET /resize/300/200/photo.jpg@webp
GET /resize/1600/1600/photo.png@avifv0.11 · experimental
A Rust image server, CLI and library for JPEG, PNG, WebP and AVIF. It leads every format cell of imgproxy's own benchmark, resizing in linear light at measurably higher quality.
docker run -p 8081:8081 -v $PWD/images:/images:ro ghcr.io/oximg/oximgGET /resize/512/512/0801.jpg@webp
200 image/webp·p95 33 msc7g.large, JPEG→WebP cell
91.2req/s
JPEG throughput
vs imgproxy 68.0
c7g.large, official harness ↗
2.1×
JPEG → WebP
vs 79.3 vs 37.0 req/s
c7g.large, cross-format cell ↗
+6.3SSIMULACRA2
sharper at the same q80
vs 77.5 vs imgproxy 71.2
Kodak corpus, end-to-end JPEG ↗
37.5MB
peak RSS as a library
vs ruby-vips 373.3 MB
4000×2667 → 750×500, M2 Max ↗
benchmarks
imgproxy publishes numbers from its own benchmark on AWS c7i and c7g instances. We ran the same harness, unmodified, on the same instance types — every server re-measured together in one run.
44.6 req/s against imgproxy's 44.9 — within run-to-run variance, not a lead.
On photographic sources AVIF output measures 10–28% larger than WebP at default settings.
Scaled out behind round-robin, identical requests rarely meet: 0 followers across 6 Cloud Run instances in the field. Put a CDN in front.
quality
SSIMULACRA2 scores how close an output looks to its source. At the same settings, oximg's output scores higher — the throughput above is not a quality trade.
End-to-end JPEG, q80
Kodak corpus; the gap widens with source size
Pure resize (lossless PNG path)
isolates the resampler from the encoder
urls
Moving off imgproxy or Cloudflare Images shouldn't mean rewriting every image URL in your templates. Sources come from a directory, any HTTP(S) origin, or a privategs:// bucket.
Fit within w×h, never enlarge. 0 frees an axis — what srcset and Next.js loaders emit.
GET /resize/750/0/photos/2026/a.jpg
GET /resize/300/200/photo.jpg@webp
GET /resize/1600/1600/photo.png@avifMount OXIMG_OPTIONS_PREFIX and URLs built for Cloudflare survive the migration as-is.
GET /image/width=750,quality=80/albums/photo.png
GET /image/width=1200,format=auto/hero.jpgSet OXIMG_KEY and OXIMG_SALT: every URL needs an HMAC-SHA256 prefix, or it answers 403.
GET /{base64url(HMAC(key, salt ‖ path))}/resize/750/0/a.jpguse
The server, the CLI, the Rust crate and the Ruby gems all run the same decode → resize → encode path, so a number measured on one holds for the others.
server
Multi-arch Docker image, linux/amd64 + arm64, AVIF included. Put a CDN in front; responses carry a one-year Cache-Control.
docker run -p 8081:8081 \
-v $PWD/images:/images:ro \
ghcr.io/oximg/oximg
curl localhost:8081/resize/500/500/photo.jpg -o out.jpgcli
The same pipeline with no server, for CI asset builds. Prebuilt binaries for Linux and macOS, or Homebrew.
brew install oximg/tap/oximg
oximg resize photo.jpg 1600 1600 out.webp
oximg resize photo.jpg 750 0 out.jpg -q 70
oximg probe photo.webprust
default-features = false drops the whole HTTP stack. Failures are typed: match on ErrorKind, not on messages.
use oximg::pipeline::{self, Params};
let params = Params {
max_width: 300,
max_height: 200,
..Default::default()
};
let (bytes, format) = pipeline::process_path(path, ¶ms)?;ruby
Platform gems bundle the executable, so bundle install is the whole installation. oximg-rails signs URLs and wires into ActiveStorage.
gem "oximg"
Oximg.resize("in.jpg", "out.webp", width: 750, quality: 80)
Oximg.probe("in.jpg")
#=> {format: :jpeg, width: 4000, height: 3000, ...}production
errors by fault
A client's bad key is 400, a missing object 404, a broken origin 502, a slow one 504. CDNs retry 5xx and cache 4xx, so misfiling turns crawlers into origin load.
/metrics
Opt-in Prometheus metrics separate queue wait from processing time — telling “needs more CPU” apart from “sources got bigger”.
SIGTERM
Stops accepting, finishes in-flight encodes, exits 0 — what docker stop, Kubernetes and Cloud Run expect.
fail-closed
Every setting is an environment variable. Set but unparseable means the process exits naming it, not a silent default.
orientation
EXIF, PNG eXIf, WebP EXIF and AVIF irot/imir are applied; pixels come out upright and nothing double-rotates.
GIF → WebP
Frames stream into an animated WebP, one canvas in memory. Measured: WebP reached 25% of the source bytes where the best GIF managed 81%.
status
oximg is a proof of concept: the HTTP interface and the library API can change without notice. Pin a version tag in production. Here is what isn't there yet.