1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct Rect {
12 pub x: u32,
14 pub y: u32,
16 pub width: u32,
18 pub height: u32,
20}
21
22impl Rect {
23 #[must_use]
25 pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
26 Self {
27 x,
28 y,
29 width,
30 height,
31 }
32 }
33
34 #[must_use]
36 pub fn aspect_ratio(&self) -> f64 {
37 f64::from(self.width) / f64::from(self.height)
38 }
39
40 #[must_use]
42 pub fn area(&self) -> u64 {
43 u64::from(self.width) * u64::from(self.height)
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum AspectMode {
50 Pad,
52 Crop,
54 Stretch,
56 Fit,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum PadAlignment {
63 Center,
65 TopLeft,
67 BottomRight,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum PadType {
74 Letterbox,
76 Pillarbox,
78 None,
80}
81
82#[derive(Debug, Clone)]
84pub struct CropScaleConfig {
85 pub source_width: u32,
87 pub source_height: u32,
89 pub target_width: u32,
91 pub target_height: u32,
93 pub aspect_mode: AspectMode,
95 pub pad_alignment: PadAlignment,
97 pub pad_color: (u8, u8, u8),
99 pub manual_crop: Option<Rect>,
101}
102
103impl CropScaleConfig {
104 #[must_use]
106 pub fn new(
107 source_width: u32,
108 source_height: u32,
109 target_width: u32,
110 target_height: u32,
111 ) -> Self {
112 Self {
113 source_width,
114 source_height,
115 target_width,
116 target_height,
117 aspect_mode: AspectMode::Pad,
118 pad_alignment: PadAlignment::Center,
119 pad_color: (0, 0, 0),
120 manual_crop: None,
121 }
122 }
123
124 #[must_use]
126 pub fn with_aspect_mode(mut self, mode: AspectMode) -> Self {
127 self.aspect_mode = mode;
128 self
129 }
130
131 #[must_use]
133 pub fn with_pad_alignment(mut self, alignment: PadAlignment) -> Self {
134 self.pad_alignment = alignment;
135 self
136 }
137
138 #[must_use]
140 pub fn with_pad_color(mut self, r: u8, g: u8, b: u8) -> Self {
141 self.pad_color = (r, g, b);
142 self
143 }
144
145 #[must_use]
147 pub fn with_manual_crop(mut self, crop: Rect) -> Self {
148 self.manual_crop = Some(crop);
149 self
150 }
151
152 #[must_use]
154 pub fn source_aspect(&self) -> f64 {
155 f64::from(self.source_width) / f64::from(self.source_height)
156 }
157
158 #[must_use]
160 pub fn target_aspect(&self) -> f64 {
161 f64::from(self.target_width) / f64::from(self.target_height)
162 }
163
164 #[must_use]
166 pub fn pad_type(&self) -> PadType {
167 let sa = self.source_aspect();
168 let ta = self.target_aspect();
169 if (sa - ta).abs() < 1e-4 {
170 PadType::None
171 } else if sa > ta {
172 PadType::Letterbox
174 } else {
175 PadType::Pillarbox
177 }
178 }
179
180 #[must_use]
184 pub fn compute_scaled_size(&self) -> (u32, u32) {
185 let source_w = f64::from(self.source_width);
186 let source_h = f64::from(self.source_height);
187 let target_w = f64::from(self.target_width);
188 let target_h = f64::from(self.target_height);
189
190 match self.aspect_mode {
191 AspectMode::Stretch => (self.target_width, self.target_height),
192 AspectMode::Pad | AspectMode::Fit => {
193 let scale = (target_w / source_w).min(target_h / source_h);
194 let w = (source_w * scale).round() as u32;
195 let h = (source_h * scale).round() as u32;
196 (w, h)
197 }
198 AspectMode::Crop => {
199 let scale = (target_w / source_w).max(target_h / source_h);
200 let w = (source_w * scale).round() as u32;
201 let h = (source_h * scale).round() as u32;
202 (w, h)
203 }
204 }
205 }
206
207 #[must_use]
211 pub fn compute_pad_offsets(&self) -> (u32, u32) {
212 let (sw, sh) = self.compute_scaled_size();
213 match self.pad_alignment {
214 PadAlignment::Center => {
215 let x = (self.target_width.saturating_sub(sw)) / 2;
216 let y = (self.target_height.saturating_sub(sh)) / 2;
217 (x, y)
218 }
219 PadAlignment::TopLeft => (0, 0),
220 PadAlignment::BottomRight => {
221 let x = self.target_width.saturating_sub(sw);
222 let y = self.target_height.saturating_sub(sh);
223 (x, y)
224 }
225 }
226 }
227
228 #[must_use]
230 pub fn compute_crop_rect(&self) -> Rect {
231 let (sw, sh) = self.compute_scaled_size();
232 let x = (sw.saturating_sub(self.target_width)) / 2;
233 let y = (sh.saturating_sub(self.target_height)) / 2;
234 Rect::new(x, y, self.target_width, self.target_height)
235 }
236}
237
238#[derive(Debug, Clone)]
240pub struct SmartCropDetector {
241 pub saliency_threshold: f32,
243 pub face_weight: f32,
245 pub motion_weight: f32,
247}
248
249impl SmartCropDetector {
250 #[must_use]
252 pub fn new() -> Self {
253 Self {
254 saliency_threshold: 0.5,
255 face_weight: 2.0,
256 motion_weight: 1.5,
257 }
258 }
259
260 #[must_use]
262 pub fn with_saliency_threshold(mut self, threshold: f32) -> Self {
263 self.saliency_threshold = threshold;
264 self
265 }
266
267 #[must_use]
271 pub fn compute_crop(
272 &self,
273 frame_width: u32,
274 frame_height: u32,
275 target_width: u32,
276 target_height: u32,
277 ) -> Rect {
278 let x = (frame_width.saturating_sub(target_width)) / 2;
280 let y = (frame_height.saturating_sub(target_height)) / 2;
281 let w = target_width.min(frame_width);
282 let h = target_height.min(frame_height);
283 Rect::new(x, y, w, h)
284 }
285}
286
287impl Default for SmartCropDetector {
288 fn default() -> Self {
289 Self::new()
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_rect_aspect_ratio() {
299 let r = Rect::new(0, 0, 1920, 1080);
300 assert!((r.aspect_ratio() - 16.0 / 9.0).abs() < 1e-6);
301 }
302
303 #[test]
304 fn test_rect_area() {
305 let r = Rect::new(0, 0, 1920, 1080);
306 assert_eq!(r.area(), 1920 * 1080);
307 }
308
309 #[test]
310 fn test_pad_type_none_when_same_aspect() {
311 let cfg = CropScaleConfig::new(1920, 1080, 1280, 720);
312 assert_eq!(cfg.pad_type(), PadType::None);
313 }
314
315 #[test]
316 fn test_pad_type_letterbox() {
317 let cfg = CropScaleConfig::new(1920, 1080, 1024, 768);
319 assert_eq!(cfg.pad_type(), PadType::Letterbox);
320 }
321
322 #[test]
323 fn test_pad_type_pillarbox() {
324 let cfg = CropScaleConfig::new(1024, 768, 1920, 1080);
326 assert_eq!(cfg.pad_type(), PadType::Pillarbox);
327 }
328
329 #[test]
330 fn test_compute_scaled_size_pad() {
331 let cfg = CropScaleConfig::new(1920, 1080, 1280, 720).with_aspect_mode(AspectMode::Pad);
332 let (w, h) = cfg.compute_scaled_size();
333 assert_eq!(w, 1280);
334 assert_eq!(h, 720);
335 }
336
337 #[test]
338 fn test_compute_scaled_size_stretch() {
339 let cfg = CropScaleConfig::new(1920, 1080, 800, 600).with_aspect_mode(AspectMode::Stretch);
340 let (w, h) = cfg.compute_scaled_size();
341 assert_eq!(w, 800);
342 assert_eq!(h, 600);
343 }
344
345 #[test]
346 fn test_compute_pad_offsets_center() {
347 let cfg = CropScaleConfig::new(1024, 768, 1920, 1080)
349 .with_aspect_mode(AspectMode::Pad)
350 .with_pad_alignment(PadAlignment::Center);
351 let (x, y) = cfg.compute_pad_offsets();
352 assert_eq!(y, 0); assert!(x > 0); }
356
357 #[test]
358 fn test_compute_pad_offsets_topleft() {
359 let cfg = CropScaleConfig::new(1024, 768, 1920, 1080)
360 .with_aspect_mode(AspectMode::Pad)
361 .with_pad_alignment(PadAlignment::TopLeft);
362 let (x, y) = cfg.compute_pad_offsets();
363 assert_eq!(x, 0);
364 assert_eq!(y, 0);
365 }
366
367 #[test]
368 fn test_compute_crop_rect() {
369 let cfg = CropScaleConfig::new(1920, 1080, 1280, 720).with_aspect_mode(AspectMode::Crop);
370 let rect = cfg.compute_crop_rect();
371 assert_eq!(rect.width, 1280);
372 assert_eq!(rect.height, 720);
373 }
374
375 #[test]
376 fn test_pad_color() {
377 let cfg = CropScaleConfig::new(1920, 1080, 1280, 720).with_pad_color(255, 255, 255);
378 assert_eq!(cfg.pad_color, (255, 255, 255));
379 }
380
381 #[test]
382 fn test_manual_crop() {
383 let crop = Rect::new(100, 50, 1720, 980);
384 let cfg = CropScaleConfig::new(1920, 1080, 1280, 720).with_manual_crop(crop);
385 assert!(cfg.manual_crop.is_some());
386 assert_eq!(cfg.manual_crop.expect("should succeed in test").x, 100);
387 }
388
389 #[test]
390 fn test_smart_crop_detector_default() {
391 let det = SmartCropDetector::new();
392 assert!((det.saliency_threshold - 0.5).abs() < 1e-6);
393 }
394
395 #[test]
396 fn test_smart_crop_computes_center_crop() {
397 let det = SmartCropDetector::new();
398 let rect = det.compute_crop(1920, 1080, 1280, 720);
399 assert_eq!(rect.width, 1280);
400 assert_eq!(rect.height, 720);
401 assert_eq!(rect.x, (1920 - 1280) / 2);
402 assert_eq!(rect.y, (1080 - 720) / 2);
403 }
404}