use std::{
borrow::Cow,
io::{BufRead, Read, Seek, SeekFrom},
pin::Pin,
sync::{Arc, Mutex, OnceLock},
};
use image_webp::{DecodingError, WebPDecoder};
use ribir_algo::Resource;
use ribir_geom::DeviceSize;
use ribir_painter::{ColorFormat, PixelImage};
use crate::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LoopCount {
#[default]
Infinite,
Finite(u32),
}
#[derive(Clone)]
pub struct DecodedFrame {
pub image: Resource<PixelImage>,
pub delay_ms: u32,
}
#[derive(Clone)]
pub struct Image(Arc<ImageInner>);
pub struct FrameIterator<'a> {
image: &'a Image,
index: usize,
}
impl Image {
pub fn new(raw: impl Into<Cow<'static, [u8]>>) -> Result<Self, DecodingError> {
let raw = StableData::from_cow(raw.into());
let decoder = WebPDecoder::new(std::io::Cursor::new(raw.as_slice()))?;
let (width, height) = decoder.dimensions();
let is_animated = decoder.is_animated();
let frame_count = if is_animated { decoder.num_frames() as usize } else { 1 };
let loop_count = match decoder.loop_count() {
image_webp::LoopCount::Forever => LoopCount::Infinite,
image_webp::LoopCount::Times(n) => LoopCount::Finite(n.get() as u32),
};
Ok(Self(Arc::new(ImageInner {
decoder_state: Mutex::new(DecoderState::new()),
raw,
width,
height,
loop_count,
is_animated,
frame_cache: new_frame_cache(frame_count),
})))
}
pub fn from_parts(
raw: impl Into<Cow<'static, [u8]>>, width: u32, height: u32, loop_count: LoopCount,
frames: Vec<DecodedFrame>,
) -> Self {
let frame_count = frames.len();
let frame_cache: Box<[_]> = frames
.into_iter()
.map(|f| {
let lock = OnceLock::new();
let _ = lock.set(f);
lock
})
.collect();
Self(Arc::new(ImageInner {
decoder_state: Mutex::new(DecoderState::with_decoded(frame_count)),
raw: StableData::from_cow(raw.into()),
width,
height,
loop_count,
is_animated: frame_count > 1,
frame_cache,
}))
}
#[inline]
pub fn size(&self) -> DeviceSize { DeviceSize::new(self.0.width as i32, self.0.height as i32) }
#[inline]
pub fn width(&self) -> u32 { self.0.width }
#[inline]
pub fn height(&self) -> u32 { self.0.height }
#[inline]
pub fn frame_count(&self) -> u32 { self.0.frame_cache.len() as u32 }
#[inline]
pub fn is_animated(&self) -> bool { self.0.is_animated }
#[inline]
pub fn loop_count(&self) -> LoopCount { self.0.loop_count }
pub fn total_duration_ms(&self) -> u64 {
if !self.0.frame_cache.is_empty() {
self.ensure_decoded_up_to(self.0.frame_cache.len() - 1);
}
self
.0
.frame_cache
.iter()
.filter_map(|l| l.get())
.map(|f| f.delay_ms as u64)
.sum()
}
pub fn frame(&self, index: usize) -> Option<DecodedFrame> {
if index >= self.0.frame_cache.len() {
return None;
}
if let Some(frame) = self.0.frame_cache[index].get() {
return Some(frame.clone());
}
self.ensure_decoded_up_to(index);
self.0.frame_cache[index].get().cloned()
}
#[inline]
pub fn first_frame(&self) -> DecodedFrame { self.frame(0).expect("Image has no frames") }
#[inline]
pub fn frame_iter(&self) -> FrameIterator<'_> { FrameIterator { image: self, index: 0 } }
#[inline]
pub fn global_frame_count(&self) -> Option<usize> {
match self.0.loop_count {
LoopCount::Infinite => None,
LoopCount::Finite(n) => Some(self.0.frame_cache.len() * n as usize),
}
}
#[inline]
pub fn frame_by_global_idx(&self, global: usize) -> Option<DecodedFrame> {
self.frame(global % self.0.frame_cache.len())
}
fn ensure_decoded_up_to(&self, target: usize) {
let inner = &self.0;
let mut state = inner.decoder_state.lock().unwrap();
if state.decoded_count > target {
return;
}
let start = state.decoded_count;
let decoder = state
.decoder
.get_or_insert_with(|| WebPDecoder::new(RawPtrReader::new(inner.raw.as_ptr())).unwrap());
let buf_size = decoder.output_buffer_size().unwrap_or(0);
let mut buf = vec![0u8; buf_size];
if inner.is_animated {
for i in start..=target {
let delay_ms = decoder
.read_frame(&mut buf)
.expect("Failed to decode frame");
let _ = inner.frame_cache[i].set(create_frame(inner, buf.clone(), delay_ms));
}
} else {
assert_eq!(target, 0, "Static image has only one frame");
decoder
.read_image(&mut buf)
.expect("Failed to decode image");
let _ = inner.frame_cache[0].set(create_frame(inner, buf, 0));
}
state.decoded_count = target + 1;
}
}
impl std::fmt::Debug for Image {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let decoded = self
.0
.decoder_state
.lock()
.map(|s| s.decoded_count)
.unwrap_or(0);
f.debug_struct("Image")
.field("size", &format!("{}x{}", self.0.width, self.0.height))
.field("frames", &format!("{}/{}", decoded, self.0.frame_cache.len()))
.field("animated", &self.0.is_animated)
.field("loop_count", &self.0.loop_count)
.finish()
}
}
impl Iterator for FrameIterator<'_> {
type Item = DecodedFrame;
fn next(&mut self) -> Option<Self::Item> {
let frame = self.image.frame(self.index)?;
self.index += 1;
Some(frame)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.image.frame_count() as usize - self.index;
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for FrameIterator<'_> {}
impl Compose for Image {
fn compose(this: impl StateWriter<Value = Self>) -> Widget<'static> {
pipe! {
if $read(this).is_animated() {
let frame_idx = Stateful::new(0usize);
pipe! {
let img = $read(this);
let idx = *$read(frame_idx);
let frame = img.frame_by_global_idx(idx).expect("Invalid frame index");
if img.global_frame_count().is_none_or(|c| idx + 1 < c) {
Local::timer(Duration::from_millis(frame.delay_ms as u64))
.subscribe(move |_| *$write(frame_idx) += 1);
}
frame.image
}.into_widget()
} else {
$read(this).first_frame().image.into_widget()
}
}
.into_widget()
}
}
impl Render for Resource<PixelImage> {
fn perform_layout(&self, clamp: BoxClamp, _: &mut LayoutCtx) -> Size {
let size = Size::new(self.width() as f32, self.height() as f32);
clamp.clamp(size)
}
fn paint(&self, ctx: &mut PaintingCtx) {
let size = ctx.box_size().unwrap();
let box_rect = Rect::from_size(size);
let img_rect = Rect::from_size(Size::new(self.width() as f32, self.height() as f32));
let painter = ctx.painter();
if let Some(rc) = img_rect.intersection(&box_rect) {
painter.draw_img(self.clone(), &rc, &Some(rc));
}
}
fn visual_box(&self, ctx: &mut VisualCtx) -> Option<Rect> {
let box_rect = Rect::from_size(ctx.box_size()?);
let img_rect = Rect::from_size(Size::new(self.width() as f32, self.height() as f32));
img_rect.intersection(&box_rect)
}
}
struct ImageInner {
decoder_state: Mutex<DecoderState>,
raw: StableData,
width: u32,
height: u32,
loop_count: LoopCount,
is_animated: bool,
frame_cache: Box<[OnceLock<DecodedFrame>]>,
}
enum StableData {
Static(&'static [u8]),
Owned(Pin<Box<[u8]>>),
}
impl StableData {
fn from_cow(cow: Cow<'static, [u8]>) -> Self {
match cow {
Cow::Borrowed(b) => Self::Static(b),
Cow::Owned(v) => Self::Owned(Pin::new(v.into_boxed_slice())),
}
}
fn as_ptr(&self) -> *const [u8] {
match self {
Self::Static(s) => *s,
Self::Owned(b) => &**b,
}
}
fn as_slice(&self) -> &[u8] {
match self {
Self::Static(s) => s,
Self::Owned(b) => b,
}
}
}
struct DecoderState {
decoder: Option<WebPDecoder<RawPtrReader>>,
decoded_count: usize,
}
impl DecoderState {
fn new() -> Self { Self { decoder: None, decoded_count: 0 } }
fn with_decoded(count: usize) -> Self { Self { decoder: None, decoded_count: count } }
}
struct RawPtrReader {
data: *const [u8],
pos: usize,
}
unsafe impl Send for RawPtrReader {}
impl RawPtrReader {
fn new(data: *const [u8]) -> Self { Self { data, pos: 0 } }
unsafe fn data(&self) -> &[u8] { unsafe { &*self.data } }
}
impl Read for RawPtrReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let data = unsafe { self.data() };
let remaining = data.len().saturating_sub(self.pos);
let len = buf.len().min(remaining);
if len > 0 {
buf[..len].copy_from_slice(&data[self.pos..self.pos + len]);
self.pos += len;
}
Ok(len)
}
}
impl Seek for RawPtrReader {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let len = unsafe { self.data() }.len() as i64;
let new_pos = match pos {
SeekFrom::Start(n) => n as i64,
SeekFrom::End(n) => len + n,
SeekFrom::Current(n) => self.pos as i64 + n,
};
if new_pos < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"seek to negative position",
));
}
self.pos = new_pos as usize;
Ok(self.pos as u64)
}
}
impl BufRead for RawPtrReader {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
let data = unsafe { self.data() };
Ok(data.get(self.pos..).unwrap_or_default())
}
fn consume(&mut self, amt: usize) { self.pos += amt; }
}
fn new_frame_cache(count: usize) -> Box<[OnceLock<DecodedFrame>]> {
let mut v = Vec::with_capacity(count);
v.resize_with(count, OnceLock::new);
v.into_boxed_slice()
}
fn create_frame(inner: &ImageInner, buf: Vec<u8>, delay_ms: u32) -> DecodedFrame {
DecodedFrame {
image: Resource::new(PixelImage::new(
buf.into(),
inner.width,
inner.height,
ColorFormat::Rgba8,
)),
delay_ms,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loop_count_default() {
assert_eq!(LoopCount::default(), LoopCount::Infinite);
}
#[test]
fn loop_count_equality() {
assert_eq!(LoopCount::Finite(3), LoopCount::Finite(3));
assert_ne!(LoopCount::Finite(3), LoopCount::Finite(5));
assert_ne!(LoopCount::Infinite, LoopCount::Finite(1));
}
#[test]
fn image_clone_shares_cache() {
let pixel_data = vec![255u8; 4]; let frame = DecodedFrame {
image: Resource::new(PixelImage::new(pixel_data.into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 0,
};
let img = Image::from_parts(Vec::new(), 1, 1, LoopCount::Infinite, vec![frame]);
let img2 = img.clone();
assert!(Arc::ptr_eq(&img.0, &img2.0));
}
#[test]
fn from_parts_static_image() {
let pixel_data = vec![255u8; 16]; let frame = DecodedFrame {
image: Resource::new(PixelImage::new(pixel_data.into(), 2, 2, ColorFormat::Rgba8)),
delay_ms: 0,
};
let img = Image::from_parts(Vec::new(), 2, 2, LoopCount::Infinite, vec![frame]);
assert_eq!(img.width(), 2);
assert_eq!(img.height(), 2);
assert_eq!(img.frame_count(), 1);
assert!(!img.is_animated());
assert_eq!(img.loop_count(), LoopCount::Infinite);
}
#[test]
fn from_parts_animated_image() {
let frame1 = DecodedFrame {
image: Resource::new(PixelImage::new(vec![255u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 100,
};
let frame2 = DecodedFrame {
image: Resource::new(PixelImage::new(vec![0u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 200,
};
let img = Image::from_parts(Vec::new(), 1, 1, LoopCount::Finite(2), vec![frame1, frame2]);
assert_eq!(img.frame_count(), 2);
assert!(img.is_animated());
assert_eq!(img.loop_count(), LoopCount::Finite(2));
assert_eq!(img.total_duration_ms(), 300);
}
#[test]
fn frame_access() {
let frame = DecodedFrame {
image: Resource::new(PixelImage::new(vec![255u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 50,
};
let img = Image::from_parts(Vec::new(), 1, 1, LoopCount::Infinite, vec![frame]);
let f = img.frame(0);
assert!(f.is_some());
assert_eq!(f.unwrap().delay_ms, 50);
assert!(img.frame(1).is_none());
assert!(img.frame(100).is_none());
}
#[test]
fn first_frame() {
let frame = DecodedFrame {
image: Resource::new(PixelImage::new(vec![128u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 0,
};
let img = Image::from_parts(Vec::new(), 1, 1, LoopCount::Infinite, vec![frame]);
let f = img.first_frame();
assert_eq!(f.delay_ms, 0);
}
#[test]
fn frame_iterator() {
let frames = vec![
DecodedFrame {
image: Resource::new(PixelImage::new(vec![0u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 10,
},
DecodedFrame {
image: Resource::new(PixelImage::new(vec![1u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 20,
},
DecodedFrame {
image: Resource::new(PixelImage::new(vec![2u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 30,
},
];
let img = Image::from_parts(Vec::new(), 1, 1, LoopCount::Infinite, frames);
let mut iter = img.frame_iter();
assert_eq!(iter.len(), 3);
assert_eq!(iter.next().unwrap().delay_ms, 10);
assert_eq!(iter.len(), 2);
assert_eq!(iter.next().unwrap().delay_ms, 20);
assert_eq!(iter.next().unwrap().delay_ms, 30);
assert!(iter.next().is_none());
}
#[test]
fn global_frame_count() {
let frame = DecodedFrame {
image: Resource::new(PixelImage::new(vec![0u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 0,
};
let img = Image::from_parts(Vec::new(), 1, 1, LoopCount::Infinite, vec![frame]);
assert_eq!(img.global_frame_count(), None);
let frames: Vec<_> = (0..3)
.map(|_| DecodedFrame {
image: Resource::new(PixelImage::new(vec![0u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 0,
})
.collect();
let img = Image::from_parts(Vec::new(), 1, 1, LoopCount::Finite(2), frames);
assert_eq!(img.global_frame_count(), Some(6));
}
#[test]
fn frame_by_global_idx_wrapping() {
let frames = vec![
DecodedFrame {
image: Resource::new(PixelImage::new(vec![0u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 10,
},
DecodedFrame {
image: Resource::new(PixelImage::new(vec![1u8; 4].into(), 1, 1, ColorFormat::Rgba8)),
delay_ms: 20,
},
];
let img = Image::from_parts(Vec::new(), 1, 1, LoopCount::Finite(3), frames);
assert_eq!(img.frame_by_global_idx(0).unwrap().delay_ms, 10);
assert_eq!(img.frame_by_global_idx(1).unwrap().delay_ms, 20);
assert_eq!(img.frame_by_global_idx(2).unwrap().delay_ms, 10);
assert_eq!(img.frame_by_global_idx(3).unwrap().delay_ms, 20);
assert_eq!(img.frame_by_global_idx(4).unwrap().delay_ms, 10);
assert_eq!(img.frame_by_global_idx(5).unwrap().delay_ms, 20);
}
#[test]
fn debug_format() {
let frame = DecodedFrame {
image: Resource::new(PixelImage::new(vec![0u8; 16].into(), 2, 2, ColorFormat::Rgba8)),
delay_ms: 0,
};
let img = Image::from_parts(Vec::new(), 2, 2, LoopCount::Infinite, vec![frame]);
let debug = format!("{:?}", img);
assert!(debug.contains("Image"));
assert!(debug.contains("2x2"));
assert!(debug.contains("1/1"));
assert!(debug.contains("Infinite"));
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod visual_tests {
use ribir::{core::test_helper::*, material as ribir_material, prelude::*};
use ribir_dev_helper::*;
fn colored_image(width: u32, height: u32) -> Image {
let size = (width * height * 4) as usize;
let mut pixels = Vec::with_capacity(size);
for y in 0..height {
for x in 0..width {
let r = ((x as f32 / width as f32) * 255.0) as u8;
let g = 100;
let b = ((y as f32 / height as f32) * 255.0) as u8;
pixels.extend_from_slice(&[r, g, b, 255]);
}
}
let frame = DecodedFrame {
image: Resource::new(PixelImage::new(pixels.into(), width, height, ColorFormat::Rgba8)),
delay_ms: 0,
};
Image::from_parts(Vec::new(), width, height, LoopCount::Infinite, vec![frame])
}
fn colored_pixel_image(width: u32, height: u32) -> Resource<PixelImage> {
let size = (width * height * 4) as usize;
let mut pixels = Vec::with_capacity(size);
for y in 0..height {
for x in 0..width {
let r = ((y as f32 / height as f32) * 255.0) as u8;
let g = ((x as f32 / width as f32) * 255.0) as u8;
let b = 80;
pixels.extend_from_slice(&[r, g, b, 255]);
}
}
Resource::new(PixelImage::new(pixels.into(), width, height, ColorFormat::Rgba8))
}
widget_image_tests!(
image_widget,
WidgetTester::new(fn_widget! { @colored_image(100, 80) }).with_wnd_size(Size::new(120., 100.)),
);
widget_image_tests!(
pixel_image_widget,
WidgetTester::new(fn_widget! { @colored_pixel_image(80, 60) })
.with_wnd_size(Size::new(100., 80.)),
);
}