diff --git a/CHANGELOG.md b/CHANGELOG.md index 81105451..769d739e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ -**Version 0.1.2** (unreleased) +**Version 0.2.0** - 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)) + - [SEMVER_MAJOR] Allow 'safe', 'all', or comma-separated list as options for `--strip` + - [SEMVER_MINOR] Add `-s` alias for `--strip` **Version 0.1.1** - - Fix `oxipng *` writing all input files to one output file (#15) + - Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/shssoichiro/oxipng/issues/15)) **Version 0.1.0** - Initial beta release diff --git a/Cargo.toml b/Cargo.toml index 90259126..34a4dd61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxipng" -version = "0.1.1" +version = "0.2.0" authors = ["Joshua Holmer "] description = "A lossless PNG compression optimizer" license = "MIT" diff --git a/README.md b/README.md index 5472eed5..96179102 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ More advanced options can be found by running `oxipng -h`. ## History Oxipng began as a completely rewrite of the OptiPNG project, -which is assumed to be dead as no commit has been made to it since 2013. +which is assumed to be dead as no commit has been made to it since March 2014. The name has been changed to avoid confusion and potential legal issues. The core goal of rewriting OptiPNG was to implement multithreading, diff --git a/src/lib.rs b/src/lib.rs index 773048aa..aa04b22d 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 { @@ -40,12 +40,15 @@ pub struct Options { pub color_type_reduction: bool, pub palette_reduction: bool, pub idat_recoding: bool, - pub strip: bool, + pub strip: png::Headers, + pub use_heuristics: bool, } pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { // Decode PNG from file - if opts.verbosity.is_some() { println!("Processing: {}", filepath.to_str().unwrap()) }; + if opts.verbosity.is_some() { + 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, @@ -56,21 +59,58 @@ 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(); + } + 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(); + let compression = opts.compression.clone(); + let memory = opts.memory.clone(); + let mut strategies = opts.strategies.clone(); + + if opts.use_heuristics { + // Heuristically determine which set of options to use + if png.ihdr_data.bit_depth.as_u8() >= 8 && + png.ihdr_data.color_type != png::ColorType::Indexed { + if filter.is_empty() { + filter.insert(5); + } + if strategies.is_empty() { + strategies.insert(1); + } + } else { + if filter.is_empty() { + filter.insert(0); + } + if strategies.is_empty() { + strategies.insert(0); + } } - println!(" IDAT size = {} bytes", idat_original_size); - println!(" File size = {} bytes", file_original_size); } let mut something_changed = false; @@ -118,16 +158,17 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { if opts.idat_recoding || something_changed { // Go through selected permutations and determine the best let mut best: Option<(u8, u8, u8, u8, Vec)> = None; - let combinations = opts.filter.len() * opts.compression.len() * opts.memory.len() * - opts.strategies.len(); + 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) }; + if opts.verbosity.is_some() { + writeln!(&mut stderr(), "Trying: {} combinations", combinations).ok(); + } crossbeam::scope(|scope| { - for f in &opts.filter { + for f in &filter { let filtered = png.filter_image(*f); - for zc in &opts.compression { - for zm in &opts.memory { - for zs in &opts.strategies { + for zc in &compression { + for zm in &memory { + for zs in &strategies { let moved_filtered = filtered.clone(); results.push(scope.spawn(move || { let new_idat = match deflate::deflate::deflate(&moved_filtered, @@ -140,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())) @@ -173,30 +214,51 @@ 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", - better.1, - better.2, - better.3, - better.0, - png.idat_data.len()); + 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()) + .ok(); } } } - if opts.strip { + match opts.strip.clone() { // Strip headers - png.aux_headers = HashMap::new(); + png::Headers::None => (), + png::Headers::Some(hdrs) => { + for hdr in &hdrs { + png.aux_headers.remove(hdr); + } + } + png::Headers::Safe => { + const PRESERVED_HEADERS: [&'static str; 9] = ["cHRM", "gAMA", "iCCP", "sBIT", "sRGB", + "bKGD", "hIST", "pHYs", "sPLT"]; + let mut preserved = HashMap::new(); + for (hdr, contents) in png.aux_headers.iter() { + if PRESERVED_HEADERS.contains(&hdr.as_ref()) { + preserved.insert(hdr.clone(), contents.clone()); + } + } + png.aux_headers = preserved; + } + png::Headers::All => { + png.aux_headers = HashMap::new(); + } } 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, @@ -227,38 +289,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)", - png.idat_data.len(), - idat_original_size - png.idat_data.len()); + writeln!(&mut stderr(), + " IDAT size = {} bytes ({} bytes decrease)", + png.idat_data.len(), + idat_original_size - png.idat_data.len()) + .ok(); } else { - println!(" IDAT size = {} bytes ({} bytes increase)", - png.idat_data.len(), - png.idat_data.len() - idat_original_size); + writeln!(&mut stderr(), + " IDAT size = {} bytes ({} bytes increase)", + png.idat_data.len(), + png.idat_data.len() - idat_original_size) + .ok(); } if file_original_size >= output_data.len() { - println!(" 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); + 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) + .ok(); } else { - println!(" 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); + 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) + .ok(); } } Ok(()) @@ -266,13 +338,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 0044ea24..e5a4e16a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,13 @@ extern crate clap; extern crate regex; use clap::{App, Arg, ArgMatches}; +use oxipng::png; use regex::Regex; use std::collections::HashSet; +use std::io::{Write, stderr}; use std::path::PathBuf; -const VERSION_STRING: &'static str = "0.1.1"; +const VERSION_STRING: &'static str = "0.2.0"; fn main() { let mut filter = HashSet::new(); @@ -45,164 +47,168 @@ fn main() { color_type_reduction: true, palette_reduction: true, idat_recoding: true, - strip: false, + strip: png::Headers::None, + use_heuristics: false, }; - let matches = App::new("oxipng") - .version(VERSION_STRING) - .author("Joshua Holmer ") - .about("Losslessly improves compression of PNG files") - .arg(Arg::with_name("files") - .help("File(s) to compress") - .index(1) - .multiple(true) - .required(true)) - .arg(Arg::with_name("optimization") - .help("Optimization level - Default: 2") - .short("o") - .long("opt") - .takes_value(true) - .possible_value("0") - .possible_value("1") - .possible_value("2") - .possible_value("3") - .possible_value("4") - .possible_value("5") - .possible_value("6")) - .arg(Arg::with_name("backup") - .help("Back up modified files") - .short("b") - .long("backup")) - .arg(Arg::with_name("force") - .help("Write output even if larger than the original") - .short("F") - .long("force")) - .arg(Arg::with_name("recursive") - .help("Recurse into subdirectories") - .short("r") - .long("recursive")) - .arg(Arg::with_name("output_dir") - .help("Write output file(s) to ") - .long("dir") - .takes_value(true) - .conflicts_with("output_file") - .conflicts_with("stdout")) - .arg(Arg::with_name("output_file") - .help("Write output file to ") - .long("out") - .takes_value(true) - .conflicts_with("output_dir") - .conflicts_with("stdout")) - .arg(Arg::with_name("stdout") - .help("Write output to stdout") - .long("stdout") - .conflicts_with("output_dir") - .conflicts_with("output_file")) - .arg(Arg::with_name("fix") - .help("Enable error recovery") - .long("fix")) - .arg(Arg::with_name("no-clobber") - .help("Do not overwrite existing files") - .long("no-clobber")) - .arg(Arg::with_name("pretend") - .help("Do not write any files, only calculate compression gains") - .short("P") - .long("pretend")) - .arg(Arg::with_name("preserve") - .help("Preserve file attributes if possible") - .short("p") - .long("preserve")) - .arg(Arg::with_name("quiet") - .help("Run in quiet mode") - .short("q") - .long("quiet") - .conflicts_with("verbose")) - .arg(Arg::with_name("verbose") - .help("Run in verbose mode") - .short("v") - .long("verbose") - .conflicts_with("quiet")) - .arg(Arg::with_name("filters") - .help("PNG delta filters (0-5) - Default: 0,5") - .short("f") - .long("filters") - .takes_value(true) - .validator(|x| { - match parse_numeric_range_opts(&x, 0, 5) { - Ok(_) => Ok(()), - Err(_) => Err("Invalid option for filters".to_owned()), - } - })) - .arg(Arg::with_name("interlace") - .help("PNG interlace type") - .short("i") - .long("interlace") - .takes_value(true) - .possible_value("0") - .possible_value("1")) - .arg(Arg::with_name("compression") - .help("zlib compression levels (1-9) - Default: 9") - .long("zc") - .takes_value(true) - .validator(|x| { - match parse_numeric_range_opts(&x, 1, 9) { - Ok(_) => Ok(()), - Err(_) => Err("Invalid option for compression".to_owned()), - } - })) - .arg(Arg::with_name("memory") - .help("zlib memory levels (1-9) - Default: 9") - .long("zm") - .takes_value(true) - .validator(|x| { - match parse_numeric_range_opts(&x, 1, 9) { - Ok(_) => Ok(()), - Err(_) => Err("Invalid option for memory".to_owned()), - } - })) - .arg(Arg::with_name("strategies") - .help("zlib compression strategies (0-3) - Default: 0-3") - .long("zs") - .takes_value(true) - .validator(|x| { - match parse_numeric_range_opts(&x, 0, 3) { - Ok(_) => Ok(()), - Err(_) => Err("Invalid option for strategies".to_owned()), - } - })) - .arg(Arg::with_name("window") - .help("zlib window size - Default: 32k") - .long("zw") - .takes_value(true) - .possible_value("256") - .possible_value("512") - .possible_value("1k") - .possible_value("2k") - .possible_value("4k") - .possible_value("8k") - .possible_value("16k") - .possible_value("32k")) - .arg(Arg::with_name("no-bit-reduction") - .help("No bit depth reduction") - .long("nb")) - .arg(Arg::with_name("no-color-reduction") - .help("No color type reduction") - .long("nc")) - .arg(Arg::with_name("no-palette-reduction") - .help("No palette reduction") - .long("np")) - .arg(Arg::with_name("no-reductions") - .help("No reductions") - .long("nx")) - .arg(Arg::with_name("no-recoding") - .help("No IDAT recoding unless necessary") - .long("nz")) - .arg(Arg::with_name("strip") - .help("Strip all metadata objects") - .long("strip")) - .after_help("Optimization levels: + let matches = + App::new("oxipng") + .version(VERSION_STRING) + .author("Joshua Holmer ") + .about("Losslessly improves compression of PNG files") + .arg(Arg::with_name("files") + .help("File(s) to compress") + .index(1) + .multiple(true) + .required(true)) + .arg(Arg::with_name("optimization") + .help("Optimization level - Default: 2") + .short("o") + .long("opt") + .takes_value(true) + .possible_value("0") + .possible_value("1") + .possible_value("2") + .possible_value("3") + .possible_value("4") + .possible_value("5") + .possible_value("6")) + .arg(Arg::with_name("backup") + .help("Back up modified files") + .short("b") + .long("backup")) + .arg(Arg::with_name("force") + .help("Write output even if larger than the original") + .short("F") + .long("force")) + .arg(Arg::with_name("recursive") + .help("Recurse into subdirectories") + .short("r") + .long("recursive")) + .arg(Arg::with_name("output_dir") + .help("Write output file(s) to ") + .long("dir") + .takes_value(true) + .conflicts_with("output_file") + .conflicts_with("stdout")) + .arg(Arg::with_name("output_file") + .help("Write output file to ") + .long("out") + .takes_value(true) + .conflicts_with("output_dir") + .conflicts_with("stdout")) + .arg(Arg::with_name("stdout") + .help("Write output to stdout") + .long("stdout") + .conflicts_with("output_dir") + .conflicts_with("output_file")) + .arg(Arg::with_name("fix") + .help("Enable error recovery") + .long("fix")) + .arg(Arg::with_name("no-clobber") + .help("Do not overwrite existing files") + .long("no-clobber")) + .arg(Arg::with_name("pretend") + .help("Do not write any files, only calculate compression gains") + .short("P") + .long("pretend")) + .arg(Arg::with_name("preserve") + .help("Preserve file attributes if possible") + .short("p") + .long("preserve")) + .arg(Arg::with_name("quiet") + .help("Run in quiet mode") + .short("q") + .long("quiet") + .conflicts_with("verbose")) + .arg(Arg::with_name("verbose") + .help("Run in verbose mode") + .short("v") + .long("verbose") + .conflicts_with("quiet")) + .arg(Arg::with_name("filters") + .help("PNG delta filters (0-5) - Default: 0,5") + .short("f") + .long("filters") + .takes_value(true) + .validator(|x| { + match parse_numeric_range_opts(&x, 0, 5) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid option for filters".to_owned()), + } + })) + .arg(Arg::with_name("interlace") + .help("PNG interlace type") + .short("i") + .long("interlace") + .takes_value(true) + .possible_value("0") + .possible_value("1")) + .arg(Arg::with_name("compression") + .help("zlib compression levels (1-9) - Default: 9") + .long("zc") + .takes_value(true) + .validator(|x| { + match parse_numeric_range_opts(&x, 1, 9) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid option for compression".to_owned()), + } + })) + .arg(Arg::with_name("memory") + .help("zlib memory levels (1-9) - Default: 9") + .long("zm") + .takes_value(true) + .validator(|x| { + match parse_numeric_range_opts(&x, 1, 9) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid option for memory".to_owned()), + } + })) + .arg(Arg::with_name("strategies") + .help("zlib compression strategies (0-3) - Default: 0-3") + .long("zs") + .takes_value(true) + .validator(|x| { + match parse_numeric_range_opts(&x, 0, 3) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid option for strategies".to_owned()), + } + })) + .arg(Arg::with_name("window") + .help("zlib window size - Default: 32k") + .long("zw") + .takes_value(true) + .possible_value("256") + .possible_value("512") + .possible_value("1k") + .possible_value("2k") + .possible_value("4k") + .possible_value("8k") + .possible_value("16k") + .possible_value("32k")) + .arg(Arg::with_name("no-bit-reduction") + .help("No bit depth reduction") + .long("nb")) + .arg(Arg::with_name("no-color-reduction") + .help("No color type reduction") + .long("nc")) + .arg(Arg::with_name("no-palette-reduction") + .help("No palette reduction") + .long("np")) + .arg(Arg::with_name("no-reductions") + .help("No reductions") + .long("nx")) + .arg(Arg::with_name("no-recoding") + .help("No IDAT recoding unless necessary") + .long("nz")) + .arg(Arg::with_name("strip") + .help("Strip metadata objects ['safe', 'all', or comma-separated list]") + .long("strip") + .short("s") + .takes_value(true)) + .after_help("Optimization levels: -o 0 => --zc 3 --nz (0 or 1 trials) - -o 1 => --zc 9 (1 trial) + -o 1 => --zc 9 (1 trial, determined heuristically) -o 2 => --zc 9 --zs 0-3 --f 0,5 (8 trials) -o 3 => --zc 9 --zm 8-9 --zs 0-3 --f 0,5 (16 trials) -o 4 => --zc 9 --zm 8-9 --zs 0-3 --f 0-5 (48 trials) @@ -215,14 +221,14 @@ fn main() { Manually specifying a compression option (zc, zm, etc.) will override the optimization preset, regardless of the order you write the arguments.") - .get_matches(); + .get_matches(); let mut opts = default_opts; match parse_opts_into_struct(&matches, &mut opts) { Ok(_) => (), Err(x) => { - println!("{}", x); + writeln!(&mut stderr(), "{}", x).ok(); return (); } } @@ -243,7 +249,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; } @@ -254,7 +263,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(); + } }; } } @@ -268,12 +279,11 @@ fn parse_opts_into_struct(matches: &ArgMatches, opts: &mut oxipng::Options) -> R opts.compression = compression; } Some("1") => { - let mut filter = HashSet::new(); - filter.insert(0); + let filter = HashSet::new(); opts.filter = filter; - let mut strategies = HashSet::new(); - strategies.insert(0); + let strategies = HashSet::new(); opts.strategies = strategies; + opts.use_heuristics = true; } // 2 is the default Some("3") => { @@ -471,8 +481,27 @@ fn parse_opts_into_struct(matches: &ArgMatches, opts: &mut oxipng::Options) -> R opts.idat_recoding = false; } - if matches.is_present("strip") { - opts.strip = true; + if let Some(hdrs) = matches.value_of("strip") { + let hdrs = hdrs.split(',').map(|x| x.trim().to_owned()).collect::>(); + if hdrs.contains(&"safe".to_owned()) || hdrs.contains(&"all".to_owned()) { + if hdrs.len() > 1 { + return Err("'safe' or 'all' presets for --strip should be used by themselves" + .to_owned()); + } + if hdrs[0] == "safe" { + opts.strip = png::Headers::Safe; + } else { + opts.strip = png::Headers::All; + } + } else { + const FORBIDDEN_CHUNKS: [&'static str; 5] = ["IHDR", "IDAT", "tRNS", "PLTE", "IEND"]; + for i in &hdrs { + if FORBIDDEN_CHUNKS.contains(&i.as_ref()) { + return Err(format!("{} chunk is not allowed to be stripped", i)); + } + } + opts.strip = png::Headers::Some(hdrs); + } } Ok(()) @@ -482,8 +511,8 @@ fn parse_numeric_range_opts(input: &str, min_value: u8, max_value: u8) -> Result, String> { - let one_item = Regex::new(format!("^[{}-{}]$", min_value, max_value).as_ref()).unwrap(); - let multiple_items = Regex::new(format!("^([{}-{}])(,|-)([{}-{}])$", + let one_item = Regex::new(format!(r"^[{}-{}]$", min_value, max_value).as_ref()).unwrap(); + let multiple_items = Regex::new(format!(r"^([{}-{}])(,|-)([{}-{}])$", min_value, max_value, min_value, diff --git a/src/png.rs b/src/png.rs index a3f73b74..f2ccebdc 100644 --- a/src/png.rs +++ b/src/png.rs @@ -68,7 +68,7 @@ impl fmt::Display for BitDepth { } impl BitDepth { - fn as_u8(&self) -> u8 { + pub fn as_u8(&self) -> u8 { match *self { BitDepth::One => 1, BitDepth::Two => 2, @@ -77,7 +77,7 @@ impl BitDepth { BitDepth::Sixteen => 16, } } - fn from_u8(depth: u8) -> BitDepth { + pub fn from_u8(depth: u8) -> BitDepth { match depth { 1 => BitDepth::One, 2 => BitDepth::Two, @@ -89,6 +89,14 @@ impl BitDepth { } } +#[derive(Debug,PartialEq,Clone)] +pub enum Headers { + None, + Some(Vec), + Safe, + All, +} + #[derive(Debug,Clone)] pub struct ScanLines<'a> { pub png: &'a PngData, diff --git a/tests/files/strip_headers.png b/tests/files/quiet_mode.png similarity index 100% rename from tests/files/strip_headers.png rename to tests/files/quiet_mode.png diff --git a/tests/files/strip_headers_all.png b/tests/files/strip_headers_all.png new file mode 100644 index 00000000..d714a05d Binary files /dev/null and b/tests/files/strip_headers_all.png differ diff --git a/tests/files/strip_headers_list.png b/tests/files/strip_headers_list.png new file mode 100644 index 00000000..d714a05d Binary files /dev/null and b/tests/files/strip_headers_list.png differ diff --git a/tests/files/strip_headers_none.png b/tests/files/strip_headers_none.png new file mode 100644 index 00000000..d714a05d Binary files /dev/null and b/tests/files/strip_headers_none.png differ diff --git a/tests/files/strip_headers_safe.png b/tests/files/strip_headers_safe.png new file mode 100644 index 00000000..d714a05d Binary files /dev/null and b/tests/files/strip_headers_safe.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 d10d23ed..f544ef45 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, @@ -44,7 +44,8 @@ fn get_opts(input: &Path) -> oxipng::Options { color_type_reduction: true, palette_reduction: true, idat_recoding: true, - strip: false, + strip: png::Headers::None, + use_heuristics: false, } } diff --git a/tests/flags.rs b/tests/flags.rs index c69913ed..cc71450b 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, @@ -44,20 +44,78 @@ fn get_opts(input: &Path) -> oxipng::Options { color_type_reduction: true, palette_reduction: true, idat_recoding: true, - strip: false, + strip: png::Headers::None, + use_heuristics: false, } } +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 strip_headers() { - let input = PathBuf::from("tests/files/strip_headers.png"); +fn verbose_mode() { + let input = PathBuf::from("tests/files/verbose_mode.png"); let mut opts = get_opts(&input); - opts.strip = true; + 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_list() { + let input = PathBuf::from("tests/files/strip_headers_list.png"); + let mut opts = get_opts(&input); + opts.strip = png::Headers::Some(vec!["iCCP".to_owned(), "tEXt".to_owned()]); let output = opts.out_file.clone(); let png = png::PngData::new(&input).unwrap(); assert!(png.aux_headers.contains_key("tEXt")); + assert!(png.aux_headers.contains_key("iTXt")); + assert!(png.aux_headers.contains_key("iCCP")); match oxipng::optimize(&input, &opts) { Ok(_) => (), @@ -74,6 +132,131 @@ fn strip_headers() { }; assert!(!png.aux_headers.contains_key("tEXt")); + assert!(png.aux_headers.contains_key("iTXt")); + assert!(!png.aux_headers.contains_key("iCCP")); + + 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 strip_headers_safe() { + let input = PathBuf::from("tests/files/strip_headers_safe.png"); + let mut opts = get_opts(&input); + opts.strip = png::Headers::Safe; + let output = opts.out_file.clone(); + + let png = png::PngData::new(&input).unwrap(); + + assert!(png.aux_headers.contains_key("tEXt")); + assert!(png.aux_headers.contains_key("iTXt")); + assert!(png.aux_headers.contains_key("iCCP")); + + 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.aux_headers.contains_key("tEXt")); + assert!(!png.aux_headers.contains_key("iTXt")); + assert!(png.aux_headers.contains_key("iCCP")); + + 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 strip_headers_all() { + let input = PathBuf::from("tests/files/strip_headers_all.png"); + let mut opts = get_opts(&input); + opts.strip = png::Headers::All; + let output = opts.out_file.clone(); + + let png = png::PngData::new(&input).unwrap(); + + assert!(png.aux_headers.contains_key("tEXt")); + assert!(png.aux_headers.contains_key("iTXt")); + assert!(png.aux_headers.contains_key("iCCP")); + + 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.aux_headers.contains_key("tEXt")); + assert!(!png.aux_headers.contains_key("iTXt")); + assert!(!png.aux_headers.contains_key("iCCP")); + + 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 strip_headers_none() { + let input = PathBuf::from("tests/files/strip_headers_none.png"); + let mut opts = get_opts(&input); + opts.strip = png::Headers::None; + let output = opts.out_file.clone(); + + let png = png::PngData::new(&input).unwrap(); + + assert!(png.aux_headers.contains_key("tEXt")); + assert!(png.aux_headers.contains_key("iTXt")); + assert!(png.aux_headers.contains_key("iCCP")); + + 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.aux_headers.contains_key("tEXt")); + assert!(png.aux_headers.contains_key("iTXt")); + assert!(png.aux_headers.contains_key("iCCP")); let old_png = image::open(&input).unwrap(); let new_png = image::open(&output).unwrap(); diff --git a/tests/reduction.rs b/tests/reduction.rs index 3662302e..6c0cafde 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, @@ -44,7 +44,8 @@ fn get_opts(input: &Path) -> oxipng::Options { color_type_reduction: true, palette_reduction: true, idat_recoding: true, - strip: false, + strip: png::Headers::None, + use_heuristics: false, } }