From d110949d2afd245274975c2d8063db0fb4fa74b0 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Fri, 20 Jul 2018 19:19:20 -0400 Subject: [PATCH] Version 2.0.0 --- CHANGELOG.md | 13 +++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 26 +++--- benches/deflate.rs | 6 +- benches/filters.rs | 2 +- benches/interlacing.rs | 2 +- benches/reductions.rs | 2 +- benches/zopfli.rs | 2 +- package-lock.json | 104 +++++++++++----------- src/atomicmin.rs | 14 ++- src/deflate/miniz_stream.rs | 10 ++- src/deflate/mod.rs | 41 ++++++--- src/error.rs | 4 +- src/headers.rs | 24 +++-- src/lib.rs | 173 ++++++++++++++++++++---------------- src/main.rs | 52 +++++++---- src/png/mod.rs | 20 +++-- src/png/scan_lines.rs | 2 +- tests/filters.rs | 5 +- tests/flags.rs | 10 ++- tests/interlaced.rs | 7 +- tests/lib.rs | 18 +++- tests/reduction.rs | 7 +- tests/regression.rs | 9 +- 25 files changed, 339 insertions(+), 218 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a37c4decb..cca34e528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +### Version 2.0.0 + - [SEMVER_MAJOR] Make PngError a proper Error enum + - [SEMVER_MINOR] Bump minimum Rust version to 1.27.0 + - [SEMVER_MINOR] Make Rayon an optional dependency (enabled by default) + - [SEMVER_MINOR] Option to limit wall clock time spent in optimization trials + - [SEMVER_MINOR] `--keep` option (works similar to `--strip`, but takes a comma-separated list of headers to keep, and removes all other non-critical headers) + - Use faster Cloudflare zlib compression on platforms that support it + - Allow specifying more than 2 filter types via the CLI + - Avoid double glob processing on unix + - Fix reading from stdin + - Cleanup help text + - Various performance improvements + ### Version 1.0.4 - Bump `image` to 0.19.0 - Bump `bit-vec` to 0.5.0 diff --git a/Cargo.lock b/Cargo.lock index 4f9e94108..b395bb032 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,7 +265,7 @@ dependencies = [ [[package]] name = "oxipng" -version = "1.0.4" +version = "2.0.0" dependencies = [ "bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 1107725f3..c32376ae3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ homepage = "https://github.com/shssoichiro/oxipng" license = "MIT" name = "oxipng" repository = "https://github.com/shssoichiro/oxipng" -version = "1.0.4" +version = "2.0.0" [badges] travis-ci = { repository = "shssoichiro/oxipng", branch = "master" } diff --git a/README.md b/README.md index 0d9d37557..870de7089 100644 --- a/README.md +++ b/README.md @@ -94,39 +94,39 @@ Oxipng is open-source software, distributed under the MIT license. ## Benchmarks -Tested oxipng 1.0.3 (compiled on rustc 1.25.0 (84203cac6 2018-03-25)) against OptiPNG version 0.7.7 on Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz with 8 logical cores +Tested oxipng 2.0.0 (compiled on rustc 1.27.2 (58cc626de 2018-07-18)) against OptiPNG version 0.7.7 on Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz with 8 logical cores Benchmark #1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png - Time (mean ± σ): 201.1 ms ± 4.1 ms [User: 340.1 ms, System: 42.8 ms] + Time (mean ± σ): 91.2 ms ± 2.4 ms [User: 173.7 ms, System: 15.7 ms] - Range (min … max): 195.3 ms … 210.1 ms + Range (min … max): 86.4 ms … 97.7 ms Benchmark #2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png - Time (mean ± σ): 418.8 ms ± 4.6 ms [User: 415.0 ms, System: 2.7 ms] + Time (mean ± σ): 281.0 ms ± 2.1 ms [User: 280.4 ms, System: 0.8 ms] - Range (min … max): 411.0 ms … 425.4 ms + Range (min … max): 279.3 ms … 286.4 ms Summary -'./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran - 2.08x faster than 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' + './target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran + 3.08x faster than 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' Benchmark #1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png - Time (mean ± σ): 330.9 ms ± 10.3 ms [User: 1.062 s, System: 0.045 s] + Time (mean ± σ): 116.5 ms ± 3.0 ms [User: 438.8 ms, System: 20.0 ms] - Range (min … max): 315.3 ms … 344.0 ms + Range (min … max): 111.6 ms … 122.3 ms Benchmark #2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png - Time (mean ± σ): 1.424 s ± 0.011 s [User: 1.418 s, System: 0.002 s] + Time (mean ± σ): 942.6 ms ± 1.4 ms [User: 939.3 ms, System: 2.7 ms] - Range (min … max): 1.412 s … 1.448 s + Range (min … max): 941.0 ms … 945.5 ms Summary -'./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran - 4.30x faster than 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' + './target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran + 8.09x faster than 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' diff --git a/benches/deflate.rs b/benches/deflate.rs index e082f3b09..1b4ef8638 100644 --- a/benches/deflate.rs +++ b/benches/deflate.rs @@ -4,8 +4,8 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; -use test::Bencher; use std::path::PathBuf; +use test::Bencher; #[bench] fn deflate_16_bits_strategy_0(b: &mut Bencher) { @@ -256,7 +256,5 @@ fn inflate_generic(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| { - inflate(png.idat_data.as_ref()) - }); + b.iter(|| inflate(png.idat_data.as_ref())); } diff --git a/benches/filters.rs b/benches/filters.rs index 3e24d1072..7e0483070 100644 --- a/benches/filters.rs +++ b/benches/filters.rs @@ -4,8 +4,8 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; -use test::Bencher; use std::path::PathBuf; +use test::Bencher; #[bench] fn filters_16_bits_filter_0(b: &mut Bencher) { diff --git a/benches/interlacing.rs b/benches/interlacing.rs index e5de69b15..45a36cf01 100644 --- a/benches/interlacing.rs +++ b/benches/interlacing.rs @@ -4,8 +4,8 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; -use test::Bencher; use std::path::PathBuf; +use test::Bencher; #[bench] fn interlacing_16_bits(b: &mut Bencher) { diff --git a/benches/reductions.rs b/benches/reductions.rs index 11dd83b83..1ce5d512e 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -4,8 +4,8 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; -use test::Bencher; use std::path::PathBuf; +use test::Bencher; #[bench] fn reductions_16_to_8_bits(b: &mut Bencher) { diff --git a/benches/zopfli.rs b/benches/zopfli.rs index b701c01ed..012094963 100644 --- a/benches/zopfli.rs +++ b/benches/zopfli.rs @@ -4,8 +4,8 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; -use test::Bencher; use std::path::PathBuf; +use test::Bencher; #[bench] fn zopfli_16_bits_strategy_0(b: &mut Bencher) { diff --git a/package-lock.json b/package-lock.json index 2edd00463..811dc5b4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,8 +34,8 @@ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { - "camelcase": "2.1.1", - "map-obj": "1.0.1" + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" } }, "currently-unhandled": { @@ -44,7 +44,7 @@ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", "dev": true, "requires": { - "array-find-index": "1.0.2" + "array-find-index": "^1.0.1" } }, "decamelize": { @@ -59,7 +59,7 @@ "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", "dev": true, "requires": { - "is-arrayish": "0.2.1" + "is-arrayish": "^0.2.1" } }, "find-up": { @@ -68,8 +68,8 @@ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "get-stdin": { @@ -96,7 +96,7 @@ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, "requires": { - "repeating": "2.0.1" + "repeating": "^2.0.0" } }, "is-arrayish": { @@ -111,7 +111,7 @@ "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { - "builtin-modules": "1.1.1" + "builtin-modules": "^1.0.0" } }, "is-finite": { @@ -120,7 +120,7 @@ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "is-utf8": { @@ -135,11 +135,11 @@ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" } }, "loud-rejection": { @@ -148,8 +148,8 @@ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", "dev": true, "requires": { - "currently-unhandled": "0.4.1", - "signal-exit": "3.0.2" + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" } }, "map-obj": { @@ -164,16 +164,16 @@ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { - "camelcase-keys": "2.1.0", - "decamelize": "1.2.0", - "loud-rejection": "1.6.0", - "map-obj": "1.0.1", - "minimist": "1.2.0", - "normalize-package-data": "2.4.0", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "redent": "1.0.0", - "trim-newlines": "1.0.0" + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" } }, "minimist": { @@ -188,10 +188,10 @@ "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", "dev": true, "requires": { - "hosted-git-info": "2.5.0", - "is-builtin-module": "1.0.0", - "semver": "5.5.0", - "validate-npm-package-license": "3.0.1" + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, "number-is-nan": { @@ -212,7 +212,7 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "1.3.1" + "error-ex": "^1.2.0" } }, "path-exists": { @@ -221,7 +221,7 @@ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, "requires": { - "pinkie-promise": "2.0.1" + "pinkie-promise": "^2.0.0" } }, "path-type": { @@ -230,9 +230,9 @@ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "pify": { @@ -253,7 +253,7 @@ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "read-pkg": { @@ -262,9 +262,9 @@ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" } }, "read-pkg-up": { @@ -273,8 +273,8 @@ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" } }, "redent": { @@ -283,8 +283,8 @@ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", "dev": true, "requires": { - "indent-string": "2.1.0", - "strip-indent": "1.0.1" + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" } }, "repeating": { @@ -293,7 +293,7 @@ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, "requires": { - "is-finite": "1.0.2" + "is-finite": "^1.0.0" } }, "semver": { @@ -314,7 +314,7 @@ "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", "dev": true, "requires": { - "spdx-license-ids": "1.2.2" + "spdx-license-ids": "^1.0.2" } }, "spdx-expression-parse": { @@ -335,7 +335,7 @@ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "3.0.0" + "ansi-regex": "^3.0.0" } }, "strip-ansi-cli": { @@ -344,8 +344,8 @@ "integrity": "sha1-D91Cq86DJTm/cv+ROSpxqzVGIIw=", "dev": true, "requires": { - "meow": "3.7.0", - "strip-ansi": "4.0.0" + "meow": "^3.7.0", + "strip-ansi": "^4.0.0" } }, "strip-bom": { @@ -354,7 +354,7 @@ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "requires": { - "is-utf8": "0.2.1" + "is-utf8": "^0.2.0" } }, "strip-indent": { @@ -363,7 +363,7 @@ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", "dev": true, "requires": { - "get-stdin": "4.0.1" + "get-stdin": "^4.0.1" } }, "trim-newlines": { @@ -378,8 +378,8 @@ "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", "dev": true, "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.4" + "spdx-correct": "~1.0.0", + "spdx-expression-parse": "~1.0.0" } } } diff --git a/src/atomicmin.rs b/src/atomicmin.rs index de87af485..9e22dad37 100644 --- a/src/atomicmin.rs +++ b/src/atomicmin.rs @@ -1,5 +1,5 @@ use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering::{SeqCst, Relaxed}; +use std::sync::atomic::Ordering::{Relaxed, SeqCst}; pub struct AtomicMin { val: AtomicUsize, @@ -8,20 +8,26 @@ pub struct AtomicMin { impl AtomicMin { pub fn new(init: Option) -> Self { Self { - val: AtomicUsize::new(init.unwrap_or(usize::max_value())) + val: AtomicUsize::new(init.unwrap_or(usize::max_value())), } } pub fn get(&self) -> Option { let val = self.val.load(SeqCst); - if val == usize::max_value() {None} else {Some(val)} + if val == usize::max_value() { + None + } else { + Some(val) + } } pub fn set_min(&self, new_val: usize) { let mut current_val = self.val.load(Relaxed); loop { if new_val < current_val { - if let Err(v) = self.val.compare_exchange(current_val, new_val, SeqCst, Relaxed) { + if let Err(v) = self.val + .compare_exchange(current_val, new_val, SeqCst, Relaxed) + { current_val = v; continue; } diff --git a/src/deflate/miniz_stream.rs b/src/deflate/miniz_stream.rs index 2fbaa51da..3e5ea33c2 100644 --- a/src/deflate/miniz_stream.rs +++ b/src/deflate/miniz_stream.rs @@ -2,7 +2,13 @@ use atomicmin::AtomicMin; use error::PngError; use miniz_oxide::deflate::core::*; -pub fn compress_to_vec_oxipng(input: &[u8], level: u8, window_bits: i32, strategy: i32, max_size: &AtomicMin) -> Result, PngError> { +pub fn compress_to_vec_oxipng( + input: &[u8], + level: u8, + window_bits: i32, + strategy: i32, + max_size: &AtomicMin, +) -> Result, PngError> { // The comp flags function sets the zlib flag if the window_bits parameter is > 0. let flags = create_comp_flags_from_zip_params(level.into(), window_bits, strategy); let mut compressor = CompressorOxide::new(flags); @@ -35,7 +41,7 @@ pub fn compress_to_vec_oxipng(input: &[u8], level: u8, window_bits: i32, strateg TDEFLStatus::Okay => { if let Some(max) = max_size.get() { if output.len() > max { - return Err(PngError::DeflatedDataTooLong(output.len())) + return Err(PngError::DeflatedDataTooLong(output.len())); } } // We need more space, so extend the vector. diff --git a/src/deflate/mod.rs b/src/deflate/mod.rs index 01f31b7a7..2b4ef6e4d 100644 --- a/src/deflate/mod.rs +++ b/src/deflate/mod.rs @@ -14,21 +14,29 @@ pub fn inflate(data: &[u8]) -> Result, PngError> { } /// Compress a data stream using the DEFLATE algorithm -pub fn deflate(data: &[u8], zc: u8, zs: u8, zw: u8, max_size: &AtomicMin) -> Result, PngError> { +pub fn deflate( + data: &[u8], + zc: u8, + zs: u8, + zw: u8, + max_size: &AtomicMin, +) -> Result, PngError> { if is_cfzlib_supported() { - return cfzlib_deflate(data, zc, zs, zw, max_size) + return cfzlib_deflate(data, zc, zs, zw, max_size); } miniz_stream::compress_to_vec_oxipng(data, zc, zw.into(), zs.into(), max_size) } fn is_cfzlib_supported() -> bool { - #[cfg(target_arch = "x86_64")] { + #[cfg(target_arch = "x86_64")] + { if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") { return true; } } - #[cfg(target_arch = "aarch64")] { + #[cfg(target_arch = "aarch64")] + { if is_arm_feature_detected!("neon") && is_arm_feature_detected!("crc") { return true; } @@ -36,20 +44,27 @@ fn is_cfzlib_supported() -> bool { false } -pub fn cfzlib_deflate(data: &[u8], level: u8, strategy: u8, window_bits: u8, max_size: &AtomicMin) -> Result, PngError> { - use std::mem; +pub fn cfzlib_deflate( + data: &[u8], + level: u8, + strategy: u8, + window_bits: u8, + max_size: &AtomicMin, +) -> Result, PngError> { use cloudflare_zlib_sys::*; + use std::mem; assert!(data.len() < u32::max_value() as usize); unsafe { let mut stream = mem::zeroed(); if Z_OK != deflateInit2( - &mut stream, - level.into(), - Z_DEFLATED, - window_bits.into(), - MAX_MEM_LEVEL, - strategy.into()) { + &mut stream, + level.into(), + Z_DEFLATED, + window_bits.into(), + MAX_MEM_LEVEL, + strategy.into(), + ) { return Err(PngError::new("deflateInit2")); } @@ -65,7 +80,7 @@ pub fn cfzlib_deflate(data: &[u8], level: u8, strategy: u8, window_bits: u8, max stream.next_out = out.as_mut_ptr(); stream.avail_out = out.capacity() as uInt; match deflate(&mut stream, Z_FINISH) { - Z_STREAM_END => {}, + Z_STREAM_END => {} Z_OK => return Err(PngError::DeflatedDataTooLong(stream.total_out as usize)), _ => return Err(PngError::new("deflate")), } diff --git a/src/error.rs b/src/error.rs index 157bd7b26..4144ab64f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -30,7 +30,9 @@ impl fmt::Display for PngError { PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"), PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"), PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"), - PngError::TruncatedData => f.write_str("Missing data in the file; the file is truncated"), + PngError::TruncatedData => { + f.write_str("Missing data in the file; the file is truncated") + } PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"), PngError::ChunkMissing(s) => write!(f, "Chunk {} missing or empty", s), PngError::Other(ref s) => f.write_str(s), diff --git a/src/headers.rs b/src/headers.rs index 7979e1ba9..9947b91ce 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -1,8 +1,8 @@ -use std::collections::HashSet; use byteorder::{BigEndian, ReadBytesExt}; use colors::{BitDepth, ColorType}; use crc::crc32; use error::PngError; +use std::collections::HashSet; use std::io::Cursor; #[derive(Debug, Clone, Copy)] @@ -50,26 +50,36 @@ pub fn parse_next_header<'a>( byte_data: &'a [u8], byte_offset: &mut usize, fix_errors: bool, - ) -> Result, PngError> { - let mut rdr = Cursor::new(byte_data.get(*byte_offset..*byte_offset + 4).ok_or(PngError::TruncatedData)?); +) -> Result, PngError> { + let mut rdr = Cursor::new(byte_data + .get(*byte_offset..*byte_offset + 4) + .ok_or(PngError::TruncatedData)?); let length = rdr.read_u32::().unwrap(); *byte_offset += 4; let header_start = *byte_offset; - let chunk_name = byte_data.get(header_start..header_start + 4).ok_or(PngError::TruncatedData)?; + let chunk_name = byte_data + .get(header_start..header_start + 4) + .ok_or(PngError::TruncatedData)?; if chunk_name == b"IEND" { // End of data return Ok(None); } *byte_offset += 4; - let data = byte_data.get(*byte_offset..*byte_offset + length as usize).ok_or(PngError::TruncatedData)?; + let data = byte_data + .get(*byte_offset..*byte_offset + length as usize) + .ok_or(PngError::TruncatedData)?; *byte_offset += length as usize; - let mut rdr = Cursor::new(byte_data.get(*byte_offset..*byte_offset + 4).ok_or(PngError::TruncatedData)?); + let mut rdr = Cursor::new(byte_data + .get(*byte_offset..*byte_offset + 4) + .ok_or(PngError::TruncatedData)?); let crc = rdr.read_u32::().unwrap(); *byte_offset += 4; - let header_bytes = byte_data.get(header_start..header_start + 4 + length as usize).ok_or(PngError::TruncatedData)?; + let header_bytes = byte_data + .get(header_start..header_start + 4 + length as usize) + .ok_or(PngError::TruncatedData)?; if !fix_errors && crc32::checksum_ieee(header_bytes) != crc { return Err(PngError::new(&format!( "CRC Mismatch in {} header; May be recoverable by using --fix", diff --git a/src/lib.rs b/src/lib.rs index d8063e50a..553b7f2a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ extern crate bit_vec; extern crate byteorder; +extern crate cloudflare_zlib_sys; extern crate crc; extern crate image; extern crate itertools; @@ -8,8 +9,8 @@ extern crate num_cpus; #[cfg(feature = "parallel")] extern crate rayon; extern crate zopfli; -extern crate cloudflare_zlib_sys; +use atomicmin::AtomicMin; use image::{DynamicImage, GenericImage, ImageFormat, Pixel}; use png::PngData; #[cfg(feature = "parallel")] @@ -19,15 +20,15 @@ use std::fmt; use std::fs::{copy, File}; use std::io::{stdin, stdout, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; -use std::time::{Duration, Instant}; use std::sync::Mutex; -use atomicmin::AtomicMin; +use std::time::{Duration, Instant}; pub use colors::AlphaOptim; pub use deflate::Deflaters; pub use error::PngError; pub use headers::Headers; +mod atomicmin; mod colors; mod deflate; mod error; @@ -36,14 +37,13 @@ mod headers; mod interlace; mod png; mod reduction; -mod atomicmin; /// Private to oxipng; don't use outside tests and benches #[doc(hidden)] pub mod internal_tests { pub use atomicmin::*; - pub use deflate::*; pub use colors::*; + pub use deflate::*; pub use headers::*; pub use png::*; } @@ -338,7 +338,8 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Result<(), InFile::Path(ref input_path) => PngData::read_file(input_path)?, InFile::StdIn => { let mut data = Vec::new(); - stdin().read_to_end(&mut data) + stdin() + .read_to_end(&mut data) .map_err(|e| PngError::new(&format!("Error reading stdin: {}", e)))?; data } @@ -352,9 +353,11 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Result<(), eprintln!("File already optimized"); match (output, input) { // if p is None, it also means same as the input path - (&OutFile::Path(ref p), &InFile::Path(ref input_path)) 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(()); - }, + } _ => { optimized_output = in_data; } @@ -369,21 +372,27 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Result<(), } match (output, input) { - (&OutFile::StdOut, _) | - (&OutFile::Path(None), &InFile::StdIn) => { + (&OutFile::StdOut, _) | (&OutFile::Path(None), &InFile::StdIn) => { let mut buffer = BufWriter::new(stdout()); - buffer.write_all(&optimized_output) + 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_else(|| input.path().unwrap()); + 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)?; } - let out_file = File::create(output_path) - .map_err(|err| PngError::new(&format!( - "Unable to write to file {}: {}", output_path.display(), err - )))?; + let out_file = File::create(output_path).map_err(|err| { + PngError::new(&format!( + "Unable to write to file {}: {}", + output_path.display(), + err + )) + })?; if opts.preserve_attrs { if let Some(input_path) = input.path() { copy_permissions(input_path, &out_file, opts.verbosity); @@ -391,12 +400,17 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Result<(), } let mut buffer = BufWriter::new(out_file); - buffer.write_all(&optimized_output) - .map_err(|e| PngError::new(&format!("Unable to write to {}: {}", output_path.display(), e)))?; + buffer.write_all(&optimized_output).map_err(|e| { + PngError::new(&format!( + "Unable to write to {}: {}", + output_path.display(), + e + )) + })?; if opts.verbosity.is_some() { eprintln!("Output: {}", output_path.display()); } - }, + } } Ok(()) } @@ -554,68 +568,74 @@ fn optimize_png( let original_len = original_png.idat_data.len(); let added_interlacing = opts.interlace == Some(1) && original_png.ihdr_data.interlaced == 0; - let best_size = AtomicMin::new(if opts.force {None} else {Some(original_len)}); + let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) }); #[cfg(feature = "parallel")] let results_iter = results.into_par_iter().with_max_len(1); #[cfg(not(feature = "parallel"))] let results_iter = results.into_iter(); - let best = results_iter - .filter_map(|trial| { - if deadline.passed() { - return None; - } - let filtered = &filters[&trial.filter]; - let new_idat = if opts.deflate == Deflaters::Zlib { - deflate::deflate(filtered, trial.compression, trial.strategy, opts.window, &best_size) - } else { - deflate::zopfli_deflate(filtered) - }; - let new_idat = match new_idat { - Ok(n) => n, - Err(PngError::DeflatedDataTooLong(max)) if opts.verbosity == Some(1) => { - eprintln!( - " zc = {} zs = {} f = {} >{} bytes", - trial.compression, - trial.strategy, - trial.filter, - max, - ); - return None; - }, - _ => return None, - }; - - // update best size across all threads - let new_size = new_idat.len(); - best_size.set_min(new_size); - - if opts.verbosity == Some(1) { + let best = results_iter.filter_map(|trial| { + if deadline.passed() { + return None; + } + let filtered = &filters[&trial.filter]; + let new_idat = if opts.deflate == Deflaters::Zlib { + deflate::deflate( + filtered, + trial.compression, + trial.strategy, + opts.window, + &best_size, + ) + } else { + deflate::zopfli_deflate(filtered) + }; + let new_idat = match new_idat { + Ok(n) => n, + Err(PngError::DeflatedDataTooLong(max)) if opts.verbosity == Some(1) => { eprintln!( - " zc = {} zs = {} f = {} {} bytes", - trial.compression, - trial.strategy, - trial.filter, - new_idat.len() + " zc = {} zs = {} f = {} >{} bytes", + trial.compression, trial.strategy, trial.filter, max, ); + return None; } + _ => return None, + }; - if new_size < original_len || added_interlacing || opts.force { - Some((trial, new_idat)) - } else { - None - } - }); + // update best size across all threads + let new_size = new_idat.len(); + best_size.set_min(new_size); + + if opts.verbosity == Some(1) { + eprintln!( + " zc = {} zs = {} f = {} {} bytes", + trial.compression, + trial.strategy, + trial.filter, + new_idat.len() + ); + } + + if new_size < original_len || added_interlacing || opts.force { + Some((trial, new_idat)) + } else { + None + } + }); #[cfg(feature = "parallel")] - let best: Option = best - .reduce_with(|i, j| if i.1.len() <= j.1.len() { i } else { j }); + let best: Option = + best.reduce_with(|i, j| if i.1.len() <= j.1.len() { i } else { j }); #[cfg(not(feature = "parallel"))] let best: Option = best.fold(None, |i, j| { - if let Some(i) = i { - if i.1.len() <= j.1.len() { Some(i) } else { Some(j) } + if let Some(i) = i { + if i.1.len() <= j.1.len() { + Some(i) } else { Some(j) } - }); + } else { + Some(j) + } + }); if let Some(better) = best { png.idat_data = better.1; @@ -746,7 +766,6 @@ fn perform_reductions(png: &mut PngData, opts: &Options, deadline: &Deadline) -> reduction_occurred } - /// Keep track of processing timeout struct Deadline { start: Instant, @@ -756,7 +775,7 @@ struct Deadline { impl Deadline { pub fn new(opts: &Options) -> Self { - Self { + Self { start: Instant::now(), timeout: opts.timeout, print_message: Mutex::new(opts.verbosity.is_some()), @@ -805,16 +824,14 @@ fn perform_strip(png: &mut PngData, opts: &Options) { // Strip headers Headers::None => (), Headers::Keep(ref hdrs) => { - png.aux_headers.retain(|chunk, _| { - hdrs.contains(chunk) - }); - }, + png.aux_headers.retain(|chunk, _| hdrs.contains(chunk)); + } Headers::Strip(ref hdrs) => for hdr in hdrs { png.aux_headers.remove(hdr); }, Headers::Safe => { const PRESERVED_HEADERS: [&str; 9] = [ - "cHRM", "gAMA", "iCCP", "sBIT", "sRGB", "bKGD", "hIST", "pHYs", "sPLT" + "cHRM", "gAMA", "iCCP", "sBIT", "sRGB", "bKGD", "hIST", "pHYs", "sPLT", ]; let hdrs = png.aux_headers.keys().cloned().collect::>(); for hdr in hdrs { @@ -883,10 +900,12 @@ fn copy_permissions(input_path: &Path, out_file: &File, verbosity: Option) { /// Compares images pixel by pixel for equivalent content fn images_equal(old_png: &DynamicImage, new_png: &DynamicImage) -> bool { - let a = old_png.pixels() + let a = old_png + .pixels() .map(|x| x.2.channels().to_owned()) .filter(|p| !(p.len() == 4 && p[3] == 0)); - let b = new_png.pixels() + let b = new_png + .pixels() .map(|x| x.2.channels().to_owned()) .filter(|p| !(p.len() == 4 && p[3] == 0)); a.eq(b) diff --git a/src/main.rs b/src/main.rs index deb05d29d..2a77e2240 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,10 @@ #![deny(missing_debug_implementations, missing_copy_implementations)] extern crate clap; -extern crate wild; extern crate oxipng; +extern crate wild; -use clap::{App, Arg, ArgMatches, AppSettings}; +use clap::{App, AppSettings, Arg, ArgMatches}; use oxipng::AlphaOptim; use oxipng::Deflaters; use oxipng::Headers; @@ -228,12 +228,22 @@ fn main() { } }; - let files = collect_files(matches.values_of("files").unwrap() - .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) - }).collect(); + let files = collect_files( + matches + .values_of("files") + .unwrap() + .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)) + .collect(); if let Err(e) = res { eprintln!("{}", e); @@ -241,7 +251,13 @@ fn main() { } } -fn collect_files(files: Vec, out_dir: &Option, out_file: &OutFile, recursive: bool, allow_stdin: bool) -> Vec<(InFile, OutFile)> { +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 { @@ -275,7 +291,9 @@ fn collect_files(files: Vec, out_dir: &Option, out_file: &OutF in_out_pairs } -fn parse_opts_into_struct(matches: &ArgMatches) -> Result<(OutFile, Option, Options), String> { +fn parse_opts_into_struct( + matches: &ArgMatches, +) -> Result<(OutFile, Option, Options), String> { let mut opts = if let Some(x) = matches.value_of("optimization") { if let Ok(opt) = x.parse::() { Options::from_preset(opt) @@ -303,7 +321,8 @@ fn parse_opts_into_struct(matches: &ArgMatches) -> Result<(OutFile, Option Result<(OutFile, Option>();; + let list_items = input.split(',').collect::>(); if list_items.len() > 1 { for value in list_items { if let Ok(value_int) = value.parse::() { - if (min_value <= value_int) && (value_int <= max_value) && !items.contains(&value_int) { + if (min_value <= value_int) + && (value_int <= max_value) + && !items.contains(&value_int) + { items.insert(value_int); continue; } diff --git a/src/png/mod.rs b/src/png/mod.rs index 5d8d432ff..72a2b06e7 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -1,3 +1,4 @@ +use atomicmin::AtomicMin; use bit_vec::BitVec; use byteorder::{BigEndian, WriteBytesExt}; use colors::{AlphaOptim, BitDepth, ColorType}; @@ -8,6 +9,8 @@ use filters::*; use headers::*; use interlace::{deinterlace_image, interlace_image}; use itertools::{flatten, Itertools}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; use reduction::bit_depth::*; use reduction::color::*; use std::collections::{HashMap, HashSet}; @@ -15,9 +18,6 @@ use std::fs::File; use std::io::{Read, Seek, SeekFrom}; use std::iter::Iterator; use std::path::Path; -#[cfg(feature = "parallel")] -use rayon::prelude::*; -use atomicmin::AtomicMin; const STD_COMPRESSION: u8 = 8; const STD_STRATEGY: u8 = 2; // Huffman only @@ -105,9 +105,10 @@ impl PngData { b"IDAT" => idat_headers.extend(data), b"acTL" => return Err(PngError::APNGNotSupported), _ => { - let name = String::from_utf8(name.to_owned()).map_err(|_| PngError::InvalidData)?; + let name = + String::from_utf8(name.to_owned()).map_err(|_| PngError::InvalidData)?; aux_headers.insert(name, data.to_owned()); - }, + } } } // Parse the headers into our PngData @@ -618,10 +619,11 @@ impl PngData { STD_WINDOW, &best_size, ).ok() - .as_ref().map(|l| { - best_size.set_min(l.len()); - l.len() - }) + .as_ref() + .map(|l| { + best_size.set_min(l.len()); + l.len() + }) }) .min() .map(|size| (size, image)) diff --git a/src/png/scan_lines.rs b/src/png/scan_lines.rs index bbf36a9dc..9e33f1b40 100644 --- a/src/png/scan_lines.rs +++ b/src/png/scan_lines.rs @@ -135,7 +135,7 @@ pub struct ScanLine<'a> { /// The filter type used to encode the current scan line (0-4) pub filter: u8, /// The byte data for the current scan line, encoded with the filter specified in the `filter` field - pub data: &'a[u8], + pub data: &'a [u8], /// The current pass if the image is interlaced pub pass: Option, } diff --git a/tests/filters.rs b/tests/filters.rs index 4a9e69397..5aff51c50 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -15,7 +15,10 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { filter.insert(0); options.filter = filter; - (OutFile::Path(Some(input.with_extension("out.png").to_owned())), options) + ( + OutFile::Path(Some(input.with_extension("out.png").to_owned())), + options, + ) } fn test_it_converts( diff --git a/tests/flags.rs b/tests/flags.rs index e99f3ffd6..13ab2c638 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,7 +1,7 @@ extern crate oxipng; -use oxipng::{InFile, OutFile}; use oxipng::internal_tests::*; +use oxipng::{InFile, OutFile}; use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; @@ -15,7 +15,10 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { filter.insert(0); options.filter = filter; - (OutFile::Path(Some(input.with_extension("out.png").to_owned())), options) + ( + OutFile::Path(Some(input.with_extension("out.png").to_owned())), + options, + ) } fn test_it_converts( @@ -427,7 +430,8 @@ fn zopfli_mode() { test_it_converts( input, - output, opts, + output, + opts, ColorType::RGB, BitDepth::Eight, ColorType::RGB, diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 2be7f1acc..2184e73f4 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -1,7 +1,7 @@ extern crate oxipng; -use oxipng::{InFile, OutFile}; use oxipng::internal_tests::*; +use oxipng::{InFile, OutFile}; use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; @@ -15,7 +15,10 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { filter.insert(0); options.filter = filter; - (OutFile::Path(Some(input.with_extension("out.png").to_owned())), options) + ( + OutFile::Path(Some(input.with_extension("out.png").to_owned())), + options, + ) } fn test_it_converts( diff --git a/tests/lib.rs b/tests/lib.rs index 250433611..448fb41d3 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -49,7 +49,11 @@ fn optimize() { let mut opts: oxipng::Options = Default::default(); opts.verbosity = Some(1); - let result = oxipng::optimize(&"tests/files/fully_optimized.png".into(), &OutFile::Path(None), &opts); + let result = oxipng::optimize( + &"tests/files/fully_optimized.png".into(), + &OutFile::Path(None), + &opts, + ); assert!(result.is_ok()); } @@ -58,7 +62,11 @@ fn optimize_corrupted() { let mut opts: oxipng::Options = Default::default(); opts.verbosity = Some(1); - let result = oxipng::optimize(&"tests/files/corrupted_header.png".into(), &OutFile::Path(None), &opts); + let result = oxipng::optimize( + &"tests/files/corrupted_header.png".into(), + &OutFile::Path(None), + &opts, + ); assert!(result.is_err()); } @@ -67,6 +75,10 @@ fn optimize_apng() { let mut opts: oxipng::Options = Default::default(); opts.verbosity = Some(1); - let result = oxipng::optimize(&"tests/files/apng_file.png".into(), &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 5254106e8..afe23dccd 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -1,7 +1,7 @@ extern crate oxipng; -use oxipng::{InFile, OutFile}; use oxipng::internal_tests::*; +use oxipng::{InFile, OutFile}; use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; @@ -15,7 +15,10 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { filter.insert(0); options.filter = filter; - (OutFile::Path(Some(input.with_extension("out.png").to_owned())), options) + ( + OutFile::Path(Some(input.with_extension("out.png").to_owned())), + options, + ) } fn test_it_converts( diff --git a/tests/regression.rs b/tests/regression.rs index 0f69d65b6..0175cb991 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -15,7 +15,10 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { filter.insert(0); options.filter = filter; - (OutFile::Path(Some(input.with_extension("out.png").to_owned())), options) + ( + OutFile::Path(Some(input.with_extension("out.png").to_owned())), + options, + ) } fn test_it_converts( @@ -273,7 +276,9 @@ fn issue_92_filter_5() { let input = "tests/files/issue-92.png"; let (_, mut opts) = get_opts(Path::new(input)); opts.filter = [5].iter().cloned().collect(); - let output = OutFile::Path(Some(Path::new(input).with_extension("-f5-out.png").to_owned())); + let output = OutFile::Path(Some( + Path::new(input).with_extension("-f5-out.png").to_owned(), + )); test_it_converts( &input,