Skip to main content

src/main.rs

1#![deny(warnings)]
3use std::path::PathBuf;
5use clap::Parser;
7mod create_parent_directory;
8mod create_thumbnail;
9mod errors;
10mod get_thumbnail_dimensions;
11mod is_animated_gif;
13use crate::create_thumbnail::create_thumbnail;
14use crate::get_thumbnail_dimensions::TargetDimension;
16#[derive(Debug, Parser)]
17#[clap(version, about)]
18struct Cli {
19 /// Path to the image to be thumbnailed
20 path: PathBuf,
22 /// Path to the directory to save the thumbnail in
23 #[arg(long)]
24 out_dir: PathBuf,
26 /// Height of the thumbnail to create
27 #[arg(long)]
28 height: Option<u32>,
30 /// Width of the thumbnail to create
31 #[arg(long)]
32 width: Option<u32>,
35fn main() {
36 let cli = Cli::parse();
38 let target = match (cli.width, cli.height) {
39 (Some(w), Some(h)) => TargetDimension::BoundingBox(w, h),
40 (Some(w), None) => TargetDimension::MaxWidth(w),
41 (None, Some(h)) => TargetDimension::MaxHeight(h),
42 _ => {
43 eprintln!(
44 "Failed to create thumbnail: you must pass at least one of --width or --height"
45 );
46 std::process::exit(1);
47 }
48 };
50 match create_thumbnail(&cli.path, &cli.out_dir, target) {
51 Ok(thumbnail_path) => print!("{}", thumbnail_path.display()),
52 Err(e) => {
53 eprintln!("{}", e);
54 std::process::exit(1);
55 }
56 };
59#[expect(
60 deprecated,
61 reason = "cargo_bin is deprecated, cargo_bin! is not, `use` does not differentiate them. See https://github.com/assert-rs/assert_cmd/issues/258"
62)]
63#[cfg(test)]
64mod test_cli {
65 use std::path::PathBuf;
67 use assert_cmd::Command;
68 use predicates::prelude::*;
70 use crate::test_utils::get_dimensions;
72 #[test]
73 fn it_creates_a_thumbnail_with_max_width() {
74 Command::cargo_bin("create_thumbnail")
75 .unwrap()
76 .args(&["src/tests/red.png", "--width=50", "--out-dir=/tmp"])
77 .assert()
78 .success()
79 .stdout("/tmp/red.png")
80 .stderr("");
82 assert_eq!(get_dimensions(&PathBuf::from("/tmp/red.png")), (50, 100));
83 }
85 #[test]
86 fn it_creates_a_thumbnail_with_max_height() {
87 Command::cargo_bin("create_thumbnail")
88 .unwrap()
89 .args(&["src/tests/noise.jpg", "--height=128", "--out-dir=/tmp"])
90 .assert()
91 .success()
92 .stdout("/tmp/noise.jpg")
93 .stderr("");
95 assert_eq!(get_dimensions(&PathBuf::from("/tmp/noise.jpg")), (64, 128));
96 }
98 #[test]
99 fn it_creates_a_thumbnail_with_a_bounding_box() {
100 Command::cargo_bin("create_thumbnail")
101 .unwrap()
102 .args(&[
103 "src/tests/noise.jpg",
104 "--width=64",
105 "--height=64",
106 "--out-dir=/tmp",
107 ])
108 .assert()
109 .success()
110 .stdout("/tmp/noise.jpg")
111 .stderr("");
113 assert_eq!(get_dimensions(&PathBuf::from("/tmp/noise.jpg")), (32, 64));
114 }
116 #[test]
117 fn it_fails_if_you_pass_neither_width_nor_height() {
118 Command::cargo_bin("create_thumbnail")
119 .unwrap()
120 .args(&["src/tests/red.png", "--out-dir=/tmp"])
121 .assert()
122 .failure()
123 .code(1)
124 .stdout("")
125 .stderr(
126 "Failed to create thumbnail: you must pass at least one of --width or --height\n",
127 );
128 }
130 #[test]
131 fn it_fails_if_you_pass_a_non_existent_file() {
132 Command::cargo_bin("create_thumbnail")
133 .unwrap()
134 .args(&["doesnotexist.txt", "--width=50", "--out-dir=/tmp"])
135 .assert()
136 .failure()
137 .code(1)
138 .stdout("")
139 .stderr("Failed to open image: No such file or directory (os error 2)\n");
140 }
142 #[test]
143 fn it_fails_if_you_pass_a_non_image() {
144 Command::cargo_bin("create_thumbnail")
145 .unwrap()
146 .args(&["Cargo.toml", "--width=50", "--out-dir=/tmp"])
147 .assert()
148 .failure()
149 .code(1)
150 .stdout("")
151 .stderr("Failed to open image: The file extension `.\"toml\"` was not recognized as an image format\n");
152 }
154 // TODO: Improve this error message.
155 //
156 // It's good to know the tool won't completely break when this happens, but ideally
157 // we'd return a more meaningful error message in this case.
158 #[test]
159 fn it_fails_if_out_dir_is_a_file() {
160 Command::cargo_bin("create_thumbnail")
161 .unwrap()
162 .args(&["src/images/noise.jpg", "--width=50", "--out-dir=README.md"])
163 .assert()
164 .failure()
165 .code(1)
166 .stdout("")
167 .stderr("I/O error: File exists (os error 17)\n");
168 }
170 #[test]
171 fn it_fails_if_you_try_to_overwrite_the_original_file() {
172 Command::cargo_bin("create_thumbnail")
173 .unwrap()
174 .args(&["src/images/noise.jpg", "--width=50", "--out-dir=src/images"])
175 .assert()
176 .failure()
177 .code(1)
178 .stdout("")
179 .stderr("Cannot write thumbnail to the same path as the original image\n");
180 }
182 #[test]
183 fn it_prints_the_version() {
184 // Match strings like `create_thumbnail 1.2.3`
185 let is_version_string =
186 predicate::str::is_match(r"^create_thumbnail [0-9]+\.[0-9]+\.[0-9]+\n$").unwrap();
188 Command::cargo_bin("create_thumbnail")
189 .unwrap()
190 .arg("--version")
191 .assert()
192 .success()
193 .stdout(is_version_string)
194 .stderr("");
195 }
197 #[test]
198 fn it_prints_the_help() {
199 // Match strings like `create_thumbnail 1.2.3`
200 let is_help_text =
201 predicate::str::is_match(r"create_thumbnail \[OPTIONS\] --out-dir").unwrap();
203 Command::cargo_bin("create_thumbnail")
204 .unwrap()
205 .arg("--help")
206 .assert()
207 .success()
208 .stdout(is_help_text)
209 .stderr("");
210 }
213#[cfg(test)]
214pub mod test_utils {
215 use std::path::PathBuf;
217 use image::GenericImageView;
219 /// Return a path to a temporary directory to use for testing.
220 ///
221 /// This function does *not* create the directory, just the path.
222 pub fn test_dir() -> PathBuf {
223 let tmp_dir = tempfile::tempdir().unwrap();
225 tmp_dir.path().to_owned()
226 }
228 /// Return the dimensions for an image.
229 pub fn get_dimensions(path: &PathBuf) -> (u32, u32) {
230 let img = image::open(path).unwrap();
232 img.dimensions()
233 }