use std::
{
sync::Arc,
io::Cursor,
time::Duration,
};
use tokio::
{
task,
sync::mpsc::Sender,
};
use image::
{
Frames,
Limits,
ImageFormat,
ImageReader,
ImageDecoder,
Delay,
Frame,
DynamicImage,
AnimationDecoder,
imageops::FilterType,
codecs::
{
gif::{ GifDecoder, GifEncoder, Repeat },
png::PngDecoder,
webp::WebPDecoder,
},
};
use crate::
{
cache,
crypto,
config,
consts,
network::client::ClientEvent,
};
pub struct ImageFrame
{
pub image: DynamicImage,
pub delay: Duration, }
pub type Animation = Vec<ImageFrame>;
fn decode_limits() -> Limits
{
let mut limits = Limits::default();
limits.max_image_width = Some(consts::MAX_IMAGE_DIMENSION);
limits.max_image_height = Some(consts::MAX_IMAGE_DIMENSION);
limits.max_alloc = Some(consts::MAX_IMAGE_ALLOC);
limits
}
fn gif_frames(data: &[u8]) -> Option<Animation>
{
let mut decoder = GifDecoder::new(Cursor::new(data)).ok()?;
decoder.set_limits(decode_limits()).ok()?;
collect_frames(decoder.into_frames())
}
fn webp_frames(data: &[u8]) -> Option<Animation>
{
let mut decoder = WebPDecoder::new(Cursor::new(data)).ok()?;
if !decoder.has_animation() { return None; }
decoder.set_limits(decode_limits()).ok()?;
collect_frames(decoder.into_frames())
}
fn apng_frames(data: &[u8]) -> Option<Animation>
{
let mut decoder = PngDecoder::new(Cursor::new(data)).ok()?;
if !decoder.is_apng().ok()? { return None; }
decoder.set_limits(decode_limits()).ok()?;
collect_frames(decoder.apng().ok()?.into_frames())
}
fn collect_frames(frames: Frames<'_>) -> Option<Animation>
{
let mut animation: Animation = Vec::new();
let mut alloc = 0u64;
for frame in frames.take(consts::MAX_ANIMATION_FRAMES)
{
let Ok(frame) = frame else { break };
let delay = match frame.delay().numer_denom_ms()
{
(_, 0) => consts::DEFAULT_FRAME_DELAY,
(numer, denom) => Duration::from_micros(numer as u64 * 1_000 / denom as u64),
};
let image = DynamicImage::from(frame.into_buffer());
alloc += image.width() as u64 * image.height() as u64 * 4;
if alloc > consts::MAX_ANIMATION_ALLOC && !animation.is_empty() { break; }
animation.push(ImageFrame
{
image,
delay: match delay < consts::MIN_FRAME_DELAY
{
true => consts::DEFAULT_FRAME_DELAY,
false => delay,
},
});
}
match animation.is_empty()
{
true => None,
false => Some(animation),
}
}
pub fn auto_show_images() -> bool
{
config::read_config::<bool>("auto_show_images")
}
pub fn decode_image(data: &[u8]) -> Option<Animation>
{
let mut reader = ImageReader::new(Cursor::new(data)).with_guessed_format().ok()?;
reader.limits(decode_limits());
let animated = match reader.format()
{
Some(ImageFormat::Gif) => gif_frames(data),
Some(ImageFormat::WebP) => webp_frames(data),
Some(ImageFormat::Png) => apng_frames(data),
_ => None,
};
if let Some(frames) = animated && frames.len() > 1 { return Some(frames); }
Some(vec![ImageFrame { image: reader.decode().ok()?, delay: Duration::ZERO }])
}
pub fn make_avatar(data: &[u8]) -> Option<(Vec<u8>, &'static str)>
{
let frames = decode_image(data)?;
let first = &frames.first()?.image;
let side = first.width().min(first.height());
if side == 0 { return None; }
let (x, y) = ((first.width() - side) / 2, (first.height() - side) / 2);
let animated = frames.len() > 1;
let target = side.min(if animated { consts::ANIMATED_AVATAR_DIMENSION } else { consts::AVATAR_DIMENSION });
let square = |image: &DynamicImage| image.crop_imm(x, y, side, side).resize_exact(target, target, FilterType::Triangle);
let mut out = Vec::new();
match animated
{
false =>
{
square(first).write_to(&mut Cursor::new(&mut out), ImageFormat::Png).ok()?;
Some((out, "png"))
},
true =>
{
{
let mut encoder = GifEncoder::new_with_speed(&mut out, consts::AVATAR_GIF_SPEED);
encoder.set_repeat(Repeat::Infinite).ok()?;
encoder.encode_frames(frames.iter().map(|frame| Frame::from_parts(square(&frame.image).to_rgba8(),
0, 0, Delay::from_saturating_duration(frame.delay)))).ok()?;
}
Some((out, "gif"))
},
}
}
pub async fn digest_and_decode(data: Arc<Vec<u8>>) -> ([u8; 32], Option<Animation>)
{
task::spawn_blocking(move || (crypto::sha256(&data), decode_image(&data)))
.await.expect("Decoding image panicked")
}
pub fn fetch_image(hash: [u8; 32], tx: Sender<ClientEvent>)
{
tokio::spawn(async move
{
let cached = match cache::load(&hash).await
{
Some(data) => task::spawn_blocking(move || decode_image(&data))
.await.expect("Decoding image panicked"),
None => None,
};
tx.send(match cached
{
Some(image) => ClientEvent::ImageData(hash, Some(image)),
None => ClientEvent::ImageRequest(hash),
}).await.unwrap();
});
}