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

Animated WebP support #5470

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
17 changes: 17 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2104,12 +2104,23 @@ dependencies = [
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]

[[package]]
name = "image-webp"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
dependencies = [
"byteorder-lite",
"quick-error",
]

[[package]]
name = "images"
version = "0.1.0"
Expand Down Expand Up @@ -3071,6 +3082,12 @@ dependencies = [
"puffin_http",
]

[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"

[[package]]
name = "quick-xml"
version = "0.30.0"
Expand Down
101 changes: 68 additions & 33 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, sync::Arc, time::Duration};
use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};

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

/// Returns the URI of the image.
///
/// For GIFs, returns the URI without the frame number.
/// For animated images, returns the URI without the frame number.
#[inline]
pub fn uri(&self) -> Option<&str> {
let uri = self.source.uri()?;

if let Ok((gif_uri, _index)) = decode_gif_uri(uri) {
if let Ok((gif_uri, _index)) = decode_animated_image_uri(uri) {
Some(gif_uri)
} else {
Some(uri)
Expand All @@ -306,13 +306,15 @@ impl<'a> Image<'a> {
#[inline]
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(uri) if is_animated_image_uri(uri) => {
let frame_uri =
encode_animated_image_uri(uri, animated_image_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, bytes } if are_animated_image_bytes(uri, bytes) => {
let frame_uri =
encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
ctx.include_bytes(uri.clone(), bytes.clone());
ImageSource::Uri(Cow::Owned(frame_uri))
}
Expand Down Expand Up @@ -796,57 +798,90 @@ 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 {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
/// Stores the durations between each frame of an animated image
pub struct FrameDurations(Arc<Vec<Duration>>);

impl FrameDurations {
pub fn new(durations: Vec<Duration>) -> Self {
Self(Arc::new(durations))
}

pub fn all(&self) -> Iter<'_, Duration> {
self.0.iter()
}
}

/// Animated image uris contain the uri & the frame that will be displayed
fn encode_animated_image_uri(uri: &str, frame_index: usize) -> String {
format!("{uri}#{frame_index}")
}

/// extracts uri and 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), String> {
pub fn decode_animated_image_uri(uri: &str) -> Result<(&str, usize), String> {
let (uri, index) = uri
.rsplit_once('#')
.ok_or("Failed to find index separator '#'")?;
let index: usize = index
.parse()
.map_err(|_err| format!("Failed to parse gif frame index: {index:?} is not an integer"))?;
let index: usize = index.parse().map_err(|_err| {
format!("Failed to parse animated image frame index: {index:?} is not an integer")
})?;
Ok((uri, index))
}

/// checks if uri is a gif file
fn is_gif_uri(uri: &str) -> bool {
uri.ends_with(".gif") || uri.contains(".gif#")
}

/// checks if bytes are gifs
pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
}
/// Calculates at which frame the animated image is
fn animated_image_frame_index(ctx: &Context, uri: &str) -> usize {
let now = ctx.input(|input| Duration::from_secs_f64(input.time));

/// 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<FrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));

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 frames: Duration = durations.all().sum();
let pos_ms = now.as_millis() % frames.as_millis().max(1);

let mut cumulative_ms = 0;
for (i, duration) in durations.0.iter().enumerate() {

for (index, duration) in durations.all().enumerate() {
cumulative_ms += duration.as_millis();

if pos_ms < cumulative_ms {
let ms_until_next_frame = cumulative_ms - pos_ms;
ctx.request_repaint_after(Duration::from_millis(ms_until_next_frame as u64));
return i;
return index;
}
}

0
} 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>>);
/// Checks if uri is a gif file
fn is_gif_uri(uri: &str) -> bool {
uri.ends_with(".gif") || uri.contains(".gif#")
}

/// Checks if bytes are gifs
pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
}

/// Checks if uri is a webp file
fn is_webp_uri(uri: &str) -> bool {
uri.ends_with(".webp") || uri.contains(".webp#")
}

/// Checks if bytes are webp
pub fn has_webp_header(bytes: &[u8]) -> bool {
bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP"
}

fn is_animated_image_uri(uri: &str) -> bool {
is_gif_uri(uri) || is_webp_uri(uri)
}

fn are_animated_image_bytes(uri: &str, bytes: &[u8]) -> bool {
(is_gif_uri(uri) && has_gif_magic_header(bytes)) || (is_webp_uri(uri) && has_webp_header(bytes))
}
emilk marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions crates/egui/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ pub use self::{
drag_value::DragValue,
hyperlink::{Hyperlink, Link},
image::{
decode_gif_uri, has_gif_magic_header, paint_texture_at, GifFrameDurations, Image, ImageFit,
ImageOptions, ImageSize, ImageSource,
decode_animated_image_uri, has_gif_magic_header, has_webp_header, paint_texture_at,
FrameDurations, Image, ImageFit, ImageOptions, ImageSize, ImageSource,
},
image_button::ImageButton,
label::Label,
Expand Down
5 changes: 4 additions & 1 deletion crates/egui_extras/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ rustdoc-args = ["--generate-link-to-definition"]
default = ["dep:mime_guess2"]

## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`).
all_loaders = ["file", "http", "image", "svg", "gif"]
all_loaders = ["file", "http", "image", "svg", "gif", "webp"]

## Enable [`DatePickerButton`] widget.
datepicker = ["chrono"]
Expand All @@ -42,6 +42,9 @@ file = ["dep:mime_guess2"]
## Support loading gif images.
gif = ["image", "image/gif"]

## Support loading webp images.
webp = ["image", "image/webp"]

## Add support for loading images via HTTP.
http = ["dep:ehttp"]

Expand Down
8 changes: 8 additions & 0 deletions crates/egui_extras/src/loaders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ pub fn install_image_loaders(ctx: &egui::Context) {
log::trace!("installed GifLoader");
}

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

#[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 Down Expand Up @@ -113,3 +119,5 @@ mod gif_loader;
mod image_loader;
#[cfg(feature = "svg")]
mod svg_loader;
#[cfg(feature = "webp")]
mod webp_loader;
10 changes: 5 additions & 5 deletions crates/egui_extras/src/loaders/gif_loader.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use ahash::HashMap;
use egui::{
decode_gif_uri, has_gif_magic_header,
decode_animated_image_uri, has_gif_magic_header,
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
ColorImage, GifFrameDurations, Id,
ColorImage, FrameDurations, Id,
};
use image::AnimationDecoder as _;
use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
Expand All @@ -12,7 +12,7 @@ use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
#[derive(Debug, Clone)]
pub struct AnimatedImage {
frames: Vec<Arc<ColorImage>>,
frame_durations: GifFrameDurations,
frame_durations: FrameDurations,
}

impl AnimatedImage {
Expand All @@ -35,7 +35,7 @@ impl AnimatedImage {
}
Ok(Self {
frames: images,
frame_durations: GifFrameDurations(Arc::new(durations)),
frame_durations: FrameDurations::new(durations),
})
}
}
Expand Down Expand Up @@ -75,7 +75,7 @@ impl ImageLoader for GifLoader {

fn load(&self, ctx: &egui::Context, frame_uri: &str, _: SizeHint) -> ImageLoadResult {
let (image_uri, frame_index) =
decode_gif_uri(frame_uri).map_err(|_err| LoadError::NotSupported)?;
decode_animated_image_uri(frame_uri).map_err(|_err| LoadError::NotSupported)?;
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(image_uri).cloned() {
match entry {
Expand Down
Loading
Loading