Skip to main content

Merge pull request #1 from alexwlchan/choose-dimensions

ID
10196ad
date
2024-08-19 23:57:03+00:00
author
Alex Chan <alex@alexwlchan.net>
parents
6cf1c8a, 5aadb6d
message
Merge pull request #1 from alexwlchan/choose-dimensions

Extract the code for choosing the thumbnail dimensions
changed files
4 files, 145 additions, 23 deletions

Changed files

README.md (720) → README.md (595)

diff --git a/README.md b/README.md
index ad49124..98a99ae 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,6 @@ create-thumbnail PATH [--width=WIDTH | --height=HEIGHT] --out-dir=OUT_DIR
 
 focusing on a small piece of code makes it better
 
-* pull out dimension choosing
-    -> test that code
-        image which is smaller in width/height
-        rounding errors?
-
 * CLI:
     -> version
     -> help

src/get_thumbnail_dimensions.rs (0) → src/get_thumbnail_dimensions.rs (4497)

diff --git a/src/get_thumbnail_dimensions.rs b/src/get_thumbnail_dimensions.rs
new file mode 100644
index 0000000..d596f7c
--- /dev/null
+++ b/src/get_thumbnail_dimensions.rs
@@ -0,0 +1,139 @@
+use std::path::PathBuf;
+
+use image::GenericImageView;
+
+/// Given the path to the original image and the target width/height,
+/// calculate the dimensions of the new image.
+///
+/// This function expects that exactly one of width/height will be
+/// specified, and then the image will be resized to be no larger
+/// than this dimension.
+///
+/// If the image is smaller than the target dimensions, it will be
+/// left as-is.
+///
+/// TODO: Are there any scenarios in which this division could round
+/// one dimension of an image to zero, if it was very tall or very long?
+///
+pub fn get_thumbnail_dimensions(
+    path: &PathBuf,
+    target_width: Option<u32>,
+    target_height: Option<u32>,
+) -> Result<(u32, u32), image::error::ImageError> {
+    // Assert that exactly one of `target_width` and `target_height` are defined
+    assert!(target_width.is_some() || target_height.is_some());
+    assert!(target_width.is_none() || target_height.is_none());
+
+    // Open the image, and compare its dimensions to the target
+    let img = image::open(path)?;
+
+    // Calculate the new width/height of the image
+    let (new_width, new_height) = match (target_width, target_height) {
+        (Some(w), None) if w >= img.width() => img.dimensions(),
+        (None, Some(h)) if h >= img.height() => img.dimensions(),
+
+        (Some(w), None) => (w, w * img.height() / img.width()),
+        (None, Some(h)) => (h * img.width() / img.height(), h),
+
+        _ => unreachable!(),
+    };
+
+    Ok((new_width, new_height))
+}
+
+#[cfg(test)]
+mod test_get_thumbnail_dimensions {
+    use std::path::PathBuf;
+
+    use super::*;
+
+    // The `red.png` file used in this test has dimensions 100×200
+
+    #[test]
+    fn with_target_width() {
+        let p = PathBuf::from("src/tests/red.png");
+
+        let target_width: Option<u32> = Some(50);
+        let target_height: Option<u32> = None;
+
+        let dimensions = get_thumbnail_dimensions(&p, target_width, target_height);
+        assert_eq!(dimensions.unwrap(), (50, 100));
+    }
+
+    #[test]
+    fn with_target_height() {
+        let p = PathBuf::from("src/tests/red.png");
+
+        let target_width: Option<u32> = None;
+        let target_height: Option<u32> = Some(100);
+
+        let dimensions = get_thumbnail_dimensions(&p, target_width, target_height);
+        assert_eq!(dimensions.unwrap(), (50, 100));
+    }
+
+    #[test]
+    fn leaves_image_as_is_if_target_width_greater_than_width() {
+        let p = PathBuf::from("src/tests/red.png");
+
+        let target_width: Option<u32> = Some(500);
+        let target_height: Option<u32> = None;
+
+        let dimensions = get_thumbnail_dimensions(&p, target_width, target_height);
+        assert_eq!(dimensions.unwrap(), (100, 200));
+    }
+
+    #[test]
+    fn leaves_image_as_is_if_target_width_equal_to_width() {
+        let p = PathBuf::from("src/tests/red.png");
+
+        let target_width: Option<u32> = Some(500);
+        let target_height: Option<u32> = None;
+
+        let dimensions = get_thumbnail_dimensions(&p, target_width, target_height);
+        assert_eq!(dimensions.unwrap(), (100, 200));
+    }
+
+    #[test]
+    fn leaves_image_as_is_if_target_height_greater_than_height() {
+        let p = PathBuf::from("src/tests/red.png");
+
+        let target_width: Option<u32> = None;
+        let target_height: Option<u32> = Some(500);
+
+        let dimensions = get_thumbnail_dimensions(&p, target_width, target_height);
+        assert_eq!(dimensions.unwrap(), (100, 200));
+    }
+
+    #[test]
+    fn leaves_image_as_is_if_target_height_equal_to_height() {
+        let p = PathBuf::from("src/tests/red.png");
+
+        let target_width: Option<u32> = None;
+        let target_height: Option<u32> = Some(500);
+
+        let dimensions = get_thumbnail_dimensions(&p, target_width, target_height);
+        assert_eq!(dimensions.unwrap(), (100, 200));
+    }
+
+    #[test]
+    fn errors_if_image_does_not_exist() {
+        let p = PathBuf::from("src/tests/doesnotexist.png");
+
+        let target_width: Option<u32> = Some(50);
+        let target_height: Option<u32> = None;
+
+        let dimensions = get_thumbnail_dimensions(&p, target_width, target_height);
+        assert!(dimensions.is_err());
+    }
+
+    #[test]
+    fn errors_if_cannot_read_image() {
+        let p = PathBuf::from("README.md");
+
+        let target_width: Option<u32> = Some(50);
+        let target_height: Option<u32> = None;
+
+        let dimensions = get_thumbnail_dimensions(&p, target_width, target_height);
+        assert!(dimensions.is_err());
+    }
+}

src/main.rs (2841) → src/main.rs (2427)

diff --git a/src/main.rs b/src/main.rs
index babbf5b..74d793c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,10 +4,11 @@ use std::process::Command;
 
 use clap::{ArgGroup, Parser};
 use image::imageops::FilterType;
-use image::GenericImageView;
 
+mod get_thumbnail_dimensions;
 mod is_animated_gif;
 
+use crate::get_thumbnail_dimensions::get_thumbnail_dimensions;
 use crate::is_animated_gif::is_animated_gif;
 
 /// Create a thumbnail for the image, and return the relative path of
@@ -23,23 +24,8 @@ pub fn create_thumbnail(
     // TODO: Does this check do what I think?
     assert!(*path != thumbnail_path);
 
-    let img =
-        image::open(path).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
-
-    // Assert that exactly one of `width` and `height` are defined
-    assert!(width.is_some() || height.is_some());
-    assert!(width.is_none() || height.is_none());
-
-    // Calculate the new width/height of the image
-    let (new_width, new_height) = match (width, height) {
-        (Some(w), None) if w >= img.width() => img.dimensions(),
-        (None, Some(h)) if h >= img.height() => img.dimensions(),
-
-        (Some(w), None) => (w, w * img.height() / img.width()),
-        (None, Some(h)) => (h * img.width() / img.height(), h),
-
-        _ => unreachable!(),
-    };
+    let (new_width, new_height) = get_thumbnail_dimensions(&path, width, height)
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
 
     println!("w = {:?}, h = {:?}", new_width, new_height);
 
@@ -65,6 +51,8 @@ pub fn create_thumbnail(
         Ok(mp4_path)
     } else {
         println!("thumbnail_path = {:?}", thumbnail_path);
+        let img = image::open(path).unwrap();
+
         img.resize(new_width, new_height, FilterType::Lanczos3)
             .save(&thumbnail_path)
             .unwrap();

src/tests/red.png (0) → src/tests/red.png (1835)

diff --git a/src/tests/red.png b/src/tests/red.png
new file mode 100644
index 0000000..452b5a5
Binary files /dev/null and b/src/tests/red.png differ