Merge remote-tracking branch 'upstream/master'
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
188
src/lib.rs
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
365
src/main.rs
|
|
@ -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, ¤t_opts) {
|
match oxipng::optimize(&input, ¤t_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,
|
||||||
|
|
|
||||||
12
src/png.rs
|
|
@ -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,
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
BIN
tests/files/strip_headers_all.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
tests/files/strip_headers_list.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
tests/files/strip_headers_none.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
tests/files/strip_headers_safe.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
tests/files/verbose_mode.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
193
tests/flags.rs
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||