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:
parent
01a7e31b13
commit
1e0f3a5e2d
17
Cargo.lock
17
Cargo.lock
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
@ -113,3 +119,5 @@ mod gif_loader;
|
|||
mod image_loader;
|
||||
#[cfg(feature = "svg")]
|
||||
mod svg_loader;
|
||||
#[cfg(feature = "webp")]
|
||||
mod webp_loader;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
@ -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 {
|
||||
|
|
@ -35,7 +35,7 @@ impl AnimatedImage {
|
|||
}
|
||||
Ok(Self {
|
||||
frames: images,
|
||||
frame_durations: GifFrameDurations(Arc::new(durations)),
|
||||
frame_durations: FrameDurations::new(durations),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
use ahash::HashMap;
|
||||
use egui::{
|
||||
decode_animated_image_uri, has_webp_header,
|
||||
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
|
||||
mutex::Mutex,
|
||||
ColorImage, FrameDurations, Id,
|
||||
};
|
||||
use image::{codecs::webp::WebPDecoder, AnimationDecoder as _, ImageDecoder, Rgba};
|
||||
use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum WebP {
|
||||
Static(Arc<ColorImage>),
|
||||
Animated(AnimatedImage),
|
||||
}
|
||||
|
||||
impl WebP {
|
||||
fn load(data: &[u8]) -> Result<Self, String> {
|
||||
let mut decoder = WebPDecoder::new(Cursor::new(data))
|
||||
.map_err(|error| format!("WebP decode failure ({error})"))?;
|
||||
|
||||
if decoder.has_animation() {
|
||||
decoder
|
||||
.set_background_color(Rgba([0, 0, 0, 0]))
|
||||
.map_err(|error| {
|
||||
format!("Failure to set default background color for animated WebP ({error})")
|
||||
})?;
|
||||
|
||||
let mut images = vec![];
|
||||
let mut durations = vec![];
|
||||
|
||||
for frame in decoder.into_frames() {
|
||||
let frame =
|
||||
frame.map_err(|error| format!("WebP frame decode failure ({error})"))?;
|
||||
let image = frame.buffer();
|
||||
let pixels = image.as_flat_samples();
|
||||
|
||||
images.push(Arc::new(ColorImage::from_rgba_unmultiplied(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
pixels.as_slice(),
|
||||
)));
|
||||
|
||||
let delay: Duration = frame.delay().into();
|
||||
durations.push(delay);
|
||||
}
|
||||
Ok(Self::Animated(AnimatedImage {
|
||||
frames: images,
|
||||
frame_durations: FrameDurations::new(durations),
|
||||
}))
|
||||
} else {
|
||||
let (width, height) = decoder.dimensions();
|
||||
let size = decoder.total_bytes() as usize;
|
||||
|
||||
let mut data = vec![0; size];
|
||||
decoder
|
||||
.read_image(&mut data)
|
||||
.map_err(|error| format!("WebP image read failure ({error})"))?;
|
||||
|
||||
let image =
|
||||
ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &data);
|
||||
|
||||
Ok(Self::Static(Arc::new(image)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_image(&self, frame_index: usize) -> Arc<ColorImage> {
|
||||
match self {
|
||||
Self::Static(image) => image.clone(),
|
||||
Self::Animated(animation) => animation.get_image_by_index(frame_index),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn byte_len(&self) -> usize {
|
||||
size_of::<Self>()
|
||||
+ match self {
|
||||
Self::Static(image) => image.pixels.len() * size_of::<egui::Color32>(),
|
||||
Self::Animated(animation) => animation.byte_len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnimatedImage {
|
||||
frames: Vec<Arc<ColorImage>>,
|
||||
frame_durations: FrameDurations,
|
||||
}
|
||||
|
||||
impl AnimatedImage {
|
||||
pub fn byte_len(&self) -> usize {
|
||||
size_of::<Self>()
|
||||
+ self
|
||||
.frames
|
||||
.iter()
|
||||
.map(|image| {
|
||||
image.pixels.len() * size_of::<egui::Color32>() + size_of::<Duration>()
|
||||
})
|
||||
.sum::<usize>()
|
||||
}
|
||||
|
||||
pub fn get_image_by_index(&self, index: usize) -> Arc<ColorImage> {
|
||||
self.frames[index % self.frames.len()].clone()
|
||||
}
|
||||
}
|
||||
|
||||
type Entry = Result<WebP, String>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WebPLoader {
|
||||
cache: Mutex<HashMap<String, Entry>>,
|
||||
}
|
||||
|
||||
impl WebPLoader {
|
||||
pub const ID: &'static str = egui::generate_loader_id!(WebPLoader);
|
||||
}
|
||||
|
||||
impl ImageLoader for WebPLoader {
|
||||
fn id(&self) -> &str {
|
||||
Self::ID
|
||||
}
|
||||
|
||||
fn load(&self, ctx: &egui::Context, frame_uri: &str, _: SizeHint) -> ImageLoadResult {
|
||||
let (image_uri, frame_index) =
|
||||
decode_animated_image_uri(frame_uri).map_err(|_error| LoadError::NotSupported)?;
|
||||
|
||||
let mut cache = self.cache.lock();
|
||||
if let Some(entry) = cache.get(image_uri).cloned() {
|
||||
match entry {
|
||||
Ok(image) => Ok(ImagePoll::Ready {
|
||||
image: image.get_image(frame_index),
|
||||
}),
|
||||
Err(error) => Err(LoadError::Loading(error)),
|
||||
}
|
||||
} else {
|
||||
match ctx.try_load_bytes(image_uri) {
|
||||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
||||
if !has_webp_header(&bytes) {
|
||||
return Err(LoadError::NotSupported);
|
||||
}
|
||||
|
||||
log::trace!("started loading {image_uri:?}");
|
||||
|
||||
let result = WebP::load(&bytes);
|
||||
|
||||
if let Ok(WebP::Animated(animated_image)) = &result {
|
||||
ctx.data_mut(|data| {
|
||||
*data.get_temp_mut_or_default(Id::new(image_uri)) =
|
||||
animated_image.frame_durations.clone();
|
||||
});
|
||||
}
|
||||
|
||||
log::trace!("finished loading {image_uri:?}");
|
||||
|
||||
cache.insert(image_uri.into(), result.clone());
|
||||
|
||||
match result {
|
||||
Ok(image) => Ok(ImagePoll::Ready {
|
||||
image: image.get_image(frame_index),
|
||||
}),
|
||||
Err(error) => Err(LoadError::Loading(error)),
|
||||
}
|
||||
}
|
||||
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|entry| match entry {
|
||||
Ok(entry_value) => entry_value.byte_len(),
|
||||
Err(error) => error.len(),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:12eb9463cda6c2b1a160f085324f1afdfc5ced9ff0857df117030d8771259e5e
|
||||
size 303453
|
||||
oid sha256:a836741d52e1972b2047cefaabf59f601637d430d4b41bf6407ebda4f7931dac
|
||||
size 273450
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 480 KiB |
|
|
@ -6,7 +6,7 @@ use eframe::egui;
|
|||
fn main() -> eframe::Result {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([400.0, 800.0]),
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 880.0]),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
|
|
@ -27,11 +27,16 @@ impl eframe::App for MyApp {
|
|||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
ui.image(egui::include_image!("ferris.gif"));
|
||||
ui.add(
|
||||
egui::Image::new("https://picsum.photos/seed/1.759706314/1024").rounding(10.0),
|
||||
);
|
||||
ui.image(egui::include_image!("ferris.svg"));
|
||||
ui.image(egui::include_image!("cat.webp"))
|
||||
.on_hover_text_at_pointer("WebP");
|
||||
ui.image(egui::include_image!("ferris.gif"))
|
||||
.on_hover_text_at_pointer("Gif");
|
||||
ui.image(egui::include_image!("ferris.svg"))
|
||||
.on_hover_text_at_pointer("Svg");
|
||||
|
||||
let url = "https://picsum.photos/seed/1.759706314/1024";
|
||||
ui.add(egui::Image::new(url).rounding(10.0))
|
||||
.on_hover_text_at_pointer(url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue