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
|
/// Perform optimization on the input file using the options provided
|
||||||
pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
|
pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
|
||||||
type TrialWithData = (u8, u8, u8, u8, Vec<u8>);
|
// Read in the file and try to decode as PNG.
|
||||||
|
|
||||||
// Decode PNG from file
|
|
||||||
if opts.verbosity.is_some() {
|
if opts.verbosity.is_some() {
|
||||||
writeln!(&mut stderr(), "Processing: {}", filepath.to_str().unwrap()).ok();
|
writeln!(&mut stderr(), "Processing: {}", filepath.to_str().unwrap()).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let in_file = Path::new(filepath);
|
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) {
|
let mut png = match png::PngData::new(&in_file, opts.fix_errors) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => return Err(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
|
// Print png info
|
||||||
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;
|
|
||||||
if opts.verbosity.is_some() {
|
if opts.verbosity.is_some() {
|
||||||
writeln!(&mut stderr(),
|
writeln!(&mut stderr(),
|
||||||
" {}x{} pixels, PNG format",
|
" {}x{} pixels, PNG format",
|
||||||
|
|
@ -285,103 +414,8 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
|
||||||
|
|
||||||
perform_strip(&mut png, &opts);
|
perform_strip(&mut png, &opts);
|
||||||
|
|
||||||
let output_data = png.output();
|
let output = png.output();
|
||||||
if file_original_size <= output_data.len() && !opts.force && opts.interlace.is_none() {
|
|
||||||
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(&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 opts.verbosity.is_some() {
|
||||||
if idat_original_size >= png.idat_data.len() {
|
if idat_original_size >= png.idat_data.len() {
|
||||||
writeln!(&mut stderr(),
|
writeln!(&mut stderr(),
|
||||||
|
|
@ -396,25 +430,26 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
|
||||||
png.idat_data.len() - idat_original_size)
|
png.idat_data.len() - idat_original_size)
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
if file_original_size >= output_data.len() {
|
if file_original_size >= output.len() {
|
||||||
writeln!(&mut stderr(),
|
writeln!(&mut stderr(),
|
||||||
" file size = {} bytes ({} bytes = {:.2}% decrease)",
|
" file size = {} bytes ({} bytes = {:.2}% decrease)",
|
||||||
output_data.len(),
|
output.len(),
|
||||||
file_original_size - output_data.len(),
|
file_original_size - output.len(),
|
||||||
(file_original_size - output_data.len()) as f64 / file_original_size as f64 *
|
(file_original_size - output.len()) as f64 / file_original_size as f64 *
|
||||||
100f64)
|
100f64)
|
||||||
.ok();
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
writeln!(&mut stderr(),
|
writeln!(&mut stderr(),
|
||||||
" file size = {} bytes ({} bytes = {:.2}% increase)",
|
" file size = {} bytes ({} bytes = {:.2}% increase)",
|
||||||
output_data.len(),
|
output.len(),
|
||||||
output_data.len() - file_original_size,
|
output.len() - file_original_size,
|
||||||
(output_data.len() - file_original_size) as f64 / file_original_size as f64 *
|
(output.len() - file_original_size) as f64 / file_original_size as f64 *
|
||||||
100f64)
|
100f64)
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_reductions(png: &mut png::PngData, opts: &Options) -> bool {
|
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(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => return Err("Failed to read from file".to_owned()),
|
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;
|
let mut byte_offset: usize = 0;
|
||||||
// Test that png header is valid
|
// Test that png header is valid
|
||||||
let header: Vec<u8> = byte_data.iter().take(8).cloned().collect();
|
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