use std::error::Error;
use std::fs;
use std::io::{BufWriter, StdoutLock, Write, stdout};
use std::path::{Path, PathBuf};
use fast_image_resize as fr;
use fast_image_resize::ResizeOptions;
use image::ImageReader;
use image::codecs::{jpeg::JpegEncoder, png::PngEncoder};
use image::{ExtendedColorType, ImageEncoder};
use oxipng::{Options, optimize_from_memory};
use pathdiff::diff_paths;
use walkdir::WalkDir;
fn get_file_list(src_dir: &Path, recursive: bool) -> impl Iterator<Item = walkdir::DirEntry> {
match recursive {
true => WalkDir::new(src_dir).into_iter().filter_map(Result::ok),
false => WalkDir::new(src_dir)
.min_depth(0)
.max_depth(1)
.into_iter()
.filter_map(Result::ok),
}
.filter(|entry| entry.file_type().is_file())
}
fn resize_image(src_path: &Path) -> Result<Vec<u8>, Box<dyn Error>> {
let img = ImageReader::open(src_path)?
.with_guessed_format()?
.decode()?;
let width = img.width();
let height = img.height();
let img = &*img.to_rgba8().into_raw();
let src_image = fr::images::ImageRef::new(width, height, img, fr::PixelType::U8x4)?;
let dst_width = width / 2;
let dst_height = height / 2;
let mut dst_image = fr::images::Image::new(dst_width, dst_height, src_image.pixel_type());
let mut resizer = fr::Resizer::new();
let resize_options = ResizeOptions {
algorithm: fr::ResizeAlg::Convolution(fr::FilterType::Lanczos3),
cropping: Default::default(),
mul_div_alpha: true,
};
resizer.resize(&src_image, &mut dst_image, &resize_options)?;
let mut result_buf = BufWriter::new(Vec::new());
let extension = src_path
.extension()
.expect("Expected the file to have an extension at this point!");
match extension.to_string_lossy().to_lowercase().as_str() {
"jpg" | "jpeg" => JpegEncoder::new(&mut result_buf).write_image(
dst_image.buffer(),
dst_width,
dst_height,
ExtendedColorType::Rgba8, )?,
"png" => PngEncoder::new(&mut result_buf).write_image(
dst_image.buffer(),
dst_width,
dst_height,
ExtendedColorType::Rgba8,
)?,
_ => panic!("Unsupported image format (file extension): {:?}", extension),
}
let result = result_buf.into_inner()?;
Ok(result)
}
fn get_image_data(
src_path: &Path,
resize: bool,
lock: &mut StdoutLock,
) -> Result<Vec<u8>, std::io::Error> {
match resize {
true => match resize_image(src_path) {
Ok(data) => Ok(data),
Err(err) => {
writeln!(
lock,
"\t[ERROR] Trying to resize \"{}\" failed with the following error: {}.\n\
\tWill attempt to reduce the file size of the image without resizing the image.",
src_path.display(),
err
)
.expect("Failed to write to stdout.");
fs::read(src_path)
}
},
false => fs::read(src_path),
}
}
fn process_jpeg(
src_path: &Path,
dst_path: &Path,
resize: bool,
quality: i32,
lock: &mut StdoutLock,
) -> Result<(), Box<dyn Error>> {
let image_data = get_image_data(src_path, resize, lock)?;
let img: image::RgbaImage = turbojpeg::decompress_image(&image_data)?;
let optimized = turbojpeg::compress_image(&img, quality, turbojpeg::Subsamp::Sub2x2)?;
fs::write(dst_path, &optimized)?;
Ok(())
}
fn process_png(
src_path: &Path,
dst_path: &Path,
resize: bool,
lock: &mut StdoutLock,
) -> Result<(), Box<dyn Error>> {
let image_data = get_image_data(src_path, resize, lock)?;
let optimized = optimize_from_memory(&image_data, &Options::default())?;
fs::write(dst_path, optimized)?;
Ok(())
}
#[inline]
fn print_success(src_path: &Path, dst_path: &Path, different_paths: bool, lock: &mut StdoutLock) {
match different_paths {
true => writeln!(
lock,
"Reduced \"{}\" to \"{}\".",
src_path.display(),
dst_path.display()
)
.expect("Failed to write to stdout."),
false => writeln!(lock, "Reduced \"{}\".", src_path.display())
.expect("Failed to write to stdout."),
}
}
#[inline]
fn set_and_print_error(
src_path: &Path,
err: Box<dyn Error>,
lock: &mut StdoutLock,
has_error: &mut bool,
) {
*has_error = true;
writeln!(
lock,
"\t[ERROR] Trying to reduce size of \"{}\" failed with the following error: {}.\n\
\tSkipping that file.\n",
src_path.display(),
err
)
.expect("Failed to write to stdout.");
}
fn copy_or_skip(
src_path: &Path,
dst_path: &Path,
different_paths: bool,
lock: &mut StdoutLock,
err: Option<Box<dyn Error>>,
has_error: &mut bool,
) {
if let Some(error) = err {
writeln!(lock, "{}", error).expect("Failed to write to stdout.");
};
if different_paths {
match fs::copy(src_path, dst_path) {
Ok(_) => writeln!(
lock,
"Copied \"{}\" to \"{}\".",
src_path.display(),
dst_path.display()
)
.expect("Failed to write to stdout."),
Err(e) => set_and_print_error(src_path, Box::from(e), lock, has_error),
};
} else {
writeln!(lock, "Skipped \"{}\".", src_path.display()).expect("Failed to write to stdout.");
}
}
pub fn process_images(
src_dir: PathBuf,
dst_dir: PathBuf,
recursive: bool,
resize: bool,
quality: i32,
min_size: u64,
) -> bool {
let mut has_error = false;
let different_paths = src_dir != dst_dir;
let mut lock = stdout().lock();
for src_path in get_file_list(&src_dir, recursive) {
let src_path = src_path.path();
let mut dst_path = PathBuf::from(src_path);
if different_paths {
dst_path = dst_dir.as_path().join(
diff_paths(
src_path.to_str().expect("Expected some src_path."),
src_dir.to_str().expect("Expected some src_dir."),
)
.expect("Expected diff_paths() to work."),
);
if let Some(parent) = dst_path.parent() {
match fs::create_dir_all(parent) {
Ok(_) => {}
Err(err) => {
let err = format!(
"\n\tFailed to create the subdirectory {:?} with the following error: {}",
parent, err
);
set_and_print_error(src_path, Box::from(err), &mut lock, &mut has_error);
continue;
}
};
} else {
let err_msg = format!("Destination path {:?} doesn't have a parent.", dst_path);
set_and_print_error(src_path, Box::from(err_msg), &mut lock, &mut has_error);
continue;
};
}
let file_size = src_path.metadata().expect("Expected file metadata.").len();
let extension = src_path.extension();
if file_size >= min_size && extension.is_some() {
#[allow(clippy::unnecessary_unwrap)]
match extension
.expect("Expected a file extension")
.to_string_lossy()
.to_lowercase()
.as_str()
{
"jpg" | "jpeg" => {
match process_jpeg(src_path, &dst_path, resize, quality, &mut lock) {
Ok(_) => print_success(src_path, &dst_path, different_paths, &mut lock),
Err(err) => copy_or_skip(
src_path,
&dst_path,
different_paths,
&mut lock,
Some(err),
&mut has_error,
),
}
}
"png" => match process_png(src_path, &dst_path, resize, &mut lock) {
Ok(_) => print_success(src_path, &dst_path, different_paths, &mut lock),
Err(err) => copy_or_skip(
src_path,
&dst_path,
different_paths,
&mut lock,
Some(err),
&mut has_error,
),
},
_ => copy_or_skip(
src_path,
&dst_path,
different_paths,
&mut lock,
None,
&mut has_error,
),
}
} else {
copy_or_skip(
src_path,
&dst_path,
different_paths,
&mut lock,
None,
&mut has_error,
);
}
}
has_error
}