Implement option parsing - Part 1

The compression options help text keeps getting split up by rustfmt.
Don't let it.
This commit is contained in:
Joshua Holmer 2016-01-07 16:29:02 -05:00
parent 052b047222
commit 273880ea0b
4 changed files with 299 additions and 29 deletions

View file

@ -1,10 +1,12 @@
[package]
name = "optipng"
version = "2.0.0"
version = "2.0.0-alpha.1"
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
[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"

72
README.md Normal file
View file

@ -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.)

View file

@ -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<u8>,
pub filter: HashSet<u8>,
pub interlaced: u8,
pub interlace: Option<u8>,
pub compression: HashSet<u8>,
pub zm: HashSet<u8>,
pub zs: HashSet<u8>,
pub zw: u16,
pub memory: HashSet<u8>,
pub strategies: HashSet<u8>,
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),
};

View file

@ -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 <jholmer.in@gmail.com>")
.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 <directory>")
.long("dir")
.takes_value(true))
.arg(Arg::with_name("output_file")
.help("Write output file to <file>")
.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<Vec<u8>, 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::<u8>().unwrap()]);
}
if let Some(captures) = multiple_items.captures(input) {
let first = captures[1].parse::<u8>().unwrap();
let second = captures[3].parse::<u8>().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())
}