1use crate::codec::ImageData;
2use crate::error::{Error, Result};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum FillColor {
7 Solid([u8; 4]),
9 Transparent,
11}
12
13impl FillColor {
14 pub fn as_rgba(&self) -> [u8; 4] {
16 match *self {
17 FillColor::Solid(c) => c,
18 FillColor::Transparent => [0, 0, 0, 0],
19 }
20 }
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub enum ExtendMode {
26 AspectRatio { width: u32, height: u32 },
29 Size { width: u32, height: u32 },
31}
32
33pub fn calculate_extend_region(
38 img_w: u32,
39 img_h: u32,
40 mode: &ExtendMode,
41) -> Result<(u32, u32, u32, u32)> {
42 match *mode {
43 ExtendMode::AspectRatio {
44 width: rw,
45 height: rh,
46 } => {
47 if rw == 0 || rh == 0 {
48 return Err(Error::Extend(
49 "aspect ratio must be non-zero".to_string(),
50 ));
51 }
52
53 let target_ratio = rw as f64 / rh as f64;
54 let img_ratio = img_w as f64 / img_h as f64;
55
56 let (canvas_w, canvas_h) = if img_ratio < target_ratio {
57 let h = img_h;
59 let w = (h as f64 * target_ratio).round() as u32;
60 (w, h)
61 } else {
62 let w = img_w;
64 let h = (w as f64 / target_ratio).round() as u32;
65 (w, h)
66 };
67
68 let off_x = (canvas_w - img_w) / 2;
69 let off_y = (canvas_h - img_h) / 2;
70
71 Ok((canvas_w, canvas_h, off_x, off_y))
72 }
73 ExtendMode::Size { width, height } => {
74 if width == 0 || height == 0 {
75 return Err(Error::Extend(
76 "extend dimensions must be non-zero".to_string(),
77 ));
78 }
79 if width < img_w || height < img_h {
80 return Err(Error::Extend(format!(
81 "target size ({width}x{height}) is smaller than image ({img_w}x{img_h})"
82 )));
83 }
84
85 let off_x = (width - img_w) / 2;
86 let off_y = (height - img_h) / 2;
87
88 Ok((width, height, off_x, off_y))
89 }
90 }
91}
92
93pub fn extend(image: &ImageData, mode: &ExtendMode, fill: &FillColor) -> Result<ImageData> {
95 let (canvas_w, canvas_h, off_x, off_y) =
96 calculate_extend_region(image.width, image.height, mode)?;
97
98 let expected_size = image.width as usize * image.height as usize * 4;
99 if image.data.len() != expected_size {
100 return Err(Error::Extend(format!(
101 "invalid image data: expected {} bytes ({}x{}x4), got {}",
102 expected_size, image.width, image.height, image.data.len()
103 )));
104 }
105
106 if canvas_w == image.width && canvas_h == image.height {
108 return Ok(image.clone());
109 }
110
111 let bytes_per_pixel = 4usize;
112 let canvas_stride = canvas_w as usize * bytes_per_pixel;
113 let src_stride = image.width as usize * bytes_per_pixel;
114
115 let mut data = vec![0u8; canvas_h as usize * canvas_stride];
117 if !matches!(fill, FillColor::Transparent) {
118 let fill_rgba = fill.as_rgba();
119 for pixel in data.chunks_exact_mut(bytes_per_pixel) {
120 pixel.copy_from_slice(&fill_rgba);
121 }
122 }
123
124 for row in 0..image.height as usize {
126 let src_offset = row * src_stride;
127 let dst_offset = (off_y as usize + row) * canvas_stride + off_x as usize * bytes_per_pixel;
128 data[dst_offset..dst_offset + src_stride]
129 .copy_from_slice(&image.data[src_offset..src_offset + src_stride]);
130 }
131
132 Ok(ImageData::new(canvas_w, canvas_h, data))
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
142 fn aspect_square_on_landscape() {
143 let (w, h, ox, oy) = calculate_extend_region(
144 200,
145 100,
146 &ExtendMode::AspectRatio {
147 width: 1,
148 height: 1,
149 },
150 )
151 .unwrap();
152 assert_eq!((w, h, ox, oy), (200, 200, 0, 50));
153 }
154
155 #[test]
156 fn aspect_square_on_portrait() {
157 let (w, h, ox, oy) = calculate_extend_region(
158 100,
159 200,
160 &ExtendMode::AspectRatio {
161 width: 1,
162 height: 1,
163 },
164 )
165 .unwrap();
166 assert_eq!((w, h, ox, oy), (200, 200, 50, 0));
167 }
168
169 #[test]
170 fn aspect_16_9_on_square() {
171 let (w, h, ox, oy) = calculate_extend_region(
172 100,
173 100,
174 &ExtendMode::AspectRatio {
175 width: 16,
176 height: 9,
177 },
178 )
179 .unwrap();
180 assert_eq!((w, h, ox, oy), (178, 100, 39, 0));
181 }
182
183 #[test]
184 fn aspect_9_16_on_square() {
185 let (w, h, ox, oy) = calculate_extend_region(
186 100,
187 100,
188 &ExtendMode::AspectRatio {
189 width: 9,
190 height: 16,
191 },
192 )
193 .unwrap();
194 assert_eq!((w, h, ox, oy), (100, 178, 0, 39));
195 }
196
197 #[test]
198 fn aspect_same_as_image() {
199 let (w, h, ox, oy) = calculate_extend_region(
200 200,
201 100,
202 &ExtendMode::AspectRatio {
203 width: 2,
204 height: 1,
205 },
206 )
207 .unwrap();
208 assert_eq!((w, h, ox, oy), (200, 100, 0, 0));
209 }
210
211 #[test]
212 fn aspect_zero_ratio_errors() {
213 let result = calculate_extend_region(
214 200,
215 100,
216 &ExtendMode::AspectRatio {
217 width: 0,
218 height: 1,
219 },
220 );
221 assert!(result.is_err());
222 }
223
224 #[test]
227 fn size_larger_canvas() {
228 let (w, h, ox, oy) = calculate_extend_region(
229 800,
230 600,
231 &ExtendMode::Size {
232 width: 1000,
233 height: 1000,
234 },
235 )
236 .unwrap();
237 assert_eq!((w, h, ox, oy), (1000, 1000, 100, 200));
238 }
239
240 #[test]
241 fn size_same_as_image() {
242 let (w, h, ox, oy) = calculate_extend_region(
243 800,
244 600,
245 &ExtendMode::Size {
246 width: 800,
247 height: 600,
248 },
249 )
250 .unwrap();
251 assert_eq!((w, h, ox, oy), (800, 600, 0, 0));
252 }
253
254 #[test]
255 fn size_only_width_larger() {
256 let (w, h, ox, oy) = calculate_extend_region(
257 800,
258 600,
259 &ExtendMode::Size {
260 width: 1000,
261 height: 600,
262 },
263 )
264 .unwrap();
265 assert_eq!((w, h, ox, oy), (1000, 600, 100, 0));
266 }
267
268 #[test]
269 fn size_smaller_than_image_errors() {
270 let result = calculate_extend_region(
271 800,
272 600,
273 &ExtendMode::Size {
274 width: 500,
275 height: 500,
276 },
277 );
278 assert!(result.is_err());
279 }
280
281 #[test]
282 fn size_width_smaller_errors() {
283 let result = calculate_extend_region(
284 800,
285 600,
286 &ExtendMode::Size {
287 width: 700,
288 height: 600,
289 },
290 );
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn size_zero_errors() {
296 let result = calculate_extend_region(
297 800,
298 600,
299 &ExtendMode::Size {
300 width: 0,
301 height: 0,
302 },
303 );
304 assert!(result.is_err());
305 }
306
307 use crate::codec::ImageData;
310
311 fn create_test_image(width: u32, height: u32) -> ImageData {
312 let data = vec![128u8; (width * height * 4) as usize];
313 ImageData::new(width, height, data)
314 }
315
316 #[test]
317 fn extend_returns_correct_dimensions() {
318 let img = create_test_image(200, 100);
319 let result = extend(
320 &img,
321 &ExtendMode::AspectRatio {
322 width: 1,
323 height: 1,
324 },
325 &FillColor::Solid([255, 255, 255, 255]),
326 )
327 .unwrap();
328 assert_eq!(result.width, 200);
329 assert_eq!(result.height, 200);
330 assert_eq!(result.data.len(), (200 * 200 * 4) as usize);
331 }
332
333 #[test]
334 fn extend_fills_with_solid_color() {
335 let img = create_test_image(2, 2);
336 let result = extend(
337 &img,
338 &ExtendMode::Size {
339 width: 4,
340 height: 4,
341 },
342 &FillColor::Solid([255, 0, 0, 255]),
343 )
344 .unwrap();
345 assert_eq!(result.data[0], 255); assert_eq!(result.data[1], 0); assert_eq!(result.data[2], 0); assert_eq!(result.data[3], 255); }
351
352 #[test]
353 fn extend_fills_with_transparent() {
354 let img = create_test_image(2, 2);
355 let result = extend(
356 &img,
357 &ExtendMode::Size {
358 width: 4,
359 height: 4,
360 },
361 &FillColor::Transparent,
362 )
363 .unwrap();
364 assert_eq!(&result.data[0..4], &[0, 0, 0, 0]);
365 }
366
367 #[test]
368 fn extend_preserves_pixel_data() {
369 let data = vec![10, 20, 30, 255, 40, 50, 60, 255];
371 let img = ImageData::new(2, 1, data);
372
373 let result = extend(
375 &img,
376 &ExtendMode::Size {
377 width: 4,
378 height: 3,
379 },
380 &FillColor::Solid([0, 0, 0, 0]),
381 )
382 .unwrap();
383
384 let stride = 4 * 4; let offset = 1 * stride + 1 * 4; assert_eq!(&result.data[offset..offset + 4], &[10, 20, 30, 255]);
387
388 let offset2 = 1 * stride + 2 * 4;
389 assert_eq!(&result.data[offset2..offset2 + 4], &[40, 50, 60, 255]);
390 }
391
392 #[test]
393 fn extend_noop_when_already_matching() {
394 let img = create_test_image(200, 100);
395 let result = extend(
396 &img,
397 &ExtendMode::AspectRatio {
398 width: 2,
399 height: 1,
400 },
401 &FillColor::Solid([255, 255, 255, 255]),
402 )
403 .unwrap();
404 assert_eq!(result.width, 200);
405 assert_eq!(result.height, 100);
406 assert_eq!(result.data, img.data);
407 }
408}