From 2573b209660b2350800e85cd04d8f1c436530e80 Mon Sep 17 00:00:00 2001 From: Toby Lawrence Date: Wed, 4 May 2016 16:04:39 -0400 Subject: [PATCH] Add support for optimizing PNGs loaded in-memory. In keeping with being able to use oxipng as a library, this adds a way for developers to optimize a PNG already in memory, instead of requiring it to exist on disk first. --- src/lib.rs | 255 ++++++++++++++++++------------- src/png.rs | 5 + tests/files/corrupted_header.png | 1 + tests/files/fully_optimized.png | Bin 0 -> 67 bytes tests/lib.rs | 51 +++++++ 5 files changed, 204 insertions(+), 108 deletions(-) create mode 100644 tests/files/corrupted_header.png create mode 100644 tests/files/fully_optimized.png create mode 100644 tests/lib.rs diff --git a/src/lib.rs b/src/lib.rs index 6049cfb5..085cce3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,21 +134,150 @@ impl Default for Options { /// Perform optimization on the input file using the options provided pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { - type TrialWithData = (u8, u8, u8, u8, Vec); - - // Decode PNG from file + // Read in the file and try to decode as PNG. if opts.verbosity.is_some() { writeln!(&mut stderr(), "Processing: {}", filepath.to_str().unwrap()).ok(); } + let in_file = Path::new(filepath); + let original_size = in_file.metadata().unwrap().len() as usize; let mut png = match png::PngData::new(&in_file, opts.fix_errors) { Ok(x) => x, Err(x) => return Err(x), }; + // Run the optimizer on the decoded PNG. + let optimized_output = optimize_png(&mut png, original_size, opts); + + if is_fully_optimized(original_size, optimized_output.len(), opts) { + writeln!(&mut stderr(), "File already optimized").ok(); + return Ok(()); + } + + if opts.pretend { + if opts.verbosity.is_some() { + writeln!(&mut stderr(), "Running in pretend mode, no output").ok(); + } + } else { + if opts.backup { + match copy(in_file, + in_file.with_extension(format!("bak.{}", + in_file.extension() + .unwrap() + .to_str() + .unwrap()))) { + Ok(x) => x, + Err(_) => { + return Err(format!("Unable to write to backup file at {}", + opts.out_file.display())) + } + }; + } + + if opts.stdout { + let mut buffer = BufWriter::new(stdout()); + match buffer.write_all(&optimized_output) { + Ok(_) => (), + Err(_) => return Err("Unable to write to stdout".to_owned()), + } + } else { + let out_file = match File::create(&opts.out_file) { + Ok(x) => x, + Err(_) => { + return Err(format!("Unable to write to file {}", opts.out_file.display())) + } + }; + + if opts.preserve_attrs { + match File::open(filepath) { + Ok(f) => { + match f.metadata() { + Ok(metadata) => { + // TODO: Implement full permission changing on Unix + // Not available in stable, requires block cfg statements + // See https://github.com/rust-lang/rust/issues/15701 + { + match out_file.metadata() { + Ok(out_meta) => { + let readonly = metadata.permissions() + .readonly(); + out_meta.permissions() + .set_readonly(readonly); + } + Err(_) => { + if opts.verbosity.is_some() { + writeln!(&mut stderr(), + "Failed to set permissions on output file") + .ok(); + } + } + } + } + } + Err(_) => { + if opts.verbosity.is_some() { + writeln!(&mut stderr(), + "Failed to read permissions on input file") + .ok(); + } + } + } + } + Err(_) => { + if opts.verbosity.is_some() { + writeln!(&mut stderr(), "Failed to read permissions on input file") + .ok(); + } + } + }; + } + + let mut buffer = BufWriter::new(out_file); + match buffer.write_all(&optimized_output) { + 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())) + } + } + } + } + Ok(()) +} + +/// Perform optimization on the input file using the options provided +pub fn optimize_from_memory(data: &[u8], opts: &Options) -> Result, String> { + // Read in the file and try to decode as PNG. + if opts.verbosity.is_some() { + writeln!(&mut stderr(), "Processing from memory"); + } + let original_size = data.len() as usize; + let mut png = match png::PngData::from_slice(&data, opts.fix_errors) { + Ok(x) => x, + Err(x) => return Err(x), + }; + + // Run the optimizer on the decoded PNG. + let optimized_output = optimize_png(&mut png, original_size, opts); + + match is_fully_optimized(original_size, optimized_output.len(), opts) { + true => { + writeln!(&mut stderr(), "Image already optimized").ok(); + Ok(data.to_vec()) + }, + false => Ok(optimized_output) + } +} + +/// Perform optimization on the input file using the options provided +fn optimize_png(mut png: &mut png::PngData, file_original_size: usize, opts: &Options) -> Vec { + type TrialWithData = (u8, u8, u8, u8, Vec); + // Print png info let idat_original_size = png.idat_data.len(); - let file_original_size = filepath.metadata().unwrap().len() as usize; if opts.verbosity.is_some() { writeln!(&mut stderr(), " {}x{} pixels, PNG format", @@ -285,103 +414,8 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { perform_strip(&mut png, &opts); - let output_data = png.output(); - if file_original_size <= output_data.len() && !opts.force && opts.interlace.is_none() { - writeln!(&mut stderr(), "File already optimized").ok(); - return Ok(()); - } + let output = png.output(); - if opts.pretend { - if opts.verbosity.is_some() { - writeln!(&mut stderr(), "Running in pretend mode, no output").ok(); - } - } else { - if opts.backup { - match copy(in_file, - in_file.with_extension(format!("bak.{}", - in_file.extension() - .unwrap() - .to_str() - .unwrap()))) { - Ok(x) => x, - Err(_) => { - return Err(format!("Unable to write to backup file at {}", - opts.out_file.display())) - } - }; - } - - if opts.stdout { - let mut buffer = BufWriter::new(stdout()); - match buffer.write_all(&output_data) { - Ok(_) => (), - Err(_) => return Err("Unable to write to stdout".to_owned()), - } - } else { - let out_file = match File::create(&opts.out_file) { - Ok(x) => x, - Err(_) => { - return Err(format!("Unable to write to file {}", opts.out_file.display())) - } - }; - - if opts.preserve_attrs { - match File::open(filepath) { - Ok(f) => { - match f.metadata() { - Ok(metadata) => { - // TODO: Implement full permission changing on Unix - // Not available in stable, requires block cfg statements - // See https://github.com/rust-lang/rust/issues/15701 - { - match out_file.metadata() { - Ok(out_meta) => { - let readonly = metadata.permissions() - .readonly(); - out_meta.permissions() - .set_readonly(readonly); - } - Err(_) => { - if opts.verbosity.is_some() { - writeln!(&mut stderr(), - "Failed to set permissions on output file") - .ok(); - } - } - } - } - } - Err(_) => { - if opts.verbosity.is_some() { - writeln!(&mut stderr(), - "Failed to read permissions on input file") - .ok(); - } - } - } - } - Err(_) => { - if opts.verbosity.is_some() { - writeln!(&mut stderr(), "Failed to read permissions on input file") - .ok(); - } - } - }; - } - - let mut buffer = BufWriter::new(out_file); - 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() { writeln!(&mut stderr(), @@ -396,25 +430,26 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> { png.idat_data.len() - idat_original_size) .ok(); } - if file_original_size >= output_data.len() { + if file_original_size >= output.len() { 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 * + output.len(), + file_original_size - output.len(), + (file_original_size - output.len()) as f64 / file_original_size as f64 * 100f64) .ok(); } else { 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 * + output.len(), + output.len() - file_original_size, + (output.len() - file_original_size) as f64 / file_original_size as f64 * 100f64) .ok(); } } - Ok(()) + + output } fn perform_reductions(png: &mut png::PngData, opts: &Options) -> bool { @@ -503,3 +538,7 @@ fn perform_strip(png: &mut png::PngData, opts: &Options) { } } } + +fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool { + return original_size <= optimized_size && !opts.force && opts.interlace.is_none() +} diff --git a/src/png.rs b/src/png.rs index be009855..c325ab45 100644 --- a/src/png.rs +++ b/src/png.rs @@ -302,6 +302,11 @@ impl PngData { Ok(_) => (), Err(_) => return Err("Failed to read from file".to_owned()), } + + return PngData::from_slice(&byte_data, fix_errors) + } + + pub fn from_slice(byte_data: &[u8], fix_errors: bool) -> Result { let mut byte_offset: usize = 0; // Test that png header is valid let header: Vec = byte_data.iter().take(8).cloned().collect(); diff --git a/tests/files/corrupted_header.png b/tests/files/corrupted_header.png new file mode 100644 index 00000000..d48c32cb --- /dev/null +++ b/tests/files/corrupted_header.png @@ -0,0 +1 @@ +abcdefghjik diff --git a/tests/files/fully_optimized.png b/tests/files/fully_optimized.png new file mode 100644 index 0000000000000000000000000000000000000000..91a99b94e23a00cc8133f22e3fe2a0b48a015808 GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv6UAQ@H$MqV!6EkIEQ MPgg&ebxsLQ07X&@0{{R3 literal 0 HcmV?d00001 diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 00000000..65d2d592 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,51 @@ +extern crate image; +extern crate oxipng; + +use std::default::Default; +use std::fs::File; +use std::path::Path; +use std::io::prelude::*; + +#[test] +fn optimize_from_memory() { + let mut in_file = File::open("tests/files/fully_optimized.png").unwrap(); + let mut in_file_buf: Vec = Vec::new(); + in_file.read_to_end(&mut in_file_buf).unwrap(); + + let mut opts: oxipng::Options = Default::default(); + opts.verbosity = Some(1); + + let result = oxipng::optimize_from_memory(&in_file_buf, &opts); + assert!(result.is_ok()); +} + +#[test] +fn optimize_from_memory_corrupted() { + let mut in_file = File::open("tests/files/corrupted_header.png").unwrap(); + let mut in_file_buf: Vec = Vec::new(); + in_file.read_to_end(&mut in_file_buf).unwrap(); + + let mut opts: oxipng::Options = Default::default(); + opts.verbosity = Some(1); + + let result = oxipng::optimize_from_memory(&in_file_buf, &opts); + assert!(result.is_err()); +} + +#[test] +fn optimize() { + let mut opts: oxipng::Options = Default::default(); + opts.verbosity = Some(1); + + let result = oxipng::optimize(Path::new("tests/files/fully_optimized.png"), &opts); + assert!(result.is_ok()); +} + +#[test] +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"), &opts); + assert!(result.is_err()); +}