1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 06:10:06 -04:00

Animated WebP support (#5470)

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 <emil.ernerfeldt@gmail.com>
This commit is contained in:
Aely
2024-12-29 12:46:08 +02:00
committed by GitHub
parent 01a7e31b13
commit 1e0f3a5e2d
10 changed files with 303 additions and 49 deletions

View File

@@ -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;
@@ -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)
@@ -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))
}
@@ -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#")
}
/// 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));
/// checks if bytes are gifs
pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
}
let durations: Option<FrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));
/// 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 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)
}

View File

@@ -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,