From cb52d8466a68a87c4eba834c20cf3e22d209366b Mon Sep 17 00:00:00 2001 From: Kornel Date: Thu, 12 Jul 2018 12:54:06 +0100 Subject: [PATCH] Reading from stdin (#118) --- src/lib.rs | 65 +++++++++++++++++++++++++++++++++++++-------- src/main.rs | 38 +++++++++++++++----------- tests/filters.rs | 4 +-- tests/flags.rs | 24 ++++++++--------- tests/interlaced.rs | 4 +-- tests/lib.rs | 7 +++-- tests/reduction.rs | 10 +++---- tests/regression.rs | 6 ++--- 8 files changed, 104 insertions(+), 54 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index de1f95e5..a7031218 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,8 +19,9 @@ use png::PngData; #[cfg(feature = "parallel")] use rayon::prelude::*; use std::collections::{HashMap, HashSet}; +use std::fmt; use std::fs::{copy, File}; -use std::io::{stdout, BufWriter, Write}; +use std::io::{stdin, stdout, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use std::sync::Mutex; @@ -62,6 +63,37 @@ impl OutFile { } } +/// Where to read images from +#[derive(Clone, Debug)] +pub enum InFile { + Path(PathBuf), + StdIn, +} + +impl InFile { + pub fn path(&self) -> Option<&Path> { + match *self { + InFile::Path(ref p) => Some(p.as_path()), + _ => None, + } + } +} + +impl fmt::Display for InFile { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + InFile::Path(ref p) => write!(f, "{}", p.display()), + InFile::StdIn => f.write_str("stdin"), + } + } +} + +impl> From for InFile { + fn from(s: T) -> Self { + InFile::Path(s.into()) + } +} + #[derive(Clone, Debug)] /// Options controlling the output of the `optimize` function pub struct Options { @@ -287,7 +319,7 @@ impl Default for Options { } /// Perform optimization on the input file using the options provided -pub fn optimize(input_path: &Path, output: &OutFile, opts: &Options) -> Result<(), PngError> { +pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Result<(), PngError> { // Initialize the thread pool with correct number of threads #[cfg(feature = "parallel")] let thread_count = opts.threads; @@ -298,10 +330,18 @@ pub fn optimize(input_path: &Path, output: &OutFile, opts: &Options) -> Result<( // Read in the file and try to decode as PNG. if opts.verbosity.is_some() { - eprintln!("Processing: {}", input_path.to_str().unwrap()); + eprintln!("Processing: {}", input); } - let in_data = PngData::read_file(input_path)?; + let in_data = match *input { + InFile::Path(ref input_path) => PngData::read_file(input_path)?, + InFile::StdIn => { + let mut data = Vec::new(); + stdin().read_to_end(&mut data) + .map_err(|e| PngError::new(&format!("Error reading stdin: {}", e)))?; + data + } + }; let mut png = PngData::from_slice(&in_data, opts.fix_errors)?; // Run the optimizer on the decoded PNG. @@ -309,9 +349,9 @@ pub fn optimize(input_path: &Path, output: &OutFile, opts: &Options) -> Result<( if is_fully_optimized(in_data.len(), optimized_output.len(), opts) { eprintln!("File already optimized"); - match *output { + match (output, input) { // if p is None, it also means same as the input path - OutFile::Path(ref p) if p.as_ref().map_or(true, |p| p == input_path) => { + (&OutFile::Path(ref p), &InFile::Path(ref input_path)) if p.as_ref().map_or(true, |p| p == input_path) => { return Ok(()); }, _ => { @@ -327,14 +367,15 @@ pub fn optimize(input_path: &Path, output: &OutFile, opts: &Options) -> Result<( return Ok(()); } - match *output { - OutFile::StdOut => { + match (output, input) { + (&OutFile::StdOut, _) | + (&OutFile::Path(None), &InFile::StdIn) => { let mut buffer = BufWriter::new(stdout()); buffer.write_all(&optimized_output) .map_err(|e| PngError::new(&format!("Unable to write to stdout: {}", e)))?; }, - OutFile::Path(ref output_path) => { - let output_path = output_path.as_ref().map(|p| p.as_path()).unwrap_or(input_path); + (&OutFile::Path(ref output_path), _) => { + let output_path = output_path.as_ref().map(|p| p.as_path()).unwrap_or_else(|| input.path().unwrap()); if opts.backup { perform_backup(output_path)?; } @@ -343,7 +384,9 @@ pub fn optimize(input_path: &Path, output: &OutFile, opts: &Options) -> Result<( "Unable to write to file {}: {}", output_path.display(), err )))?; if opts.preserve_attrs { - copy_permissions(input_path, &out_file, opts.verbosity); + if let Some(input_path) = input.path() { + copy_permissions(input_path, &out_file, opts.verbosity); + } } let mut buffer = BufWriter::new(out_file); diff --git a/src/main.rs b/src/main.rs index fd16e2d6..cbc8c102 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use clap::{App, Arg, ArgMatches}; use oxipng::AlphaOptim; use oxipng::Deflaters; use oxipng::Headers; -use oxipng::OutFile; +use oxipng::{InFile, OutFile}; use oxipng::{Options, PngError}; use std::collections::HashSet; use std::fs::DirBuilder; @@ -29,7 +29,7 @@ fn main() { .author("Joshua Holmer ") .about("Losslessly improves compression of PNG files") .arg(Arg::with_name("files") - .help("File(s) to compress") + .help("File(s) to compress (use \"-\" for stdin)") .index(1) .multiple(true) .use_delimiter(false) @@ -233,7 +233,7 @@ fn main() { }; let files = collect_files(matches.values_of("files").unwrap() - .map(PathBuf::from).collect(), &out_dir, &out_file, opts.recursive); + .map(PathBuf::from).collect(), &out_dir, &out_file, opts.recursive, true); let res: Result<(), PngError> = files.into_iter().map(|(input, output)| { oxipng::optimize(&input, &output, &opts) @@ -245,30 +245,38 @@ fn main() { } } -fn collect_files(files: Vec, out_dir: &Option, out_file: &OutFile, recursive: bool) -> Vec<(PathBuf, OutFile)> { - let mut out = Vec::new(); +fn collect_files(files: Vec, out_dir: &Option, out_file: &OutFile, recursive: bool, allow_stdin: bool) -> Vec<(InFile, OutFile)> { + let mut in_out_pairs = Vec::new(); + let allow_stdin = allow_stdin && files.len() == 1; for input in files { - if input.is_dir() { + let using_stdin = allow_stdin && input.to_str().map_or(false, |p| p == "-"); + if !using_stdin && input.is_dir() { if recursive { let files = input .read_dir() .unwrap() .map(|x| x.unwrap().path().to_owned()) .collect(); - out.extend(collect_files(files, out_dir, out_file, recursive)); + in_out_pairs.extend(collect_files(files, out_dir, out_file, recursive, false)); } else { eprintln!("{} is a directory, skipping", input.display()); } + continue; + }; + let out_file = if let Some(ref out_dir) = *out_dir { + let out_path = Some(out_dir.join(input.file_name().unwrap())); + OutFile::Path(out_path) } else { - out.push(if let Some(ref out_dir) = *out_dir { - let out_path = Some(out_dir.join(input.file_name().unwrap())); - (input, OutFile::Path(out_path)) - } else { - (input, (*out_file).clone()) - }); - } + (*out_file).clone() + }; + let in_file = if using_stdin { + InFile::StdIn + } else { + InFile::Path(input) + }; + in_out_pairs.push((in_file, out_file)); } - out + in_out_pairs } #[cfg_attr(feature = "clippy", allow(cyclomatic_complexity))] diff --git a/tests/filters.rs b/tests/filters.rs index 7586d240..6c0dd640 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -2,7 +2,7 @@ extern crate oxipng; use oxipng::colors::{BitDepth, ColorType}; use oxipng::png; -use oxipng::OutFile; +use oxipng::{InFile, OutFile}; use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; @@ -36,7 +36,7 @@ fn test_it_converts( assert_eq!(png.ihdr_data.color_type, color_type_in); assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; diff --git a/tests/flags.rs b/tests/flags.rs index 13260ef1..b47242f9 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,6 +1,6 @@ extern crate oxipng; -use oxipng::OutFile; +use oxipng::{InFile, OutFile}; use oxipng::colors::{BitDepth, ColorType}; use oxipng::deflate::Deflaters; use oxipng::headers::Headers; @@ -35,7 +35,7 @@ fn test_it_converts( assert_eq!(png.ihdr_data.color_type, color_type_in); assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -85,7 +85,7 @@ fn strip_headers_list() { assert!(png.aux_headers.contains_key("iTXt")); assert!(png.aux_headers.contains_key("iCCP")); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -119,7 +119,7 @@ fn strip_headers_safe() { assert!(png.aux_headers.contains_key("iTXt")); assert!(png.aux_headers.contains_key("iCCP")); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -153,7 +153,7 @@ fn strip_headers_all() { assert!(png.aux_headers.contains_key("iTXt")); assert!(png.aux_headers.contains_key("iCCP")); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -187,7 +187,7 @@ fn strip_headers_none() { assert!(png.aux_headers.contains_key("iTXt")); assert!(png.aux_headers.contains_key("iCCP")); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -219,7 +219,7 @@ fn interlacing_0_to_1() { assert_eq!(png.ihdr_data.interlaced, 0); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -249,7 +249,7 @@ fn interlacing_1_to_0() { assert_eq!(png.ihdr_data.interlaced, 1); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -281,7 +281,7 @@ fn interlacing_0_to_1_small_files() { assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -315,7 +315,7 @@ fn interlacing_1_to_0_small_files() { assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -350,7 +350,7 @@ fn interlaced_0_to_1_other_filter_mode() { assert_eq!(png.ihdr_data.interlaced, 0); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -400,7 +400,7 @@ fn fix_errors() { assert_eq!(png.ihdr_data.color_type, ColorType::RGBA); assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 5dfc75dd..b5dc3ce9 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -1,6 +1,6 @@ extern crate oxipng; -use oxipng::OutFile; +use oxipng::{InFile, OutFile}; use oxipng::colors::{BitDepth, ColorType}; use oxipng::png; use std::collections::HashSet; @@ -34,7 +34,7 @@ fn test_it_converts( assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); assert_eq!(png.ihdr_data.interlaced, 1); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; diff --git a/tests/lib.rs b/tests/lib.rs index 8336c5f5..25043361 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -3,7 +3,6 @@ extern crate oxipng; use oxipng::OutFile; use std::default::Default; use std::fs::File; -use std::path::Path; use std::io::prelude::*; #[test] @@ -50,7 +49,7 @@ fn optimize() { let mut opts: oxipng::Options = Default::default(); opts.verbosity = Some(1); - let result = oxipng::optimize(Path::new("tests/files/fully_optimized.png"), &OutFile::Path(None), &opts); + let result = oxipng::optimize(&"tests/files/fully_optimized.png".into(), &OutFile::Path(None), &opts); assert!(result.is_ok()); } @@ -59,7 +58,7 @@ fn optimize_corrupted() { let mut opts: oxipng::Options = Default::default(); opts.verbosity = Some(1); - let result = oxipng::optimize(Path::new("tests/files/corrupted_header.png"), &OutFile::Path(None), &opts); + let result = oxipng::optimize(&"tests/files/corrupted_header.png".into(), &OutFile::Path(None), &opts); assert!(result.is_err()); } @@ -68,6 +67,6 @@ fn optimize_apng() { let mut opts: oxipng::Options = Default::default(); opts.verbosity = Some(1); - let result = oxipng::optimize(Path::new("tests/files/apng_file.png"), &OutFile::Path(None), &opts); + let result = oxipng::optimize(&"tests/files/apng_file.png".into(), &OutFile::Path(None), &opts); assert!(result.is_err()); } diff --git a/tests/reduction.rs b/tests/reduction.rs index be5a45e5..8b390524 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -1,6 +1,6 @@ extern crate oxipng; -use oxipng::OutFile; +use oxipng::{InFile, OutFile}; use oxipng::colors::{AlphaOptim, BitDepth, ColorType}; use oxipng::png; use std::collections::HashSet; @@ -38,7 +38,7 @@ fn test_it_converts( assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); assert_eq!(png.ihdr_data.interlaced, 0); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -718,7 +718,7 @@ fn palette_should_be_reduced_with_dupes() { assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); assert_eq!(png.palette.unwrap().len(), 43 * 3); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -751,7 +751,7 @@ fn palette_should_be_reduced_with_unused() { assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); assert_eq!(png.palette.unwrap().len(), 35 * 3); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -784,7 +784,7 @@ fn palette_should_be_reduced_with_both() { assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); assert_eq!(png.palette.unwrap().len(), 43 * 3); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; diff --git a/tests/regression.rs b/tests/regression.rs index c4dfc46e..4b1effd5 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -2,7 +2,7 @@ extern crate oxipng; use oxipng::colors::{BitDepth, ColorType}; use oxipng::png; -use oxipng::OutFile; +use oxipng::{InFile, OutFile}; use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; @@ -34,7 +34,7 @@ fn test_it_converts( assert_eq!(png.ihdr_data.color_type, color_type_in); assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; @@ -79,7 +79,7 @@ fn issue_42() { assert_eq!(png.ihdr_data.color_type, ColorType::GrayscaleAlpha); assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - match oxipng::optimize(&input, &output, &opts) { + match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), Err(x) => panic!("{}", x), };