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 1 commit
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
83 changes: 52 additions & 31 deletions crates/egui/src/widgets/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ use crate::{
/// .paint_at(ui, rect);
/// # });
/// ```
///
const RENDER_TIME: Duration = Duration::from_millis(6);
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug, Clone)]
pub struct Image<'a> {
Expand Down Expand Up @@ -290,22 +292,21 @@ impl<'a> Image<'a> {
#[inline]
pub fn source(&'a self, ctx: &Context) -> ImageSource<'a> {
match &self.source {
ImageSource::Bytes { uri, .. } => match uri.starts_with("gif://") {
true => Some(uri),
false => None,
},
_ => None,
ImageSource::Uri(uri) if is_gif_uri(uri) => {
let frame_uri = encode_gif_uri(uri, gif_frame_index(ctx, uri));
ImageSource::Uri(Cow::Owned(frame_uri))
}

ImageSource::Bytes { uri, bytes } if is_gif_uri(uri) || has_gif_magic_header(bytes) => {
let frame_uri = encode_gif_uri(uri, gif_frame_index(ctx, uri));
ImageSource::Bytes {
uri: Cow::Owned(frame_uri),
bytes: bytes.clone(),
}
}

_ => self.source.clone(),
}
.map(|v| format!("{}-{}", v, get_index(ctx, v)))
.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())
}

/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
Expand Down Expand Up @@ -786,38 +787,58 @@ pub fn paint_texture_at(
}
}

fn get_index(ctx: &Context, uri: &str) -> usize {
/// gif uris contain the uri & the frame that will be displayed
fn encode_gif_uri(uri: &str, frame_index: usize) -> String {
format!("{uri}-{frame_index}")
}

/// extracts uri and frame index
pub fn decode_gif_uri(uri: &str) -> Result<(&str, usize), String> {
let (uri, index) = uri
.rsplit_once('-')
.ok_or("Failed to find index seperator '-'")?;
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
let index: usize = index
.parse()
.map_err(|_err| "Failed to parse index".to_string())?;
Ok((uri, index))
}

/// checks if uri is a gif file or starts with gif://
fn is_gif_uri(uri: &str) -> bool {
uri.ends_with(".gif") || uri.starts_with("gif://")
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
}

/// checks if bytes are gifs
fn has_gif_magic_header(bytes: &Bytes) -> bool {
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
}

/// calculates at which frame the gif is
fn gif_frame_index(ctx: &Context, uri: &str) -> usize {
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)));
let durations: Option<GifFrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));
if let Some(durations) = durations {
let frames: Duration = durations.iter().sum();
let frames: Duration = durations.0.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() {
for (i, duration) in durations.0.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);
if let Some(duration) = durations.0.get(index) {
ctx.request_repaint_after(*duration - RENDER_TIME);
}
index
} else {
0
}
}

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ImageDataIdIndex;

impl ImageDataIdIndex {
#[inline]
pub fn id(self, uri: &str) -> Id {
Id::new((std::any::TypeId::of::<Self>(), self, uri))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
/// Stores the durations between each frame of a gif
pub struct GifFrameDurations(pub Arc<Vec<Duration>>);
3 changes: 2 additions & 1 deletion crates/egui/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pub use self::{
drag_value::DragValue,
hyperlink::{Hyperlink, Link},
image::{
paint_texture_at, Image, ImageDataIdIndex, ImageFit, ImageOptions, ImageSize, ImageSource,
decode_gif_uri, paint_texture_at, GifFrameDurations, Image, ImageFit, ImageOptions,
ImageSize, ImageSource,
},
image_button::ImageButton,
label::Label,
Expand Down
33 changes: 14 additions & 19 deletions crates/egui_extras/src/loaders/gif_loader.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use egui::{
ahash::HashMap,
decode_gif_uri,
load::{Bytes, BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
ColorImage, ImageDataIdIndex,
ColorImage, GifFrameDurations, Id,
};
use image::AnimationDecoder as _;
use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
Expand All @@ -11,7 +12,7 @@ use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
#[derive(Debug, Clone)]
pub struct AnimatedImage {
frames: Vec<Arc<ColorImage>>,
delays: Arc<Vec<Duration>>,
frame_durations: GifFrameDurations,
}

impl AnimatedImage {
Expand All @@ -28,7 +29,7 @@ impl AnimatedImage {

/// Gets image at index
pub fn get_image(&self, index: usize) -> Arc<ColorImage> {
self.frames.get(index % self.frames.len()).cloned().unwrap()
self.frames[index % self.frames.len()].clone()
}
}
type Entry = Result<Arc<AnimatedImage>, String>;
Expand All @@ -42,10 +43,6 @@ 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
Expand All @@ -65,7 +62,7 @@ pub fn gif_to_sources(data: Bytes) -> Result<AnimatedImage, String> {
}
Ok(AnimatedImage {
frames: images,
delays: Arc::new(durations),
frame_durations: GifFrameDurations(Arc::new(durations)),
})
}

Expand All @@ -75,17 +72,11 @@ impl ImageLoader for GifLoader {
}

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()))?;
let uri_index = decode_gif_uri(uri_data).map_err(LoadError::Loading);
let uri = uri_index.as_ref().map(|v| v.0).unwrap_or(uri_data);
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(uri).cloned() {
let index = uri_index?.1;
match entry {
Ok(image) => Ok(ImagePoll::Ready {
image: image.get_image(index),
Expand All @@ -95,12 +86,16 @@ impl ImageLoader for GifLoader {
} else {
match ctx.try_load_bytes(uri_data) {
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
Ok(BytesPoll::Ready { bytes, .. }) => {
let is_gif = bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a");
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
if !is_gif {
return Err(LoadError::NotSupported);
}
let index = uri_index?.1;
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
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();
*data.get_temp_mut_or_default(Id::new(uri)) = v.frame_durations.clone()
});
}
log::trace!("finished loading {uri:?}");
Expand Down