Skip to content

imazen/zenjxl

Repository files navigation

zenjxl CI MSRV License

zenjxl is a JPEG XL encoding and decoding library combining zenjxl-decoder and jxl-encoder with resource limits, cancellation, and gain map support.

zenjxl-decoder is Imazen's fork of jxl-rs with additional metadata extraction, gain map parsing, and resource limiting. jxl-encoder is a pure Rust JPEG XL encoder supporting both lossless (modular) and lossy (VarDCT) modes. zenjxl wraps both behind a unified API and provides zencodec integration for use in zenpipe pipelines.

#![forbid(unsafe_code)], no_std + alloc, edition 2024.

Install

[dependencies]
zenjxl = "0.2.1"

To read decoded pixels back out as packed RGBA8 bytes (see the Decode example), also add the pixel crates and — for cancellation — almost-enough:

zenpixels = "0.2.13"           # PixelBuffer / PixelDescriptor (returned by decode)
zenpixels-convert = "0.2.13"   # the `.to_rgba8()` extension trait
almost-enough = "0.4.4"        # a ready-made cancellation token (optional)

Quick start

Decode

use zenjxl::{decode, probe, JxlLimits};
use zenpixels::PixelDescriptor;

let jxl_bytes: &[u8] = &std::fs::read("photo.jxl").unwrap();

// Metadata-only probe (no pixel decode).
let info = probe(jxl_bytes).unwrap();
println!("{}x{}, alpha={}, gray={}", info.width, info.height, info.has_alpha, info.is_gray);

// Full decode with resource limits. The 3rd arg is a pixel-format preference
// list (`&[zenpixels::PixelDescriptor]`); `&[]` lets the decoder pick natively.
let limits = JxlLimits {
    max_pixels: Some(120_000_000),                  // 108 MP photos are common
    max_memory_bytes: Some(2 * 1024 * 1024 * 1024),
};
let output = decode(jxl_bytes, Some(&limits), &[]).unwrap();

// `output.pixels` is a `zenpixels::PixelBuffer` in the image's NATIVE format
// (opaque → RGB8, with alpha → RGBA8). Normalize to packed RGBA8 bytes with the
// `zenpixels-convert` extension trait:
use zenpixels_convert::PixelBufferConvertTypedExt;
let rgba: Vec<u8> = output.pixels.to_rgba8().copy_to_contiguous_bytes(); // w*h*4, R,G,B,A

// Need the bytes in their native layout instead? Read them straight off the
// PixelBuffer — no conversion crate required. `descriptor()` tells you the layout
// (e.g. RGB8 vs RGBA8); `width()`/`height()` give the dimensions.
let (w, h) = (output.pixels.width(), output.pixels.height());
let desc = output.pixels.descriptor();              // PixelDescriptor: channels + bit depth
let native: Vec<u8> = output.pixels.into_vec();      // owned, tightly packed, w*h*desc.bytes_per_pixel()
// (use `output.pixels.contiguous_bytes()` for a borrowing `Cow<[u8]>` instead of an owned Vec.)

Dependencies & errors. Besides zenjxl, add zenpixels (PixelBuffer/ PixelDescriptor), zenpixels-convert (the .to_rgba8() trait), and enough (cancellation). decode/probe/encode_* return Result<_, whereat::At<E>> (At<JxlError>): the At<…> adds a build-time source location for logs — read it with err.location() (Option<&Location>, giving file()/line()). Get the underlying error with err.error() (borrow) or err.decompose().0 (owned), then match the [JxlError] enum (it is #[non_exhaustive], so keep a wildcard arm).

Cancellation. decode_with_options(data, limits, preferred, parallel, stop) adds a cancellation token. parallel: Option<bool> toggles multithreaded decode (None = default); stop: Option<Arc<dyn enough::Stop>> is the token. Build a real one with almost_enough::Stopper (cargo add almost-enough):

use std::sync::Arc;
use zenjxl::{decode_with_options, JxlLimits};

let stopper = almost_enough::Stopper::new();
let watcher = stopper.clone(); // Stopper is Clone; shares the cancel flag
std::thread::spawn(move || watcher.cancel()); // e.g. on a deadline / client disconnect

let limits = JxlLimits { max_pixels: Some(120_000_000), max_memory_bytes: Some(2 * 1024 * 1024 * 1024) };
let stop: Arc<dyn enough::Stop> = Arc::new(stopper);
let output = decode_with_options(jxl_bytes, Some(&limits), &[], None, Some(stop))?;

Encode

use zenjxl::{LossyConfig, LosslessConfig, PixelLayout, calibrated_jxl_quality, quality_to_distance};

let rgb: &[u8] = &[0u8; 256 * 256 * 3]; // packed RGB8 pixels

// Lossy. JXL is parameterized by butteraugli *distance*, where LOWER = better
// (0.0 = mathematically lossless, ~1.0 = visually lossless, larger = smaller file).
// Map a 0..=100 quality to a distance with the calibrated chain:
//   calibrated_jxl_quality(generic_q) -> native JXL quality (0..=100),
//   quality_to_distance(native_q)     -> butteraugli distance.
let distance = quality_to_distance(calibrated_jxl_quality(85.0));
let lossy = LossyConfig::new(distance)
    .encode(rgb, 256, 256, PixelLayout::Rgb8)
    .unwrap();

// Lossless.
let lossless = LosslessConfig::new()
    .encode(rgb, 256, 256, PixelLayout::Rgb8)
    .unwrap();

PixelLayout also covers Rgba8, Bgra8, and Gray8 (plus 16-bit and linear-f32 variants). quality_to_distance alone maps quality straight to distance; calibrated_jxl_quality first re-maps a libjpeg-turbo-style quality onto JXL's native quality scale so a given number lands at the same perceptual level it would in a JPEG encoder. Convenience wrappers (encode_rgb8/encode_rgba8/…) exist for the imgref ImgRef<rgb::Rgb<u8>> pixel types if you already hold those.

Features

Decode -- probe() returns JxlInfo with dimensions, bit depth, ICC profile, CICP signaling, EXIF orientation, raw EXIF/XMP bytes, extra channel metadata, HDR tone mapping fields, preview size, and gain map bundles. decode() returns a PixelBuffer with automatic format negotiation. decode_with_parallel() enables multithreaded decoding; decode_with_options() adds cancellation via enough::Stop.

Encode -- Convenience functions for RGB, RGBA, BGRA, and grayscale u8 data in both lossy and lossless modes. calibrated_jxl_quality() maps a 0--100 quality scale to JXL distance. Container utilities (append_gain_map_box, is_bare_codestream) for gain map authoring.

Gain maps -- Decode extracts GainMapBundle from jhgm container boxes (ISO 21496-1). Encode can append gain map boxes to existing codestreams.

HDR metadata -- JxlInfo exposes intensity_target, min_nits, relative_to_max_display, and linear_below from the JXL tone mapping header.

Feature flags

Flag Default Description
decode yes JPEG XL decoding via zenjxl-decoder
encode yes JPEG XL encoding via jxl-encoder
threads no Multithreaded decoding via rayon (requires decode)
parallel no Per-frame parallelism inside the encoder via rayon (requires encode)
butteraugli-loop no Perceptual quality tuning (requires encode)
zencodec no Config/Job/Executor trait integration for zen codec pipelines

Limitations

  • The encode_rgb8/encode_rgba8/… convenience wrappers are u8-only. The LossyConfig/LosslessConfig encode() path and the zencodec adapter support wider bit depths (16-bit and linear-f32 layouts via PixelLayout).
  • zenjxl-decoder does not yet support all JPEG XL features (e.g., some edge cases in progressive decoding).

Image tech I maintain

State of the art codecs* zenjpeg · zenpng · zenwebp · zengif · zenavif (rav1d-safe · zenrav1e · zenavif-parse · zenavif-serialize) · zenjxl (jxl-encoder · zenjxl-decoder) · zentiff · zenbitmaps · heic · zenraw · zenpdf · ultrahdr · mozjpeg-rs · webpx
Compression zenflate · zenzop
Processing zenresize · zenfilters · zenquant · zenblend
Metrics zensim · fast-ssim2 · butteraugli · resamplescope-rs · codec-eval · codec-corpus
Pixel types & color zenpixels · zenpixels-convert · linear-srgb · garb
Pipeline zenpipe · zencodec · zencodecs · zenlayout · zennode
ImageResizer ImageResizer (C#) — 24M+ NuGet downloads across all packages
Imageflow Image optimization engine (Rust) — .NET · node · go — 9M+ NuGet downloads across all packages
Imageflow Server The fast, safe image server (Rust+C#) — 552K+ NuGet downloads, deployed by Fortune 500s and major brands

* as of 2026

General Rust awesomeness

archmage · magetypes · enough · whereat · zenbench · cargo-copter

And other projects · GitHub @imazen · GitHub @lilith · lib.rs/~lilith · NuGet (over 30 million downloads / 87 packages)

License

Dual-licensed: AGPL-3.0 or commercial.

I've maintained and developed open-source image server 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. Support sustainable and secure software; swap patch tuesday for patch leap-year.

Our open-source products

Your options:

  • Startup license -- $1 if your company has under $1M revenue and fewer than 5 employees. Get a key
  • Commercial subscription -- Governed by the Imazen Site-wide Subscription License v1.1 or later. 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 and improvements are dual-licensed (AGPL-3.0 or commercial) as above.

Upstream contribution

We are willing to release our improvements under the original BSD-3-Clause license if upstream takes over maintenance of those improvements. We'd rather contribute back than maintain a parallel codebase. Open an issue or reach out.

About

No description, website, or topics provided.

Resources

License

AGPL-3.0, Unknown licenses found

Licenses found

AGPL-3.0
LICENSE-AGPL3
Unknown
LICENSE-COMMERCIAL

Stars

Watchers

Forks

Packages

 
 
 

Contributors