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`
- 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**
- 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**
- Initial beta release

View file

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

View file

@ -63,7 +63,7 @@ More advanced options can be found by running `oxipng -h`.
## History
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 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::fs::{File, copy};
use std::io::{BufWriter, Write, stdout};
use std::io::{BufWriter, Write, stderr, stdout};
use std::path::{Path, PathBuf};
pub mod deflate {
@ -40,12 +40,15 @@ pub struct Options {
pub color_type_reduction: bool,
pub palette_reduction: 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> {
// 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 mut png = match png::PngData::new(&in_file) {
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 file_original_size = filepath.metadata().unwrap().len() as usize;
if opts.verbosity.is_some() {
println!(" {}x{} pixels, PNG format",
writeln!(&mut stderr(),
" {}x{} pixels, PNG format",
png.ihdr_data.width,
png.ihdr_data.height);
png.ihdr_data.height)
.ok();
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,
palette.len() / 3);
palette.len() / 3)
.ok();
} else {
println!(" {}x{} bits/pixel, {:?}",
writeln!(&mut stderr(),
" {}x{} bits/pixel, {:?}",
png.channels_per_pixel(),
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;
@ -118,16 +158,17 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
if opts.idat_recoding || something_changed {
// Go through selected permutations and determine the best
let mut best: Option<(u8, u8, u8, u8, Vec<u8>)> = None;
let combinations = opts.filter.len() * opts.compression.len() * opts.memory.len() *
opts.strategies.len();
let combinations = filter.len() * compression.len() * memory.len() * strategies.len();
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| {
for f in &opts.filter {
for f in &filter {
let filtered = png.filter_image(*f);
for zc in &opts.compression {
for zm in &opts.memory {
for zs in &opts.strategies {
for zc in &compression {
for zm in &memory {
for zs in &strategies {
let moved_filtered = filtered.clone();
results.push(scope.spawn(move || {
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) {
println!(" zc = {} zm = {} zs = {} f = {} {} bytes",
writeln!(&mut stderr(), " zc = {} zm = {} zs = {} f = {} {} bytes",
*zc,
*zm,
*zs,
*f,
new_idat.len());
new_idat.len()).ok();
}
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 {
png.idat_data = better.4.clone();
if opts.verbosity.is_some() {
println!("Found better combination:");
println!(" zc = {} zm = {} zs = {} f = {} {} bytes",
better.1,
better.2,
better.3,
better.0,
png.idat_data.len());
writeln!(&mut stderr(), "Found better combination:").ok();
writeln!(&mut stderr(),
" zc = {} zm = {} zs = {} f = {} {} bytes",
better.1,
better.2,
better.3,
better.0,
png.idat_data.len())
.ok();
}
}
}
if opts.strip {
match opts.strip.clone() {
// 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();
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(());
}
if opts.pretend {
println!("Running in pretend mode, no output");
writeln!(&mut stderr(), "Running in pretend mode, no output").ok();
} else {
if opts.backup {
match copy(in_file,
@ -227,38 +289,48 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
}
};
let mut buffer = BufWriter::new(out_file);
if opts.verbosity.is_some() {
match buffer.write_all(&output_data) {
Ok(_) => println!("Output: {}", opts.out_file.display()),
Err(_) => {
return Err(format!("Unable to write to file {}", opts.out_file.display()))
match buffer.write_all(&output_data) {
Ok(_) => {
if opts.verbosity.is_some() {
writeln!(&mut stderr(), "Output: {}", opts.out_file.display()).ok();
}
}
Err(_) => {
return Err(format!("Unable to write to file {}", opts.out_file.display()))
}
}
}
}
if opts.verbosity.is_some() {
if idat_original_size >= png.idat_data.len() {
println!(" IDAT size = {} bytes ({} bytes decrease)",
png.idat_data.len(),
idat_original_size - png.idat_data.len());
writeln!(&mut stderr(),
" IDAT size = {} bytes ({} bytes decrease)",
png.idat_data.len(),
idat_original_size - png.idat_data.len())
.ok();
} else {
println!(" IDAT size = {} bytes ({} bytes increase)",
png.idat_data.len(),
png.idat_data.len() - idat_original_size);
writeln!(&mut stderr(),
" IDAT size = {} bytes ({} bytes increase)",
png.idat_data.len(),
png.idat_data.len() - idat_original_size)
.ok();
}
if file_original_size >= output_data.len() {
println!(" file size = {} bytes ({} bytes = {:.2}% decrease)",
output_data.len(),
file_original_size - output_data.len(),
(file_original_size - output_data.len()) as f64 / file_original_size as f64 *
100f64);
writeln!(&mut stderr(),
" file size = {} bytes ({} bytes = {:.2}% decrease)",
output_data.len(),
file_original_size - output_data.len(),
(file_original_size - output_data.len()) as f64 / file_original_size as f64 *
100f64)
.ok();
} else {
println!(" file size = {} bytes ({} bytes = {:.2}% increase)",
output_data.len(),
output_data.len() - file_original_size,
(output_data.len() - file_original_size) as f64 / file_original_size as f64 *
100f64);
writeln!(&mut stderr(),
" file size = {} bytes ({} bytes = {:.2}% increase)",
output_data.len(),
output_data.len() - file_original_size,
(output_data.len() - file_original_size) as f64 / file_original_size as f64 *
100f64)
.ok();
}
}
Ok(())
@ -266,13 +338,17 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
fn report_reduction(png: &png::PngData) {
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,
palette.len() / 3);
palette.len() / 3)
.ok();
} else {
println!("Reducing image to {}x{} bits/pixel, {}",
writeln!(&mut stderr(),
"Reducing image to {}x{} bits/pixel, {}",
png.channels_per_pixel(),
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;
use clap::{App, Arg, ArgMatches};
use oxipng::png;
use regex::Regex;
use std::collections::HashSet;
use std::io::{Write, stderr};
use std::path::PathBuf;
const VERSION_STRING: &'static str = "0.1.1";
const VERSION_STRING: &'static str = "0.2.0";
fn main() {
let mut filter = HashSet::new();
@ -45,164 +47,168 @@ fn main() {
color_type_reduction: true,
palette_reduction: true,
idat_recoding: true,
strip: false,
strip: png::Headers::None,
use_heuristics: false,
};
let matches = App::new("oxipng")
.version(VERSION_STRING)
.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)
.multiple(true)
.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")
.short("b")
.long("backup"))
.arg(Arg::with_name("force")
.help("Write output even if larger than the original")
.short("F")
.long("force"))
.arg(Arg::with_name("recursive")
.help("Recurse into subdirectories")
.short("r")
.long("recursive"))
.arg(Arg::with_name("output_dir")
.help("Write output file(s) to <directory>")
.long("dir")
.takes_value(true)
.conflicts_with("output_file")
.conflicts_with("stdout"))
.arg(Arg::with_name("output_file")
.help("Write output file to <file>")
.long("out")
.takes_value(true)
.conflicts_with("output_dir")
.conflicts_with("stdout"))
.arg(Arg::with_name("stdout")
.help("Write output to stdout")
.long("stdout")
.conflicts_with("output_dir")
.conflicts_with("output_file"))
.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("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-palette-reduction")
.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 unless necessary")
.long("nz"))
.arg(Arg::with_name("strip")
.help("Strip all metadata objects")
.long("strip"))
.after_help("Optimization levels:
let matches =
App::new("oxipng")
.version(VERSION_STRING)
.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)
.multiple(true)
.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")
.short("b")
.long("backup"))
.arg(Arg::with_name("force")
.help("Write output even if larger than the original")
.short("F")
.long("force"))
.arg(Arg::with_name("recursive")
.help("Recurse into subdirectories")
.short("r")
.long("recursive"))
.arg(Arg::with_name("output_dir")
.help("Write output file(s) to <directory>")
.long("dir")
.takes_value(true)
.conflicts_with("output_file")
.conflicts_with("stdout"))
.arg(Arg::with_name("output_file")
.help("Write output file to <file>")
.long("out")
.takes_value(true)
.conflicts_with("output_dir")
.conflicts_with("stdout"))
.arg(Arg::with_name("stdout")
.help("Write output to stdout")
.long("stdout")
.conflicts_with("output_dir")
.conflicts_with("output_file"))
.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("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-palette-reduction")
.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 unless necessary")
.long("nz"))
.arg(Arg::with_name("strip")
.help("Strip metadata objects ['safe', 'all', or comma-separated list]")
.long("strip")
.short("s")
.takes_value(true))
.after_help("Optimization levels:
-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 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)
@ -215,14 +221,14 @@ fn main() {
Manually specifying a compression option (zc, zm, etc.) will override the optimization preset,
regardless of the order you write the arguments.")
.get_matches();
.get_matches();
let mut opts = default_opts;
match parse_opts_into_struct(&matches, &mut opts) {
Ok(_) => (),
Err(x) => {
println!("{}", x);
writeln!(&mut stderr(), "{}", x).ok();
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(),
current_opts)
} else {
println!("{} is a directory, skipping", input.display());
writeln!(&mut stderr(),
"{} is a directory, skipping",
input.display())
.ok();
}
continue;
}
@ -254,7 +263,9 @@ fn handle_optimization(inputs: Vec<PathBuf>, opts: oxipng::Options) {
}
match oxipng::optimize(&input, &current_opts) {
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;
}
Some("1") => {
let mut filter = HashSet::new();
filter.insert(0);
let filter = HashSet::new();
opts.filter = filter;
let mut strategies = HashSet::new();
strategies.insert(0);
let strategies = HashSet::new();
opts.strategies = strategies;
opts.use_heuristics = true;
}
// 2 is the default
Some("3") => {
@ -471,8 +481,27 @@ fn parse_opts_into_struct(matches: &ArgMatches, opts: &mut oxipng::Options) -> R
opts.idat_recoding = false;
}
if matches.is_present("strip") {
opts.strip = true;
if let Some(hdrs) = matches.value_of("strip") {
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(())
@ -482,8 +511,8 @@ fn parse_numeric_range_opts(input: &str,
min_value: u8,
max_value: u8)
-> Result<HashSet<u8>, String> {
let one_item = Regex::new(format!("^[{}-{}]$", min_value, max_value).as_ref()).unwrap();
let multiple_items = Regex::new(format!("^([{}-{}])(,|-)([{}-{}])$",
let one_item = Regex::new(format!(r"^[{}-{}]$", min_value, max_value).as_ref()).unwrap();
let multiple_items = Regex::new(format!(r"^([{}-{}])(,|-)([{}-{}])$",
min_value,
max_value,
min_value,

View file

@ -68,7 +68,7 @@ impl fmt::Display for BitDepth {
}
impl BitDepth {
fn as_u8(&self) -> u8 {
pub fn as_u8(&self) -> u8 {
match *self {
BitDepth::One => 1,
BitDepth::Two => 2,
@ -77,7 +77,7 @@ impl BitDepth {
BitDepth::Sixteen => 16,
}
}
fn from_u8(depth: u8) -> BitDepth {
pub fn from_u8(depth: u8) -> BitDepth {
match depth {
1 => BitDepth::One,
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)]
pub struct ScanLines<'a> {
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,
create: true,
preserve_attrs: false,
verbosity: Some(0),
verbosity: None,
filter: filter,
interlace: None,
compression: compression,
@ -44,7 +44,8 @@ fn get_opts(input: &Path) -> oxipng::Options {
color_type_reduction: true,
palette_reduction: 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,
create: true,
preserve_attrs: false,
verbosity: Some(0),
verbosity: None,
filter: filter,
interlace: None,
compression: compression,
@ -44,20 +44,78 @@ fn get_opts(input: &Path) -> oxipng::Options {
color_type_reduction: true,
palette_reduction: 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]
fn strip_headers() {
let input = PathBuf::from("tests/files/strip_headers.png");
fn verbose_mode() {
let input = PathBuf::from("tests/files/verbose_mode.png");
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 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(_) => (),
@ -74,6 +132,131 @@ fn strip_headers() {
};
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 new_png = image::open(&output).unwrap();

View file

@ -33,7 +33,7 @@ fn get_opts(input: &Path) -> oxipng::Options {
clobber: true,
create: true,
preserve_attrs: false,
verbosity: Some(0),
verbosity: None,
filter: filter,
interlace: None,
compression: compression,
@ -44,7 +44,8 @@ fn get_opts(input: &Path) -> oxipng::Options {
color_type_reduction: true,
palette_reduction: true,
idat_recoding: true,
strip: false,
strip: png::Headers::None,
use_heuristics: false,
}
}