Print console output to stderr instead of stdout
Makes it friendlier with --stdout option See #20 for discussion
This commit is contained in:
parent
0ae632c7f4
commit
7d1e3a5bb9
8 changed files with 133 additions and 43 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
**Version 0.1.2** (unreleased)
|
**Version 0.1.2** (unreleased)
|
||||||
- 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))
|
- 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))
|
- Use heuristics to determine best combination for `-o1` ([#21](https://github.com/shssoichiro/oxipng/issues/21))
|
||||||
|
|
||||||
**Version 0.1.1**
|
**Version 0.1.1**
|
||||||
|
|
|
||||||
102
src/lib.rs
102
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 {
|
||||||
|
|
@ -47,8 +47,8 @@ pub struct Options {
|
||||||
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() {
|
if opts.verbosity.is_some() {
|
||||||
println!("Processing: {}", filepath.to_str().unwrap())
|
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,
|
||||||
|
|
@ -59,21 +59,33 @@ 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();
|
||||||
}
|
}
|
||||||
println!(" IDAT size = {} bytes", idat_original_size);
|
writeln!(&mut stderr(),
|
||||||
println!(" File size = {} bytes", file_original_size);
|
" IDAT size = {} bytes",
|
||||||
|
idat_original_size)
|
||||||
|
.ok();
|
||||||
|
writeln!(&mut stderr(),
|
||||||
|
" File size = {} bytes",
|
||||||
|
file_original_size)
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut filter = opts.filter.clone();
|
let mut filter = opts.filter.clone();
|
||||||
|
|
@ -149,8 +161,8 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
|
||||||
let combinations = filter.len() * compression.len() * memory.len() * strategies.len();
|
let combinations = filter.len() * compression.len() * memory.len() * strategies.len();
|
||||||
let mut results = Vec::with_capacity(combinations);
|
let mut results = Vec::with_capacity(combinations);
|
||||||
if opts.verbosity.is_some() {
|
if opts.verbosity.is_some() {
|
||||||
println!("Trying: {} combinations", combinations)
|
writeln!(&mut stderr(), "Trying: {} combinations", combinations).ok();
|
||||||
};
|
}
|
||||||
crossbeam::scope(|scope| {
|
crossbeam::scope(|scope| {
|
||||||
for f in &filter {
|
for f in &filter {
|
||||||
let filtered = png.filter_image(*f);
|
let filtered = png.filter_image(*f);
|
||||||
|
|
@ -169,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()))
|
||||||
|
|
@ -202,13 +214,15 @@ 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(),
|
||||||
|
" zc = {} zm = {} zs = {} f = {} {} bytes",
|
||||||
better.1,
|
better.1,
|
||||||
better.2,
|
better.2,
|
||||||
better.3,
|
better.3,
|
||||||
better.0,
|
better.0,
|
||||||
png.idat_data.len());
|
png.idat_data.len())
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,12 +234,12 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -256,38 +270,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(),
|
||||||
|
" IDAT size = {} bytes ({} bytes decrease)",
|
||||||
png.idat_data.len(),
|
png.idat_data.len(),
|
||||||
idat_original_size - png.idat_data.len());
|
idat_original_size - png.idat_data.len())
|
||||||
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
println!(" IDAT size = {} bytes ({} bytes increase)",
|
writeln!(&mut stderr(),
|
||||||
|
" IDAT size = {} bytes ({} bytes increase)",
|
||||||
png.idat_data.len(),
|
png.idat_data.len(),
|
||||||
png.idat_data.len() - idat_original_size);
|
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(),
|
||||||
|
" file size = {} bytes ({} bytes = {:.2}% decrease)",
|
||||||
output_data.len(),
|
output_data.len(),
|
||||||
file_original_size - output_data.len(),
|
file_original_size - output_data.len(),
|
||||||
(file_original_size - output_data.len()) as f64 / file_original_size as f64 *
|
(file_original_size - output_data.len()) as f64 / file_original_size as f64 *
|
||||||
100f64);
|
100f64)
|
||||||
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
println!(" file size = {} bytes ({} bytes = {:.2}% increase)",
|
writeln!(&mut stderr(),
|
||||||
|
" file size = {} bytes ({} bytes = {:.2}% increase)",
|
||||||
output_data.len(),
|
output_data.len(),
|
||||||
output_data.len() - file_original_size,
|
output_data.len() - file_original_size,
|
||||||
(output_data.len() - file_original_size) as f64 / file_original_size as f64 *
|
(output_data.len() - file_original_size) as f64 / file_original_size as f64 *
|
||||||
100f64);
|
100f64)
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -295,13 +319,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
src/main.rs
12
src/main.rs
|
|
@ -5,6 +5,7 @@ extern crate regex;
|
||||||
use clap::{App, Arg, ArgMatches};
|
use clap::{App, Arg, ArgMatches};
|
||||||
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.1.1";
|
||||||
|
|
@ -223,7 +224,7 @@ fn main() {
|
||||||
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 ();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +245,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;
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +259,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();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
tests/files/quiet_mode.png
Normal file
BIN
tests/files/quiet_mode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
tests/files/verbose_mode.png
Normal file
BIN
tests/files/verbose_mode.png
Normal file
Binary file not shown.
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -49,6 +49,61 @@ fn get_opts(input: &Path) -> oxipng::Options {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 verbose_mode() {
|
||||||
|
let input = PathBuf::from("tests/files/verbose_mode.png");
|
||||||
|
let mut opts = get_opts(&input);
|
||||||
|
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]
|
#[test]
|
||||||
fn strip_headers() {
|
fn strip_headers() {
|
||||||
let input = PathBuf::from("tests/files/strip_headers.png");
|
let input = PathBuf::from("tests/files/strip_headers.png");
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue