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 12 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
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
84 changes: 79 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 All @@ -8,6 +8,8 @@ use crate::{
*,
};

const RENDER_TIME: Duration = Duration::from_millis(6);

/// A widget which displays an image.
///
/// The task of actually loading the image is deferred to when the `Image` is added to the [`Ui`],
Expand Down Expand Up @@ -40,6 +42,7 @@ use crate::{
/// .paint_at(ui, rect);
/// # });
/// ```
///
#[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 @@ -288,8 +291,23 @@ 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::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(),
}
}

/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
Expand All @@ -300,7 +318,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 +362,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 +787,59 @@ pub fn paint_texture_at(
}
}
}

/// 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
/// # Errors
/// Will return `Err` if `uri` does not match pattern {uri}-{frame_index}
pub fn decode_gif_uri(uri: &str) -> Result<(&str, usize), &'static str> {
let (uri, index) = uri
.rsplit_once('-')
.ok_or("Failed to find index seperator '-'")?;
let index: usize = index.parse().map_err(|_err| "Failed to parse index")?;
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<GifFrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));
if let Some(durations) = durations {
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.0.iter().enumerate() {
cumulative_duration += duration.as_millis();
if cumulative_duration >= pos {
index = i;
break;
}
}
if let Some(duration) = durations.0.get(index) {
ctx.request_repaint_after(*duration - RENDER_TIME);
}
index
JustFrederik marked this conversation as resolved.
Show resolved Hide resolved
} else {
0
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
/// Stores the durations between each frame of a gif
pub struct GifFrameDurations(pub Arc<Vec<Duration>>);
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)
}
}
5 changes: 4 additions & 1 deletion crates/egui/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ pub use self::{
checkbox::Checkbox,
drag_value::DragValue,
hyperlink::{Hyperlink, Link},
image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource},
image::{
decode_gif_uri, paint_texture_at, GifFrameDurations, Image, 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;
9 changes: 9 additions & 0 deletions crates/egui_extras/src/loaders/ehttp_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ impl BytesLoader for EhttpLoader {
return Err(LoadError::NotSupported);
}

#[cfg(feature = "gif")]
let uri = match uri.rsplit_once('-').map(|v| v.0) {
Some(base_uri) => match base_uri.ends_with(".gif") {
true => base_uri,
false => uri,
},
None => uri,
};

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 {
Expand Down
9 changes: 9 additions & 0 deletions crates/egui_extras/src/loaders/file_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ impl BytesLoader for FileLoader {
return Err(LoadError::NotSupported);
};

#[cfg(feature = "gif")]
let uri = match uri.rsplit_once('-').map(|v| v.0) {
Some(base_uri) => match base_uri.ends_with(".gif") {
true => base_uri,
false => uri,
},
None => uri,
};

let mut cache = self.cache.lock();
if let Some(entry) = cache.get(uri).cloned() {
// `path` has either begun loading, is loaded, or has failed to load.
Expand Down
Loading