Show summary of results
This commit is contained in:
parent
5b5717e552
commit
6b882073a4
2 changed files with 102 additions and 14 deletions
|
|
@ -212,7 +212,7 @@ losslessly.")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("verbose")
|
Arg::new("verbose")
|
||||||
.help("Run in verbose mode (use twice to increase verbosity)")
|
.help("Show per-file info (use multiple times for more detail)")
|
||||||
.short('v')
|
.short('v')
|
||||||
.long("verbose")
|
.long("verbose")
|
||||||
.action(ArgAction::Count)
|
.action(ArgAction::Count)
|
||||||
|
|
@ -220,7 +220,7 @@ losslessly.")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("quiet")
|
Arg::new("quiet")
|
||||||
.help("Run in quiet mode")
|
.help("Suppress all output messages")
|
||||||
.short('q')
|
.short('q')
|
||||||
.long("quiet")
|
.long("quiet")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
|
|
|
||||||
112
src/main.rs
112
src/main.rs
|
|
@ -58,7 +58,7 @@ fn main() -> ExitCode {
|
||||||
if matches!(out_file, OutFile::Path { path: None, .. }) {
|
if matches!(out_file, OutFile::Path { path: None, .. }) {
|
||||||
out_file = OutFile::StdOut;
|
out_file = OutFile::StdOut;
|
||||||
}
|
}
|
||||||
vec![(InFile::StdIn, out_file)]
|
vec![(InFile::StdIn, out_file.clone())]
|
||||||
} else {
|
} else {
|
||||||
collect_files(
|
collect_files(
|
||||||
inputs,
|
inputs,
|
||||||
|
|
@ -70,28 +70,76 @@ fn main() -> ExitCode {
|
||||||
};
|
};
|
||||||
|
|
||||||
let parallel_files = matches.get_flag("parallel-files");
|
let parallel_files = matches.get_flag("parallel-files");
|
||||||
let summary = if parallel_files {
|
let results: Vec<OptimizationResult> = if parallel_files {
|
||||||
files
|
files
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|(input, output)| process_file(&input, &output, &opts))
|
.map(|(input, output)| process_file(&input, &output, &opts))
|
||||||
.min()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
files
|
files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(input, output)| process_file(&input, &output, &opts))
|
.map(|(input, output)| process_file(&input, &output, &opts))
|
||||||
.min()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
match summary.unwrap_or(OptimizationResult::Skipped) {
|
// Collect stats
|
||||||
OptimizationResult::Ok => ExitCode::SUCCESS,
|
let mut num_succeeded = 0;
|
||||||
OptimizationResult::Failed => ExitCode::FAILURE,
|
let mut num_not_optimized = 0;
|
||||||
OptimizationResult::Skipped => ExitCode::from(3),
|
let mut num_failed = 0;
|
||||||
|
let mut total_in: i64 = 0;
|
||||||
|
let mut total_out: i64 = 0;
|
||||||
|
for result in results {
|
||||||
|
match result {
|
||||||
|
OptimizationResult::Ok(insize, outsize) => {
|
||||||
|
num_succeeded += 1;
|
||||||
|
total_in += insize as i64;
|
||||||
|
total_out += outsize as i64;
|
||||||
|
if !opts.force && insize == outsize {
|
||||||
|
num_not_optimized += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OptimizationResult::Failed => num_failed += 1,
|
||||||
|
OptimizationResult::Skipped => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print summary
|
||||||
|
if !matches.get_flag("quiet") && !matches!(out_file, OutFile::StdOut) {
|
||||||
|
let in_bytes = format_bytes(total_in, true);
|
||||||
|
let out_bytes = format_bytes(total_out, true);
|
||||||
|
let saved = total_in - total_out;
|
||||||
|
let saved_bytes = format_bytes(saved, false);
|
||||||
|
let percent = if total_in > 0 {
|
||||||
|
saved as f64 / total_in as f64 * 100_f64
|
||||||
|
} else {
|
||||||
|
0_f64
|
||||||
|
};
|
||||||
|
println!("Files processed: {num_succeeded}");
|
||||||
|
println!("Input size: {}", in_bytes);
|
||||||
|
println!("Output size: {}", out_bytes);
|
||||||
|
println!("Total saved: {} ({:.2}%)", saved_bytes, percent);
|
||||||
|
if num_not_optimized == 1 {
|
||||||
|
println!("({num_not_optimized} file could not be optimized further)");
|
||||||
|
} else if num_not_optimized > 0 {
|
||||||
|
println!("({num_not_optimized} files could not be optimized further)");
|
||||||
|
}
|
||||||
|
if matches.get_flag("dry-run") {
|
||||||
|
println!("Dry run, no changes saved");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if num_succeeded > 0 {
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
} else if num_failed > 0 {
|
||||||
|
ExitCode::FAILURE
|
||||||
|
} else {
|
||||||
|
ExitCode::from(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Eq, PartialEq, Ord, PartialOrd)]
|
||||||
enum OptimizationResult {
|
enum OptimizationResult {
|
||||||
Ok,
|
Ok(usize, usize),
|
||||||
Failed,
|
Failed,
|
||||||
Skipped,
|
Skipped,
|
||||||
}
|
}
|
||||||
|
|
@ -166,8 +214,9 @@ fn parse_opts_into_struct(
|
||||||
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
|
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
|
||||||
let log_level = match matches.get_count("verbose") {
|
let log_level = match matches.get_count("verbose") {
|
||||||
_ if matches.get_flag("quiet") => LevelFilter::Off,
|
_ if matches.get_flag("quiet") => LevelFilter::Off,
|
||||||
0 => LevelFilter::Info,
|
0 => LevelFilter::Warn,
|
||||||
1 => LevelFilter::Debug,
|
1 => LevelFilter::Info,
|
||||||
|
2 => LevelFilter::Debug,
|
||||||
_ => LevelFilter::Trace,
|
_ => LevelFilter::Trace,
|
||||||
};
|
};
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
|
|
@ -453,7 +502,7 @@ fn process_file(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
|
||||||
// The reason for this is that recursion may pick up files that are not
|
// The reason for this is that recursion may pick up files that are not
|
||||||
// PNG files, and return an error for them.
|
// PNG files, and return an error for them.
|
||||||
// We don't really want to return an error code for those files.
|
// We don't really want to return an error code for those files.
|
||||||
Ok(_) => OptimizationResult::Ok,
|
Ok((insize, outsize)) => OptimizationResult::Ok(insize, outsize),
|
||||||
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
|
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
|
||||||
warn!("{input}: Skipped: {e}");
|
warn!("{input}: Skipped: {e}");
|
||||||
OptimizationResult::Skipped
|
OptimizationResult::Skipped
|
||||||
|
|
@ -464,3 +513,42 @@ fn process_file(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format byte counts as IEC units to 3 significant figures.
|
||||||
|
fn format_bytes(count: i64, include_raw: bool) -> String {
|
||||||
|
const K: i64 = 1 << 10;
|
||||||
|
const M: i64 = 1 << 20;
|
||||||
|
const G: i64 = 1 << 30;
|
||||||
|
fn format_3sf(value: f64) -> String {
|
||||||
|
match value.abs() {
|
||||||
|
..9.995 => format!("{:.2}", value),
|
||||||
|
9.995..99.95 => format!("{:.1}", value),
|
||||||
|
_ => format!("{:.0}", value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let formatted = match count.abs() {
|
||||||
|
..K => format!("{} bytes", count),
|
||||||
|
K..M => format!("{} KiB", format_3sf(count as f64 / K as f64)),
|
||||||
|
M..G => format!("{} MiB", format_3sf(count as f64 / M as f64)),
|
||||||
|
_ => format!("{} GiB", format_3sf(count as f64 / G as f64)),
|
||||||
|
};
|
||||||
|
if include_raw && count.abs() >= K {
|
||||||
|
format!("{} ({} bytes)", formatted, count)
|
||||||
|
} else {
|
||||||
|
formatted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::format_bytes;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_bytes() {
|
||||||
|
assert_eq!(format_bytes(1023, false), "1023 bytes");
|
||||||
|
assert_eq!(format_bytes(800_000, false), "781 KiB");
|
||||||
|
assert_eq!(format_bytes(12_500_000, false), "11.9 MiB");
|
||||||
|
assert_eq!(format_bytes(2_000_000_000, false), "1.86 GiB");
|
||||||
|
assert_eq!(format_bytes(-1024, false), "-1.00 KiB");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue