use image::imageops::FilterType;
use crate::config::Ratio;
#[derive(Debug, Clone, Copy)]
pub struct ArtRules {
pub min_width: u32,
pub max_width: u32,
pub quality: u8,
pub ratio: Option<Ratio>,
}
pub struct Prepared {
pub bytes: Vec<u8>,
pub resized: bool,
}
pub fn prepare(bytes: &[u8], rules: &ArtRules) -> Option<Prepared> {
let format = image::guess_format(bytes).ok()?;
let img = image::load_from_memory_with_format(bytes, format).ok()?;
let (width, height) = (img.width(), img.height());
if width < rules.min_width {
return None;
}
if let Some(ratio) = rules.ratio
&& !ratio.allows(width, height)
{
return None;
}
if rules.max_width == 0 || width <= rules.max_width {
return Some(Prepared {
bytes: bytes.to_vec(),
resized: false,
});
}
let new_height =
((f64::from(height) * f64::from(rules.max_width) / f64::from(width)).round() as u32).max(1);
let resized = image::DynamicImage::ImageRgb8(
img.resize_exact(rules.max_width, new_height, FilterType::Lanczos3)
.to_rgb8(),
);
let mut out = Vec::new();
let encoder =
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, rules.quality.clamp(1, 100));
resized.write_with_encoder(encoder).ok()?;
Some(Prepared {
bytes: out,
resized: true,
})
}
#[cfg(test)]
mod tests {
use super::*;
use image::{ImageBuffer, Rgb};
fn jpeg(width: u32, height: u32) -> Vec<u8> {
let img = ImageBuffer::from_fn(width, height, |x, y| {
Rgb([(x % 256) as u8, (y % 256) as u8, 128])
});
let mut out = Vec::new();
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, 90);
image::DynamicImage::ImageRgb8(img)
.write_with_encoder(encoder)
.unwrap();
out
}
fn rules(min_width: u32, max_width: u32, ratio: Option<Ratio>) -> ArtRules {
ArtRules {
min_width,
max_width,
quality: 90,
ratio,
}
}
#[test]
fn percent_ratio_accepts_within_tolerance_and_rejects_outside() {
let r = rules(0, 0, Some(Ratio::Percent(10.0)));
assert!(prepare(&jpeg(1000, 950), &r).is_some());
assert!(prepare(&jpeg(1000, 800), &r).is_none());
}
#[test]
fn pixel_ratio_rejects_outside_tolerance() {
let r = rules(0, 0, Some(Ratio::Pixels(10)));
assert!(prepare(&jpeg(1000, 980), &r).is_none());
}
#[test]
fn minwidth_rejects_a_smaller_image() {
let r = rules(500, 0, None);
assert!(prepare(&jpeg(400, 400), &r).is_none());
}
#[test]
fn oversized_jpeg_is_resized_and_reencoded() {
let r = rules(0, 1200, None);
let prepared = prepare(&jpeg(3000, 3000), &r).unwrap();
assert!(prepared.resized);
let img = image::load_from_memory(&prepared.bytes).unwrap();
assert_eq!(img.width(), 1200);
}
#[test]
fn jpeg_within_max_width_is_unchanged() {
let bytes = jpeg(1000, 1000);
let r = rules(0, 1200, None);
let prepared = prepare(&bytes, &r).unwrap();
assert!(!prepared.resized);
assert_eq!(prepared.bytes, bytes);
}
#[test]
fn an_oversized_transparent_png_is_resized_not_dropped() {
let img = image::RgbaImage::from_pixel(2000, 2000, image::Rgba([10, 20, 30, 128]));
let mut png = Vec::new();
image::DynamicImage::ImageRgba8(img)
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
.unwrap();
let out = prepare(&png, &rules(0, 1200, None)).expect("resized");
assert!(out.resized);
assert_eq!(
image::guess_format(&out.bytes).unwrap(),
image::ImageFormat::Jpeg
);
}
}