Skip to content

Flexible and convenient texture loading #3291

Closed
@emilk

Description

Intro

Showing images in egui is currently very cumbersome. There is egui_extras::RetainedImage, but it requires you to store the RetainedImage image somewhere and is obviously not very immediate mode.

Ideally we want users to be able to write something like:

ui.image("file://image.png");
ui.image("https://www.example.com/imag.png");

We also want 3rd party crates like egui_commonmark to be able to use the same system, without having to implement their own image loading.

Desired features

egui is designed to have minimal dependencies and never doing any system calls, and I'd like to keep it that way. Therefor the solution needs to be some sort of plugin system with callbacks. This is also the best choice for flexibility.

There is three steps to loading an image:

  • Getting the image bytes
    • file://
    • https://
    • From a static list of include_bytes!("my_icon.png")
  • Decoding the image
    • png, jpg, …
    • svg rasterization
  • Uploading it to a texture

In most cases we want the loaded image to be passed to egui::Context::load_texture, which wil hand the image off to whatever rendering backend egui is hooked up to. This will allow the image loaded to work with any egui integration.

We should also allow users to have more exotic ways of loading images. At Rerun we have a data store where we store images, and it would be great if we could just reference images in that store with e.g. ui.image(rerun://data/store/path);.

In some cases however, the user may want to refer to textures that they themselves have uploaded to the GPU, i.e. return a TextureId::User. We do this at Rerun.

Proposal

I propose we introduce three new traits in egui:

  • BytesLoader
  • ImageLoader
  • TextureLoaader

The user can then mix-and-match these as they will.

Users can register them with ctx.add_bytes_loader, ctx.add_image_loader, ctx.add_texture_loader,
and use them with ctx.load_bytes, ctx.load_image, ctx.load_texture.

We will supply good default implementations for these traits in egui_extras. Most users will never need to look at the details of these.

All these traits are designed to be immediate, i.e. callable each frame. It is therefor up to the implementation to cache any results.

They are designed to be able to suppor background loading (e.g. slow downloading of images).
For this they return Pending when something is being loaded. When the loading is done, they are responsible for calling ctx.request_repaint so that the now-loaded image will be shown.

They can also return an error.

Pending will be shown in egui using ui.spinner, and errors with red text.

Common code

enum LoadError {
    /// This loader does not support this protocol or image format.
    ///
    /// Try the next loader instead!
    NotSupported,

    /// A custom error string (e.g. "File not found: foo.png")
    Custom(String),
}

/// Given as a hint. Used mostly for rendering SVG:s to a good size.
///
/// All variants will preserve the original aspect ratio.
///
/// Similar to `usvg::FitTo`.
pub enum SizeHint {
    /// Keep original size.
    Original,

    /// Scale to width.
    Width(u32),

    /// Scale to height.
    Height(u32),

    /// Scale to size.
    Size(u32, u32),
}

BytesLoader

// E.g. from file or from the web
trait BytesLoader {
    /// Try loading the bytes from the given uri.
    ///
    /// When loading is done, call `ctx.request_repaint` to wake up the ui.
    fn load(&self, ctx: &egui::Context, uri: &str) -> Result<BytesPoll, LoadError>;

    /// Allow implementations to evict the cache
    fn end_frame(&self, frame_index: usize) { }
}


enum BytesPoll {
    /// Data is being loaded,
    Pending {
        /// Set if known (e.g. from a HTTP header, or by parsing the image file header).
        size: Option<Vec2u>,
    },

    /// Bytes are loaded.
    Ready {
        /// Set if known (e.g. from a HTTP header, or by parsing the image file header).
        size: Option<Vec2u>,

        /// File contents, e.g. the contents of a `.png`.
        bytes: Arc<u8>,
    },
}

ImageLoader

// Will usually defer to an `Arc<dyn BytesLoader>`, and then decode the result.
trait ImageLoader {
    fn load(
        &self,
        ctx: &egui::Context,
        uri: &str,
        size_hint: SizeHint,
    ) -> Result<TexturePoll, TextureLoadError>;

    /// Allow implementations to evict the cache
    fn end_frame(&self, frame_index: usize) { }
}

trait ImagePoll {
    /// Image is loading.
    Pending {
        /// Set if known (e.g. from parsing the image header).
        size: Option<Vec2u>,
    },

    Ready {
        image: epaint::ColorImage, // Yes, only color images for now. Keep it simple.
    }
}

TextureLoader

// Will usually defer to an `Arc<dyn ImageLoader>`,
// and then just pipe the results to `egui::Context::laod_texture`.
trait TextureLoader {
    fn load(
        &self,
        ctx: &egui::Context,
        uri: &str,
        texture_options: TextureOptions,
        size_hint: FitTo,
    ) -> Result<TexturePoll, TextureLoadError>;

    /// Allow implementations to evict the cache
    fn end_frame(&self, frame_index: usize) { }
}

struct SizedTexture {
    pub id: TextureId,
    pub size: Vec2u,
}

enum TexturePoll {
    /// Texture is loading.
    Pending {
        /// Set if known (e.g. from parsing the image header).
        size: Option<Vec2u>,
    },

    /// Texture is ready.
    Ready(SizedTexture),
}

Common usage

egui_extras::install_texture_loader(ctx);

ui.image("file://foo.png");

Advanced usage:

egui_ctx.add_texture_loader(Box::new(MyTextureLoader::new()));

let texture: Result<TexturePoll, _> = egui_ctx.load_texture(uri);

ui.add(egui::Image::from_uri("file://example.svg").fit_to_width());

Implementation

For version one, let's ignore cache eviction, and lets parse all images inline (on the main thread). We can always improve this in future PRs.

in egui

We just have the bare traits here, plus:

impl Context {
    pub fn add_bytes_loader(&self, loader: Arc<dyn BytesLoader>) {}
    pub fn add_image_loader(&self, loader: Arc<dyn ImageLoader>) {}
    pub fn add_texture_loader(&self, loaded: Arc<dyn TextureLoaader>) {}

    // Uses the above registered loaders:
    pub fn load_bytes(&self, uri: &str) { 
        for loaders in &self.bytes_loaders {
            let result = loader.load(uri);
            if matches!(result, Err(LoadError::NotSupported)) {
                continue;  // try next loader
            } else {
                return result;
            }
        }
    }
    pub fn load_image(&self, uri: &str, size_hint: FitTo) {}
    pub fn load_texture(&self, uri: &str, texture_options: TextureOptions, size_hint: FitTo) {}
}


/// a bytes loader that loads from static sources (`include_bytes`)
struct IncludedBytesLoader {
    HashMap<String, &'static [u8]>,
}

impl DefaultTextureLoader { } 

impl TextureLoaader for DefaultTextureLoader {
    fn load(
        &self,
        ctx: &egui::Context,
        uri: &str,
        texture_options: TextureOptions,
        size_hint: FitTo,
    ) -> Result<TexturePoll, TextureLoadError>
    {
        let img = ctx.load_image(uri, size_hint)?;
        match img {
            ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
            ImagePoll::Ready { image } => Ok(ctx.load_texture(image, texture_options)),
        }
    }    
}

in egui_extras

fn install_texture_loader(ctx: &egui::Context) {
    #[cfg(not(target_os == "wasm32"))]
    ctx.egui_add_bytes_loader(Arc::new(FileLoader::new()));

    #[cfg(feature = "ehttp")]
    ctx.egui_add_bytes_loader(Arc::new(EhttpLoader::new()));

    #[cfg(feature = "image")]
    ctx.add_texture_loader(Arc::new(ImageLoader::new()));

    #[cfg(feature = "svg")]
    ctx.add_texture_loader(Arc::new(SvgLoader::new()));
}

/// Just uses `std::fs::read` directly
#[cfg(not(target_os == "wasm32"))]
struct FileLoader {
    cached: HashMap<String, Arc<[u8]>,
}

/// Uses `ehttp` to fetch images from the web in background threads/tasks.
#[cfg(feature = "ehttp")]
struct EhttpLoader {
    cached: HashMap<String, poll_promise::Promise<Result<Arc<[u8]>>>>
}

#[cfg(feature = "image")]
struct ImageCrateLoader {
    cached: HashMap<String, ColoredImage>, // loaded parsed with `image` crate
}
impl ImageLoader for ImageCrateLoader {}

#[cfg(feature = "svg")]
struct SvgLoader {
    cached: HashMap<(String, SizeHint), ColoredImage>,
}
impl ImageLoader for SvgLoader {}

Implementation notes

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions