Make --strip option more versatile

Closes #22
This commit is contained in:
Joshua Holmer 2016-03-05 22:36:31 -05:00
parent 5f834a06f8
commit b889c73a3b
11 changed files with 348 additions and 169 deletions

View file

@ -1,8 +1,10 @@
**Version 0.1.2** (unreleased) **Version 0.2.0** (unreleased)
- Fix program version that is displayed when running `oxipng -V` - 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)) - Ensure `--quiet` mode is actually quiet (@SethDusek [#20](https://github.com/shssoichiro/oxipng/pull/20))
- Write status/debug information to stderr instead of stdout - 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)) - 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** **Version 0.1.1**
- Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/shssoichiro/oxipng/issues/15)) - Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/shssoichiro/oxipng/issues/15))

View file

@ -40,7 +40,7 @@ pub struct Options {
pub color_type_reduction: bool, pub color_type_reduction: bool,
pub palette_reduction: bool, pub palette_reduction: bool,
pub idat_recoding: bool, pub idat_recoding: bool,
pub strip: bool, pub strip: png::Headers,
pub use_heuristics: bool, pub use_heuristics: bool,
} }
@ -227,9 +227,28 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
} }
} }
if opts.strip { match opts.strip.clone() {
// Strip headers // 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(); let output_data = png.output();

View file

@ -3,6 +3,7 @@ extern crate clap;
extern crate regex; extern crate regex;
use clap::{App, Arg, ArgMatches}; use clap::{App, Arg, ArgMatches};
use oxipng::png;
use regex::Regex; use regex::Regex;
use std::collections::HashSet; use std::collections::HashSet;
use std::io::{Write, stderr}; use std::io::{Write, stderr};
@ -46,163 +47,166 @@ fn main() {
color_type_reduction: true, color_type_reduction: true,
palette_reduction: true, palette_reduction: true,
idat_recoding: true, idat_recoding: true,
strip: false, strip: png::Headers::None,
use_heuristics: false, use_heuristics: false,
}; };
let matches = App::new("oxipng") let matches =
.version(VERSION_STRING) App::new("oxipng")
.author("Joshua Holmer <jholmer.in@gmail.com>") .version(VERSION_STRING)
.about("Losslessly improves compression of PNG files") .author("Joshua Holmer <jholmer.in@gmail.com>")
.arg(Arg::with_name("files") .about("Losslessly improves compression of PNG files")
.help("File(s) to compress") .arg(Arg::with_name("files")
.index(1) .help("File(s) to compress")
.multiple(true) .index(1)
.required(true)) .multiple(true)
.arg(Arg::with_name("optimization") .required(true))
.help("Optimization level - Default: 2") .arg(Arg::with_name("optimization")
.short("o") .help("Optimization level - Default: 2")
.long("opt") .short("o")
.takes_value(true) .long("opt")
.possible_value("0") .takes_value(true)
.possible_value("1") .possible_value("0")
.possible_value("2") .possible_value("1")
.possible_value("3") .possible_value("2")
.possible_value("4") .possible_value("3")
.possible_value("5") .possible_value("4")
.possible_value("6")) .possible_value("5")
.arg(Arg::with_name("backup") .possible_value("6"))
.help("Back up modified files") .arg(Arg::with_name("backup")
.short("b") .help("Back up modified files")
.long("backup")) .short("b")
.arg(Arg::with_name("force") .long("backup"))
.help("Write output even if larger than the original") .arg(Arg::with_name("force")
.short("F") .help("Write output even if larger than the original")
.long("force")) .short("F")
.arg(Arg::with_name("recursive") .long("force"))
.help("Recurse into subdirectories") .arg(Arg::with_name("recursive")
.short("r") .help("Recurse into subdirectories")
.long("recursive")) .short("r")
.arg(Arg::with_name("output_dir") .long("recursive"))
.help("Write output file(s) to <directory>") .arg(Arg::with_name("output_dir")
.long("dir") .help("Write output file(s) to <directory>")
.takes_value(true) .long("dir")
.conflicts_with("output_file") .takes_value(true)
.conflicts_with("stdout")) .conflicts_with("output_file")
.arg(Arg::with_name("output_file") .conflicts_with("stdout"))
.help("Write output file to <file>") .arg(Arg::with_name("output_file")
.long("out") .help("Write output file to <file>")
.takes_value(true) .long("out")
.conflicts_with("output_dir") .takes_value(true)
.conflicts_with("stdout")) .conflicts_with("output_dir")
.arg(Arg::with_name("stdout") .conflicts_with("stdout"))
.help("Write output to stdout") .arg(Arg::with_name("stdout")
.long("stdout") .help("Write output to stdout")
.conflicts_with("output_dir") .long("stdout")
.conflicts_with("output_file")) .conflicts_with("output_dir")
.arg(Arg::with_name("fix") .conflicts_with("output_file"))
.help("Enable error recovery") .arg(Arg::with_name("fix")
.long("fix")) .help("Enable error recovery")
.arg(Arg::with_name("no-clobber") .long("fix"))
.help("Do not overwrite existing files") .arg(Arg::with_name("no-clobber")
.long("no-clobber")) .help("Do not overwrite existing files")
.arg(Arg::with_name("pretend") .long("no-clobber"))
.help("Do not write any files, only calculate compression gains") .arg(Arg::with_name("pretend")
.short("P") .help("Do not write any files, only calculate compression gains")
.long("pretend")) .short("P")
.arg(Arg::with_name("preserve") .long("pretend"))
.help("Preserve file attributes if possible") .arg(Arg::with_name("preserve")
.short("p") .help("Preserve file attributes if possible")
.long("preserve")) .short("p")
.arg(Arg::with_name("quiet") .long("preserve"))
.help("Run in quiet mode") .arg(Arg::with_name("quiet")
.short("q") .help("Run in quiet mode")
.long("quiet") .short("q")
.conflicts_with("verbose")) .long("quiet")
.arg(Arg::with_name("verbose") .conflicts_with("verbose"))
.help("Run in verbose mode") .arg(Arg::with_name("verbose")
.short("v") .help("Run in verbose mode")
.long("verbose") .short("v")
.conflicts_with("quiet")) .long("verbose")
.arg(Arg::with_name("filters") .conflicts_with("quiet"))
.help("PNG delta filters (0-5) - Default: 0,5") .arg(Arg::with_name("filters")
.short("f") .help("PNG delta filters (0-5) - Default: 0,5")
.long("filters") .short("f")
.takes_value(true) .long("filters")
.validator(|x| { .takes_value(true)
match parse_numeric_range_opts(&x, 0, 5) { .validator(|x| {
Ok(_) => Ok(()), match parse_numeric_range_opts(&x, 0, 5) {
Err(_) => Err("Invalid option for filters".to_owned()), Ok(_) => Ok(()),
} Err(_) => Err("Invalid option for filters".to_owned()),
})) }
.arg(Arg::with_name("interlace") }))
.help("PNG interlace type") .arg(Arg::with_name("interlace")
.short("i") .help("PNG interlace type")
.long("interlace") .short("i")
.takes_value(true) .long("interlace")
.possible_value("0") .takes_value(true)
.possible_value("1")) .possible_value("0")
.arg(Arg::with_name("compression") .possible_value("1"))
.help("zlib compression levels (1-9) - Default: 9") .arg(Arg::with_name("compression")
.long("zc") .help("zlib compression levels (1-9) - Default: 9")
.takes_value(true) .long("zc")
.validator(|x| { .takes_value(true)
match parse_numeric_range_opts(&x, 1, 9) { .validator(|x| {
Ok(_) => Ok(()), match parse_numeric_range_opts(&x, 1, 9) {
Err(_) => Err("Invalid option for compression".to_owned()), Ok(_) => Ok(()),
} Err(_) => Err("Invalid option for compression".to_owned()),
})) }
.arg(Arg::with_name("memory") }))
.help("zlib memory levels (1-9) - Default: 9") .arg(Arg::with_name("memory")
.long("zm") .help("zlib memory levels (1-9) - Default: 9")
.takes_value(true) .long("zm")
.validator(|x| { .takes_value(true)
match parse_numeric_range_opts(&x, 1, 9) { .validator(|x| {
Ok(_) => Ok(()), match parse_numeric_range_opts(&x, 1, 9) {
Err(_) => Err("Invalid option for memory".to_owned()), Ok(_) => Ok(()),
} Err(_) => Err("Invalid option for memory".to_owned()),
})) }
.arg(Arg::with_name("strategies") }))
.help("zlib compression strategies (0-3) - Default: 0-3") .arg(Arg::with_name("strategies")
.long("zs") .help("zlib compression strategies (0-3) - Default: 0-3")
.takes_value(true) .long("zs")
.validator(|x| { .takes_value(true)
match parse_numeric_range_opts(&x, 0, 3) { .validator(|x| {
Ok(_) => Ok(()), match parse_numeric_range_opts(&x, 0, 3) {
Err(_) => Err("Invalid option for strategies".to_owned()), Ok(_) => Ok(()),
} Err(_) => Err("Invalid option for strategies".to_owned()),
})) }
.arg(Arg::with_name("window") }))
.help("zlib window size - Default: 32k") .arg(Arg::with_name("window")
.long("zw") .help("zlib window size - Default: 32k")
.takes_value(true) .long("zw")
.possible_value("256") .takes_value(true)
.possible_value("512") .possible_value("256")
.possible_value("1k") .possible_value("512")
.possible_value("2k") .possible_value("1k")
.possible_value("4k") .possible_value("2k")
.possible_value("8k") .possible_value("4k")
.possible_value("16k") .possible_value("8k")
.possible_value("32k")) .possible_value("16k")
.arg(Arg::with_name("no-bit-reduction") .possible_value("32k"))
.help("No bit depth reduction") .arg(Arg::with_name("no-bit-reduction")
.long("nb")) .help("No bit depth reduction")
.arg(Arg::with_name("no-color-reduction") .long("nb"))
.help("No color type reduction") .arg(Arg::with_name("no-color-reduction")
.long("nc")) .help("No color type reduction")
.arg(Arg::with_name("no-palette-reduction") .long("nc"))
.help("No palette reduction") .arg(Arg::with_name("no-palette-reduction")
.long("np")) .help("No palette reduction")
.arg(Arg::with_name("no-reductions") .long("np"))
.help("No reductions") .arg(Arg::with_name("no-reductions")
.long("nx")) .help("No reductions")
.arg(Arg::with_name("no-recoding") .long("nx"))
.help("No IDAT recoding unless necessary") .arg(Arg::with_name("no-recoding")
.long("nz")) .help("No IDAT recoding unless necessary")
.arg(Arg::with_name("strip") .long("nz"))
.help("Strip all metadata objects") .arg(Arg::with_name("strip")
.long("strip")) .help("Strip metadata objects ['safe', 'all', or comma-separated list]")
.after_help("Optimization levels: .long("strip")
.short("s")
.takes_value(true))
.after_help("Optimization levels:
-o 0 => --zc 3 --nz (0 or 1 trials) -o 0 => --zc 3 --nz (0 or 1 trials)
-o 1 => --zc 9 (1 trial, determined heuristically) -o 1 => --zc 9 (1 trial, determined heuristically)
-o 2 => --zc 9 --zs 0-3 --f 0,5 (8 trials) -o 2 => --zc 9 --zs 0-3 --f 0,5 (8 trials)
@ -217,7 +221,7 @@ fn main() {
Manually specifying a compression option (zc, zm, etc.) will override the optimization preset, Manually specifying a compression option (zc, zm, etc.) will override the optimization preset,
regardless of the order you write the arguments.") regardless of the order you write the arguments.")
.get_matches(); .get_matches();
let mut opts = default_opts; let mut opts = default_opts;
@ -477,8 +481,27 @@ fn parse_opts_into_struct(matches: &ArgMatches, opts: &mut oxipng::Options) -> R
opts.idat_recoding = false; opts.idat_recoding = false;
} }
if matches.is_present("strip") { if let Some(hdrs) = matches.value_of("strip") {
opts.strip = true; let hdrs = hdrs.split(',').map(|x| x.trim().to_owned()).collect::<Vec<String>>();
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(()) Ok(())
@ -488,8 +511,8 @@ fn parse_numeric_range_opts(input: &str,
min_value: u8, min_value: u8,
max_value: u8) max_value: u8)
-> Result<HashSet<u8>, String> { -> Result<HashSet<u8>, String> {
let one_item = Regex::new(format!("^[{}-{}]$", min_value, max_value).as_ref()).unwrap(); let one_item = Regex::new(format!(r"^[{}-{}]$", min_value, max_value).as_ref()).unwrap();
let multiple_items = Regex::new(format!("^([{}-{}])(,|-)([{}-{}])$", let multiple_items = Regex::new(format!(r"^([{}-{}])(,|-)([{}-{}])$",
min_value, min_value,
max_value, max_value,
min_value, min_value,

View file

@ -89,6 +89,14 @@ impl BitDepth {
} }
} }
#[derive(Debug,PartialEq,Clone)]
pub enum Headers {
None,
Some(Vec<String>),
Safe,
All,
}
#[derive(Debug,Clone)] #[derive(Debug,Clone)]
pub struct ScanLines<'a> { pub struct ScanLines<'a> {
pub png: &'a PngData, pub png: &'a PngData,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View file

@ -44,7 +44,7 @@ fn get_opts(input: &Path) -> oxipng::Options {
color_type_reduction: true, color_type_reduction: true,
palette_reduction: true, palette_reduction: true,
idat_recoding: true, idat_recoding: true,
strip: false, strip: png::Headers::None,
use_heuristics: false, use_heuristics: false,
} }
} }

View file

@ -44,7 +44,7 @@ fn get_opts(input: &Path) -> oxipng::Options {
color_type_reduction: true, color_type_reduction: true,
palette_reduction: true, palette_reduction: true,
idat_recoding: true, idat_recoding: true,
strip: false, strip: png::Headers::None,
use_heuristics: false, use_heuristics: false,
} }
} }
@ -105,15 +105,17 @@ fn verbose_mode() {
} }
#[test] #[test]
fn strip_headers() { fn strip_headers_list() {
let input = PathBuf::from("tests/files/strip_headers.png"); let input = PathBuf::from("tests/files/strip_headers_list.png");
let mut opts = get_opts(&input); let mut opts = get_opts(&input);
opts.strip = true; opts.strip = png::Headers::Some(vec!["iCCP".to_owned(), "tEXt".to_owned()]);
let output = opts.out_file.clone(); let output = opts.out_file.clone();
let png = png::PngData::new(&input).unwrap(); let png = png::PngData::new(&input).unwrap();
assert!(png.aux_headers.contains_key("tEXt")); 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) { match oxipng::optimize(&input, &opts) {
Ok(_) => (), Ok(_) => (),
@ -130,6 +132,131 @@ fn strip_headers() {
}; };
assert!(!png.aux_headers.contains_key("tEXt")); 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::<Vec<Vec<u8>>>() ==
new_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>());
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::<Vec<Vec<u8>>>() ==
new_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>());
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::<Vec<Vec<u8>>>() ==
new_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>());
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 old_png = image::open(&input).unwrap();
let new_png = image::open(&output).unwrap(); let new_png = image::open(&output).unwrap();

View file

@ -44,7 +44,7 @@ fn get_opts(input: &Path) -> oxipng::Options {
color_type_reduction: true, color_type_reduction: true,
palette_reduction: true, palette_reduction: true,
idat_recoding: true, idat_recoding: true,
strip: false, strip: png::Headers::None,
use_heuristics: false, use_heuristics: false,
} }
} }