Expand description
§🌄 egui-jxl
A native, batteries-included library for progressively loading and rendering JPEG XL (.jxl) images in egui.
Supports both native (Tokio) and wasm32 (Web) targets out of the box using pure-Rust decoding via jxl-rs.
§📖 Overview
JPEG XL is an incredibly powerful, modern image format. One of its best features is progressive decoding—allowing an image to be displayed instantly as a blurry low-frequency preview, gradually becoming sharper as more bytes are loaded over the network or from disk.
However, decoding JXL streams in an immediate-mode GUI like egui is tricky. Synchronous decoding will completely freeze your UI loop.
egui-jxl bridges this gap. It handles the complex typestate parsing, memory management, and progressive rendering pipeline entirely on background threads (or Web Workers). It incrementally flushes higher-quality frames directly to the egui context lock-free, ensuring your app runs at a buttery smooth 60fps+ while images magically sharpen on the screen.
§✨ Features
- 🖼️ True Progressive Rendering: See your images instantly! Renders the 1/8th scale low-frequency preview as soon as the first few kilobytes arrive, upgrading through progressive passes as data streams in.
- 🔌 Drop-in
ImageLoader: Installs easily into the standardeguiecosystem. Once installed,ui.image("https://.../file.jxl")just works! - 🎛️ Granular Widget Control: Includes an
JxlHandlewidget andDecoderCommandAPI for fine-grained depth control—perfect for loading galleries of thumbnails and only fully rendering what the user clicks. - 🌐 Universal Support: Seamlessly switches between native threads (
tokio) and Web Workers (wasm32). Write once, run everywhere. - 📡 Chunked Streaming & Simulation: Native support for simulating network streams, limiting memory consumption, and applying artificial delays to throttle loading for testing.
- 🦀 Pure Rust: Powered by
jxl-rs. No C/C++ bindings or external system libraries required.
§📦 Installation
cargo add egui-jxl§🧩 Compatibility
egui APIs change frequently. Ensure you are using a compatible version of egui-jxl for your project.
egui-jxl | egui |
|---|---|
>=0.2.0 | 0.34 |
<=0.1.0 | 0.33 |
§🚀 Quick Start
Using egui-jxl requires two steps: registering the loader and displaying your image.
§1. Register the Plugin / Loader
You must install the JxlLoader into your egui::Context during application startup.
For Native (Tokio):
use eframe::egui;
use egui_jxl::loaders::jxl_loader::JxlLoader;
fn main() -> eframe::Result<()> {
eframe::run_native(
"JXL Viewer",
eframe::NativeOptions::default(),
Box::new(|cc| {
// 👇 Optional: Install standard HTTP/file loaders
egui_extras::install_image_loaders(&cc.egui_ctx);
// 👇 Crucial: Install the egui-jxl progressive loader
JxlLoader::new().install(&cc.egui_ctx);
Ok(Box::new(MyApp::default()))
}),
)
}For Web (WASM):
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(async {
eframe::WebRunner::new()
.start(
canvas,
web_options,
Box::new(|cc| {
egui_extras::install_image_loaders(&cc.egui_ctx);
// Works exactly the same on the web! tokio_with_wasm will
// automatically route the heavy decoding to a Web Worker.
egui_jxl::loaders::jxl_loader::JxlLoader::new().install(&cc.egui_ctx);
Ok(Box::new(MyApp::default()))
}),
)
.await;
});§2. Display the Image
Now, anywhere in your ui loop, just ask egui to render a .jxl URI. The loader will automatically spawn a background task, decode it progressively, and request repaints as the image gets sharper!
struct MyApp;
impl eframe::App for MyApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show_inside(ui, |ui| {
ui.heading("Progressive JPEG XL");
// This will instantly show a blurry preview if streaming over the network,
// refining automatically until lossless!
ui.image("https://jpegxl.info/images/zoltan-tasi-CLJeQCr2F_A-unsplash.jxl");
});
}
}§⚡ Performance Tradeoff: ui.image() vs JxlHandle
When building your app, you have two choices for rendering: the standard ui.image() pipeline, or the custom JxlHandle widget.
- The Easy Way (
ui.image): Using the globalJxlLoaderis incredibly convenient. However, because of howegui’s standard image caching works, the loader must clear the cache and re-allocate a brand new GPU texture for every single progressive pass that arrives. For one or two images, this is fine. For a gallery, this causes massive CPU/GPU thrashing. - The Fast Way (
JxlHandle): The customJxlHandlebypasses the standard loader pipeline entirely. It allocates a singleTextureHandleon the GPU once, and updates the pixels in-place (tex.set()) as new data streams in.
Recommendation: If you are displaying a lot of JXL images, or targeting lower-end WASM devices, use JxlHandle.
§🧠 Philosophy: The Power of Progressive Decoding
JPEG XL isn’t just a flat grid of pixels; it’s a stream of increasingly detailed “passes.” Traditional image loaders block the main thread, waiting for 100% of the file to download and decode before rendering a single pixel.
egui-jxl embraces a different philosophy: Show something immediately, and only compute what the user actually cares about. By decoupling the UI loop from the background decoding thread, we can intercept the bitstream at various depths. This is where the manual JxlHandle API shines, turning heavy image decoding into an interactive, memory-safe pipeline:
max_depth: When initializing aJxlHandle, you pass a depth limit. Setting this to1fetches the “low-frequency preview” (a highly compressed, 1/8th scale blurry thumbnail). The background thread then safely goes to sleep, saving massive amounts of RAM and CPU.handle.upgrade_quality(usize::MAX): The “Enhance” command. If a user clicks a thumbnail, you call this to wake the background thread back up. It resumes streaming and decoding exactly where it left off, sharpening the GPU texture in-place until it hits full lossless quality.- State Telemetry (
is_final,bytes_loaded,completed_passes): You aren’t left in the dark. The handle exposes real-time metrics so your UI can react dynamically—for instance, rendering loading spinners if!handle.is_final, or displaying network streaming progress viahandle.bytes_loaded.
§💡 Common Usage Patterns & Examples
egui-jxl is designed to fit several different UI patterns depending on how much control you need.
§1. Simulated Throttling (For Testing)
Want to see the progressive rendering in action locally? Native disk reads are often too fast to see the progressive passes. You can artificially delay the byte stream when setting up the loader to simulate slow network conditions.
use egui_jxl::loaders::jxl_loader::JxlLoader;
// Inside your eframe setup:
JxlLoader::new()
.chunk_size(4096) // Only feed 4KB at a time
.chunk_delay_ms(150) // Wait 150ms between chunks
.install(&cc.egui_ctx);§2. Manual Typestate Control (JxlHandle)
If you are building an image gallery, you don’t want to decode 50 images to full lossless quality all at once—that would consume massive amounts of CPU and memory.
Instead, you can use the JxlHandle widget to decode only the first pass (the low-frequency thumbnail) for all images. When a user clicks a thumbnail, you instruct the widget to resume decoding to full quality.
use std::fs::File;
use egui_jxl::JxlHandle;
struct Gallery {
// Manages the background decoding thread and state
jxl_widget: JxlHandle,
}
impl Gallery {
fn new(ctx: &egui::Context) -> Self {
let file = File::open("massive_image.jxl").unwrap();
Self {
// Start decoding, but PAUSE after the 1st progressive pass (thumbnail mode)!
jxl_widget: JxlHandle::new(file, 1, ctx.clone()),
}
}
}
// Inside your ui loop...
impl eframe::App for Gallery {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
// Fetch the highest-quality texture currently decoded
if let Some(tex) = self.jxl_widget.texture(ui.ctx()) {
let response = ui.add(
egui::Image::new(tex)
.fit_to_exact_size(egui::vec2(200.0, 200.0))
.sense(egui::Sense::click())
);
// If the user clicks the blurry thumbnail, upgrade to lossless!
if response.clicked() {
// Resumes the background decoder until the image is 100% finished
self.jxl_widget.upgrade_quality(usize::MAX);
}
// Show a loading spinner if it's currently crunching higher-res passes
if !self.jxl_widget.is_final {
ui.spinner();
ui.label(format!("Loaded: {} bytes", self.jxl_widget.bytes_loaded));
}
} else {
ui.spinner(); // Still parsing the initial JXL headers
}
}
}§⚙️ Under the Hood
How does egui-jxl bridge the gap between Immediate Mode GUI (60fps loop) and heavy synchronous decoding?
- Chunked Streaming: Native
Readstreams and network sockets are wrapped in aChunkedReader. This limits memory consumption and forces the pure-Rustjxl-rsdecoder to yield. - The Background Worker: A dedicated thread (or WASM local spawn via
tokio_with_wasm) initializes thejxl-rsstate machine (Initialized→WithFrameInfo→ProgressivePass). - Lock-Free Master Buffer: During the rendering phase, a persistent
Vec<u8>is allocated once. As new data arrives, the decoder overwrites the blurry pixels with sharp ones in place. - egui Interception: Once a pass is flushed, the worker safely clones the master buffer, packs it into an
egui::ColorImage, and passes it over acrossbeamchannel. If using the global loader, it safely invokesctx.forget_image()to clear the texture cache, swaps the updated texture in instantly, and callsctx.request_repaint(). The user sees a seamless fade-in of quality without the UI stuttering!
§🧑💻 See it in action:
You can find complete, runnable examples for these patterns in the examples/ directories of the repository:
single_image.rs– A minimal network streaming example utilizing the drop-inJxlLoaderwith simulated delays.gallery.rs– A fully-featured gallery viewer! Demonstrates mixing async HTTP streams, local file reading, intercepting early passes for memory-safe thumbnails, and upgrading to full quality upon selection.
Look at the code before you run it and try to predict what it does and what it will look like!
§🚩 Feature Flags
egui(Default): Enables theloadersandwidgetmodules, pulling in theeguidependency. Disable this if you are writing a custom frontend and only want to use the headlessdecoderandsourcemodules for raw background processing.
§⚠️ Known Limitations & Performance
- CPU Intensive: Pure-Rust JPEG XL decoding is incredibly powerful but inherently CPU-bound. Large multi-megapixel images on slower devices (especially in WASM without Web Workers/SIMD fully optimized) may take a few seconds to reach their final lossless state.
- Memory Footprint:
egui-jxlallocates a master pixel buffer for each actively decoding image. If you are rendering a massive gallery of 4K images, utilize theJxlHandlewidget to pause decoding after the first low-frequency pass (thumbnail) to prevent out-of-memory errors, as demonstrated in the gallery example.
§🤝 Contributing
Contributions are more than welcome! If you find a bug, have a feature request, or want to add support for new JPEG XL features (like animations or extra channels), please open an issue. If you want to contribute code, please submit a pull request.
§⚖️ License
This project is licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
§Note
This is not an official egui product, nor is it officially associated with the Joint Photographic Experts Group. Please refer to egui for official crates, and jxl-rs for the underlying JPEG XL decoder.
Re-exports§
pub use loaders::install_jxl_loader;pub use widget::JxlHandle;