Skip to main content

src/get_thumbnail_dimensions.rs

1use std::path::PathBuf;
3use image::GenericImageView;
5use crate::errors::ThumbnailError;
7/// Represents the target dimensions of the thumbnail.
8pub enum TargetDimension {
9 BoundingBox(u32, u32),
10 MaxWidth(u32),
11 MaxHeight(u32),
14/// Given the path to the original image and the target width/height,
15/// calculate the dimensions of the new image.
16///
17/// If the image is smaller than the target dimensions, it will be
18/// left as-is.
19///
20/// TODO: Are there any scenarios in which this division could round
21/// one dimension of an image to zero, if it was very tall or very long?
22///
23pub fn get_thumbnail_dimensions(
24 path: &PathBuf,
25 target: TargetDimension,
26) -> Result<(u32, u32), ThumbnailError> {
27 let img = image::open(path)?;
29 Ok(calculate_dimensions(img.dimensions(), target))
32// Calculate the dimensions of the new image, given the original dimensions
33// and target dimensions.
34fn calculate_dimensions(dimensions: (u32, u32), target: TargetDimension) -> (u32, u32) {
35 let (img_w, img_h) = dimensions;
37 match target {
38 TargetDimension::MaxWidth(max_w) if max_w >= img_w => dimensions,
39 TargetDimension::MaxHeight(max_h) if max_h >= img_h => dimensions,
41 TargetDimension::MaxWidth(max_w) => (
42 max_w,
43 ((max_w as f64) * (img_h as f64) / (img_w as f64)).round() as u32,
44 ),
45 TargetDimension::MaxHeight(max_h) => (
46 ((max_h as f64) * (img_w as f64) / (img_h as f64)).round() as u32,
47 max_h,
48 ),
50 // The bounding box has a wider aspect ratio than the original image,
51 // so filter by height.
52 TargetDimension::BoundingBox(max_w, max_h)
53 if (max_w as f64) / (max_h as f64) >= (img_w as f64) / (img_h as f64) =>
54 {
55 calculate_dimensions(dimensions, TargetDimension::MaxHeight(max_h))
56 }
57 TargetDimension::BoundingBox(max_w, _) => {
58 calculate_dimensions(dimensions, TargetDimension::MaxWidth(max_w))
59 }
60 }
63#[cfg(test)]
64mod test_get_thumbnail_dimensions {
65 use std::path::PathBuf;
67 use super::*;
69 macro_rules! get_thumb_dimensions_tests {
70 ($($name:ident: $value:expr,)*) => {
71 $(
72 #[test]
73 fn $name() {
74 let (input, target, expected) = $value;
76 let dimensions = calculate_dimensions(input, target);
77 assert_eq!(dimensions, expected);
78 }
79 )*
80 }
81 }
83 get_thumb_dimensions_tests! {
84 width_lt: ((100, 200), TargetDimension::MaxWidth(50), ( 50, 100)),
85 width_eq: ((100, 200), TargetDimension::MaxWidth(100), (100, 200)),
86 width_gt: ((100, 200), TargetDimension::MaxWidth(200), (100, 200)),
88 height_lt: ((100, 200), TargetDimension::MaxHeight(100), ( 50, 100)),
89 height_eq: ((100, 200), TargetDimension::MaxHeight(200), (100, 200)),
90 height_gt: ((100, 200), TargetDimension::MaxHeight(400), (100, 200)),
92 // bounding box which is larger than the image in one or both
93 // dimensions
94 bbox_larger_w: ((100, 200), TargetDimension::BoundingBox(500, 200), (100, 200)),
95 bbox_larger_h: ((100, 200), TargetDimension::BoundingBox(100, 500), (100, 200)),
96 bbox_larger_wh: ((100, 200), TargetDimension::BoundingBox(500, 500), (100, 200)),
98 // bounding box with an equal aspect ratio to the image
99 bbox_equal_lt: ((100, 200), TargetDimension::BoundingBox( 50, 100), ( 50, 100)),
100 bbox_equal_eq: ((100, 200), TargetDimension::BoundingBox(100, 200), (100, 200)),
101 bbox_equal_gt: ((100, 200), TargetDimension::BoundingBox(200, 400), (100, 200)),
103 // bounding box which is skinnier than the image
104 bbox_skinnier_lt: ((100, 200), TargetDimension::BoundingBox(10, 100), (10, 20)),
105 bbox_skinnier_eq: ((100, 200), TargetDimension::BoundingBox(20, 200), (20, 40)),
106 bbox_skinnier_gt: ((100, 200), TargetDimension::BoundingBox(40, 400), (40, 80)),
108 // bounding box which is wider than the image
109 bbox_wider_lt: ((100, 200), TargetDimension::BoundingBox(100, 20), (10, 20)),
110 bbox_wider_eq: ((100, 200), TargetDimension::BoundingBox(200, 40), (20, 40)),
111 bbox_wider_gt: ((100, 200), TargetDimension::BoundingBox(400, 80), (40, 80)),
113 // case to ensure we do floating point division correctly, and
114 // aren't making rounding errors
115 fp_width: ((500, 333), TargetDimension::MaxWidth(300), (300, 200)),
116 fp_height: ((333, 500), TargetDimension::MaxHeight(300), (200, 300)),
117 }
119 #[test]
120 fn errors_if_image_does_not_exist() {
121 let p = PathBuf::from("src/tests/doesnotexist.png");
123 let target = TargetDimension::MaxWidth(50);
125 let dimensions = get_thumbnail_dimensions(&p, target);
126 assert!(dimensions.is_err());
127 }
129 #[test]
130 fn errors_if_cannot_read_image() {
131 let p = PathBuf::from("README.md");
133 let target = TargetDimension::MaxWidth(50);
135 let dimensions = get_thumbnail_dimensions(&p, target);
136 assert!(dimensions.is_err());
137 }