//! the small webps an avatar is served at, so a page asking for 42 px doesn't load the original
use std::io::Cursor;
use std::path::Path;
use anyhow::Context as _;
use image::imageops::FilterType;
use image::{DynamicImage, ImageFormat, ImageReader, Limits};
use crate::xrpc::social::{AVATAR_SIZES, sized_avatar};
/// past this the image isn't decoded at all, whatever its bytes
const MAX_PIXELS: u64 = 25_000_000;
pub(super) fn has_sized(avatars: &Path, source: &str) -> bool {
AVATAR_SIZES
.iter()
.all(|&size| avatars.join(sized_avatar(source, size)).is_file())
}
/// square crops of the image, each size from the next bigger one
pub(super) fn write_sized(avatars: &Path, source: &str, bytes: &[u8]) -> anyhow::Result<()> {
let reader = || ImageReader::new(Cursor::new(bytes)).with_guessed_format();
let (width, height) = reader()?.into_dimensions()?;
anyhow::ensure!(
u64::from(width) * u64::from(height) <= MAX_PIXELS,
"a {width}x{height} avatar is past {MAX_PIXELS} pixels"
);
let mut limits = Limits::default();
limits.max_alloc = Some(MAX_PIXELS * 8);
let mut decoder = reader()?;
decoder.limits(limits);
let mut image = decoder.decode().context("decode the avatar")?;
std::fs::create_dir_all(avatars)?;
for &size in AVATAR_SIZES.iter().rev() {
image = image.resize_to_fill(size, size, FilterType::Lanczos3);
let opaque = if image.color().has_alpha() {
DynamicImage::ImageRgba8(image.to_rgba8())
} else {
DynamicImage::ImageRgb8(image.to_rgb8())
};
let mut webp = Vec::new();
opaque.write_to(&mut Cursor::new(&mut webp), ImageFormat::WebP)?;
crate::cas::atomic_write(&avatars.join(sized_avatar(source, size)), &webp)?;
}
Ok(())
}