diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..02fed40 Binary files /dev/null and b/.DS_Store differ diff --git a/chapter4/.DS_Store b/chapter4/.DS_Store new file mode 100644 index 0000000..748a747 Binary files /dev/null and b/chapter4/.DS_Store differ diff --git a/chapter4/.gitignore b/chapter4/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/chapter4/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/chapter4/Cargo.toml b/chapter4/Cargo.toml new file mode 100644 index 0000000..cbbb971 --- /dev/null +++ b/chapter4/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "imagecli" +version = "0.1.0" +authors = ["peshwar9"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "imagix" +path = "src/imagix/mod.rs" + +[[bin]] +name = "imagecli" +path = "src/imagecli.rs" + +[dependencies] +image = "0.23.7" +structopt = "0.3.15" diff --git a/chapter4/src/imagecli.rs b/chapter4/src/imagecli.rs new file mode 100644 index 0000000..3838897 --- /dev/null +++ b/chapter4/src/imagecli.rs @@ -0,0 +1,63 @@ +mod imagix; +use ::imagix::error::ImagixError; +use ::imagix::resize::{process_resize_request, Mode, SizeOption}; +use ::imagix::stats::get_stats; +use std::path::PathBuf; +use structopt::StructOpt; +// Define commandline arguments in a struct + +#[derive(StructOpt, Debug)] +#[structopt( + name = "resize", + about = "This is a tool for image resizing and stats", + help = "Specify subcommand resize or stats. For help, type imagecli resize --help or imagecli stats --help" +)] +enum Commandline { + #[structopt(help = "Specify size(small/medium/large) , mode(single/all) and srcfolder")] + Resize { + #[structopt(long)] + size: SizeOption, + #[structopt(long)] + mode: Mode, + #[structopt(long, parse(from_os_str))] + srcfolder: PathBuf, + }, + #[structopt(help = "Specify srcfolder")] + Stats { + #[structopt(long, parse(from_os_str))] + srcfolder: PathBuf, + }, +} + +fn main() { + let args: Commandline = Commandline::from_args(); + match args { + Commandline::Resize { + size, + mode, + srcfolder, + } => { + let mut src_folder = srcfolder; + match process_resize_request(size, mode, &mut src_folder) { + Ok(_) => println!("Image(s) resized successfully"), + Err(e) => match e { + ImagixError::FileIOError(e) => println!("{}", e), + ImagixError::UserInputError(e) => println!("{}", e), + ImagixError::ImageResizingError(e) => println!("{}", e), + _ => println!("Error in processing"), + }, + }; + } + Commandline::Stats { srcfolder } => match get_stats(srcfolder) { + Ok((count, size)) => println!( + "Found {:?} image files with aggregate size of {:?} MB", + count, size + ), + Err(e) => match e { + ImagixError::FileIOError(e) => println!("{}", e), + ImagixError::UserInputError(e) => println!("{}", e), + _ => println!("Error in processing"), + }, + }, + } +} diff --git a/chapter4/src/imagix/error.rs b/chapter4/src/imagix/error.rs new file mode 100644 index 0000000..901c83d --- /dev/null +++ b/chapter4/src/imagix/error.rs @@ -0,0 +1,39 @@ +use image::error; +use std::convert::From; +use std::{fmt, io}; + +#[derive(Debug)] +pub enum ImagixError { + FileIOError(String), + UserInputError(String), + ImageResizingError(String), + FormatError(String), +} + +impl From for ImagixError { + fn from(error: io::Error) -> Self { + ImagixError::FileIOError("Error in File I/O".to_string()) + } +} + +impl From for ImagixError { + fn from(error: error::ImageError) -> Self { + ImagixError::ImageResizingError("Error in image processing".to_string()) + } +} + +impl From for ImagixError { + fn from(error: io::ErrorKind) -> Self { + ImagixError::UserInputError("Error in user input".to_string()) + } +} + +impl fmt::Display for ImagixError { + fn fmt(&self, out: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!( + out, + "{}", + ImagixError::FormatError("Error occurred".to_string()) + ) + } +} diff --git a/chapter4/src/imagix/mod.rs b/chapter4/src/imagix/mod.rs new file mode 100644 index 0000000..2e0e46f --- /dev/null +++ b/chapter4/src/imagix/mod.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod resize; +pub mod stats; diff --git a/chapter4/src/imagix/resize.rs b/chapter4/src/imagix/resize.rs new file mode 100644 index 0000000..6fc952a --- /dev/null +++ b/chapter4/src/imagix/resize.rs @@ -0,0 +1,176 @@ +use image::ImageFormat; +use std::path::PathBuf; +use std::result::Result; +use std::str::FromStr; +use std::time::{Duration, Instant}; +use std::{fmt, fs, io}; + +use super::error::ImagixError; + +struct Elapsed(Duration); + +impl Elapsed { + fn from(start: &Instant) -> Self { + Elapsed(start.elapsed()) + } +} +#[derive(Debug)] +pub enum SizeOption { + Small, + Medium, + Large, +} +impl fmt::Display for Elapsed { + fn fmt(&self, out: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match (self.0.as_secs(), self.0.subsec_nanos()) { + (0, n) if n < 1000 => write!(out, "{} ns", n), + (0, n) if n < 1000_000 => write!(out, "{} µs", n / 1000), + (0, n) => write!(out, "{} ms", n / 1000_000), + (s, n) if s < 10 => write!(out, "{}.{:02} s", s, n / 10_000_000), + (s, _) => write!(out, "{} s", s), + } + } +} +impl FromStr for SizeOption { + type Err = ImagixError; + fn from_str(s: &str) -> Result { + match s { + "small" => Ok(SizeOption::Small), + "medium" => Ok(SizeOption::Medium), + "large" => Ok(SizeOption::Large), + _ => Err(ImagixError::UserInputError( + "Invalid input for size".to_string(), + )), + //default + } + } +} + +impl FromStr for Mode { + type Err = ImagixError; + fn from_str(s: &str) -> Result { + match s { + "single" => Ok(Mode::Single), + "all" => Ok(Mode::All), + _ => Err(ImagixError::UserInputError( + "Wrong value for mode".to_string(), + )), + } + } +} +#[derive(Debug, PartialEq)] +pub enum Mode { + Single, + All, +} + +pub fn process_resize_request( + size: SizeOption, + mode: Mode, + src_folder: &mut PathBuf, +) -> Result<(), ImagixError> { + let size = match size { + SizeOption::Small => 200, + SizeOption::Medium => 400, + SizeOption::Large => 800, + }; + let _ = match mode { + Mode::All => resize_all(size, src_folder)?, + Mode::Single => resize_single(size, src_folder)?, + }; + Ok(()) +} + +fn resize_single(size: u32, src_folder: &mut PathBuf) -> Result<(), ImagixError> { + let mut src_folder = src_folder; + // Get file stem from src_folder + resize_image(size, &mut src_folder)?; + Ok(()) +} + +fn resize_all(size: u32, src_folder: &mut PathBuf) -> Result<(), ImagixError> { + if let Ok(entries) = get_image_files(src_folder.to_path_buf()) { + for mut entry in entries { + resize_image(size, &mut entry)?; + } + }; + Ok(()) +} + +fn resize_image(size: u32, src_folder: &mut PathBuf) -> Result<(), ImagixError> { + // Construct destination file name th .png extension + let new_file_name = src_folder + .file_stem() + .unwrap() + .to_str() + .ok_or(std::io::ErrorKind::InvalidInput) + // .map(|f| format!("{}-{:?}.png", f, size)); + .map(|f| format!("{}.png", f)); + + // Construct path to destination folder i.e. create /tmp under source folder if not exists + let mut dest_folder = src_folder.clone(); + dest_folder.pop(); + dest_folder.push("tmp/"); + if !dest_folder.exists() { + fs::create_dir(&dest_folder)?; + } + dest_folder.pop(); + dest_folder.push("tmp/tmp.png"); + dest_folder.set_file_name(new_file_name?.as_str()); + + // Open source image file, scale it to desired size and write output to destination-folder/destination-file + let timer = Instant::now(); + let img = image::open(&src_folder)?; + let scaled = img.thumbnail(size, size); + let mut output = fs::File::create(&dest_folder)?; + scaled.write_to(&mut output, ImageFormat::Png)?; + println!( + "Thumbnailed file: {:?} to size {}x{} in {}. Output file in {:?}", + src_folder, + size, + size, + Elapsed::from(&timer), + dest_folder + ); + Ok(()) +} + +pub fn get_image_files(src_folder: PathBuf) -> Result, ImagixError> { + let entries = fs::read_dir(src_folder) + .map_err(|e| ImagixError::UserInputError("Invalid source folder".to_string()))? + .map(|res| res.map(|e| e.path())) + .collect::, io::Error>>()? + .into_iter() + .filter(|r| { + r.extension() == Some("JPG".as_ref()) + || r.extension() == Some("jpg".as_ref()) + || r.extension() == Some("PNG".as_ref()) + || r.extension() == Some("png".as_ref()) + }) + .collect(); + Ok(entries) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_single_image_resize() { + let mut path = PathBuf::from("/tmp/images/image1.jpg"); + let destination_path = PathBuf::from("/tmp/images/tmp/image1.png"); + match process_resize_request(SizeOption::Small, Mode::Single, &mut path) { + Ok(dest) => assert_eq!(true, destination_path.exists()), + Err(e) => println!("Error in single image: {:?}", e), + } + assert_eq!(true, destination_path.exists()); + } + #[test] + fn test_multiple_image_resize() { + let mut path = PathBuf::from("/tmp/images/"); + let _res = process_resize_request(SizeOption::Small, Mode::All, &mut path); + let destination_path1 = PathBuf::from("/tmp/images/tmp/image1.png"); + let destination_path2 = PathBuf::from("/tmp/images/tmp/image2.png"); + assert_eq!(true, destination_path1.exists()); + assert_eq!(true, destination_path2.exists()); + } +} diff --git a/chapter4/src/imagix/stats.rs b/chapter4/src/imagix/stats.rs new file mode 100644 index 0000000..4184dd1 --- /dev/null +++ b/chapter4/src/imagix/stats.rs @@ -0,0 +1,24 @@ +use super::error::ImagixError; +use super::resize::get_image_files; +use std::path::PathBuf; + +pub fn get_stats(src_folder: PathBuf) -> Result<(usize, u64), ImagixError> { + let image_files = get_image_files(src_folder.to_path_buf())?; + let size = image_files + .iter() + .map(move |f| f.metadata().unwrap().len()) + .sum::(); + Ok((image_files.len(), size / 1000000)) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_get_stats() { + let path = PathBuf::from("/tmp/images"); + let (count, size) = get_stats(path).unwrap(); + assert_eq!(count, 2); + assert_eq!(size, 8); + } +}