Skip to main content

Crate egui_jxl

Crate egui_jxl 

Source
Expand description

§🌄 egui-jxl

Crates.io Docs.rs License

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 standard egui ecosystem. Once installed, ui.image("https://.../file.jxl") just works!
  • 🎛️ Granular Widget Control: Includes an JxlHandle widget and DecoderCommand API 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-jxlegui
>=0.2.00.34
<=0.1.00.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 global JxlLoader is incredibly convenient. However, because of how egui’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 custom JxlHandle bypasses the standard loader pipeline entirely. It allocates a single TextureHandle on 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 a JxlHandle, you pass a depth limit. Setting this to 1 fetches 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 via handle.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?

  1. Chunked Streaming: Native Read streams and network sockets are wrapped in a ChunkedReader. This limits memory consumption and forces the pure-Rust jxl-rs decoder to yield.
  2. The Background Worker: A dedicated thread (or WASM local spawn via tokio_with_wasm) initializes the jxl-rs state machine (InitializedWithFrameInfoProgressivePass).
  3. 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.
  4. egui Interception: Once a pass is flushed, the worker safely clones the master buffer, packs it into an egui::ColorImage, and passes it over a crossbeam channel. If using the global loader, it safely invokes ctx.forget_image() to clear the texture cache, swaps the updated texture in instantly, and calls ctx.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-in JxlLoader with 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 the loaders and widget modules, pulling in the egui dependency. Disable this if you are writing a custom frontend and only want to use the headless decoder and source modules 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-jxl allocates a master pixel buffer for each actively decoding image. If you are rendering a massive gallery of 4K images, utilize the JxlHandle widget 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

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;

Modules§

decoder
Core decoding logic for interacting with jxl-rs on background threads.
loaders
Native egui image loaders for automatic UI integration.
source
I/O source abstractions for progressive stream reading.
widget
Custom egui widgets for advanced progressive image control.