use std::io::Cursor;
use image::{AnimationDecoder, ImageDecoder, ImageFormat};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pixels {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Animation {
pub width: u32,
pub height: u32,
pub frames: Vec<AnimationFrame>,
pub loops: Option<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AnimationFrame {
pub rgba: Vec<u8>,
pub delay: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Showing {
pub index: usize,
pub next: f64,
}
const GIF_FLOOR: f64 = 0.011;
const GIF_SLOW: f64 = 0.1;
fn format_of(bytes: &[u8]) -> Result<ImageFormat, String> {
let format = image::guess_format(bytes)
.map_err(|_| "not an image kui decodes: PNG, JPEG, WebP or GIF".to_string())?;
match format {
ImageFormat::Png | ImageFormat::Jpeg | ImageFormat::WebP | ImageFormat::Gif => Ok(format),
other => Err(format!(
"{other:?} is not a format kui decodes: PNG, JPEG, WebP or GIF"
)),
}
}
pub fn decode_image(bytes: &[u8]) -> Result<Pixels, String> {
let format = format_of(bytes)?;
let img = image::load_from_memory_with_format(bytes, format)
.map_err(|e| format!("decoding a {format:?}: {e}"))?
.into_rgba8();
Ok(Pixels {
width: img.width(),
height: img.height(),
rgba: img.into_raw(),
})
}
pub fn decode_animation(bytes: &[u8]) -> Result<Animation, String> {
let format = format_of(bytes)?;
let err = |e: image::ImageError| format!("decoding a {format:?}: {e}");
let (width, height, loops, frames) = match format {
ImageFormat::Gif => {
let d = image::codecs::gif::GifDecoder::new(Cursor::new(bytes)).map_err(err)?;
let (w, h) = d.dimensions();
let loops = loops_of(d.loop_count());
(w, h, loops, d.into_frames().collect_frames().map_err(err)?)
}
ImageFormat::Png => {
let d = image::codecs::png::PngDecoder::new(Cursor::new(bytes)).map_err(err)?;
if !d.is_apng().map_err(err)? {
return still(bytes);
}
let (w, h) = d.dimensions();
let a = d.apng().map_err(err)?;
let loops = loops_of(a.loop_count());
(w, h, loops, a.into_frames().collect_frames().map_err(err)?)
}
ImageFormat::WebP => {
let d = image::codecs::webp::WebPDecoder::new(Cursor::new(bytes)).map_err(err)?;
if !d.has_animation() {
return still(bytes);
}
let (w, h) = d.dimensions();
let loops = loops_of(d.loop_count());
(w, h, loops, d.into_frames().collect_frames().map_err(err)?)
}
_ => return still(bytes),
};
let gif = format == ImageFormat::Gif;
let mut out: Vec<AnimationFrame> = Vec::with_capacity(frames.len());
for f in frames {
let (num, den) = f.delay().numer_denom_ms();
let mut delay = f64::from(num) / f64::from(den.max(1)) / 1000.0;
if gif && delay < GIF_FLOOR {
delay = GIF_SLOW;
}
let (left, top) = (f.left(), f.top());
let buf = f.into_buffer();
let rgba = if (buf.width(), buf.height(), left, top) == (width, height, 0, 0) {
buf.into_raw()
} else {
let mut canvas = out
.last()
.map(|p| p.rgba.clone())
.unwrap_or_else(|| vec![0; width as usize * height as usize * 4]);
blit(&mut canvas, width, height, &buf, left, top);
canvas
};
out.push(AnimationFrame { rgba, delay });
}
if out.is_empty() {
return Err(format!("a {format:?} with no frames"));
}
Ok(Animation {
width,
height,
frames: out,
loops,
})
}
fn still(bytes: &[u8]) -> Result<Animation, String> {
let p = decode_image(bytes)?;
Ok(Animation {
width: p.width,
height: p.height,
frames: vec![AnimationFrame {
rgba: p.rgba,
delay: f64::INFINITY,
}],
loops: Some(1),
})
}
fn loops_of(count: image::metadata::LoopCount) -> Option<u32> {
match count {
image::metadata::LoopCount::Infinite => None,
image::metadata::LoopCount::Finite(n) => Some(n.get()),
}
}
fn blit(canvas: &mut [u8], w: u32, h: u32, src: &image::RgbaImage, left: u32, top: u32) {
for (y, row) in src.rows().enumerate() {
let cy = top as usize + y;
if cy >= h as usize {
break;
}
for (x, px) in row.enumerate() {
let cx = left as usize + x;
if cx >= w as usize {
break;
}
if px.0[3] != 0 {
let at = (cy * w as usize + cx) * 4;
canvas[at..at + 4].copy_from_slice(&px.0);
}
}
}
}
impl Animation {
pub fn duration(&self) -> f64 {
self.frames.iter().map(|f| f.delay).sum()
}
pub fn at(&self, elapsed: f64) -> Showing {
let last = self.frames.len().saturating_sub(1);
let total = self.duration();
if self.frames.len() < 2 || !total.is_finite() || total <= 0.0 {
return Showing {
index: 0,
next: f64::INFINITY,
};
}
let elapsed = elapsed.max(0.0);
let pass = (elapsed / total).floor();
if let Some(n) = self.loops
&& pass >= f64::from(n)
{
return Showing {
index: last,
next: f64::INFINITY,
};
}
let mut t = pass * total;
for (i, f) in self.frames.iter().enumerate() {
t += f.delay;
if elapsed < t {
return Showing { index: i, next: t };
}
}
Showing {
index: last,
next: (pass + 1.0) * total + self.frames[0].delay,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use gif::Repeat;
use image::{Rgba, RgbaImage};
fn gif(repeat: gif::Repeat) -> Vec<u8> {
let mut out = Vec::new();
{
let mut enc = gif::Encoder::new(&mut out, 4, 2, &[]).unwrap();
enc.set_repeat(repeat).unwrap();
let mut frame = |w: u16, h: u16, left: u16, top: u16, c: [u8; 4], cs: u16| {
let mut px: Vec<u8> = c.repeat(usize::from(w * h));
let mut f = gif::Frame::from_rgba(w, h, &mut px);
(f.left, f.top, f.delay) = (left, top, cs);
enc.write_frame(&f).unwrap();
};
frame(4, 2, 0, 0, [255, 0, 0, 255], 5);
frame(4, 2, 0, 0, [0, 255, 0, 255], 10);
frame(2, 1, 1, 1, [0, 0, 255, 255], 0);
}
out
}
fn png() -> Vec<u8> {
let img = RgbaImage::from_fn(3, 2, |x, y| Rgba([x as u8 * 80, y as u8 * 100, 7, 200]));
let mut out = Cursor::new(Vec::new());
img.write_to(&mut out, ImageFormat::Png).unwrap();
out.into_inner()
}
#[test]
fn a_png_decodes_to_straight_rgba() {
let p = decode_image(&png()).unwrap();
assert_eq!((p.width, p.height), (3, 2));
assert_eq!(p.rgba.len(), 3 * 2 * 4);
assert_eq!(&p.rgba[4..8], &[80, 0, 7, 200], "not premultiplied");
}
#[test]
fn a_gif_decodes_to_its_first_frame_and_to_every_frame() {
let bytes = gif(Repeat::Infinite);
let first = decode_image(&bytes).unwrap();
assert_eq!((first.width, first.height), (4, 2));
assert_eq!(&first.rgba[..4], &[255, 0, 0, 255]);
let a = decode_animation(&bytes).unwrap();
assert_eq!(a.frames.len(), 3);
assert_eq!(a.loops, None);
let delays: Vec<f64> = a.frames.iter().map(|f| f.delay).collect();
assert_eq!(delays, vec![0.05, 0.1, 0.1], "a 0 ms frame shows for 100");
for f in &a.frames {
assert_eq!(f.rgba.len(), 4 * 2 * 4, "each frame the whole canvas");
}
let third = &a.frames[2].rgba;
assert_eq!(
&third[..4],
&[0, 255, 0, 255],
"the green kept under the patch"
);
assert_eq!(&third[(4 + 1) * 4..(4 + 1) * 4 + 4], &[0, 0, 255, 255]);
}
#[test]
fn at_walks_the_frames_and_loops() {
let a = decode_animation(&gif(Repeat::Infinite)).unwrap();
let near = |s: Showing, i: usize, next: f64| s.index == i && (s.next - next).abs() < 1e-9;
assert!(near(a.at(0.0), 0, 0.05));
assert!(near(a.at(0.07), 1, 0.15));
assert!(near(a.at(0.2), 2, 0.25));
assert!(near(a.at(0.26), 0, 0.30), "{:?}", a.at(0.26));
assert!(near(a.at(-1.0), 0, 0.05), "before the start");
}
#[test]
fn a_finite_gif_rests_on_its_last_frame() {
let a = decode_animation(&gif(Repeat::Finite(1))).unwrap();
assert_eq!(a.loops, Some(1));
assert_eq!(a.at(0.1).index, 1);
let end = a.at(0.3);
assert_eq!(end.index, 2);
assert!(end.next.is_infinite());
}
#[test]
fn a_still_is_one_frame_for_ever() {
let a = decode_animation(&png()).unwrap();
assert_eq!(a.frames.len(), 1);
let s = a.at(100.0);
assert_eq!(s.index, 0);
assert!(s.next.is_infinite());
}
#[test]
fn what_is_not_an_image_is_named() {
let e = decode_image(b"hello, world").unwrap_err();
assert!(e.contains("PNG, JPEG, WebP or GIF"), "{e}");
let mut cut = png();
cut.truncate(40);
let e = decode_image(&cut).unwrap_err();
assert!(e.contains("decoding a Png"), "{e}");
}
}