Skip to content

Commit

Permalink
Animated WebP support (#5470)
Browse files Browse the repository at this point in the history
Adds support for animated WebP images. Used the already existing GIF
implementation as a template for most of it.

* [x] I have followed the instructions in the PR template

---------

Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
Aely0 and emilk authored Dec 29, 2024
1 parent 01a7e31 commit 1e0f3a5
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 49 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2202,12 +2202,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 @@ -3165,6 +3176,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(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(bytes: &[u8]) -> bool {
has_gif_magic_header(bytes) || has_webp_header(bytes)
}
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

0 comments on commit 1e0f3a5

Please sign in to comment.