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:
parent
052b047222
commit
273880ea0b
4 changed files with 299 additions and 29 deletions
|
|
@ -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
72
README.md
Normal 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.)
|
||||
23
src/lib.rs
23
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<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),
|
||||
};
|
||||
|
|
|
|||
225
src/main.rs
225
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 <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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue