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.
This commit is contained in:
parent
193e6496bd
commit
2573b20966
5 changed files with 204 additions and 108 deletions
255
src/lib.rs
255
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<u8>);
|
||||
|
||||
// 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<Vec<u8>, 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<u8> {
|
||||
type TrialWithData = (u8, u8, u8, u8, Vec<u8>);
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PngData, String> {
|
||||
let mut byte_offset: usize = 0;
|
||||
// Test that png header is valid
|
||||
let header: Vec<u8> = byte_data.iter().take(8).cloned().collect();
|
||||
|
|
|
|||
1
tests/files/corrupted_header.png
Normal file
1
tests/files/corrupted_header.png
Normal file
|
|
@ -0,0 +1 @@
|
|||
abcdefghjik
|
||||
BIN
tests/files/fully_optimized.png
Normal file
BIN
tests/files/fully_optimized.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 B |
51
tests/lib.rs
Normal file
51
tests/lib.rs
Normal file
|
|
@ -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<u8> = 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<u8> = 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());
|
||||
}
|
||||
Loading…
Reference in a new issue