You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
5.3 KiB
Rust
175 lines
5.3 KiB
Rust
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<Self, Self::Err> {
|
|
match s {
|
|
"small" => Ok(SizeOption::Small),
|
|
"medium" => Ok(SizeOption::Medium),
|
|
"large" => Ok(SizeOption::Large),
|
|
_ => Ok(SizeOption::Small),
|
|
//default
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for Mode {
|
|
type Err = ImagixError;
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
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));
|
|
|
|
// 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(())
|
|
}
|
|
|
|
// The program supports only files of type jpg/JPG and png/PNG.
|
|
pub fn get_image_files(src_folder: PathBuf) -> Result<Vec<PathBuf>, 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::<Result<Vec<_>, 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(_) => println!("Successful resize of single image"),
|
|
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());
|
|
}
|
|
}
|