jxl-oxide
| Language | Rust |
|---|---|
| License | MIT or Apache-2.0 |
| Repository | github.com/tirr-c/jxl-oxide |
| Documentation | docs.rs/jxl-oxide |
| Depends on | pure Rust; optional rayon (threads), lcms2 or moxcms (color management), image (integration) |
| Read / write | yes / no, all features including animation, HDR, CMYK and JPEG reconstruction |
jxl-oxide is a complete JPEG XL decoder written in Rust, with no C code. It passes the JPEG XL conformance tests and decodes everything the format allows: lossy and lossless images, animation, extra channels, HDR, progressive and cropped decoding, and reconstruction of the original JPEG from recompressed files. It also ships a command-line tool, jxl-oxide (cargo install jxl-oxide-cli).
It only decodes. To encode JPEG XL from Rust, use jpegxl-rs (libjxl bindings) or the lossless-only zune-jpegxl.
Installation#
Shell
cargo add jxl-oxide
# with image crate integration:
cargo add jxl-oxide --features image
cargo add image
Default features include rayon for multithreaded decoding.
Basic usage: decode a JXL file to pixels#
Rust
use jxl_oxide::JxlImage;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let image = JxlImage::builder().open("in.jxl")?;
println!("{} x {}, {:?}", image.width(), image.height(), image.pixel_format());
let render = image.render_frame(0)?; // first keyframe
let mut stream = render.stream(); // interleaved samples, orientation applied
let len = (stream.width() * stream.height() * stream.channels()) as usize;
let mut pixels = vec![0u8; len]; // u8, u16 or f32
stream.write_to_buffer(&mut pixels);
println!("{} channels, first pixel {:?}", stream.channels(), &pixels[..stream.channels() as usize]);
Ok(())
}
How to convert JXL to PNG with the image crate#
With the image feature, JxlDecoder implements image::ImageDecoder:
Rust
use image::DynamicImage;
use jxl_oxide::integration::JxlDecoder;
use std::fs::File;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let decoder = JxlDecoder::new(File::open("in.jxl")?)?;
let img = DynamicImage::from_decoder(decoder)?;
img.save("out.png")?;
Ok(())
}
Or register a hook once, and image::open handles .jxl like any other format:
Rust
jxl_oxide::integration::register_image_decoding_hook();
let img = image::open("in.jxl")?;
How to read all frames of an animation#
Rust
let image = JxlImage::builder().open("anim.jxl")?;
for i in 0..image.num_loaded_keyframes() {
let render = image.render_frame(i)?;
println!("frame {i}: duration {} ticks", render.duration());
}
How to rebuild the original JPEG from a recompressed JXL#
Rust
use jxl_oxide::{JpegReconstructionStatus, JxlImage};
use std::fs::File;
let image = JxlImage::builder().open("photo.jxl")?;
if matches!(image.jpeg_reconstruction_status(), JpegReconstructionStatus::Available) {
image.reconstruct_jpeg(File::create("photo.jpg")?)?; // bit-identical to the original
}
How to get the ICC profile#
Rust
let image = JxlImage::builder().open("in.jxl")?;
if let Some(icc) = image.original_icc() {
println!("embedded ICC profile: {} bytes", icc.len());
}
let output_icc: Vec<u8> = image.rendered_icc(); // profile of the pixels render_frame returns
Tips#
JxlImage::builder().read(reader)decodes from anyRead;build_uninit()withfeed_bytesdecodes data as it arrives (progressive loading).- Use
request_color_encodingto get output in a specific color space, e.g. Display P3; the default CMS handles the common cases, enablelcms2ormoxcmsfor ICC-to-ICC conversion. - The
imageintegration doesn’t decode animation; userender_framefor that.