From 273880ea0b050aa16113f5a5976ef9a6b5d564b0 Mon Sep 17 00:00:00 2001 From: Joshua Holmer Date: Thu, 7 Jan 2016 16:29:02 -0500 Subject: [PATCH] Implement option parsing - Part 1 The compression options help text keeps getting split up by rustfmt. Don't let it. --- Cargo.toml | 8 +- README.md | 72 +++++++++++++++++ src/lib.rs | 23 +++--- src/main.rs | 225 ++++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 README.md diff --git a/Cargo.toml b/Cargo.toml index c06162e3..083b2199 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,12 @@ [package] name = "optipng" -version = "2.0.0" +version = "2.0.0-alpha.1" authors = ["Joshua Holmer "] [dependencies] -byteorder = "~0.4.0" +byteorder = "^0.4.0" +clap = "^1.5.4" crc = "^1.0.0" +libc = "^0.2.4" libz-sys = "^1.0.0" -libc = "~0.2.4" +regex = "^0.1.8" diff --git a/README.md b/README.md new file mode 100644 index 00000000..ce943f4d --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# OptiPNG-Next + +## Overview + +OptiPNG-Next version 2 is a completely rewrite of the OptiPNG project, +which is assumed to be dead as no commit has been made to it since 2013. + +OptiPNG is a PNG compression optimizer. + +In the typical use case, OptiPNG recompresses PNG image files +losslessly, converts external formats (BMP, GIF, PNM and TIFF) to +optimized PNG, and performs PNG integrity checks and corrections. +At the user's explicit request, OptiPNG is also capable to alter +image data or remove metadata. + +## Building + +Building OptiPNG-Next can be done using Rust 1.5.0 or greater with Cargo installed, as follows: +``` +git clone https://github.com/shssoichiro/optipng-next.git +cd optipng-next +cargo build --release +cp target/release/optipng /usr/local/bin +``` +Please note that zlib is a required build dependency. OptiPNG should work with any 1.x version of zlib, +but you are advised to use the latest version (currently 1.2.8) for security and bug fixes. + +## Usage + +OptiPNG-Next is a command-line utility. Basic usage looks similar to the following: + +``` +optipng -o4 -i 1 -strip all *.png +``` + +The most commonly used options are as follows: +* Optimization: `-o1` through `-o6`, lower is faster, higher is better compression. +The default (`-o2`) is sufficiently fast on a modern CPU and provides 30-50% compression +gains over an unoptimized PNG. `-o4` is 6 times slower than `-o2` but can provide 5-10% +extra compression over `-o2`. Using any setting higher than `-o4` is generally unlikely +to give any extra compression gains and is not recommended. +* Interlacing: `-i 1` will enable [Adam7](https://en.wikipedia.org/wiki/Adam7_algorithm) +PNG interlacing on any images that are processed. `-i 0` will remove interlacing from all +processed images. Not specifying either will keep the same interlacing state as the +input image. Note: Interlacing can add 25-50% to the size of an optimized image. Only use +it if you have a good reason. +* Strip: Used to remove metadata info from processed images. Generally used as `-strip all`. +Can save a few kilobytes if you don't need the metadata. + +More advanced options can be found in the man page or by running `optipng -h`. + +## Changes + +OptiPNG Next version 2 attempts to maintain functionality of the original OptiPNG as much as possible, +although command line usage may have changed (OptiPNG Next follows semantic versioning). + +As version 2 is in alpha, there are still some features that are missing. All features +that were available in the original will be implemented in OptiPNG version 2 before it moves into +beta. + +## Contributing + +Any contributions are welcome and will be accepted via pull request on GitHub. Bug reports can be +filed via GitHub issues. If you have the capability to submit a fix with the bug report, it is +preferred that you do so via pull request, however you do not need to be a Rust programmer to +submit a bug report. Other contributions (such as improving documentation or translations) +are also welcome via GitHub. + +## License + +OptiPNG Next is open-source software, distributed under the MIT license. (Version 2 is a complete rewrite and shares no +code with the original OptiPNG, which is under the zlib license.) diff --git a/src/lib.rs b/src/lib.rs index 3f8726f6..807e1473 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,11 @@ extern crate libc; use std::path::Path; use std::collections::HashSet; -pub mod png; pub mod deflate { pub mod deflate; pub mod stream; } +pub mod png; pub struct Options<'a> { pub backup: bool, @@ -22,19 +22,18 @@ pub struct Options<'a> { pub preserve_attrs: bool, pub verbosity: Option, pub filter: HashSet, - pub interlaced: u8, + pub interlace: Option, pub compression: HashSet, - pub zm: HashSet, - pub zs: HashSet, - pub zw: u16, + pub memory: HashSet, + pub strategies: HashSet, + pub window: u16, pub bit_depth_reduction: bool, pub color_type_reduction: bool, pub palette_reduction: bool, pub idat_recoding: bool, - pub idat_paranoia: bool, } -pub fn optimize(filepath: &Path, opts: Options) -> Result<(), String> { +pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { // Decode PNG from file println!("Processing: {}", filepath.to_str().unwrap()); let in_file = Path::new(filepath); @@ -71,19 +70,19 @@ pub fn optimize(filepath: &Path, opts: Options) -> Result<(), String> { // Go through selected permutations and determine the best let mut best: Option<(u8, u8, u8, u8)> = None; if opts.idat_recoding { - let combinations = opts.filter.len() * opts.compression.len() * opts.zm.len() * - opts.zs.len(); + let combinations = opts.filter.len() * opts.compression.len() * opts.memory.len() * + opts.strategies.len(); println!("Trying: {} combinations", combinations); // TODO: Multithreading for f in &opts.filter { for zc in &opts.compression { - for zm in &opts.zm { - for zs in &opts.zs { + for zm in &opts.memory { + for zs in &opts.strategies { let new_idat = match deflate::deflate::deflate(png.raw_data.as_ref(), *zc, *zm, *zs, - opts.zw) { + opts.window) { Ok(x) => x, Err(x) => return Err(x), }; diff --git a/src/main.rs b/src/main.rs index 4b32ac2b..4f153f67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,12 @@ extern crate optipng; +extern crate clap; +extern crate regex; +use clap::{App, Arg}; +use regex::Regex; +use std::collections::HashSet; use std::env; use std::path::Path; -use std::collections::HashSet; fn main() { // TODO: Handle wildcards @@ -13,11 +17,11 @@ fn main() { filter.insert(5); let mut compression = HashSet::new(); compression.insert(9); - let mut zm = HashSet::new(); - zm.insert(9); - let mut zs = HashSet::new(); + let mut memory = HashSet::new(); + memory.insert(9); + let mut strategies = HashSet::new(); for i in 0..4 { - zs.insert(i); + strategies.insert(i); } let default_opts = optipng::Options { @@ -30,21 +34,214 @@ fn main() { preserve_attrs: false, verbosity: Some(0), filter: filter, - interlaced: 0, + interlace: None, compression: compression, - zm: zm, - zs: zs, - zw: 15, + memory: memory, + strategies: strategies, + window: 15, bit_depth_reduction: true, color_type_reduction: true, palette_reduction: true, idat_recoding: true, - idat_paranoia: false, }; + // TODO: Handle command line args + let matches = App::new("optipng") + .version("2.0.0-alpha.1") + .author("Joshua Holmer ") + .about("Losslessly improves compression of PNG files") + .arg(Arg::with_name("files") + .help("File(s) to compress") + .index(1) + .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") + .long("backup")) + .arg(Arg::with_name("output_dir") + .help("Write output file(s) to ") + .long("dir") + .takes_value(true)) + .arg(Arg::with_name("output_file") + .help("Write output file to ") + .long("out") + .takes_value(true)) + .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("version") + .help("Show copyright and version info") + .short("V")) + .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-bit-depth") + .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") + .long("nz")) + .arg(Arg::with_name("strip") + .help("Strip all metadata objects") + .long("strip")) + .after_help("Optimization levels: + -o0 => -zc3 -nz (0 or 1 trials) + -o1 => -zc9 (1 trial) + -o2 => -zc9 -zs0-3 -f0,5 (8 trials) + -o3 => -zc9 -zm8-9 -zs0-3 -f0,5 (16 trials) + -o4 => -zc9 -zm8-9 -zs0-3 -f0-5 (48 trials) + -o5 => -zc3-9 -zm8-9 -zs0-3 -f0-5 (192 trials) + -o6 => -zc1-9 -zm7-9 -zs0-3 -f0-5 (360 trials) + -o6 -zm1-9 => -zc1-9 -zm1-9 -zs0-3 -f0-5 (1080 trials) + + Exhaustive combinations such as \"-o6 -zm1-9\" are not generally recommended. + These are very slow and generally provide no compression gain. + + Manually specifying a compression option (zc, zm, etc.) will override the optimization preset, + regardless of the order you write the arguments.") + .get_matches(); + + let mut opts = default_opts; + + if !matches.is_present("files") { + return (); + } + // TODO: Handle optimization presets - match optipng::optimize(infile, default_opts) { - Ok(_) => (), - Err(x) => panic!(x), - }; + for input in matches.values_of("files").unwrap() { + opts.out_file = Path::new(input); + match optipng::optimize(Path::new(input), &opts) { + Ok(_) => (), + Err(x) => println!("{}", x), + }; + } +} + +fn parse_numeric_range_opts(input: &str, min_value: u8, max_value: u8) -> Result, String> { + let one_item = Regex::new(format!("^[{}-{}]$", min_value, max_value).as_ref()).unwrap(); + let multiple_items = Regex::new(format!("^([{}-{}])(,|-)([{}-{}])$", + min_value, + max_value, + min_value, + max_value) + .as_ref()) + .unwrap(); + + if one_item.is_match(input) { + return Ok(vec![input.parse::().unwrap()]); + } + + if let Some(captures) = multiple_items.captures(input) { + let first = captures[1].parse::().unwrap(); + let second = captures[3].parse::().unwrap(); + if first >= second { + return Err("Not a valid input".to_owned()); + } + + return Ok(match &captures[2] { + "," => vec![first, second], + "-" => (first..second + 1).collect(), + _ => panic!("Unreachable"), + }); + } + + Err("Not a valid input".to_owned()) }