Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GIF support #4620

Merged
merged 23 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecdffb913a326b6c642290a0d0ec8e8d6597291acdc07cc4c9cb4b3635d44cf9"

[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"

[[package]]
name = "com"
version = "0.6.0"
Expand Down Expand Up @@ -1733,6 +1739,16 @@ dependencies = [
"wasi",
]

[[package]]
name = "gif"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
dependencies = [
"color_quant",
"weezl",
]

[[package]]
name = "gimli"
version = "0.28.0"
Expand Down Expand Up @@ -2078,6 +2094,8 @@ checksum = "a9b4f005360d32e9325029b38ba47ebd7a56f3316df09249368939562d518645"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif",
"num-traits",
"png",
"zune-core",
Expand Down Expand Up @@ -4242,6 +4260,12 @@ version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc"

[[package]]
name = "weezl"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"

[[package]]
name = "wgpu"
version = "0.19.1"
Expand Down
16 changes: 16 additions & 0 deletions crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,22 @@ macro_rules! include_image {
};
}

/// Include an image in the binary.
///
/// This is a wrapper over `include_bytes!`, and behaves in the same way.
///
/// It produces an [`ImageSource`] which can be used directly in [`Ui::image`] or [`Image::new`]:
///
#[macro_export]
macro_rules! include_gif {
($path:expr $(,)?) => {
$crate::ImageSource::Bytes {
uri: ::std::borrow::Cow::Borrowed(concat!("gif://", $path)),
bytes: $crate::load::Bytes::Static(include_bytes!($path)),
}
};
}

/// Create a [`Hyperlink`] to the current [`file!()`] (and line) on Github
///
/// ```
Expand Down
7 changes: 5 additions & 2 deletions crates/egui/src/widgets/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,11 @@ impl Widget for Button<'_> {
image.show_loading_spinner,
image.image_options(),
);
response =
widgets::image::texture_load_result_response(image.source(), &tlr, response);
response = widgets::image::texture_load_result_response(
&image.source(ui.ctx()),
&tlr,
response,
);
}

if image.is_some() && galley.is_some() {
Expand Down
62 changes: 57 additions & 5 deletions crates/egui/src/widgets/image.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::borrow::Cow;
use std::{borrow::Cow, sync::Arc, time::Duration};

use emath::{Float as _, Rot2};
use epaint::RectShape;
Expand Down Expand Up @@ -288,8 +288,24 @@ impl<'a> Image<'a> {
}

#[inline]
pub fn source(&self) -> &ImageSource<'a> {
&self.source
pub fn source(&'a self, ctx: &Context) -> ImageSource<'a> {
match &self.source {
ImageSource::Bytes { uri, .. } => match uri.starts_with("gif://") {
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
true => Some(uri),
false => None,
},
_ => None,
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
}
.map(|v| format!("{}-{}", v, get_index(ctx, v)))
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
.map(|v| match &self.source {
ImageSource::Uri(_) => ImageSource::Uri(Cow::Owned(v)),
ImageSource::Texture(v) => ImageSource::Texture(*v),
ImageSource::Bytes { bytes, .. } => ImageSource::Bytes {
uri: Cow::Owned(v),
bytes: bytes.clone(),
},
})
.unwrap_or(self.source.clone())
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
}

/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
Expand All @@ -300,7 +316,7 @@ impl<'a> Image<'a> {
/// May fail if they underlying [`Context::try_load_texture`] call fails.
pub fn load_for_size(&self, ctx: &Context, available_size: Vec2) -> TextureLoadResult {
let size_hint = self.size.hint(available_size);
self.source
self.source(ctx)
.clone()
.load(ctx, self.texture_options, size_hint)
}
Expand Down Expand Up @@ -344,7 +360,7 @@ impl<'a> Widget for Image<'a> {
&self.image_options,
);
}
texture_load_result_response(&self.source, &tlr, response)
texture_load_result_response(&self.source(ui.ctx()), &tlr, response)
}
}

Expand Down Expand Up @@ -769,3 +785,39 @@ pub fn paint_texture_at(
}
}
}

fn get_index(ctx: &Context, uri: &str) -> usize {
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
let now = ctx.input(|i| Duration::from_secs_f64(i.time));

let durations: Option<Arc<Vec<Duration>>> =
ctx.data(|data| data.get_temp(ImageDataIdIndex.id(uri)));
if let Some(durations) = durations {
let frames: Duration = durations.iter().sum();
let pos = now.as_millis() % frames.as_millis().max(1);
let mut cumulative_duration = 0;
let mut index = 0;
for (i, duration) in durations.iter().enumerate() {
cumulative_duration += duration.as_millis();
if cumulative_duration >= pos {
index = i;
break;
}
}
if let Some(duration) = durations.get(index) {
ctx.request_repaint_after(*duration);
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
}
index
} else {
0
}
}

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ImageDataIdIndex;
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved

impl ImageDataIdIndex {
#[inline]
pub fn id(self, uri: &str) -> Id {
Id::new((std::any::TypeId::of::<Self>(), self, uri))
}
}
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion crates/egui/src/widgets/image_button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,6 @@ impl<'a> Widget for ImageButton<'a> {
.rect_stroke(rect.expand2(expansion), rounding, stroke);
}

widgets::image::texture_load_result_response(self.image.source(), &tlr, response)
widgets::image::texture_load_result_response(&self.image.source(ui.ctx()), &tlr, response)
}
}
4 changes: 3 additions & 1 deletion crates/egui/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ pub use self::{
checkbox::Checkbox,
drag_value::DragValue,
hyperlink::{Hyperlink, Link},
image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource},
image::{
paint_texture_at, Image, ImageDataIdIndex, ImageFit, ImageOptions, ImageSize, ImageSource,
},
image_button::ImageButton,
label::Label,
progress_bar::ProgressBar,
Expand Down
3 changes: 3 additions & 0 deletions crates/egui_extras/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ http = ["dep:ehttp"]
## ```
image = ["dep:image"]

## Support loading gif images.
gif = ["image", "image/gif"]
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved

## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
Expand Down
9 changes: 8 additions & 1 deletion crates/egui_extras/src/loaders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ pub fn install_image_loaders(ctx: &egui::Context) {
log::trace!("installed ImageCrateLoader");
}

#[cfg(feature = "gif")]
if !ctx.is_loader_installed(self::gif_loader::GifLoader::ID) {
ctx.add_image_loader(std::sync::Arc::new(self::gif_loader::GifLoader::default()));
log::trace!("installed GifLoader");
}

#[cfg(feature = "svg")]
if !ctx.is_loader_installed(self::svg_loader::SvgLoader::ID) {
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
Expand All @@ -101,8 +107,9 @@ mod file_loader;
#[cfg(feature = "http")]
mod ehttp_loader;

#[cfg(feature = "gif")]
mod gif_loader;
#[cfg(feature = "image")]
mod image_loader;

#[cfg(feature = "svg")]
mod svg_loader;
139 changes: 139 additions & 0 deletions crates/egui_extras/src/loaders/gif_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use egui::{
ahash::HashMap,
load::{Bytes, BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
ColorImage, ImageDataIdIndex,
};
use image::AnimationDecoder as _;
use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};

/// Array of Frames and the duration for how long each frame should be shown
#[derive(Debug, Clone)]
pub struct AnimatedImage {
frames: Vec<Arc<ColorImage>>,
delays: Arc<Vec<Duration>>,
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
}

impl AnimatedImage {
pub fn byte_len(&self) -> usize {
size_of::<Self>()
+ self
.frames
.iter()
.map(|image| {
image.pixels.len() * size_of::<egui::Color32>() + size_of::<Duration>()
})
.sum::<usize>()
}

/// Gets image at index
pub fn get_image(&self, index: usize) -> Arc<ColorImage> {
self.frames.get(index % self.frames.len()).cloned().unwrap()
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
}
}
type Entry = Result<Arc<AnimatedImage>, String>;

#[derive(Default)]
pub struct GifLoader {
cache: Mutex<HashMap<String, Entry>>,
}

impl GifLoader {
pub const ID: &'static str = egui::generate_loader_id!(GifLoader);
}

fn is_supported_uri(uri: &str) -> bool {
uri.starts_with("gif://")
}

pub fn gif_to_sources(data: Bytes) -> Result<AnimatedImage, String> {
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
let decoder = image::codecs::gif::GifDecoder::new(Cursor::new(data))
.map_err(|_err| "Couldnt decode gif".to_owned())?;
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
let mut images = vec![];
let mut durations = vec![];
for frame in decoder.into_frames() {
let frame = frame.map_err(|_err| "Couldnt decode gif".to_owned())?;
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
let img = frame.buffer();
let pixels = img.as_flat_samples();

let delay: std::time::Duration = frame.delay().into();
images.push(Arc::new(ColorImage::from_rgba_unmultiplied(
[img.width() as usize, img.height() as usize],
pixels.as_slice(),
)));
durations.push(delay);
}
Ok(AnimatedImage {
frames: images,
delays: Arc::new(durations),
})
}

impl ImageLoader for GifLoader {
fn id(&self) -> &str {
Self::ID
}

fn load(&self, ctx: &egui::Context, uri_data: &str, _: SizeHint) -> ImageLoadResult {
if !is_supported_uri(uri_data) {
return Err(LoadError::NotSupported);
}
let (uri, index) = uri_data
.rsplit_once('-')
.ok_or(LoadError::Loading("No -{index} at end of uri".to_owned()))?;
let index: usize = index
.parse()
.map_err(|_err| LoadError::Loading("Failed to parse index".to_owned()))?;
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(uri).cloned() {
match entry {
Ok(image) => Ok(ImagePoll::Ready {
image: image.get_image(index),
}),
Err(err) => Err(LoadError::Loading(err)),
}
} else {
match ctx.try_load_bytes(uri_data) {
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
Ok(BytesPoll::Ready { bytes, .. }) => {
log::trace!("started loading {uri:?}");
let result = gif_to_sources(bytes).map(Arc::new);
if let Ok(v) = &result {
ctx.data_mut(|data| {
*data.get_temp_mut_or_default(ImageDataIdIndex.id(uri)) =
v.delays.clone();
});
}
log::trace!("finished loading {uri:?}");
cache.insert(uri.into(), result.clone());
match result {
Ok(image) => Ok(ImagePoll::Ready {
image: image.get_image(index),
}),
Err(err) => Err(LoadError::Loading(err)),
}
}
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
Err(err) => Err(err),
}
}
}

fn forget(&self, uri: &str) {
let _ = self.cache.lock().remove(uri);
}

fn forget_all(&self) {
self.cache.lock().clear();
}

fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|v| match v {
Ok(v) => v.byte_len(),
Err(e) => e.len(),
})
.sum()
}
}
2 changes: 1 addition & 1 deletion examples/images/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ eframe = { workspace = true, features = [
"default",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
egui_extras = { workspace = true, features = ["default", "all_loaders"] }
egui_extras = { workspace = true, features = ["default", "all_loaders", "gif"] }
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
env_logger = { version = "0.10", default-features = false, features = [
"auto-color",
"humantime",
Expand Down
Binary file added examples/images/src/ferris.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/images/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| {
ui.image(egui::include_gif!("ferris.gif"));
ui.add(
egui::Image::new("https://picsum.photos/seed/1.759706314/1024").rounding(10.0),
);
Expand Down