use std::io::Cursor;
use crate::error::{PublishError, Result};
pub fn downscale_image(bytes: &[u8], max_dimension: u32) -> Result<Vec<u8>> {
let img = image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|e| PublishError::Image(e.to_string()))?
.decode()
.map_err(|e| PublishError::Image(e.to_string()))?;
if img.width() <= max_dimension && img.height() <= max_dimension {
return Ok(bytes.to_vec());
}
let resized = img.resize(
max_dimension,
max_dimension,
image::imageops::FilterType::Lanczos3,
);
let mut out = Vec::new();
resized
.write_to(&mut Cursor::new(&mut out), image::ImageFormat::Png)
.map_err(|e| PublishError::Image(e.to_string()))?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn png_bytes(w: u32, h: u32) -> Vec<u8> {
let img = image::DynamicImage::ImageRgb8(image::RgbImage::new(w, h));
let mut bytes = Vec::new();
img.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)
.unwrap();
bytes
}
fn dimensions(bytes: &[u8]) -> (u32, u32) {
image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.unwrap()
.into_dimensions()
.unwrap()
}
#[test]
fn downscales_oversized_image_preserving_aspect() {
let src = png_bytes(2000, 1000);
let out = downscale_image(&src, 500).unwrap();
let (w, h) = dimensions(&out);
assert!(w <= 500 && h <= 500, "got {w}x{h}");
assert_eq!(w, 500);
assert_eq!(h, 250);
assert!(out.len() < src.len());
}
#[test]
fn returns_small_image_unchanged() {
let src = png_bytes(300, 200);
let out = downscale_image(&src, 500).unwrap();
assert_eq!(out, src, "already within bounds -> unchanged bytes");
}
#[test]
fn rejects_non_image_bytes() {
assert!(matches!(
downscale_image(b"not an image", 500),
Err(PublishError::Image(_))
));
}
}