use super::provider::ModelConstraints;
use crate::inspector::{ImageMetadata, MediaFormat};
use crate::mode::DriveMode;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Action {
Pass,
Resize {
target_width: u32,
target_height: u32,
},
Recompress { quality: u8 },
ConvertFormat { to: String },
RasterizeSvg {
target_width: u32,
target_height: u32,
},
Drop { reason: String },
}
pub fn evaluate(
meta: &ImageMetadata,
constraints: &ModelConstraints,
mode: DriveMode,
image_index: usize,
total_images: usize,
) -> Vec<Action> {
let mut actions = Vec::new();
if meta.format == MediaFormat::Svg {
let (w, h) = svg_raster_dimensions(meta, constraints, mode);
actions.push(Action::RasterizeSvg {
target_width: w,
target_height: h,
});
return actions;
}
if !meta.format.is_provider_safe() {
actions.push(Action::ConvertFormat {
to: "png".to_string(),
});
}
let resize_target = compute_resize_target(meta, constraints, mode);
if let Some((tw, th)) = resize_target {
actions.push(Action::Resize {
target_width: tw,
target_height: th,
});
}
if meta.size_bytes > constraints.max_image_size_bytes && meta.format == MediaFormat::Jpeg {
let quality = match mode {
DriveMode::Performance => 90,
DriveMode::Balanced => 80,
DriveMode::Economy => 60,
};
actions.push(Action::Recompress { quality });
}
if mode == DriveMode::Economy
&& total_images > constraints.max_images
&& image_index >= constraints.max_images
{
actions.clear();
actions.push(Action::Drop {
reason: format!(
"economy mode: image {} exceeds max_images limit of {}",
image_index + 1,
constraints.max_images
),
});
}
if mode == DriveMode::Economy && actions.is_empty() && meta.max_dim() > 1024 {
let scale = 1024.0 / meta.max_dim() as f64;
let tw = (meta.width as f64 * scale).max(1.0) as u32;
let th = (meta.height as f64 * scale).max(1.0) as u32;
actions.push(Action::Resize {
target_width: tw,
target_height: th,
});
}
if actions.is_empty() {
actions.push(Action::Pass);
}
actions
}
fn compute_resize_target(
meta: &ImageMetadata,
constraints: &ModelConstraints,
mode: DriveMode,
) -> Option<(u32, u32)> {
let max_dim = match mode {
DriveMode::Performance => constraints.max_image_dim,
DriveMode::Balanced => constraints.max_image_dim,
DriveMode::Economy => constraints.max_image_dim.min(1024),
};
let mut scale = 1.0_f64;
let mut needs_resize = false;
if meta.max_dim() > max_dim {
let dim_scale = max_dim as f64 / meta.max_dim() as f64;
scale = scale.min(dim_scale);
needs_resize = true;
}
if let Some(max_mp) = constraints.max_image_megapixels {
if meta.megapixels > max_mp {
let mp_scale = (max_mp / meta.megapixels).sqrt();
scale = scale.min(mp_scale);
needs_resize = true;
}
}
if needs_resize {
let tw = (meta.width as f64 * scale).max(1.0) as u32;
let th = (meta.height as f64 * scale).max(1.0) as u32;
Some((tw, th))
} else {
None
}
}
fn svg_raster_dimensions(
meta: &ImageMetadata,
constraints: &ModelConstraints,
mode: DriveMode,
) -> (u32, u32) {
let max_target = match mode {
DriveMode::Performance => constraints.max_image_dim.min(2048),
DriveMode::Balanced => constraints.max_image_dim.min(1024),
DriveMode::Economy => 512,
};
let w = meta.width;
let h = meta.height;
if w == 0 || h == 0 {
return (max_target, max_target);
}
if w.max(h) > max_target {
let scale = max_target as f64 / w.max(h) as f64;
let tw = (w as f64 * scale) as u32;
let th = (h as f64 * scale) as u32;
(tw.max(1), th.max(1))
} else if w.max(h) < 64 {
let scale = 256.0 / w.max(h) as f64;
let tw = (w as f64 * scale) as u32;
let th = (h as f64 * scale) as u32;
(tw, th)
} else {
(w, h)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inspector::Encoding;
fn make_constraints() -> ModelConstraints {
ModelConstraints {
max_images: 10,
max_image_dim: 2048,
max_image_size_bytes: 20_971_520,
max_image_megapixels: None,
supported_formats: vec!["png".into(), "jpeg".into(), "gif".into(), "webp".into()],
}
}
fn make_anthropic_constraints() -> ModelConstraints {
ModelConstraints {
max_images: 20,
max_image_dim: 8000,
max_image_size_bytes: 5_242_880,
max_image_megapixels: Some(1.15),
supported_formats: vec!["png".into(), "jpeg".into(), "gif".into(), "webp".into()],
}
}
fn make_meta(format: MediaFormat, w: u32, h: u32, size: usize) -> ImageMetadata {
ImageMetadata::new(format, w, h, size, Encoding::Base64)
}
#[test]
fn test_pass_small_png() {
let meta = make_meta(MediaFormat::Png, 640, 480, 50_000);
let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
assert_eq!(actions, vec![Action::Pass]);
}
#[test]
fn test_resize_oversized_image() {
let meta = make_meta(MediaFormat::Png, 4000, 3000, 100_000);
let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
assert!(actions.iter().any(|a| matches!(a, Action::Resize { .. })));
if let Action::Resize {
target_width,
target_height,
} = &actions[0]
{
assert!(*target_width <= 2048);
assert!(*target_height <= 2048);
}
}
#[test]
fn test_resize_performance_mode_only_if_over_limit() {
let meta = make_meta(MediaFormat::Png, 2000, 1500, 100_000);
let actions = evaluate(&meta, &make_constraints(), DriveMode::Performance, 0, 1);
assert_eq!(actions, vec![Action::Pass]);
}
#[test]
fn test_economy_mode_aggressive_resize() {
let meta = make_meta(MediaFormat::Png, 1500, 1000, 100_000);
let actions = evaluate(&meta, &make_constraints(), DriveMode::Economy, 0, 1);
assert!(actions.iter().any(|a| matches!(a, Action::Resize { .. })));
}
#[test]
fn test_economy_mode_drops_excess_images() {
let meta = make_meta(MediaFormat::Png, 640, 480, 50_000);
let constraints = make_constraints(); let actions = evaluate(&meta, &constraints, DriveMode::Economy, 10, 11);
assert!(actions.iter().any(|a| matches!(a, Action::Drop { .. })));
}
#[test]
fn test_svg_rasterized() {
let mut meta = make_meta(MediaFormat::Svg, 800, 600, 5_000);
meta.svg_source = Some("<svg></svg>".to_string());
let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
assert!(actions
.iter()
.any(|a| matches!(a, Action::RasterizeSvg { .. })));
}
#[test]
fn test_bmp_converted() {
let meta = make_meta(MediaFormat::Bmp, 640, 480, 900_000);
let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
assert!(actions
.iter()
.any(|a| matches!(a, Action::ConvertFormat { .. })));
}
#[test]
fn test_anthropic_megapixel_limit() {
let meta = make_meta(MediaFormat::Png, 2000, 1000, 100_000);
let actions = evaluate(
&meta,
&make_anthropic_constraints(),
DriveMode::Balanced,
0,
1,
);
assert!(actions.iter().any(|a| matches!(a, Action::Resize { .. })));
}
#[test]
fn test_anthropic_under_megapixel_limit() {
let meta = make_meta(MediaFormat::Png, 1000, 800, 100_000);
let actions = evaluate(
&meta,
&make_anthropic_constraints(),
DriveMode::Balanced,
0,
1,
);
assert_eq!(actions, vec![Action::Pass]);
}
#[test]
fn test_oversized_jpeg_recompressed() {
let meta = make_meta(MediaFormat::Jpeg, 1000, 800, 25_000_000);
let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
assert!(actions
.iter()
.any(|a| matches!(a, Action::Recompress { quality: 80 })));
}
#[test]
fn test_oversized_png_not_recompressed() {
let meta = make_meta(MediaFormat::Png, 1000, 800, 25_000_000);
let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
assert!(
!actions
.iter()
.any(|a| matches!(a, Action::Recompress { .. })),
"PNG should not get JPEG recompress action"
);
}
#[test]
fn test_recompress_quality_by_mode() {
let meta = make_meta(MediaFormat::Jpeg, 1000, 800, 25_000_000);
let constraints = make_constraints();
let perf_actions = evaluate(&meta, &constraints, DriveMode::Performance, 0, 1);
assert!(perf_actions
.iter()
.any(|a| matches!(a, Action::Recompress { quality: 90 })));
let eco_actions = evaluate(&meta, &constraints, DriveMode::Economy, 0, 1);
assert!(eco_actions
.iter()
.any(|a| matches!(a, Action::Recompress { quality: 60 })));
}
#[test]
fn test_dimension_and_megapixel_both_enforced() {
let meta = make_meta(MediaFormat::Png, 10000, 10000, 100_000);
let actions = evaluate(
&meta,
&make_anthropic_constraints(),
DriveMode::Balanced,
0,
1,
);
assert!(actions.iter().any(|a| matches!(a, Action::Resize { .. })));
if let Action::Resize {
target_width,
target_height,
} = &actions[0]
{
let post_mp = (*target_width as f64 * *target_height as f64) / 1_000_000.0;
assert!(
post_mp <= 1.15,
"post-resize megapixels {} exceeds 1.15",
post_mp
);
assert!(*target_width <= 8000);
assert!(*target_height <= 8000);
}
}
}