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,10 +227,29 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
} }
} }
if opts.strip { match opts.strip.clone() {
// Strip headers // Strip headers
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(); png.aux_headers = HashMap::new();
} }
}
let output_data = png.output(); let output_data = png.output();
if file_original_size <= output_data.len() && !opts.force && opts.interlace.is_none() { if file_original_size <= output_data.len() && !opts.force && opts.interlace.is_none() {

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,11 +47,12 @@ 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 =
App::new("oxipng")
.version(VERSION_STRING) .version(VERSION_STRING)
.author("Joshua Holmer <jholmer.in@gmail.com>") .author("Joshua Holmer <jholmer.in@gmail.com>")
.about("Losslessly improves compression of PNG files") .about("Losslessly improves compression of PNG files")
@ -200,8 +202,10 @@ fn main() {
.help("No IDAT recoding unless necessary") .help("No IDAT recoding unless necessary")
.long("nz")) .long("nz"))
.arg(Arg::with_name("strip") .arg(Arg::with_name("strip")
.help("Strip all metadata objects") .help("Strip metadata objects ['safe', 'all', or comma-separated list]")
.long("strip")) .long("strip")
.short("s")
.takes_value(true))
.after_help("Optimization levels: .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)
@ -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,
} }
} }