parent
5f834a06f8
commit
b889c73a3b
11 changed files with 348 additions and 169 deletions
|
|
@ -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))
|
||||||
|
|
|
||||||
23
src/lib.rs
23
src/lib.rs
|
|
@ -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() {
|
||||||
|
|
|
||||||
39
src/main.rs
39
src/main.rs
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 |
BIN
tests/files/strip_headers_list.png
Normal file
BIN
tests/files/strip_headers_list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
tests/files/strip_headers_none.png
Normal file
BIN
tests/files/strip_headers_none.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
tests/files/strip_headers_safe.png
Normal file
BIN
tests/files/strip_headers_safe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
135
tests/flags.rs
135
tests/flags.rs
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue