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`
- 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](https://github.com/shssoichiro/oxipng/issues/15))

View file

@ -40,7 +40,7 @@ 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,
}
@ -227,9 +227,28 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
}
}
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();

View file

@ -3,6 +3,7 @@ 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};
@ -46,163 +47,166 @@ 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 <jholmer.in@gmail.com>")
.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 <directory>")
.long("dir")
.takes_value(true)
.conflicts_with("output_file")
.conflicts_with("stdout"))
.arg(Arg::with_name("output_file")
.help("Write output file to <file>")
.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 <jholmer.in@gmail.com>")
.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 <directory>")
.long("dir")
.takes_value(true)
.conflicts_with("output_file")
.conflicts_with("stdout"))
.arg(Arg::with_name("output_file")
.help("Write output file to <file>")
.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, determined heuristically)
-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,
regardless of the order you write the arguments.")
.get_matches();
.get_matches();
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;
}
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::<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(())
@ -488,8 +511,8 @@ fn parse_numeric_range_opts(input: &str,
min_value: u8,
max_value: u8)
-> Result<HashSet<u8>, 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,

View file

@ -89,6 +89,14 @@ impl BitDepth {
}
}
#[derive(Debug,PartialEq,Clone)]
pub enum Headers {
None,
Some(Vec<String>),
Safe,
All,
}
#[derive(Debug,Clone)]
pub struct ScanLines<'a> {
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,
palette_reduction: true,
idat_recoding: true,
strip: false,
strip: png::Headers::None,
use_heuristics: false,
}
}

View file

@ -44,7 +44,7 @@ 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,
}
}
@ -105,15 +105,17 @@ fn verbose_mode() {
}
#[test]
fn strip_headers() {
let input = PathBuf::from("tests/files/strip_headers.png");
fn strip_headers_list() {
let input = PathBuf::from("tests/files/strip_headers_list.png");
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 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(_) => (),
@ -130,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::<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 new_png = image::open(&output).unwrap();

View file

@ -44,7 +44,7 @@ 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,
}
}