1use super::backend::{BackendError, ImageBackend};
7use super::calculations::{
8 ResponsiveSize, calculate_responsive_sizes, calculate_thumbnail_dimensions,
9};
10use super::params::{Quality, ResizeParams, Sharpening, ThumbnailParams};
11use std::path::Path;
12
13pub type Result<T> = std::result::Result<T, BackendError>;
15
16pub fn get_dimensions(backend: &impl ImageBackend, path: &Path) -> Result<(u32, u32)> {
18 let dims = backend.identify(path)?;
19 Ok((dims.width, dims.height))
20}
21
22#[derive(Debug, Clone)]
24pub struct GeneratedVariant {
25 pub target_size: u32,
26 pub avif_path: String,
27 pub width: u32,
28 pub height: u32,
29}
30
31#[derive(Debug, Clone)]
33pub struct ResponsiveConfig {
34 pub sizes: Vec<u32>,
35 pub quality: Quality,
36}
37
38pub fn create_responsive_images(
43 backend: &impl ImageBackend,
44 source: &Path,
45 output_dir: &Path,
46 filename_stem: &str,
47 original_dims: (u32, u32),
48 config: &ResponsiveConfig,
49) -> Result<Vec<GeneratedVariant>> {
50 let sizes = calculate_responsive_sizes(original_dims, &config.sizes);
51 let mut variants = Vec::new();
52
53 for ResponsiveSize {
54 target,
55 width,
56 height,
57 } in sizes
58 {
59 let avif_name = format!("{}-{}.avif", filename_stem, target);
60 let avif_path = output_dir.join(&avif_name);
61
62 backend.resize(&ResizeParams {
63 source: source.to_path_buf(),
64 output: avif_path.clone(),
65 width,
66 height,
67 quality: config.quality,
68 })?;
69
70 let relative_dir = output_dir
72 .file_name()
73 .map(|s| s.to_str().unwrap())
74 .unwrap_or("");
75
76 variants.push(GeneratedVariant {
77 target_size: target,
78 avif_path: format!("{}/{}", relative_dir, avif_name),
79 width,
80 height,
81 });
82 }
83
84 Ok(variants)
85}
86
87#[derive(Debug, Clone)]
89pub struct ThumbnailConfig {
90 pub aspect: (u32, u32),
91 pub short_edge: u32,
92 pub quality: Quality,
93 pub sharpening: Option<Sharpening>,
94}
95
96impl Default for ThumbnailConfig {
97 fn default() -> Self {
98 Self {
99 aspect: (4, 5),
100 short_edge: 400,
101 quality: Quality::default(),
102 sharpening: Some(Sharpening::light()),
103 }
104 }
105}
106
107pub fn plan_thumbnail(
111 source: &Path,
112 output_path: &Path,
113 config: &ThumbnailConfig,
114) -> ThumbnailParams {
115 let (crop_w, crop_h) = calculate_thumbnail_dimensions(config.aspect, config.short_edge);
116
117 ThumbnailParams {
118 source: source.to_path_buf(),
119 output: output_path.to_path_buf(),
120 crop_width: crop_w,
121 crop_height: crop_h,
122 quality: config.quality,
123 sharpening: config.sharpening,
124 }
125}
126
127pub fn create_thumbnail(
131 backend: &impl ImageBackend,
132 source: &Path,
133 output_dir: &Path,
134 filename_stem: &str,
135 config: &ThumbnailConfig,
136) -> Result<String> {
137 let thumb_name = format!("{}-thumb.avif", filename_stem);
138 let thumb_path = output_dir.join(&thumb_name);
139
140 let params = plan_thumbnail(source, &thumb_path, config);
141 backend.thumbnail(¶ms)?;
142
143 let relative_dir = output_dir
144 .file_name()
145 .map(|s| s.to_str().unwrap())
146 .unwrap_or("");
147
148 Ok(format!("{}/{}", relative_dir, thumb_name))
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::imaging::Dimensions;
155 use crate::imaging::backend::tests::{MockBackend, RecordedOp};
156
157 #[test]
158 fn get_dimensions_calls_backend() {
159 let backend = MockBackend::with_dimensions(vec![Dimensions {
160 width: 1920,
161 height: 1080,
162 }]);
163
164 let dims = get_dimensions(&backend, Path::new("/test.jpg")).unwrap();
165 assert_eq!(dims, (1920, 1080));
166 }
167
168 #[test]
169 fn plan_thumbnail_calculates_crop_dimensions() {
170 let params = plan_thumbnail(
172 Path::new("/source.jpg"),
173 Path::new("/thumb.avif"),
174 &ThumbnailConfig::default(),
175 );
176
177 assert_eq!(params.crop_width, 400);
178 assert_eq!(params.crop_height, 500);
179 }
180
181 #[test]
182 fn plan_thumbnail_landscape_aspect() {
183 let config = ThumbnailConfig {
184 aspect: (16, 9),
185 short_edge: 180,
186 ..ThumbnailConfig::default()
187 };
188 let params = plan_thumbnail(Path::new("/source.jpg"), Path::new("/thumb.avif"), &config);
189
190 assert_eq!(params.crop_width, 320);
191 assert_eq!(params.crop_height, 180);
192 }
193
194 #[test]
195 fn create_thumbnail_uses_backend() {
196 let backend = MockBackend::new();
197 let config = ThumbnailConfig::default();
198
199 let result = create_thumbnail(
200 &backend,
201 Path::new("/source.jpg"),
202 Path::new("/output"),
203 "001-test",
204 &config,
205 )
206 .unwrap();
207
208 assert_eq!(result, "output/001-test-thumb.avif");
209
210 let ops = backend.get_operations();
211 assert_eq!(ops.len(), 1);
212 assert!(matches!(
213 &ops[0],
214 RecordedOp::Thumbnail {
215 crop_width: 400,
216 crop_height: 500,
217 ..
218 }
219 ));
220 }
221
222 #[test]
223 fn create_responsive_caps_at_source_size() {
224 let backend = MockBackend::new();
225 let config = ResponsiveConfig {
226 sizes: vec![800, 1400, 2080],
227 quality: Quality::default(),
228 };
229
230 let variants = create_responsive_images(
232 &backend,
233 Path::new("/source.jpg"),
234 Path::new("/output"),
235 "001-test",
236 (1000, 750),
237 &config,
238 )
239 .unwrap();
240
241 assert_eq!(variants.len(), 2);
242 assert_eq!(variants[0].target_size, 800);
243 assert_eq!(variants[1].target_size, 1000); let ops = backend.get_operations();
246 assert_eq!(ops.len(), 2);
247 }
248
249 #[test]
250 fn create_responsive_generates_avif() {
251 let backend = MockBackend::new();
252 let config = ResponsiveConfig {
253 sizes: vec![800],
254 quality: Quality::new(85),
255 };
256
257 create_responsive_images(
258 &backend,
259 Path::new("/source.jpg"),
260 Path::new("/output"),
261 "001-test",
262 (2000, 1500),
263 &config,
264 )
265 .unwrap();
266
267 let ops = backend.get_operations();
268 assert_eq!(ops.len(), 1);
269
270 assert!(matches!(
271 &ops[0],
272 RecordedOp::Resize { output, quality: 85, .. } if output.ends_with(".avif")
273 ));
274 }
275
276 #[test]
277 fn create_responsive_fallback_to_original_size() {
278 let backend = MockBackend::new();
279 let config = ResponsiveConfig {
280 sizes: vec![800, 1400],
281 quality: Quality::default(),
282 };
283
284 let variants = create_responsive_images(
286 &backend,
287 Path::new("/source.jpg"),
288 Path::new("/output"),
289 "001-test",
290 (500, 400),
291 &config,
292 )
293 .unwrap();
294
295 assert_eq!(variants.len(), 1);
296 assert_eq!(variants[0].target_size, 500); assert_eq!(variants[0].width, 500);
298 assert_eq!(variants[0].height, 400);
299 }
300}