jxl-encoder [](https://github.com/imazen/jxl-encoder/actions/workflows/ci.yml) [](https://crates.io/crates/jxl-encoder) [](https://lib.rs/crates/jxl-encoder) [](https://docs.rs/jxl-encoder) [](#license)
June 28, 2026 · View on GitHub
jxl-encoder is a pure-Rust JPEG XL encoder for both
lossy (VarDCT) and lossless (Modular) images, built on the foundation of
libjxl and targeting parity with it.
#![forbid(unsafe_code)] by default; no_std + alloc capable (disable default
features for the alloc-only build — std adds the encode_to() / finish_to()
Write-target sinks).
The reference encoder, cjxl (libjxl), is a well-engineered, mature C++ codebase. This crate exists because we wanted a pure-Rust encoder we could embed in Imageflow with no C FFI, and a place to experiment with content-aware dispatch and alternative perceptual metrics. Our algorithms, quantization weights, cost models, and the bitstream format itself are derived from libjxl. Every encoded file in our test suite is verified against three independent decoders: jxl-rs, jxl-oxide, and djxl (libjxl).
On a strict per-cell Pareto scoreboard cjxl still wins more cells overall and is measurably faster; this crate leads on 8-bit lossless and a fair slice of SDR lossy, and is pure-Rust and embeddable. The honest, measured breakdown is in the benchmark index (and the scoreboard tables below on GitHub).
Where it stands vs cjxl (the honest version)
cjxl is the reference, and on a per-scenario basis it still wins more cells than
we do today, and it is measurably faster. We track this with a strict per-cell
Pareto scoreboard (docs/GOAL_BEAT_CJXL.md): a cell is "ours" only when we are
no worse on bytes, no worse on perceptual quality, and within the wall budget.
The numbers below are what that scoreboard reports — not aspirations.
Bytes + quality, 280 cells across SDR lossy, SDR lossless, HDR lossy, and a
fixed-overhead size axis (measured 2026-06-12, binary 3f025244; cjxl =
libjxl v0.12.0; reproduce with scripts/scoreboard/run_scoreboard.py,
see benchmarks/README.md):
| Verdict | Cells | Share |
|---|---|---|
| cjxl dominates | 105 | 38 % |
| mixed (we win one axis, lose another) | 89 | 32 % |
| we dominate | 78 | 28 % |
| tie | 8 | 3 % |
By content family (we-dominate / tie / mixed / cjxl-dominates), from
benchmarks/scoreboard/scoreboard_2026-06-12_run4_summary.md:
| Family | We | Tie | Mixed | cjxl |
|---|---|---|---|---|
| SDR lossless | 30 | 4 | 0 | 22 |
| SDR lossy | 33 | 0 | 47 | 24 |
| HDR lossy | 10 | 1 | 30 | 55 |
| Size axis (64²/256²) | 5 | 3 | 12 | 4 |
We lead on lossless graphics and photos at e7, and on a good chunk of SDR lossy content; HDR lossy is where cjxl wins most cells (small per-cell byte gaps at tied quality — the per-cell strictness counts every one). The smooth-gradient / HDR sky class is the historical loss locus and an open wedge.
Wall time is the weak axis, and there is no way to spin it. On a 40-cell
quiet-box grid (5 strata × e{5,7} × {1,8} threads × {lossy, lossless}, measured
2026-06-12, binary a5a9e4d6, benchmarks/scoreboard/wall_grid_2026-06-12.tsv),
39 of 40 cells are over the ≤1.2× budget — cjxl is faster on all but one
(plots lossy e7 1T, 1.12×):
| Mode | 1 thread | 8 threads |
|---|---|---|
| lossy | 1.12–2.17× cjxl | 1.57–3.76× cjxl |
| lossless | 1.33–5.90× cjxl | 2.89–10.78× cjxl |
Single-thread lossy is roughly competitive; everything else is slower, and the 8T gap is the largest. cjxl scales ~4.6–5.5× from 1T→8T while we scale ~1.8–2.5× — that's both a serial-speed gap and a parallel-coverage gap (our AC-tile search is parallel, but XYB / adaptive-quant / gaborish / transform / tokenize are still serial, and cjxl parallelizes per 256² group). Closing this is the active workstream.
Lossless, in more detail
8-bit lossless is where we are strongest. With the e5/e6 budgeted tree-learn
lift (benchmarks/lossless_8bit_tree_lift_2026-06-12.tsv, 43 imazen-26 picks,
djxl-verified pixel-exact on all 56 cells):
- e5: vs cjxl mean −2.4 %, median −0.1 % bytes, 20/43 cells smaller (worst +36.9 % on a noaa-documents scan).
- e6: vs cjxl mean −10.7 %, median −7.0 % bytes, 11/13 cells smaller.
16-bit lossless at low effort (e2/e4) still loses to cjxl on bytes, and the lossless wall gap is the widest of any mode (see the grid above).
HDR lossy, in more detail
On 12 PQ/HLG crops × e{5,7} × d{0.5,1,2,4}
(benchmarks/hdr_lossy_parity_postdispatch_2026-06-12.tsv, PQ-EOTF butteraugli
@ 1000 nits), median bytes run +1.2 % to +4.6 % over cjxl at quality
at-or-better than cjxl on 7 of 8 measured points. The QuantizeWP DC-shaping
dispatch (keyed on the resolved transfer function) closed roughly half the
median HDR byte gap; the smooth-sky residual remains.
Quick start
[dependencies]
jxl-encoder = "0.3.1"
use jxl_encoder::{LossyConfig, LosslessConfig, PixelLayout};
// Lossy — distance 1.0 is visually lossless (lower distance = higher quality).
let jxl = LossyConfig::new(1.0)
.encode(&pixels, width, height, PixelLayout::Rgb8)?;
// Lossless — exact reconstruction.
let jxl = LosslessConfig::new()
.encode(&pixels, width, height, PixelLayout::Rgb8)?;
// Full control — per-knob overrides, then a request with limits / cancellation.
use jxl_encoder::Limits;
let jxl = LossyConfig::new(1.0)
.with_ans(true)
.with_gaborish(true)
.encode_request(width, height, PixelLayout::Rgba8)
.with_limits(&Limits::default())
.encode(&pixels)?;
std is on by default; cargo add jxl-encoder pulls the latest release. MSRV is
Rust 1.89.
Quality (distance) and effort
Distance is the butteraugli target passed to LossyConfig::new(distance).
It is a perceptual error budget, so the scale runs opposite to a percent slider:
- Lower distance = higher quality (and larger files).
- Valid lossy range is
0.0 < distance <= 25.0. 1.0is the visually-lossless anchor — the libjxl default, indistinguishable from the source for most images. Go below1.0(e.g.0.5) for near-transparent quality; raise it (2.0,4.0, …) to trade quality for size.0.0(mathematically lossless) is not accepted byLossyConfig— useLosslessConfigfor exact reconstruction instead.
Effort trades encode time for compression. LossyConfig and LosslessConfig
both default to effort 7; set it with with_effort(level):
use jxl_encoder::{LossyConfig, LosslessConfig, PixelLayout};
// Slower, smaller (effort 9 = Viterbi LZ77, 4 butteraugli iterations).
let jxl = LossyConfig::new(1.0)
.with_effort(9)
.encode(&pixels, width, height, PixelLayout::Rgb8)?;
// Fast preview.
let jxl = LosslessConfig::new()
.with_effort(3)
.encode(&pixels, width, height, PixelLayout::Rgb8)?;
Valid effort is 1..=12. 1..=9 mirrors libjxl's kFalcon..=kTortoise ladder;
10..=12 are this crate's extended search budgets (longer butteraugli / tree-learn
seeds, still 100 %-spec-valid bitstreams). Higher effort = slower, better compression.
Cancellation
Encodes are cooperatively cancellable. Pass a stop token via
EncodeRequest::with_stop(&dyn Stop) — the encoder checks it periodically and
returns EncodeError::Cancelled if it fires. The Stop trait and the no-op
Unstoppable token are re-exported from jxl_encoder (originally from the
enough crate):
use jxl_encoder::{LossyConfig, PixelLayout, Unstoppable};
// No-op token — zero cost, never cancels (same as not passing one):
let jxl = LossyConfig::new(1.0)
.encode_request(width, height, PixelLayout::Rgb8)
.with_stop(&Unstoppable)
.encode(&pixels)?;
For a token you can actually trigger (e.g. from another thread, a timeout, or a
user "cancel" button), add almost-enough
and use its Stopper — clone it to share, then call .cancel():
[dependencies]
almost-enough = "0.4.4"
use jxl_encoder::{LossyConfig, PixelLayout};
use almost_enough::Stopper;
let stop = Stopper::new();
let watcher = stop.clone(); // hand a clone to a watchdog / signal handler
// ... watcher.cancel() from elsewhere when the user aborts ...
let result = LossyConfig::new(1.0)
.encode_request(width, height, PixelLayout::Rgb8)
.with_stop(&stop)
.encode(&pixels);
// If `cancel()` fired before the encode finished, `result` is `Err(e)` where
// `matches!(e.error(), EncodeError::Cancelled)` holds (see Errors below).
Errors
encode returns jxl_encoder::Result<Vec<u8>> = Result<Vec<u8>, whereat::At<EncodeError>>.
The At<…> wrapper records a source location for logs (format!("{e}")); borrow the
inner error with e.error() (or own it with e.decompose().0) to match. EncodeError
is #[non_exhaustive], so keep a wildcard arm:
use jxl_encoder::{LossyConfig, EncodeError, PixelLayout};
match LossyConfig::new(1.0).encode(&pixels, width, height, PixelLayout::Rgb8) {
Ok(_jxl) => { /* encoded bytes */ }
Err(e) => match e.error() {
EncodeError::Cancelled => { /* a Stop token requested cancellation */ }
EncodeError::LimitExceeded { message } => eprintln!("limit: {message}"),
EncodeError::Oom(_) => eprintln!("out of memory"),
EncodeError::InvalidInput { message }
| EncodeError::InvalidConfig { message } => eprintln!("bad input/config: {message}"),
other => eprintln!("encode failed: {other:?}"),
},
}
Pixel layouts
Integer and HDR/float layouts are accepted directly; the encoder converts to XYB (lossy) or the modular integer space (lossless) internally:
Rgb8, Rgba8, Bgr8, Bgra8, Gray8, GrayAlpha8, Rgb16, Rgba16,
Gray16, GrayAlpha16, RgbLinearF32, RgbaLinearF32, GrayLinearF32,
GrayAlphaLinearF32, RgbLinearF16, RgbaLinearF16, GrayLinearF16,
GrayAlphaLinearF16, RgbPqF32, RgbaPqF32, RgbHlgF32, RgbaHlgF32,
RgbBt709F32, RgbaBt709F32, Cmyk8, Cmyk16.
Lossy encoding supports all layouts including alpha (VarDCT for RGB + modular for the alpha channel). Lossless supports RGB, RGBA, grayscale, and gray+alpha. See HDR / wide-gamut for the PQ / HLG / BT.709 transfer-function variants.
Resource limits
EncodeRequest::with_limits(&Limits) bounds an encode against untrusted input.
Limits primarily caps encoder working-set memory (it also exposes optional
max_width / max_height / max_pixels / max_quant_loop_iters setters, all
None by default):
use jxl_encoder::{LossyConfig, Limits, PixelLayout};
let limits = Limits::default() // no explicit caps set …
.with_max_memory_bytes(512 * 1024 * 1024); // … 512 MB hard ceiling
let jxl = LossyConfig::new(1.0)
.encode_request(width, height, PixelLayout::Rgb8)
.with_limits(&limits)
.encode(&pixels)?;
Limits::default() sets no explicit memory bound, but the encoder still
applies a soft default cap so an unconfigured image proxy can't be OOM'd:
4 GiB for lossy, 8 GiB for lossless (lossless tree-learning is a heavier
memory regime). These defaults are fixed ceilings — they are deliberately not
scaled with image dimensions, so an oversized untrusted upload is still bounded.
For trusted batch work, raise the cap with with_max_memory_bytes(n) (or pass
u64::MAX to opt out of the soft cap entirely). If an encode exceeds the cap it
returns EncodeError::LimitExceeded. LossyConfig::estimate_peak_memory_bytes
(and the LosslessConfig equivalent) let callers plan a budget up front.
HDR / wide-gamut
| Capability | Entry point |
|---|---|
| PQ / HLG / BT.709 f32 input | Pass the matching PixelLayout variant (e.g. RgbPqF32, RgbHlgF32); the encoder inverts the transfer function before XYB. |
| BT.2100 PQ / HLG colour encoding | ColorEncoding::bt2100_pq() / bt2100_hlg(), via EncodeRequest::with_color_encoding(...). |
intensity_target / min_nits | EncodeRequest::with_intensity_target(nits) / with_min_nits(nits). |
| HDR-aware perceptual loss in the quant loop | LossyConfig::with_hdr_loss(HdrLoss::Auto) — auto-dispatches to a VDP2 path on PQ/HLG content, butteraugli elsewhere. SDR encodes stay byte-identical. Requires the butteraugli-loop feature. |
Measured HDR bytes/quality vs cjxl are in the benchmark index.
Feature coverage vs libjxl
We implement all 19 AC strategies that libjxl evaluates through effort 9, all enabled. The remaining 8 are either commented out in libjxl (DCT32x8, DCT8x32) or experimental/unused (DCT128+) — cjxl never selects them either. Effort 9 adds fine-grained strategy search (step=1 for 32×32+ blocks).
Lossy (VarDCT)
| Feature | libjxl e5 | libjxl e7 | jxl-encoder |
|---|---|---|---|
| AC strategies | 7 | 19 | 19 |
| ANS entropy coding (default-on) | Yes | Yes | Yes |
| Adaptive quantization | Yes | Yes | Yes |
| Pixel-domain loss (default-on) | Yes | Yes | Yes |
| Chroma-from-luma (per-tile least-squares) | Yes | Yes | Yes |
| Gaborish inverse pre-filter (default-on) | Yes | Yes | Yes |
| Custom coefficient ordering (default-on) | Yes | Yes | Yes |
| Butteraugli quant loop (effort 8+) | Yes | Yes | Yes (2 iters at e8, 4 at e9+) |
| EPF per-block sharpness | Yes | Yes | Yes |
| Content-adaptive block context map | Yes | Yes | Yes |
| Error diffusion in AC quantization | No | No | Yes (opt-in) |
| Noise synthesis | Yes | Yes | Yes (opt-in) |
| Lossy + alpha (VarDCT RGB + modular alpha) | Yes | Yes | Yes |
| JPEG transcode (byte-exact re-encode) | Yes | Yes | Yes (opt-in feature) |
| Animation (lossy + lossless) | Yes | Yes | Yes |
| 16-bit / float input | Yes | Yes | Yes (26 pixel layouts) |
| Patches / dictionary (default-on for screenshots) | No | Yes | Yes |
| Fine-grained AC strategy search | Yes | Yes | Yes (effort 9+) |
| Splines | No | Yes | Yes (opt-in API) |
| Dots detection | No | Yes | Yes (opt-in) |
| Progressive VarDCT (2-pass / 3-pass) | Yes | Yes | Yes |
| Photon-noise simulation | Yes | Yes | Yes (with_photon_noise_iso) |
| Forced RCT colorspace (lossless) | Yes | Yes | Yes (with_force_rct) |
| Peak-memory estimate helper | No | No | Yes (`estimate_peak_memory_bytes$) |
\text{Lossless} (\text{Modular})
| \text{Feature} | \text{libjxl} | \text{jxl}-\text{encoder} |
|---|---|---|
| \text{RCT} (\text{all} 42 \text{variants}) | \text{Yes} | \text{Yes} |
| \text{ANS} \text{entropy} \text{coding} (\text{default}-\text{on}) | \text{Yes} | \text{Yes} |
| \text{Huffman} \text{entropy} \text{coding} (\text{fallback}) | \text{Yes} | \text{Yes} |
| \text{LZ77} \text{RLE} / \text{greedy} / \text{optimal} \text{Viterbi} \text{DP} | \text{Yes} | \text{Yes} (\text{default}-\text{on} \text{at} \text{e7} / \text{e8} / \text{e9}+) |
| \text{MA} \text{tree} \text{learning} (14 \text{predictors}, 16 \text{properties}) | \text{Yes} | \text{Yes} |
| \text{Weighted} \text{predictor} | \text{Yes} | \text{Yes} (\text{bit}-\text{exact} \text{match}) |
| \text{Palette} \text{transform} (\text{auto}-\text{detect}) | \text{Yes} | \text{Yes} |
| \text{Squeeze} \text{transform} (\text{Haar} \text{wavelet}) | \text{Yes} | \text{Yes} |
| \text{Histogram} \text{clustering} | \text{Full} (\text{kDefault}) | \text{Pair}-\text{merge} \text{refinement} |
| \text{Multi}-\text{group} \text{encoding} (\text{any} \text{image} \text{size}) | \text{Yes} | \text{Yes} |
| \text{RGBA} / \text{grayscale} / \text{alpha} | \text{Yes} | \text{Yes} |
| \text{Lossy} \text{palette} / \text{delta} \text{palette} | \text{Yes} | \text{Yes} (\text{opt}-\text{in}) |
| 16-\text{bit} / \text{float} \text{input} | \text{Yes} | \text{Yes} |
| \text{Best}/\text{Variable} \text{predictors} (\text{effort} 8+) | \text{Yes} | \text{Tree} \text{learning} \text{is} \text{the} \text{Variable}-\text{mode} \text{equivalent} |
\text{Container} / \text{metadata}
| \text{Feature} | \text{libjxl} | \text{jxl}-\text{encoder} |
|---|---|---|
| \text{ICC} \text{profile} \text{embedding} (\text{PredictICC} + \text{entropy} \text{coded}) | \text{Yes} | \text{Yes} |
| \text{EXIF} / \text{XMP} \text{metadata} (\text{container} \text{box}) | \text{Yes} | \text{Yes} |
| \text{Animation} (\text{per}-\text{frame} \text{duration}) | \text{Yes} | \text{Yes} |
| \text{Multi}-\text{group} \text{framing} (>256 \times 256) | \text{Yes} | \text{Yes} |
| \text{Cancellation} / \text{resource} \text{limits} | \text{No} | \text{Yes} ($&dyn Stop, Limits`) |
Honest gaps
| Feature | libjxl | Notes |
|---|---|---|
| Streaming frame encoding | Yes | Current impl buffers the full image; estimate_peak_memory_bytes lets callers plan around this. |
ec_distance (per-extra-channel quality) | Yes | Lossy alpha is currently encoded as a lossless modular extra channel. |
decoding_speed_tier | Yes | We expose individual gating knobs that approximate the major effects. |
| Wall-time parity | — | cjxl is faster on 39/40 measured cells; see the scoreboard on GitHub. |
AC strategy coverage
| Strategy | Block | Min distance | libjxl effort |
|---|---|---|---|
| DCT8 | 8×8 | any | e1+ |
| DCT4x4 | 8×8 (4 sub) | any | e5+ |
| DCT4x8, DCT8x4 | 8×8 (2 sub) | any | e6+ |
| IDENTITY | 8×8 (pixel domain) | any | e5+ |
| DCT2x2 | 8×8 (4 sub) | any | e5+ |
| AFV0-3 | 8×8 (corner DCT) | any | e6+ |
| DCT16x8, DCT8x16 | 16×8 | any | e5+ |
| DCT16x16 | 16×16 | any | e5+ |
| DCT32x16, DCT16x32 | 32×16 | d ≥ 2.0 | e6+ |
| DCT32x32 | 32×32 | d ≥ 2.0 | e7+ |
| DCT64x32, DCT32x64 | 64×32 | d ≥ 3.0 | e7+ |
| DCT64x64 | 64×64 | d ≥ 3.0 | e7+ |
CLI
A command-line wrapper, cjxl-rs, ships in the
jxl-encoder-cli crate:
cargo install jxl-encoder-cli
# Lossy (distance 1.0 = visually lossless)
cjxl-rs input.png output.jxl -d 1.0
# Lossless
cjxl-rs input.png output.jxl --lossless
# See all options
cjxl-rs --help
Experimental: CVVDP-driven quantization loop (opt-in)
The quant loop at effort ≥ 8 normally calls butteraugli once per iteration. An
opt-in path drives it with ColorVideoVDP
(cvvdp, Mantiuk et al. 2024) instead. Default OFF; butteraugli stays the
production default, and EncoderStrategy::Libjxl forces cvvdp off regardless so
cjxl-parity byte-locks hold.
cargo build --release --features cvvdp-loop # GPU (needs CUDA)
cargo build --release --features cvvdp-loop-cpu # pure-Rust CPU
It is opt-in because cvvdp ships an uncalibrated per-distance target table: at
the same distance it converges to a tighter perceptual target than the
distance knob currently implies, producing larger files. The full 1,134-cell
tracking sweep is at benchmarks/cvvdp_vs_buttloop_tracking_2026-05-24.tsv;
methodology and the ship-rule are in
docs/CVVDP_FORK_DECISION.md. zensim is also
available as a third quant-loop metric (zensim-loop / zensim-loop-gpu).
Reproducible benchmarks
Every comparison number above traces to a committed file under benchmarks/.
benchmarks/README.md is the reproduction index: the
exact command for each board, what corpus it uses, what it measures, and the
quiet-box caveat for wall numbers. Bytes and quality are deterministic;
wall-time numbers are only disposition-grade on a quiet box — the wall
harness refuses to run under load for that reason.
Project structure
jxl-encoder/ # workspace root (this repo)
├── jxl-encoder/ # the jxl-encoder library crate
│ └── src/
│ ├── api.rs # public API (LossyConfig, LosslessConfig, EncodeRequest)
│ ├── vardct/ # VarDCT (lossy) encoder
│ ├── modular/ # Modular (lossless) encoder
│ ├── entropy_coding/ # ANS, Huffman, HybridUint, LZ77
│ └── headers/ # file / frame headers
├── jxl-encoder-simd/ # SIMD primitives (jxl-encoder-simd on crates.io)
├── jxl-encoder-macros/ # internal proc-macros (jxl-encoder-macros on crates.io)
└── jxl-encoder-cli/ # CLI tool: cjxl-rs (jxl-encoder-cli on crates.io)
Building
cargo build # debug
cargo build --release -p jxl-encoder-cli # release CLI
cargo test --workspace --lib --tests # all tests
cargo clippy --workspace -- -D warnings # lint
When to reach for cjxl instead
- When encode speed matters more than the last few percent of bytes. cjxl is faster on nearly every measured cell, especially multi-threaded.
- When you need streaming / bounded-memory encode of very large images — not yet implemented here; cjxl streams.
- For HDR / smooth-gradient lossy at the byte minimum — cjxl wins most of those cells today.
- This crate is pure-Rust,
forbid(unsafe_code), embeddable with no C FFI, and ahead on 8-bit lossless and a fair slice of SDR lossy — reach for it when those matter.
Credits
- libjxl (JPEG XL Project Authors, BSD-3-Clause) — the reference encoder and a well-engineered, battle-tested codebase. Our algorithms, quantization weights, cost models, and bitstream format are derived from it. libjxl-tiny was the initial porting target.
- zune-jpegxl (Caleb Etemesi, MIT/Apache-2.0/Zlib) — a working pure-Rust JXL lossless encoder (~2.5k lines) that was the inspiration to extend into lossy encoding and the features above.
- jxl-rs (BSD-3-Clause) — primary roundtrip validation decoder.
- jxl-oxide — secondary validation decoder.
- Claude (Anthropic) — AI-assisted development. Not all code has been manually reviewed; review critical paths before production use.
License
Dual-licensed: AGPL-3.0-or-later or commercial.
I've maintained open-source image software — and the 40+ library ecosystem it depends on — full-time since 2011. Fifteen years of continual maintenance, backwards compatibility, support, and the (very rare) security patch. That kind of stability requires sustainable funding, and dual-licensing is how we make it work without venture capital or rug-pulls.
Your options:
- Startup license — $1 if your company has under $1M revenue and fewer than 5 employees. Get a key →
- Commercial subscription — Apache-2.0-like terms, no source-sharing requirement. Sliding scale by company size. Pricing & 60-day free trial →
- AGPL v3 — free and open. Share your source if you distribute.
See LICENSE-COMMERCIAL for details. Upstream code from libjxl/libjxl is licensed under BSD-3-Clause; our additions are dual-licensed (AGPL-3.0-or-later or commercial) as above.
Image tech I maintain
| Codecs ¹ | zenjpeg · zenpng · zenwebp · zengif · zenavif · zenjxl · zenbitmaps · heic · zentiff · zenpdf · zensvg · zenjp2 · zenraw · ultrahdr |
| Codec internals | zenjxl-decoder · jxl-encoder · zenrav1e · rav1d-safe · zenavif-parse · zenavif-serialize |
| Compression | zenflate · zenzop · zenzstd |
| Processing | zenresize · zenquant · zenblend · zenfilters · zensally · zentone |
| Pixels & color | zenpixels · zenpixels-convert · linear-srgb · garb |
| Pipeline & framework | zenpipe · zencodec · zencodecs · zenlayout · zennode · zenwasm · zentract |
| Metrics | zensim · fast-ssim2 · butteraugli · zenmetrics · resamplescope-rs |
| Pickers & ML | zenanalyze · zenpredict · zenpicker |
| Products | Imageflow image engine (.NET · Node · Go) · Imageflow Server · ImageResizer (C#) |
¹ pure-Rust, #![forbid(unsafe_code)] codecs, as of 2026
General Rust awesomeness
zenbench · archmage · magetypes · enough · whereat · cargo-copter