Merge remote-tracking branch 'upstream/master'

This commit is contained in:
SethDusek 2016-03-07 02:19:11 +05:00
commit a81f16edb9
15 changed files with 542 additions and 239 deletions

View file

@ -1,8 +1,13 @@
**Version 0.1.2** (unreleased) **Version 0.2.0**
- 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))
- 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** **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** **Version 0.1.0**
- Initial beta release - Initial beta release

View file

@ -1,6 +1,6 @@
[package] [package]
name = "oxipng" name = "oxipng"
version = "0.1.1" version = "0.2.0"
authors = ["Joshua Holmer <jholmer.in@gmail.com>"] authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
description = "A lossless PNG compression optimizer" description = "A lossless PNG compression optimizer"
license = "MIT" license = "MIT"

View file

@ -63,7 +63,7 @@ More advanced options can be found by running `oxipng -h`.
## History ## History
Oxipng began as a completely rewrite of the OptiPNG project, 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 name has been changed to avoid confusion and potential legal issues.
The core goal of rewriting OptiPNG was to implement multithreading, The core goal of rewriting OptiPNG was to implement multithreading,

View file

@ -7,7 +7,7 @@ extern crate libz_sys;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs::{File, copy}; use std::fs::{File, copy};
use std::io::{BufWriter, Write, stdout}; use std::io::{BufWriter, Write, stderr, stdout};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub mod deflate { pub mod deflate {
@ -40,12 +40,15 @@ 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 fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
// Decode PNG from file // 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 in_file = Path::new(filepath);
let mut png = match png::PngData::new(&in_file) { let mut png = match png::PngData::new(&in_file) {
Ok(x) => x, 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 idat_original_size = png.idat_data.len();
let file_original_size = filepath.metadata().unwrap().len() as usize; let file_original_size = filepath.metadata().unwrap().len() as usize;
if opts.verbosity.is_some() { if opts.verbosity.is_some() {
println!(" {}x{} pixels, PNG format", writeln!(&mut stderr(),
" {}x{} pixels, PNG format",
png.ihdr_data.width, png.ihdr_data.width,
png.ihdr_data.height); png.ihdr_data.height)
.ok();
if let Some(palette) = png.palette.clone() { 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, png.ihdr_data.bit_depth,
palette.len() / 3); palette.len() / 3)
.ok();
} else { } else {
println!(" {}x{} bits/pixel, {:?}", writeln!(&mut stderr(),
" {}x{} bits/pixel, {:?}",
png.channels_per_pixel(), png.channels_per_pixel(),
png.ihdr_data.bit_depth, 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; let mut something_changed = false;
@ -118,16 +158,17 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
if opts.idat_recoding || something_changed { if opts.idat_recoding || something_changed {
// Go through selected permutations and determine the best // Go through selected permutations and determine the best
let mut best: Option<(u8, u8, u8, u8, Vec<u8>)> = None; let mut best: Option<(u8, u8, u8, u8, Vec<u8>)> = None;
let combinations = opts.filter.len() * opts.compression.len() * opts.memory.len() * let combinations = filter.len() * compression.len() * memory.len() * strategies.len();
opts.strategies.len();
let mut results = Vec::with_capacity(combinations); 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| { crossbeam::scope(|scope| {
for f in &opts.filter { for f in &filter {
let filtered = png.filter_image(*f); let filtered = png.filter_image(*f);
for zc in &opts.compression { for zc in &compression {
for zm in &opts.memory { for zm in &memory {
for zs in &opts.strategies { for zs in &strategies {
let moved_filtered = filtered.clone(); let moved_filtered = filtered.clone();
results.push(scope.spawn(move || { results.push(scope.spawn(move || {
let new_idat = match deflate::deflate::deflate(&moved_filtered, 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) { if opts.verbosity == Some(1) {
println!(" zc = {} zm = {} zs = {} f = {} {} bytes", writeln!(&mut stderr(), " zc = {} zm = {} zs = {} f = {} {} bytes",
*zc, *zc,
*zm, *zm,
*zs, *zs,
*f, *f,
new_idat.len()); new_idat.len()).ok();
} }
Ok((*f, *zc, *zm, *zs, new_idat.clone())) 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 { if let Some(better) = best {
png.idat_data = better.4.clone(); png.idat_data = better.4.clone();
if opts.verbosity.is_some() { if opts.verbosity.is_some() {
println!("Found better combination:"); writeln!(&mut stderr(), "Found better combination:").ok();
println!(" zc = {} zm = {} zs = {} f = {} {} bytes", writeln!(&mut stderr(),
better.1, " zc = {} zm = {} zs = {} f = {} {} bytes",
better.2, better.1,
better.3, better.2,
better.0, better.3,
png.idat_data.len()); better.0,
png.idat_data.len())
.ok();
} }
} }
} }
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();
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() {
println!("File already optimized"); writeln!(&mut stderr(), "File already optimized").ok();
return Ok(()); return Ok(());
} }
if opts.pretend { if opts.pretend {
println!("Running in pretend mode, no output"); writeln!(&mut stderr(), "Running in pretend mode, no output").ok();
} else { } else {
if opts.backup { if opts.backup {
match copy(in_file, match copy(in_file,
@ -227,38 +289,48 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
} }
}; };
let mut buffer = BufWriter::new(out_file); let mut buffer = BufWriter::new(out_file);
if opts.verbosity.is_some() { match buffer.write_all(&output_data) {
match buffer.write_all(&output_data) { Ok(_) => {
Ok(_) => println!("Output: {}", opts.out_file.display()), if opts.verbosity.is_some() {
Err(_) => { writeln!(&mut stderr(), "Output: {}", opts.out_file.display()).ok();
return Err(format!("Unable to write to file {}", opts.out_file.display()))
} }
} }
Err(_) => {
return Err(format!("Unable to write to file {}", opts.out_file.display()))
}
} }
} }
} }
if opts.verbosity.is_some() { if opts.verbosity.is_some() {
if idat_original_size >= png.idat_data.len() { if idat_original_size >= png.idat_data.len() {
println!(" IDAT size = {} bytes ({} bytes decrease)", writeln!(&mut stderr(),
png.idat_data.len(), " IDAT size = {} bytes ({} bytes decrease)",
idat_original_size - png.idat_data.len()); png.idat_data.len(),
idat_original_size - png.idat_data.len())
.ok();
} else { } else {
println!(" IDAT size = {} bytes ({} bytes increase)", writeln!(&mut stderr(),
png.idat_data.len(), " IDAT size = {} bytes ({} bytes increase)",
png.idat_data.len() - idat_original_size); png.idat_data.len(),
png.idat_data.len() - idat_original_size)
.ok();
} }
if file_original_size >= output_data.len() { if file_original_size >= output_data.len() {
println!(" file size = {} bytes ({} bytes = {:.2}% decrease)", writeln!(&mut stderr(),
output_data.len(), " file size = {} bytes ({} bytes = {:.2}% decrease)",
file_original_size - output_data.len(), output_data.len(),
(file_original_size - output_data.len()) as f64 / file_original_size as f64 * file_original_size - output_data.len(),
100f64); (file_original_size - output_data.len()) as f64 / file_original_size as f64 *
100f64)
.ok();
} else { } else {
println!(" file size = {} bytes ({} bytes = {:.2}% increase)", writeln!(&mut stderr(),
output_data.len(), " file size = {} bytes ({} bytes = {:.2}% increase)",
output_data.len() - file_original_size, output_data.len(),
(output_data.len() - file_original_size) as f64 / file_original_size as f64 * output_data.len() - file_original_size,
100f64); (output_data.len() - file_original_size) as f64 / file_original_size as f64 *
100f64)
.ok();
} }
} }
Ok(()) Ok(())
@ -266,13 +338,17 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
fn report_reduction(png: &png::PngData) { fn report_reduction(png: &png::PngData) {
if let Some(palette) = png.palette.clone() { 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, png.ihdr_data.bit_depth,
palette.len() / 3); palette.len() / 3)
.ok();
} else { } else {
println!("Reducing image to {}x{} bits/pixel, {}", writeln!(&mut stderr(),
"Reducing image to {}x{} bits/pixel, {}",
png.channels_per_pixel(), png.channels_per_pixel(),
png.ihdr_data.bit_depth, png.ihdr_data.bit_depth,
png.ihdr_data.color_type); png.ihdr_data.color_type)
.ok();
} }
} }

View file

@ -3,11 +3,13 @@ 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::path::PathBuf; use std::path::PathBuf;
const VERSION_STRING: &'static str = "0.1.1"; const VERSION_STRING: &'static str = "0.2.0";
fn main() { fn main() {
let mut filter = HashSet::new(); let mut filter = HashSet::new();
@ -45,164 +47,168 @@ 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,
}; };
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) -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)
-o 3 => --zc 9 --zm 8-9 --zs 0-3 --f 0,5 (16 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) -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, 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;
match parse_opts_into_struct(&matches, &mut opts) { match parse_opts_into_struct(&matches, &mut opts) {
Ok(_) => (), Ok(_) => (),
Err(x) => { Err(x) => {
println!("{}", x); writeln!(&mut stderr(), "{}", x).ok();
return (); return ();
} }
} }
@ -243,7 +249,10 @@ fn handle_optimization(inputs: Vec<PathBuf>, opts: oxipng::Options) {
handle_optimization(input.read_dir().unwrap().map(|x| x.unwrap().path()).collect(), handle_optimization(input.read_dir().unwrap().map(|x| x.unwrap().path()).collect(),
current_opts) current_opts)
} else { } else {
println!("{} is a directory, skipping", input.display()); writeln!(&mut stderr(),
"{} is a directory, skipping",
input.display())
.ok();
} }
continue; continue;
} }
@ -254,7 +263,9 @@ fn handle_optimization(inputs: Vec<PathBuf>, opts: oxipng::Options) {
} }
match oxipng::optimize(&input, &current_opts) { match oxipng::optimize(&input, &current_opts) {
Ok(_) => (), 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; opts.compression = compression;
} }
Some("1") => { Some("1") => {
let mut filter = HashSet::new(); let filter = HashSet::new();
filter.insert(0);
opts.filter = filter; opts.filter = filter;
let mut strategies = HashSet::new(); let strategies = HashSet::new();
strategies.insert(0);
opts.strategies = strategies; opts.strategies = strategies;
opts.use_heuristics = true;
} }
// 2 is the default // 2 is the default
Some("3") => { Some("3") => {
@ -471,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(())
@ -482,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

@ -68,7 +68,7 @@ impl fmt::Display for BitDepth {
} }
impl BitDepth { impl BitDepth {
fn as_u8(&self) -> u8 { pub fn as_u8(&self) -> u8 {
match *self { match *self {
BitDepth::One => 1, BitDepth::One => 1,
BitDepth::Two => 2, BitDepth::Two => 2,
@ -77,7 +77,7 @@ impl BitDepth {
BitDepth::Sixteen => 16, BitDepth::Sixteen => 16,
} }
} }
fn from_u8(depth: u8) -> BitDepth { pub fn from_u8(depth: u8) -> BitDepth {
match depth { match depth {
1 => BitDepth::One, 1 => BitDepth::One,
2 => BitDepth::Two, 2 => BitDepth::Two,
@ -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,

View file

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View file

@ -33,7 +33,7 @@ fn get_opts(input: &Path) -> oxipng::Options {
clobber: true, clobber: true,
create: true, create: true,
preserve_attrs: false, preserve_attrs: false,
verbosity: Some(0), verbosity: None,
filter: filter, filter: filter,
interlace: None, interlace: None,
compression: compression, compression: compression,
@ -44,7 +44,8 @@ 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,
} }
} }

View file

@ -33,7 +33,7 @@ fn get_opts(input: &Path) -> oxipng::Options {
clobber: true, clobber: true,
create: true, create: true,
preserve_attrs: false, preserve_attrs: false,
verbosity: Some(0), verbosity: None,
filter: filter, filter: filter,
interlace: None, interlace: None,
compression: compression, compression: compression,
@ -44,20 +44,78 @@ 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,
} }
} }
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::<Vec<Vec<u8>>>() ==
new_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>());
remove_file(output).ok();
}
#[test] #[test]
fn strip_headers() { fn verbose_mode() {
let input = PathBuf::from("tests/files/strip_headers.png"); let input = PathBuf::from("tests/files/verbose_mode.png");
let mut opts = get_opts(&input); 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 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(_) => (),
@ -74,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

@ -33,7 +33,7 @@ fn get_opts(input: &Path) -> oxipng::Options {
clobber: true, clobber: true,
create: true, create: true,
preserve_attrs: false, preserve_attrs: false,
verbosity: Some(0), verbosity: None,
filter: filter, filter: filter,
interlace: None, interlace: None,
compression: compression, compression: compression,
@ -44,7 +44,8 @@ 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,
} }
} }