1use super::provider::ModelConstraints;
2use crate::inspector::{ImageMetadata, MediaFormat};
3use crate::mode::DriveMode;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub enum Action {
9 Pass,
11 Resize {
13 target_width: u32,
14 target_height: u32,
15 },
16 Recompress { quality: u8 },
18 ConvertFormat { to: String },
20 RasterizeSvg {
22 target_width: u32,
23 target_height: u32,
24 },
25 Drop { reason: String },
27}
28
29pub fn evaluate(
31 meta: &ImageMetadata,
32 constraints: &ModelConstraints,
33 mode: DriveMode,
34 image_index: usize,
35 total_images: usize,
36) -> Vec<Action> {
37 let mut actions = Vec::new();
38
39 if meta.format == MediaFormat::Svg {
41 let (w, h) = svg_raster_dimensions(meta, constraints, mode);
42 actions.push(Action::RasterizeSvg {
43 target_width: w,
44 target_height: h,
45 });
46 return actions;
49 }
50
51 if !meta.format.is_provider_safe() {
53 actions.push(Action::ConvertFormat {
54 to: "png".to_string(),
55 });
56 }
57
58 let resize_target = compute_resize_target(meta, constraints, mode);
61 if let Some((tw, th)) = resize_target {
62 actions.push(Action::Resize {
63 target_width: tw,
64 target_height: th,
65 });
66 }
67
68 if meta.size_bytes > constraints.max_image_size_bytes && meta.format == MediaFormat::Jpeg {
71 let quality = match mode {
72 DriveMode::Performance => 90,
73 DriveMode::Balanced => 80,
74 DriveMode::Economy => 60,
75 };
76 actions.push(Action::Recompress { quality });
77 }
78
79 if mode == DriveMode::Economy
81 && total_images > constraints.max_images
82 && image_index >= constraints.max_images
83 {
84 actions.clear();
85 actions.push(Action::Drop {
86 reason: format!(
87 "economy mode: image {} exceeds max_images limit of {}",
88 image_index + 1,
89 constraints.max_images
90 ),
91 });
92 }
93
94 if mode == DriveMode::Economy && actions.is_empty() && meta.max_dim() > 1024 {
97 let scale = 1024.0 / meta.max_dim() as f64;
99 let tw = (meta.width as f64 * scale).max(1.0) as u32;
100 let th = (meta.height as f64 * scale).max(1.0) as u32;
101 actions.push(Action::Resize {
102 target_width: tw,
103 target_height: th,
104 });
105 }
106
107 if actions.is_empty() {
109 actions.push(Action::Pass);
110 }
111
112 actions
113}
114
115fn compute_resize_target(
121 meta: &ImageMetadata,
122 constraints: &ModelConstraints,
123 mode: DriveMode,
124) -> Option<(u32, u32)> {
125 let max_dim = match mode {
126 DriveMode::Performance => constraints.max_image_dim,
127 DriveMode::Balanced => constraints.max_image_dim,
128 DriveMode::Economy => constraints.max_image_dim.min(1024),
129 };
130
131 let mut scale = 1.0_f64;
133 let mut needs_resize = false;
134
135 if meta.max_dim() > max_dim {
137 let dim_scale = max_dim as f64 / meta.max_dim() as f64;
138 scale = scale.min(dim_scale);
139 needs_resize = true;
140 }
141
142 if let Some(max_mp) = constraints.max_image_megapixels {
144 if meta.megapixels > max_mp {
145 let mp_scale = (max_mp / meta.megapixels).sqrt();
146 scale = scale.min(mp_scale);
147 needs_resize = true;
148 }
149 }
150
151 if needs_resize {
152 let tw = (meta.width as f64 * scale).max(1.0) as u32;
154 let th = (meta.height as f64 * scale).max(1.0) as u32;
155 Some((tw, th))
156 } else {
157 None
158 }
159}
160
161fn svg_raster_dimensions(
163 meta: &ImageMetadata,
164 constraints: &ModelConstraints,
165 mode: DriveMode,
166) -> (u32, u32) {
167 let max_target = match mode {
168 DriveMode::Performance => constraints.max_image_dim.min(2048),
169 DriveMode::Balanced => constraints.max_image_dim.min(1024),
170 DriveMode::Economy => 512,
171 };
172
173 let w = meta.width;
174 let h = meta.height;
175
176 if w == 0 || h == 0 {
177 return (max_target, max_target);
178 }
179
180 if w.max(h) > max_target {
181 let scale = max_target as f64 / w.max(h) as f64;
182 let tw = (w as f64 * scale) as u32;
183 let th = (h as f64 * scale) as u32;
184 (tw.max(1), th.max(1))
185 } else if w.max(h) < 64 {
186 let scale = 256.0 / w.max(h) as f64;
188 let tw = (w as f64 * scale) as u32;
189 let th = (h as f64 * scale) as u32;
190 (tw, th)
191 } else {
192 (w, h)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::inspector::Encoding;
200
201 fn make_constraints() -> ModelConstraints {
202 ModelConstraints {
203 max_images: 10,
204 max_image_dim: 2048,
205 max_image_size_bytes: 20_971_520,
206 max_image_megapixels: None,
207 supported_formats: vec!["png".into(), "jpeg".into(), "gif".into(), "webp".into()],
208 }
209 }
210
211 fn make_anthropic_constraints() -> ModelConstraints {
212 ModelConstraints {
213 max_images: 20,
214 max_image_dim: 8000,
215 max_image_size_bytes: 5_242_880,
216 max_image_megapixels: Some(1.15),
217 supported_formats: vec!["png".into(), "jpeg".into(), "gif".into(), "webp".into()],
218 }
219 }
220
221 fn make_meta(format: MediaFormat, w: u32, h: u32, size: usize) -> ImageMetadata {
222 ImageMetadata::new(format, w, h, size, Encoding::Base64)
223 }
224
225 #[test]
226 fn test_pass_small_png() {
227 let meta = make_meta(MediaFormat::Png, 640, 480, 50_000);
228 let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
229 assert_eq!(actions, vec![Action::Pass]);
230 }
231
232 #[test]
233 fn test_resize_oversized_image() {
234 let meta = make_meta(MediaFormat::Png, 4000, 3000, 100_000);
235 let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
236 assert!(actions.iter().any(|a| matches!(a, Action::Resize { .. })));
237 if let Action::Resize {
238 target_width,
239 target_height,
240 } = &actions[0]
241 {
242 assert!(*target_width <= 2048);
243 assert!(*target_height <= 2048);
244 }
245 }
246
247 #[test]
248 fn test_resize_performance_mode_only_if_over_limit() {
249 let meta = make_meta(MediaFormat::Png, 2000, 1500, 100_000);
251 let actions = evaluate(&meta, &make_constraints(), DriveMode::Performance, 0, 1);
252 assert_eq!(actions, vec![Action::Pass]);
253 }
254
255 #[test]
256 fn test_economy_mode_aggressive_resize() {
257 let meta = make_meta(MediaFormat::Png, 1500, 1000, 100_000);
259 let actions = evaluate(&meta, &make_constraints(), DriveMode::Economy, 0, 1);
260 assert!(actions.iter().any(|a| matches!(a, Action::Resize { .. })));
261 }
262
263 #[test]
264 fn test_economy_mode_drops_excess_images() {
265 let meta = make_meta(MediaFormat::Png, 640, 480, 50_000);
266 let constraints = make_constraints(); let actions = evaluate(&meta, &constraints, DriveMode::Economy, 10, 11);
268 assert!(actions.iter().any(|a| matches!(a, Action::Drop { .. })));
269 }
270
271 #[test]
272 fn test_svg_rasterized() {
273 let mut meta = make_meta(MediaFormat::Svg, 800, 600, 5_000);
274 meta.svg_source = Some("<svg></svg>".to_string());
275 let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
276 assert!(actions
277 .iter()
278 .any(|a| matches!(a, Action::RasterizeSvg { .. })));
279 }
280
281 #[test]
282 fn test_bmp_converted() {
283 let meta = make_meta(MediaFormat::Bmp, 640, 480, 900_000);
284 let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
285 assert!(actions
286 .iter()
287 .any(|a| matches!(a, Action::ConvertFormat { .. })));
288 }
289
290 #[test]
291 fn test_anthropic_megapixel_limit() {
292 let meta = make_meta(MediaFormat::Png, 2000, 1000, 100_000);
294 let actions = evaluate(
295 &meta,
296 &make_anthropic_constraints(),
297 DriveMode::Balanced,
298 0,
299 1,
300 );
301 assert!(actions.iter().any(|a| matches!(a, Action::Resize { .. })));
302 }
303
304 #[test]
305 fn test_anthropic_under_megapixel_limit() {
306 let meta = make_meta(MediaFormat::Png, 1000, 800, 100_000);
308 let actions = evaluate(
309 &meta,
310 &make_anthropic_constraints(),
311 DriveMode::Balanced,
312 0,
313 1,
314 );
315 assert_eq!(actions, vec![Action::Pass]);
316 }
317
318 #[test]
319 fn test_oversized_jpeg_recompressed() {
320 let meta = make_meta(MediaFormat::Jpeg, 1000, 800, 25_000_000);
322 let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
323 assert!(actions
324 .iter()
325 .any(|a| matches!(a, Action::Recompress { quality: 80 })));
326 }
327
328 #[test]
329 fn test_oversized_png_not_recompressed() {
330 let meta = make_meta(MediaFormat::Png, 1000, 800, 25_000_000);
332 let actions = evaluate(&meta, &make_constraints(), DriveMode::Balanced, 0, 1);
333 assert!(
334 !actions
335 .iter()
336 .any(|a| matches!(a, Action::Recompress { .. })),
337 "PNG should not get JPEG recompress action"
338 );
339 }
340
341 #[test]
342 fn test_recompress_quality_by_mode() {
343 let meta = make_meta(MediaFormat::Jpeg, 1000, 800, 25_000_000);
344 let constraints = make_constraints();
345
346 let perf_actions = evaluate(&meta, &constraints, DriveMode::Performance, 0, 1);
347 assert!(perf_actions
348 .iter()
349 .any(|a| matches!(a, Action::Recompress { quality: 90 })));
350
351 let eco_actions = evaluate(&meta, &constraints, DriveMode::Economy, 0, 1);
352 assert!(eco_actions
353 .iter()
354 .any(|a| matches!(a, Action::Recompress { quality: 60 })));
355 }
356
357 #[test]
359 fn test_dimension_and_megapixel_both_enforced() {
360 let meta = make_meta(MediaFormat::Png, 10000, 10000, 100_000);
363 let actions = evaluate(
364 &meta,
365 &make_anthropic_constraints(),
366 DriveMode::Balanced,
367 0,
368 1,
369 );
370 assert!(actions.iter().any(|a| matches!(a, Action::Resize { .. })));
371 if let Action::Resize {
372 target_width,
373 target_height,
374 } = &actions[0]
375 {
376 let post_mp = (*target_width as f64 * *target_height as f64) / 1_000_000.0;
378 assert!(
379 post_mp <= 1.15,
380 "post-resize megapixels {} exceeds 1.15",
381 post_mp
382 );
383 assert!(*target_width <= 8000);
384 assert!(*target_height <= 8000);
385 }
386 }
387}