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",
|
"byteorder-lite",
|
||||||
"color_quant",
|
"color_quant",
|
||||||
"gif",
|
"gif",
|
||||||
|
"image-webp",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"png",
|
"png",
|
||||||
"zune-core",
|
"zune-core",
|
||||||
"zune-jpeg",
|
"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]]
|
[[package]]
|
||||||
name = "images"
|
name = "images"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -3165,6 +3176,12 @@ dependencies = [
|
||||||
"puffin_http",
|
"puffin_http",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-error"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.30.0"
|
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 emath::{Float as _, Rot2};
|
||||||
use epaint::RectShape;
|
use epaint::RectShape;
|
||||||
|
|
@ -286,12 +286,12 @@ impl<'a> Image<'a> {
|
||||||
|
|
||||||
/// Returns the URI of the image.
|
/// 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]
|
#[inline]
|
||||||
pub fn uri(&self) -> Option<&str> {
|
pub fn uri(&self) -> Option<&str> {
|
||||||
let uri = self.source.uri()?;
|
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)
|
Some(gif_uri)
|
||||||
} else {
|
} else {
|
||||||
Some(uri)
|
Some(uri)
|
||||||
|
|
@ -306,13 +306,15 @@ impl<'a> Image<'a> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn source(&'a self, ctx: &Context) -> ImageSource<'a> {
|
pub fn source(&'a self, ctx: &Context) -> ImageSource<'a> {
|
||||||
match &self.source {
|
match &self.source {
|
||||||
ImageSource::Uri(uri) if is_gif_uri(uri) => {
|
ImageSource::Uri(uri) if is_animated_image_uri(uri) => {
|
||||||
let frame_uri = encode_gif_uri(uri, gif_frame_index(ctx, uri));
|
let frame_uri =
|
||||||
|
encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
|
||||||
ImageSource::Uri(Cow::Owned(frame_uri))
|
ImageSource::Uri(Cow::Owned(frame_uri))
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageSource::Bytes { uri, bytes } if is_gif_uri(uri) || has_gif_magic_header(bytes) => {
|
ImageSource::Bytes { uri, bytes } if are_animated_image_bytes(bytes) => {
|
||||||
let frame_uri = encode_gif_uri(uri, gif_frame_index(ctx, uri));
|
let frame_uri =
|
||||||
|
encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
|
||||||
ctx.include_bytes(uri.clone(), bytes.clone());
|
ctx.include_bytes(uri.clone(), bytes.clone());
|
||||||
ImageSource::Uri(Cow::Owned(frame_uri))
|
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
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||||
fn encode_gif_uri(uri: &str, frame_index: usize) -> String {
|
/// 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}")
|
format!("{uri}#{frame_index}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// extracts uri and frame index
|
/// Extracts uri and frame index
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Will return `Err` if `uri` does not match pattern {uri}-{frame_index}
|
/// 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
|
let (uri, index) = uri
|
||||||
.rsplit_once('#')
|
.rsplit_once('#')
|
||||||
.ok_or("Failed to find index separator '#'")?;
|
.ok_or("Failed to find index separator '#'")?;
|
||||||
let index: usize = index
|
let index: usize = index.parse().map_err(|_err| {
|
||||||
.parse()
|
format!("Failed to parse animated image frame index: {index:?} is not an integer")
|
||||||
.map_err(|_err| format!("Failed to parse gif frame index: {index:?} is not an integer"))?;
|
})?;
|
||||||
Ok((uri, index))
|
Ok((uri, index))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// checks if uri is a gif file
|
/// Calculates at which frame the animated image is
|
||||||
fn is_gif_uri(uri: &str) -> bool {
|
fn animated_image_frame_index(ctx: &Context, uri: &str) -> usize {
|
||||||
uri.ends_with(".gif") || uri.contains(".gif#")
|
let now = ctx.input(|input| Duration::from_secs_f64(input.time));
|
||||||
}
|
|
||||||
|
|
||||||
/// checks if bytes are gifs
|
let durations: Option<FrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));
|
||||||
pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
|
|
||||||
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 {
|
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 pos_ms = now.as_millis() % frames.as_millis().max(1);
|
||||||
|
|
||||||
let mut cumulative_ms = 0;
|
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();
|
cumulative_ms += duration.as_millis();
|
||||||
|
|
||||||
if pos_ms < cumulative_ms {
|
if pos_ms < cumulative_ms {
|
||||||
let ms_until_next_frame = cumulative_ms - pos_ms;
|
let ms_until_next_frame = cumulative_ms - pos_ms;
|
||||||
ctx.request_repaint_after(Duration::from_millis(ms_until_next_frame as u64));
|
ctx.request_repaint_after(Duration::from_millis(ms_until_next_frame as u64));
|
||||||
return i;
|
return index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
/// Checks if uri is a gif file
|
||||||
/// Stores the durations between each frame of a gif
|
fn is_gif_uri(uri: &str) -> bool {
|
||||||
pub struct GifFrameDurations(pub Arc<Vec<Duration>>);
|
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,
|
drag_value::DragValue,
|
||||||
hyperlink::{Hyperlink, Link},
|
hyperlink::{Hyperlink, Link},
|
||||||
image::{
|
image::{
|
||||||
decode_gif_uri, has_gif_magic_header, paint_texture_at, GifFrameDurations, Image, ImageFit,
|
decode_animated_image_uri, has_gif_magic_header, has_webp_header, paint_texture_at,
|
||||||
ImageOptions, ImageSize, ImageSource,
|
FrameDurations, Image, ImageFit, ImageOptions, ImageSize, ImageSource,
|
||||||
},
|
},
|
||||||
image_button::ImageButton,
|
image_button::ImageButton,
|
||||||
label::Label,
|
label::Label,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||||
default = ["dep:mime_guess2"]
|
default = ["dep:mime_guess2"]
|
||||||
|
|
||||||
## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`).
|
## 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.
|
## Enable [`DatePickerButton`] widget.
|
||||||
datepicker = ["chrono"]
|
datepicker = ["chrono"]
|
||||||
|
|
@ -42,6 +42,9 @@ file = ["dep:mime_guess2"]
|
||||||
## Support loading gif images.
|
## Support loading gif images.
|
||||||
gif = ["image", "image/gif"]
|
gif = ["image", "image/gif"]
|
||||||
|
|
||||||
|
## Support loading webp images.
|
||||||
|
webp = ["image", "image/webp"]
|
||||||
|
|
||||||
## Add support for loading images via HTTP.
|
## Add support for loading images via HTTP.
|
||||||
http = ["dep:ehttp"]
|
http = ["dep:ehttp"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,12 @@ pub fn install_image_loaders(ctx: &egui::Context) {
|
||||||
log::trace!("installed GifLoader");
|
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")]
|
#[cfg(feature = "svg")]
|
||||||
if !ctx.is_loader_installed(self::svg_loader::SvgLoader::ID) {
|
if !ctx.is_loader_installed(self::svg_loader::SvgLoader::ID) {
|
||||||
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
|
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
|
||||||
|
|
@ -113,3 +119,5 @@ mod gif_loader;
|
||||||
mod image_loader;
|
mod image_loader;
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
mod svg_loader;
|
mod svg_loader;
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
mod webp_loader;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use ahash::HashMap;
|
use ahash::HashMap;
|
||||||
use egui::{
|
use egui::{
|
||||||
decode_gif_uri, has_gif_magic_header,
|
decode_animated_image_uri, has_gif_magic_header,
|
||||||
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
|
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
|
||||||
mutex::Mutex,
|
mutex::Mutex,
|
||||||
ColorImage, GifFrameDurations, Id,
|
ColorImage, FrameDurations, Id,
|
||||||
};
|
};
|
||||||
use image::AnimationDecoder as _;
|
use image::AnimationDecoder as _;
|
||||||
use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AnimatedImage {
|
pub struct AnimatedImage {
|
||||||
frames: Vec<Arc<ColorImage>>,
|
frames: Vec<Arc<ColorImage>>,
|
||||||
frame_durations: GifFrameDurations,
|
frame_durations: FrameDurations,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnimatedImage {
|
impl AnimatedImage {
|
||||||
|
|
@ -35,7 +35,7 @@ impl AnimatedImage {
|
||||||
}
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
frames: images,
|
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 {
|
fn load(&self, ctx: &egui::Context, frame_uri: &str, _: SizeHint) -> ImageLoadResult {
|
||||||
let (image_uri, frame_index) =
|
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();
|
let mut cache = self.cache.lock();
|
||||||
if let Some(entry) = cache.get(image_uri).cloned() {
|
if let Some(entry) = cache.get(image_uri).cloned() {
|
||||||
match entry {
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:12eb9463cda6c2b1a160f085324f1afdfc5ced9ff0857df117030d8771259e5e
|
oid sha256:a836741d52e1972b2047cefaabf59f601637d430d4b41bf6407ebda4f7931dac
|
||||||
size 303453
|
size 273450
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 480 KiB |
|
|
@ -6,7 +6,7 @@ use eframe::egui;
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||||
let options = eframe::NativeOptions {
|
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()
|
..Default::default()
|
||||||
};
|
};
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
|
|
@ -27,11 +27,16 @@ impl eframe::App for MyApp {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
egui::ScrollArea::both().show(ui, |ui| {
|
egui::ScrollArea::both().show(ui, |ui| {
|
||||||
ui.image(egui::include_image!("ferris.gif"));
|
ui.image(egui::include_image!("cat.webp"))
|
||||||
ui.add(
|
.on_hover_text_at_pointer("WebP");
|
||||||
egui::Image::new("https://picsum.photos/seed/1.759706314/1024").rounding(10.0),
|
ui.image(egui::include_image!("ferris.gif"))
|
||||||
);
|
.on_hover_text_at_pointer("Gif");
|
||||||
ui.image(egui::include_image!("ferris.svg"));
|
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