diff --git a/CHANGELOG.md b/CHANGELOG.md index 643dbfca..138cf591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ **Version 0.1.2** (unreleased) - Fix program version that is displayed when running `oxipng -V` - Ensure `--quiet` mode is actually quiet (@SethDusek [#20](https://github.com/shssoichiro/oxipng/pull/20)) + - Write status/debug information to stderr instead of stdout - Use heuristics to determine best combination for `-o1` ([#21](https://github.com/shssoichiro/oxipng/issues/21)) **Version 0.1.1** diff --git a/src/lib.rs b/src/lib.rs index 46334abe..8af96a30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ extern crate libz_sys; use std::collections::{HashMap, HashSet}; use std::fs::{File, copy}; -use std::io::{BufWriter, Write, stdout}; +use std::io::{BufWriter, Write, stderr, stdout}; use std::path::{Path, PathBuf}; pub mod deflate { @@ -47,8 +47,8 @@ pub struct Options { pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { // Decode PNG from file if opts.verbosity.is_some() { - println!("Processing: {}", filepath.to_str().unwrap()) - }; + writeln!(&mut stderr(), "Processing: {}", filepath.to_str().unwrap()).ok(); + } let in_file = Path::new(filepath); let mut png = match png::PngData::new(&in_file) { Ok(x) => x, @@ -59,21 +59,33 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { let idat_original_size = png.idat_data.len(); let file_original_size = filepath.metadata().unwrap().len() as usize; if opts.verbosity.is_some() { - println!(" {}x{} pixels, PNG format", + writeln!(&mut stderr(), + " {}x{} pixels, PNG format", png.ihdr_data.width, - png.ihdr_data.height); + png.ihdr_data.height) + .ok(); if let Some(palette) = png.palette.clone() { - println!(" {} bits/pixel, {} colors in palette", + writeln!(&mut stderr(), + " {} bits/pixel, {} colors in palette", png.ihdr_data.bit_depth, - palette.len() / 3); + palette.len() / 3) + .ok(); } else { - println!(" {}x{} bits/pixel, {:?}", + writeln!(&mut stderr(), + " {}x{} bits/pixel, {:?}", png.channels_per_pixel(), png.ihdr_data.bit_depth, - png.ihdr_data.color_type); + png.ihdr_data.color_type) + .ok(); } - println!(" IDAT size = {} bytes", idat_original_size); - println!(" File size = {} bytes", file_original_size); + writeln!(&mut stderr(), + " IDAT size = {} bytes", + idat_original_size) + .ok(); + writeln!(&mut stderr(), + " File size = {} bytes", + file_original_size) + .ok(); } let mut filter = opts.filter.clone(); @@ -149,8 +161,8 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { let combinations = filter.len() * compression.len() * memory.len() * strategies.len(); let mut results = Vec::with_capacity(combinations); if opts.verbosity.is_some() { - println!("Trying: {} combinations", combinations) - }; + writeln!(&mut stderr(), "Trying: {} combinations", combinations).ok(); + } crossbeam::scope(|scope| { for f in &filter { let filtered = png.filter_image(*f); @@ -169,12 +181,12 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { }; if opts.verbosity == Some(1) { - println!(" zc = {} zm = {} zs = {} f = {} {} bytes", + writeln!(&mut stderr(), " zc = {} zm = {} zs = {} f = {} {} bytes", *zc, *zm, *zs, *f, - new_idat.len()); + new_idat.len()).ok(); } Ok((*f, *zc, *zm, *zs, new_idat.clone())) @@ -202,13 +214,15 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { if let Some(better) = best { png.idat_data = better.4.clone(); if opts.verbosity.is_some() { - println!("Found better combination:"); - println!(" zc = {} zm = {} zs = {} f = {} {} bytes", + writeln!(&mut stderr(), "Found better combination:").ok(); + writeln!(&mut stderr(), + " zc = {} zm = {} zs = {} f = {} {} bytes", better.1, better.2, better.3, better.0, - png.idat_data.len()); + png.idat_data.len()) + .ok(); } } } @@ -220,12 +234,12 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { let output_data = png.output(); if file_original_size <= output_data.len() && !opts.force && opts.interlace.is_none() { - println!("File already optimized"); + writeln!(&mut stderr(), "File already optimized").ok(); return Ok(()); } if opts.pretend { - println!("Running in pretend mode, no output"); + writeln!(&mut stderr(), "Running in pretend mode, no output").ok(); } else { if opts.backup { match copy(in_file, @@ -256,38 +270,48 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { } }; let mut buffer = BufWriter::new(out_file); - if opts.verbosity.is_some() { - match buffer.write_all(&output_data) { - Ok(_) => println!("Output: {}", opts.out_file.display()), - Err(_) => { - return Err(format!("Unable to write to file {}", opts.out_file.display())) + match buffer.write_all(&output_data) { + Ok(_) => { + if opts.verbosity.is_some() { + writeln!(&mut stderr(), "Output: {}", opts.out_file.display()).ok(); } } + Err(_) => { + return Err(format!("Unable to write to file {}", opts.out_file.display())) + } } } } if opts.verbosity.is_some() { if idat_original_size >= png.idat_data.len() { - println!(" IDAT size = {} bytes ({} bytes decrease)", + writeln!(&mut stderr(), + " IDAT size = {} bytes ({} bytes decrease)", png.idat_data.len(), - idat_original_size - png.idat_data.len()); + idat_original_size - png.idat_data.len()) + .ok(); } else { - println!(" IDAT size = {} bytes ({} bytes increase)", + writeln!(&mut stderr(), + " IDAT size = {} bytes ({} bytes increase)", png.idat_data.len(), - png.idat_data.len() - idat_original_size); + png.idat_data.len() - idat_original_size) + .ok(); } if file_original_size >= output_data.len() { - println!(" file size = {} bytes ({} bytes = {:.2}% decrease)", + writeln!(&mut stderr(), + " file size = {} bytes ({} bytes = {:.2}% decrease)", output_data.len(), file_original_size - output_data.len(), (file_original_size - output_data.len()) as f64 / file_original_size as f64 * - 100f64); + 100f64) + .ok(); } else { - println!(" file size = {} bytes ({} bytes = {:.2}% increase)", + writeln!(&mut stderr(), + " file size = {} bytes ({} bytes = {:.2}% increase)", output_data.len(), output_data.len() - file_original_size, (output_data.len() - file_original_size) as f64 / file_original_size as f64 * - 100f64); + 100f64) + .ok(); } } Ok(()) @@ -295,13 +319,17 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { fn report_reduction(png: &png::PngData) { if let Some(palette) = png.palette.clone() { - println!("Reducing image to {} bits/pixel, {} colors in palette", + writeln!(&mut stderr(), + "Reducing image to {} bits/pixel, {} colors in palette", png.ihdr_data.bit_depth, - palette.len() / 3); + palette.len() / 3) + .ok(); } else { - println!("Reducing image to {}x{} bits/pixel, {}", + writeln!(&mut stderr(), + "Reducing image to {}x{} bits/pixel, {}", png.channels_per_pixel(), png.ihdr_data.bit_depth, - png.ihdr_data.color_type); + png.ihdr_data.color_type) + .ok(); } } diff --git a/src/main.rs b/src/main.rs index fe163f0e..ace4df52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ extern crate regex; use clap::{App, Arg, ArgMatches}; use regex::Regex; use std::collections::HashSet; +use std::io::{Write, stderr}; use std::path::PathBuf; const VERSION_STRING: &'static str = "0.1.1"; @@ -223,7 +224,7 @@ fn main() { match parse_opts_into_struct(&matches, &mut opts) { Ok(_) => (), Err(x) => { - println!("{}", x); + writeln!(&mut stderr(), "{}", x).ok(); return (); } } @@ -244,7 +245,10 @@ fn handle_optimization(inputs: Vec, opts: oxipng::Options) { handle_optimization(input.read_dir().unwrap().map(|x| x.unwrap().path()).collect(), current_opts) } else { - println!("{} is a directory, skipping", input.display()); + writeln!(&mut stderr(), + "{} is a directory, skipping", + input.display()) + .ok(); } continue; } @@ -255,7 +259,9 @@ fn handle_optimization(inputs: Vec, opts: oxipng::Options) { } match oxipng::optimize(&input, ¤t_opts) { Ok(_) => (), - Err(x) => println!("{}", x), + Err(x) => { + writeln!(&mut stderr(), "{}", x).ok(); + } }; } } diff --git a/tests/files/quiet_mode.png b/tests/files/quiet_mode.png new file mode 100644 index 00000000..3f9bdb1a Binary files /dev/null and b/tests/files/quiet_mode.png differ diff --git a/tests/files/verbose_mode.png b/tests/files/verbose_mode.png new file mode 100644 index 00000000..3f9bdb1a Binary files /dev/null and b/tests/files/verbose_mode.png differ diff --git a/tests/filters.rs b/tests/filters.rs index 600aa6b3..702cd376 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -33,7 +33,7 @@ fn get_opts(input: &Path) -> oxipng::Options { clobber: true, create: true, preserve_attrs: false, - verbosity: Some(0), + verbosity: None, filter: filter, interlace: None, compression: compression, diff --git a/tests/flags.rs b/tests/flags.rs index f1381b7d..1e956653 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -33,7 +33,7 @@ fn get_opts(input: &Path) -> oxipng::Options { clobber: true, create: true, preserve_attrs: false, - verbosity: Some(0), + verbosity: None, filter: filter, interlace: None, compression: compression, @@ -49,6 +49,61 @@ fn get_opts(input: &Path) -> oxipng::Options { } } +fn test_it_converts(input: &Path, + output: &Path, + opts: &oxipng::Options, + color_type_in: png::ColorType, + bit_depth_in: png::BitDepth, + color_type_out: png::ColorType, + bit_depth_out: png::BitDepth) { + let png = png::PngData::new(input).unwrap(); + + assert!(png.ihdr_data.color_type == color_type_in); + assert!(png.ihdr_data.bit_depth == bit_depth_in); + + match oxipng::optimize(input, opts) { + Ok(_) => (), + Err(x) => panic!(x.to_owned()), + }; + assert!(output.exists()); + + let png = match png::PngData::new(output) { + Ok(x) => x, + Err(x) => { + remove_file(output).ok(); + panic!(x.to_owned()) + } + }; + + assert!(png.ihdr_data.color_type == color_type_out); + assert!(png.ihdr_data.bit_depth == bit_depth_out); + + let old_png = image::open(input).unwrap(); + let new_png = image::open(output).unwrap(); + + // Conversion should be lossless + assert!(old_png.pixels().map(|x| x.2.channels().to_owned()).collect::>>() == + new_png.pixels().map(|x| x.2.channels().to_owned()).collect::>>()); + + remove_file(output).ok(); +} + +#[test] +fn verbose_mode() { + let input = PathBuf::from("tests/files/verbose_mode.png"); + let mut opts = get_opts(&input); + opts.verbosity = Some(1); + let output = opts.out_file.clone(); + + test_it_converts(&input, + &output, + &opts, + png::ColorType::RGB, + png::BitDepth::Eight, + png::ColorType::RGB, + png::BitDepth::Eight); +} + #[test] fn strip_headers() { let input = PathBuf::from("tests/files/strip_headers.png"); diff --git a/tests/reduction.rs b/tests/reduction.rs index 854eb662..e33a474a 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -33,7 +33,7 @@ fn get_opts(input: &Path) -> oxipng::Options { clobber: true, create: true, preserve_attrs: false, - verbosity: Some(0), + verbosity: None, filter: filter, interlace: None, compression: compression,