Resize options added

This commit is contained in:
Matteo Paonessa 2024-12-14 12:11:09 +01:00
parent 559e2a48a6
commit 7e0efdb5b7
7 changed files with 203 additions and 56 deletions

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

26
Cargo.lock generated
View File

@ -199,9 +199,12 @@ version = "1.0.0-beta.0"
dependencies = [ dependencies = [
"clap", "clap",
"filetime", "filetime",
"human_bytes",
"image", "image",
"imagesize",
"indicatif", "indicatif",
"infer", "infer",
"kamadak-exif 0.6.1",
"libcaesium", "libcaesium",
"rayon", "rayon",
"tempfile", "tempfile",
@ -527,6 +530,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "human_bytes"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.5" version = "0.25.5"
@ -573,6 +582,12 @@ dependencies = [
"thread_local", "thread_local",
] ]
[[package]]
name = "imagesize"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]] [[package]]
name = "img-parts" name = "img-parts"
version = "0.3.1" version = "0.3.1"
@ -683,6 +698,15 @@ dependencies = [
"mutate_once", "mutate_once",
] ]
[[package]]
name = "kamadak-exif"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837"
dependencies = [
"mutate_once",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -713,7 +737,7 @@ dependencies = [
"imagequant", "imagequant",
"img-parts", "img-parts",
"infer", "infer",
"kamadak-exif", "kamadak-exif 0.5.5",
"libc", "libc",
"lodepng", "lodepng",
"mozjpeg-sys", "mozjpeg-sys",

View File

@ -11,9 +11,10 @@ walkdir = "2.5"
infer = "0.16" infer = "0.16"
rayon = "1.10" rayon = "1.10"
#rand = "0.8" #rand = "0.8"
#human_bytes = { version = "0.4", default-features = false } human_bytes = { version = "0.4", default-features = false }
kamadak-exif = "0.6"
filetime = "0.2" filetime = "0.2"
#imagesize = "0.13" imagesize = "0.13"
libcaesium = "0.17.0" libcaesium = "0.17.0"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }

6
rustfmt.toml Normal file
View File

@ -0,0 +1,6 @@
comment_width = 120
format_code_in_doc_comments = true
imports_granularity = "Crate"
imports_layout = "Vertical"
wrap_comments = true
max_width = 120

View File

@ -1,5 +1,5 @@
use crate::options::{CommandLineArgs, OverwritePolicy}; use crate::options::{CommandLineArgs, OverwritePolicy};
use crate::scan_files::scan_files; use crate::scan_files::{get_file_mime_type, scan_files};
use caesium::compress_in_memory; use caesium::compress_in_memory;
use caesium::parameters::CSParameters; use caesium::parameters::CSParameters;
use clap::Parser; use clap::Parser;
@ -7,16 +7,18 @@ use filetime::{set_file_times, FileTime};
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressDrawTarget, ProgressStyle}; use indicatif::{ParallelProgressIterator, ProgressBar, ProgressDrawTarget, ProgressStyle};
use rayon::iter::IntoParallelRefIterator; use rayon::iter::IntoParallelRefIterator;
use rayon::iter::ParallelIterator; use rayon::iter::ParallelIterator;
use std::error::Error;
use std::fs::File; use std::fs::File;
use std::io::{Read, Write}; use std::io::{BufReader, Read, Write};
use std::num::NonZero; use std::num::NonZero;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use std::{fs, io}; use std::{fs, io};
use human_bytes::human_bytes;
mod logger;
mod options; mod options;
mod scan_files; mod scan_files;
mod logger;
enum CompressionStatus { enum CompressionStatus {
Success, Success,
@ -44,8 +46,11 @@ fn main() {
.get(), .get(),
); );
let verbose = if quiet { 0 } else { args.verbose }; let verbose = if quiet { 0 } else { args.verbose };
let compression_parameters = build_compression_parameters(&args); let needs_resize = args.resize.width.is_some()
let (base_path, input_files) = scan_files(args.files, args.recursive, quiet); || args.resize.height.is_some()
|| args.resize.long_edge.is_some()
|| args.resize.short_edge.is_some();
let (base_path, input_files) = scan_files(&args.files, args.recursive, quiet);
rayon::ThreadPoolBuilder::new() rayon::ThreadPoolBuilder::new()
.num_threads(threads_number) .num_threads(threads_number)
@ -68,14 +73,14 @@ fn main() {
message: String::new(), message: String::new(),
}; };
let original_file_size = match input_file.metadata() { let input_file_metadata = match input_file.metadata() {
Ok(m) => m.len(), Ok(m) => m,
Err(_) => { Err(_) => {
compression_result.message = "Error reading file metadata".to_string(); compression_result.message = "Error reading file metadata".to_string();
return compression_result; return compression_result;
} }
}; };
let original_file_size = input_file_metadata.len();
compression_result.original_size = original_file_size; compression_result.original_size = original_file_size;
let output_directory = if args.output_destination.same_folder_as_input { let output_directory = if args.output_destination.same_folder_as_input {
@ -115,10 +120,11 @@ fn main() {
return compression_result; return compression_result;
}; };
let compressed_image = match compress_in_memory( let compression_parameters = build_compression_parameters(&args, input_file, needs_resize);
read_file_to_vec(input_file).unwrap(),
&compression_parameters, let compressed_image =
) { match compress_in_memory(read_file_to_vec(input_file).unwrap(), &compression_parameters) {
//TODO: handle error
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
compression_result.message = format!("Error compressing file: {}", e); compression_result.message = format!("Error compressing file: {}", e);
@ -133,8 +139,7 @@ fn main() {
OverwritePolicy::None => { OverwritePolicy::None => {
compression_result.status = CompressionStatus::Skipped; compression_result.status = CompressionStatus::Skipped;
compression_result.compressed_size = original_file_size; compression_result.compressed_size = original_file_size;
compression_result.message = compression_result.message = "File already exists, skipped due overwrite policy".to_string();
"File already exists, skipped due overwrite policy".to_string();
return compression_result; return compression_result;
} }
OverwritePolicy::Bigger => { OverwritePolicy::Bigger => {
@ -166,17 +171,9 @@ fn main() {
}; };
if args.keep_dates { if args.keep_dates {
let output_file_metadata = match output_file.metadata() {
Ok(m) => m,
Err(_) => {
compression_result.message =
"Error reading output file metadata".to_string();
return compression_result;
}
};
let (last_modification_time, last_access_time) = ( let (last_modification_time, last_access_time) = (
FileTime::from_last_modification_time(&output_file_metadata), FileTime::from_last_modification_time(&input_file_metadata),
FileTime::from_last_access_time(&output_file_metadata), FileTime::from_last_access_time(&input_file_metadata),
); );
match preserve_dates(&output_full_path, last_modification_time, last_access_time) { match preserve_dates(&output_full_path, last_modification_time, last_access_time) {
Ok(_) => {} Ok(_) => {}
@ -193,7 +190,39 @@ fn main() {
}) })
.collect(); .collect();
let recap_message = format!("Processed {} files", compression_results.len()); write_recap_message(&compression_results, verbose);
}
fn write_recap_message(compression_results: &[CompressionResult], verbose: u8) {
let mut total_original_size = 0;
let mut total_compressed_size = 0;
let total_files = compression_results.len();
let mut total_success = 0;
let mut total_skipped = 0;
let mut total_errors = 0;
for result in compression_results.iter() {
total_original_size += result.original_size;
total_compressed_size += result.compressed_size;
match result.status {
CompressionStatus::Skipped => total_skipped += 1,
CompressionStatus::Error => total_errors += 1,
_ => total_success += 1
}
}
let total_saved = total_original_size as f64 - total_compressed_size as f64;
let total_saved_percent = total_saved / total_original_size as f64 * 100.0;
if verbose > 0 {
println!("Total files: {}", total_files);
println!("Total success: {}", total_success);
println!("Total skipped: {}", total_skipped);
println!("Total errors: {}", total_errors);
println!("Total original size: {}", human_bytes(total_original_size as f64));
println!("Total compressed size: {}", human_bytes(total_compressed_size as f64));
println!("Total saved: {:.2} bytes ({:.2}%)", human_bytes(total_saved), total_saved_percent);
}
} }
fn get_parallelism_count(requested_threads: u32, available_threads: usize) -> usize { fn get_parallelism_count(requested_threads: u32, available_threads: usize) -> usize {
@ -204,7 +233,7 @@ fn get_parallelism_count(requested_threads: u32, available_threads: usize) -> us
} }
} }
fn build_compression_parameters(args: &CommandLineArgs) -> CSParameters { fn build_compression_parameters(args: &CommandLineArgs, input_file: &Path, needs_resize: bool) -> CSParameters {
let mut parameters = CSParameters::new(); let mut parameters = CSParameters::new();
let quality = args.compression.quality.unwrap_or(80) as u32; let quality = args.compression.quality.unwrap_or(80) as u32;
@ -218,9 +247,46 @@ fn build_compression_parameters(args: &CommandLineArgs) -> CSParameters {
parameters.png.optimization_level = args.png_opt_level; parameters.png.optimization_level = args.png_opt_level;
parameters.png.force_zopfli = args.zopfli; parameters.png.force_zopfli = args.zopfli;
if needs_resize {
let mime_type = get_file_mime_type(input_file);
build_resize_parameters(args, &mut parameters, input_file, mime_type).unwrap();
//TODO
}
parameters parameters
} }
fn build_resize_parameters(
args: &CommandLineArgs,
parameters: &mut CSParameters,
input_file_path: &Path,
mime_type: Option<String>,
) -> Result<(), Box<dyn Error>> {
let (width, height) = get_real_resolution(input_file_path, mime_type, args.exif)?;
if args.resize.width.is_some() {
parameters.width = args.resize.width.unwrap_or(0);
} else if args.resize.height.is_some() {
parameters.height = args.resize.height.unwrap_or(0);
} else if args.resize.long_edge.is_some() {
let long_edge = args.resize.long_edge.unwrap_or(0);
if width > height {
parameters.width = long_edge;
} else {
parameters.height = long_edge;
}
} else if args.resize.short_edge.is_some() {
let short_edge = args.resize.short_edge.unwrap_or(0);
if width < height {
parameters.width = short_edge;
} else {
parameters.height = short_edge;
}
}
Ok(())
}
fn compute_output_full_path( fn compute_output_full_path(
output_directory: PathBuf, output_directory: PathBuf,
input_file_path: PathBuf, input_file_path: PathBuf,
@ -228,14 +294,8 @@ fn compute_output_full_path(
keep_structure: bool, keep_structure: bool,
suffix: &str, suffix: &str,
) -> Option<PathBuf> { ) -> Option<PathBuf> {
let extension = input_file_path let extension = input_file_path.extension().unwrap_or_default().to_os_string();
.extension() let base_name = input_file_path.file_stem().unwrap_or_default().to_os_string();
.unwrap_or_default()
.to_os_string();
let base_name = input_file_path
.file_stem()
.unwrap_or_default()
.to_os_string();
let mut output_file_name = base_name; let mut output_file_name = base_name;
output_file_name.push(suffix); output_file_name.push(suffix);
if !extension.is_empty() { if !extension.is_empty() {
@ -269,14 +329,36 @@ fn read_file_to_vec(file_path: &PathBuf) -> io::Result<Vec<u8>> {
Ok(buffer) Ok(buffer)
} }
fn preserve_dates( fn preserve_dates(output_file: &PathBuf, input_atime: FileTime, input_mtime: FileTime) -> io::Result<()> {
output_file: &PathBuf,
input_atime: FileTime,
input_mtime: FileTime,
) -> io::Result<()> {
set_file_times(output_file, input_atime, input_mtime) set_file_times(output_file, input_atime, input_mtime)
} }
fn get_real_resolution(
file: &Path,
mime_type: Option<String>,
keep_metadata: bool,
) -> Result<(usize, usize), Box<dyn Error>> {
let resolution = imagesize::size(file)?;
let mut orientation = 1;
let mime = mime_type.unwrap_or("".to_string());
if mime == "image/jpeg" && keep_metadata {
let f = File::open(file)?;
if let Ok(e) = exif::Reader::new().read_from_container(&mut BufReader::new(&f)) {
let exif_field = match e.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
Some(f) => f,
None => return Ok((resolution.width, resolution.height)),
};
orientation = exif_field.value.get_uint(0).unwrap_or(1);
};
}
let (width, height) = match orientation {
5..=8 => (resolution.height, resolution.width),
_ => (resolution.width, resolution.height),
};
Ok((width, height))
}
fn setup_progress_bar(len: usize, verbose: u8) -> ProgressBar { fn setup_progress_bar(len: usize, verbose: u8) -> ProgressBar {
let progress_bar = ProgressBar::new(len as u64); let progress_bar = ProgressBar::new(len as u64);
if verbose == 0 { if verbose == 0 {

View File

@ -17,6 +17,9 @@ pub struct CommandLineArgs {
#[command(flatten)] #[command(flatten)]
pub compression: Compression, pub compression: Compression,
#[command(flatten)]
pub resize: Resize,
#[command(flatten)] #[command(flatten)]
pub output_destination: OutputDestination, pub output_destination: OutputDestination,
@ -87,6 +90,26 @@ pub struct Compression {
pub max_size: Option<u8>, pub max_size: Option<u8>,
} }
#[derive(Args, Debug)]
#[group(required = false, multiple = true)]
pub struct Resize {
/// width of the output image, if height is not set will preserve aspect ratio
#[arg(long, conflicts_with_all = &["long_edge", "short_edge"])]
pub width: Option<u32>,
/// height of the output image, if width is not set will preserve aspect ratio
#[arg(long, conflicts_with_all = &["long_edge", "short_edge"])]
pub height: Option<u32>,
/// sets the size of the longest edge of the image
#[arg(long, conflicts_with_all = &["width", "height", "short_edge"])]
pub long_edge: Option<u32>,
/// sets the size of the shortest edge of the image
#[arg(long, conflicts_with_all = &["width", "height", "long_edge"])]
pub short_edge: Option<u32>,
}
#[derive(Args, Debug)] #[derive(Args, Debug)]
#[group(required = true, multiple = false)] #[group(required = true, multiple = false)]
pub struct OutputDestination { pub struct OutputDestination {

View File

@ -6,15 +6,21 @@ use indicatif::ProgressStyle;
use walkdir::WalkDir; use walkdir::WalkDir;
fn is_filetype_supported(path: &Path) -> bool { fn is_filetype_supported(path: &Path) -> bool {
match get_file_mime_type(path) {
Some(mime_type) => {
matches!(mime_type.as_str(), "image/jpeg" | "image/png" | "image/webp" | "image/gif")
}
None => false,
}
}
pub fn get_file_mime_type(path: &Path) -> Option<String> {
match infer::get_from_path(path) { match infer::get_from_path(path) {
Ok(v) => match v { Ok(v) => match v {
None => false, None => None,
Some(ft) => matches!( Some(ft) => Some(ft.mime_type().to_string()),
ft.mime_type(),
"image/jpeg" | "image/png" | "image/gif" | "image/webp"
),
}, },
Err(_) => false, Err(_) => None,
} }
} }
@ -22,7 +28,7 @@ fn is_valid(entry: &Path) -> bool {
entry.exists() && entry.is_file() && is_filetype_supported(entry) entry.exists() && entry.is_file() && is_filetype_supported(entry)
} }
pub fn scan_files(args: Vec<String>, recursive: bool, quiet: bool) -> (PathBuf, Vec<PathBuf>) { pub fn scan_files(args: &Vec<String>, recursive: bool, quiet: bool) -> (PathBuf, Vec<PathBuf>) {
if args.is_empty() { if args.is_empty() {
return (PathBuf::new(), vec![]); return (PathBuf::new(), vec![]);
} }