From 446c788eb3b37bca5cb77796319b1e5dbe6d556f Mon Sep 17 00:00:00 2001 From: andrews05 Date: Wed, 2 Nov 2022 02:02:08 +1300 Subject: [PATCH] Full switch to Libdeflater (#457) * Switch main compressor to libdeflate * Use libdeflater in evaluate * Use libdeflater to inflate * Use libdeflater crc * Tidy up * Fix benches * Allow libdeflater/freestanding feature * Fix building without zopfli --- Cargo.lock | 34 +-- Cargo.toml | 11 +- benches/deflate.rs | 43 ++-- benches/libdeflater.rs | 64 ----- src/deflate/cfzlib.rs | 54 ---- src/deflate/deflater.rs | 21 +- src/deflate/miniz_stream.rs | 71 ------ src/deflate/mod.rs | 102 +------- src/deflate/zopfli_oxipng.rs | 18 ++ src/evaluate.rs | 5 - src/headers.rs | 4 +- src/lib.rs | 154 ++++------- src/main.rs | 479 +++++++++++++++++------------------ src/png/mod.rs | 12 +- tests/flags.rs | 24 +- tests/interlaced.rs | 4 - 16 files changed, 367 insertions(+), 733 deletions(-) delete mode 100644 benches/libdeflater.rs delete mode 100644 src/deflate/cfzlib.rs delete mode 100644 src/deflate/miniz_stream.rs create mode 100644 src/deflate/zopfli_oxipng.rs diff --git a/Cargo.lock b/Cargo.lock index b90ce9c6..d7941738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,24 +91,6 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "cloudflare-zlib" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cfcefb5df07f146eb15756342a135eb7d76b8bb609eff9c111f7539d060f94d" -dependencies = [ - "cloudflare-zlib-sys", -] - -[[package]] -name = "cloudflare-zlib-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2040b6d1edfee6d75f172d81e2d2a7807534f3f294ce18184c70e7bb0105cd6f" -dependencies = [ - "cc", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -209,7 +191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", - "miniz_oxide 0.5.4", + "miniz_oxide", ] [[package]] @@ -324,15 +306,6 @@ dependencies = [ "adler", ] -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -391,8 +364,6 @@ version = "6.0.1" dependencies = [ "bit-vec", "clap", - "cloudflare-zlib", - "crc", "crossbeam-channel", "filetime", "image", @@ -400,7 +371,6 @@ dependencies = [ "itertools", "libdeflater", "log", - "miniz_oxide 0.6.2", "rayon", "rgb", "rustc_version", @@ -418,7 +388,7 @@ dependencies = [ "bitflags", "crc32fast", "flate2", - "miniz_oxide 0.5.4", + "miniz_oxide", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f445868f..c087d365 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,13 +24,11 @@ required-features = ["binary"] [dependencies] bit-vec = "0.6.3" -crc = "3.0.0" itertools = "0.10.3" zopfli = { version = "0.7.1", optional = true } -miniz_oxide = "0.6.2" rgb = "0.8.33" indexmap = "1.9.1" -libdeflater = { version = "0.11.0", optional = true } +libdeflater = "0.11.0" log = "0.4.17" stderrlog = { version = "0.5.3", optional = true, default-features = false } crossbeam-channel = "0.5.6" @@ -56,17 +54,14 @@ default-features = false features = ["png"] version = "0.24.3" -[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies.cloudflare-zlib] -features = ["arm-always"] -version = "0.2.9" - [build-dependencies] rustc_version = "0.4.0" [features] binary = ["clap", "wild", "stderrlog"] -default = ["binary", "filetime", "parallel", "libdeflater", "zopfli"] +default = ["binary", "filetime", "parallel", "zopfli"] parallel = ["rayon", "indexmap/rayon"] +freestanding = ["libdeflater/freestanding"] [lib] name = "oxipng" diff --git a/benches/deflate.rs b/benches/deflate.rs index 29bfada9..d83e3230 100644 --- a/benches/deflate.rs +++ b/benches/deflate.rs @@ -7,7 +7,6 @@ use std::path::PathBuf; use test::Bencher; use oxipng::internal_tests::*; -use oxipng::Deadline; #[bench] fn deflate_16_bits_strategy_0(b: &mut Bencher) { @@ -16,7 +15,7 @@ fn deflate_16_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 0, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -27,7 +26,7 @@ fn deflate_8_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 0, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -40,7 +39,7 @@ fn deflate_4_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 0, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -53,7 +52,7 @@ fn deflate_2_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 0, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -66,7 +65,7 @@ fn deflate_1_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 0, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -77,7 +76,7 @@ fn deflate_16_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 1, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -88,7 +87,7 @@ fn deflate_8_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 1, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -101,7 +100,7 @@ fn deflate_4_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 1, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -114,7 +113,7 @@ fn deflate_2_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 1, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -127,7 +126,7 @@ fn deflate_1_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 1, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -138,7 +137,7 @@ fn deflate_16_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 2, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -149,7 +148,7 @@ fn deflate_8_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 2, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -162,7 +161,7 @@ fn deflate_4_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 2, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -175,7 +174,7 @@ fn deflate_2_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 2, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -188,7 +187,7 @@ fn deflate_1_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 2, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -199,7 +198,7 @@ fn deflate_16_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 3, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -210,7 +209,7 @@ fn deflate_8_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 3, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -223,7 +222,7 @@ fn deflate_4_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 3, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -236,7 +235,7 @@ fn deflate_2_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 3, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -249,7 +248,7 @@ fn deflate_1_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw.data.as_ref(), 9, 3, 15, &min, &Deadline::new(None)) + deflate(png.raw.data.as_ref(), 12, &min) }); } @@ -258,5 +257,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(), png.raw.ihdr.raw_data_size())); } diff --git a/benches/libdeflater.rs b/benches/libdeflater.rs deleted file mode 100644 index d7917a0c..00000000 --- a/benches/libdeflater.rs +++ /dev/null @@ -1,64 +0,0 @@ -#![feature(test)] - -extern crate oxipng; -extern crate test; - -use oxipng::internal_tests::*; -use std::path::PathBuf; -use test::Bencher; - -#[bench] -fn libdeflater_16_bits_strategy_0(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(|| { - libdeflater_deflate(png.raw.data.as_ref(), 12, &AtomicMin::new(None)).ok(); - }); -} - -#[bench] -fn libdeflater_8_bits_strategy_0(b: &mut Bencher) { - let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); - - b.iter(|| { - libdeflater_deflate(png.raw.data.as_ref(), 12, &AtomicMin::new(None)).ok(); - }); -} - -#[bench] -fn libdeflater_4_bits_strategy_0(b: &mut Bencher) { - let input = test::black_box(PathBuf::from( - "tests/files/palette_4_should_be_palette_4.png", - )); - let png = PngData::new(&input, false).unwrap(); - - b.iter(|| { - libdeflater_deflate(png.raw.data.as_ref(), 12, &AtomicMin::new(None)).ok(); - }); -} - -#[bench] -fn libdeflater_2_bits_strategy_0(b: &mut Bencher) { - let input = test::black_box(PathBuf::from( - "tests/files/palette_2_should_be_palette_2.png", - )); - let png = PngData::new(&input, false).unwrap(); - - b.iter(|| { - libdeflater_deflate(png.raw.data.as_ref(), 12, &AtomicMin::new(None)).ok(); - }); -} - -#[bench] -fn libdeflater_1_bits_strategy_0(b: &mut Bencher) { - let input = test::black_box(PathBuf::from( - "tests/files/palette_1_should_be_palette_1.png", - )); - let png = PngData::new(&input, false).unwrap(); - - b.iter(|| { - libdeflater_deflate(png.raw.data.as_ref(), 12, &AtomicMin::new(None)).ok(); - }); -} diff --git a/src/deflate/cfzlib.rs b/src/deflate/cfzlib.rs deleted file mode 100644 index bad4da6f..00000000 --- a/src/deflate/cfzlib.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::atomicmin::AtomicMin; -use crate::Deadline; -use crate::PngError; -use crate::PngResult; -pub use cloudflare_zlib::is_supported; -use cloudflare_zlib::*; - -impl From for PngError { - fn from(err: ZError) -> Self { - match err { - ZError::DeflatedDataTooLarge(n) => PngError::DeflatedDataTooLong(n), - other => PngError::Other(other.to_string().into()), - } - } -} - -pub(crate) fn cfzlib_deflate( - data: &[u8], - level: u8, - strategy: u8, - window_bits: u8, - max_size: &AtomicMin, - deadline: &Deadline, -) -> PngResult> { - let mut stream = Deflate::new(level.into(), strategy.into(), window_bits.into())?; - stream.reserve(max_size.get().unwrap_or(data.len() / 2)); - let max_size = max_size.as_atomic_usize(); - // max size is generally checked after each split, - // so splitting the buffer into pieces gives more checks - // = better chance of hitting it sooner. - let chunk_size = (data.len() / 4).max(1 << 15).min(1 << 18); // 32-256KB - for chunk in data.chunks(chunk_size) { - stream.compress_with_limit(chunk, max_size)?; - if deadline.passed() { - return Err(PngError::TimedOut); - } - } - Ok(stream.finish()?) -} - -#[test] -fn compress_test() { - let vec = cfzlib_deflate( - b"azxcvbnm", - Z_BEST_COMPRESSION as u8, - Z_DEFAULT_STRATEGY as u8, - 15, - &AtomicMin::new(None), - &Deadline::new(None), - ) - .unwrap(); - let res = crate::deflate::inflate(&vec).unwrap(); - assert_eq!(&res, b"azxcvbnm"); -} diff --git a/src/deflate/deflater.rs b/src/deflate/deflater.rs index 93dbd43f..a2f9dacb 100644 --- a/src/deflate/deflater.rs +++ b/src/deflate/deflater.rs @@ -1,6 +1,6 @@ use crate::atomicmin::AtomicMin; use crate::{PngError, PngResult}; -use libdeflater::{CompressionError, CompressionLvl, Compressor}; +use libdeflater::*; pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult> { let mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap()); @@ -23,3 +23,22 @@ pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult PngResult> { + let mut decompressor = Decompressor::new(); + let mut dest = vec![0; out_size]; + let len = decompressor + .zlib_decompress(data, &mut dest) + .map_err(|err| match err { + DecompressionError::BadData => PngError::InvalidData, + DecompressionError::InsufficientSpace => PngError::new("inflated data too long"), + })?; + dest.truncate(len); + Ok(dest) +} + +pub fn crc32(data: &[u8]) -> u32 { + let mut crc = Crc::new(); + crc.update(data); + crc.sum() +} diff --git a/src/deflate/miniz_stream.rs b/src/deflate/miniz_stream.rs deleted file mode 100644 index b44ed212..00000000 --- a/src/deflate/miniz_stream.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::atomicmin::AtomicMin; -use crate::error::PngError; -use crate::PngResult; -use miniz_oxide::deflate::core::*; - -pub(crate) fn compress_to_vec_oxipng( - input: &[u8], - level: u8, - window_bits: i32, - strategy: i32, - max_size: &AtomicMin, - deadline: &crate::Deadline, -) -> PngResult> { - // 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); - // if max size is known, then expect that much data (but no more than input.len()) - let mut output = Vec::with_capacity(max_size.get().unwrap_or(input.len() / 2).min(input.len())); - // # Unsafe - // We trust compress to not read the uninitialized bytes. - unsafe { - let cap = output.capacity(); - output.set_len(cap); - } - let mut in_pos = 0; - let mut out_pos = 0; - loop { - let (status, bytes_in, bytes_out) = compress( - &mut compressor, - &input[in_pos..], - &mut output[out_pos..], - TDEFLFlush::Finish, - ); - - out_pos += bytes_out; - in_pos += bytes_in; - - match status { - TDEFLStatus::Done => { - output.truncate(out_pos); - break; - } - TDEFLStatus::Okay => { - if let Some(max) = max_size.get() { - if output.len() > max { - return Err(PngError::DeflatedDataTooLong(output.len())); - } - } - if deadline.passed() { - return Err(PngError::TimedOut); - } - // We need more space, so extend the vector. - if output.len().saturating_sub(out_pos) < 30 { - let current_len = output.len(); - output.reserve(current_len); - - // # Unsafe - // We trust compress to not read the uninitialized bytes. - unsafe { - let cap = output.capacity(); - output.set_len(cap); - } - } - } - // Not supposed to happen unless there is a bug. - _ => panic!("Bug! Unexpectedly failed to compress!"), - } - } - - Ok(output) -} diff --git a/src/deflate/mod.rs b/src/deflate/mod.rs index 0c109f96..ac17577c 100644 --- a/src/deflate/mod.rs +++ b/src/deflate/mod.rs @@ -1,98 +1,24 @@ -use crate::atomicmin::AtomicMin; -use crate::error::PngError; -use crate::Deadline; -use crate::PngResult; use indexmap::IndexSet; +mod deflater; +pub use deflater::crc32; +pub use deflater::deflate; +pub use deflater::inflate; + #[cfg(feature = "zopfli")] use std::num::NonZeroU8; - -#[doc(hidden)] -pub mod miniz_stream; - -#[cfg(feature = "libdeflater")] -mod deflater; -#[cfg(feature = "libdeflater")] -pub use deflater::deflate as libdeflater_deflate; - -#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] -pub mod cfzlib; - -#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] -pub mod cfzlib { - pub fn is_supported() -> bool { - return false; - } -} - -/// Decompress a data stream using the DEFLATE algorithm -pub fn inflate(data: &[u8]) -> PngResult> { - miniz_oxide::inflate::decompress_to_vec_zlib(data).map_err(|e| { - PngError::new(&format!( - "Error on decompress: {:?} (after {:?} decompressed bytes)", - e.status, - e.output.len() - )) - }) -} - -/// Compress a data stream using the DEFLATE algorithm -#[doc(hidden)] -pub fn deflate( - data: &[u8], - zc: u8, - zs: u8, - zw: u8, - max_size: &AtomicMin, - deadline: &Deadline, -) -> PngResult> { - #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] - { - if cfzlib::is_supported() { - return cfzlib::cfzlib_deflate(data, zc, zs, zw, max_size, deadline); - } - } - - miniz_stream::compress_to_vec_oxipng(data, zc, zw.into(), zs.into(), max_size, deadline) -} - #[cfg(feature = "zopfli")] -pub fn zopfli_deflate(data: &[u8], iterations: NonZeroU8) -> PngResult> { - use std::cmp::max; - - let mut output = Vec::with_capacity(max(1024, data.len() / 20)); - let options = zopfli::Options { - iteration_count: iterations, - ..Default::default() - }; - match zopfli::compress(&options, &zopfli::Format::Zlib, data, &mut output) { - Ok(_) => (), - Err(_) => return Err(PngError::new("Failed to compress in zopfli")), - }; - output.shrink_to_fit(); - Ok(output) -} +mod zopfli_oxipng; +#[cfg(feature = "zopfli")] +pub use zopfli_oxipng::deflate as zopfli_deflate; #[derive(Clone, Debug, PartialEq, Eq)] /// DEFLATE algorithms supported by oxipng pub enum Deflaters { - /// Use the Zlib/Miniz DEFLATE implementation - Zlib { - /// Which zlib compression levels to try on the file (1-9) - /// - /// Default: `9` + /// Use libdeflater. + Libdeflater { + /// Which compression levels to try on the file (1-12) compression: IndexSet, - /// Which zlib compression strategies to try on the file (0-3) - /// - /// Default: `0-3` - strategies: IndexSet, - /// Window size to use when compressing the file, as `2^window` bytes. - /// - /// Doesn't affect compression but may affect speed and memory usage. - /// 8-15 are valid values. - /// - /// Default: `15` - window: u8, }, #[cfg(feature = "zopfli")] /// Use the better but slower Zopfli implementation @@ -102,10 +28,4 @@ pub enum Deflaters { /// less iterations, or else they will be too slow. iterations: NonZeroU8, }, - #[cfg(feature = "libdeflater")] - /// Use libdeflater. - Libdeflater { - /// Which compression levels to try on the file (1-12) - compression: IndexSet, - }, } diff --git a/src/deflate/zopfli_oxipng.rs b/src/deflate/zopfli_oxipng.rs new file mode 100644 index 00000000..b59b93df --- /dev/null +++ b/src/deflate/zopfli_oxipng.rs @@ -0,0 +1,18 @@ +use crate::{PngError, PngResult}; +use std::num::NonZeroU8; + +pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult> { + use std::cmp::max; + + let mut output = Vec::with_capacity(max(1024, data.len() / 20)); + let options = zopfli::Options { + iteration_count: iterations, + ..Default::default() + }; + match zopfli::compress(&options, &zopfli::Format::Zlib, data, &mut output) { + Ok(_) => (), + Err(_) => return Err(PngError::new("Failed to compress in zopfli")), + }; + output.shrink_to_fit(); + Ok(output) +} diff --git a/src/evaluate.rs b/src/evaluate.rs index 0e07d308..96bca0fb 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -7,8 +7,6 @@ use crate::png::PngData; use crate::png::PngImage; use crate::png::STD_COMPRESSION; use crate::png::STD_FILTERS; -use crate::png::STD_STRATEGY; -use crate::png::STD_WINDOW; #[cfg(not(feature = "parallel"))] use crate::rayon; use crate::Deadline; @@ -119,10 +117,7 @@ impl Evaluator { if let Ok(idat_data) = deflate::deflate( &image.filter_image(filter), STD_COMPRESSION, - STD_STRATEGY, - STD_WINDOW, &best_candidate_size, - &deadline, ) { best_candidate_size.set_min(idat_data.len()); // ignore baseline images after this point diff --git a/src/headers.rs b/src/headers.rs index 1183ca69..1b92d9b3 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -1,7 +1,7 @@ use crate::colors::{BitDepth, ColorType}; +use crate::deflate::crc32; use crate::error::PngError; use crate::PngResult; -use crc::{Crc, CRC_32_ISO_HDLC}; use indexmap::IndexSet; use std::io; use std::io::{Cursor, Read}; @@ -130,7 +130,7 @@ pub fn parse_next_header<'a>( let header_bytes = byte_data .get(header_start..header_start + 4 + length as usize) .ok_or(PngError::TruncatedData)?; - if !fix_errors && Crc::::new(&CRC_32_ISO_HDLC).checksum(header_bytes) != crc { + if !fix_errors && crc32(header_bytes) != crc { return Err(PngError::new(&format!( "CRC Mismatch in {} header; May be recoverable by using --fix", String::from_utf8_lossy(chunk_name) diff --git a/src/lib.rs b/src/lib.rs index a6249756..fc57e16f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ #![allow(clippy::cognitive_complexity)] #![allow(clippy::upper_case_acronyms)] #![cfg_attr( - not(any(feature = "libdeflater", feature = "zopfli")), + not(feature = "zopfli"), allow(irrefutable_let_patterns), allow(unreachable_patterns) )] @@ -26,12 +26,11 @@ mod rayon; use crate::atomicmin::AtomicMin; use crate::colors::BitDepth; -use crate::deflate::inflate; +use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; use crate::png::PngData; use crate::png::PngImage; use crate::reduction::*; -use crc::{Crc, CRC_32_ISO_HDLC}; use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; use log::{debug, error, info, warn}; use rayon::prelude::*; @@ -190,7 +189,7 @@ pub struct Options { pub strip: Headers, /// Which DEFLATE algorithm to use /// - /// Default: `Zlib` + /// Default: `Libdeflater` pub deflate: Deflaters, /// Whether to use heuristics to pick the best filter and compression /// @@ -212,10 +211,7 @@ impl Options { 1 => opts.apply_preset_1(), 2 => opts.apply_preset_2(), 3 => opts.apply_preset_3(), - 4 => { - warn!("Level 4 is deprecated and is identical to level 3"); - opts.apply_preset_4() - } + 4 => opts.apply_preset_4(), 5 => opts.apply_preset_5(), 6 => opts.apply_preset_6(), _ => { @@ -233,17 +229,20 @@ impl Options { // on an `Options` struct generated by the `default` method. fn apply_preset_0(mut self) -> Self { self.idat_recoding = false; - if let Deflaters::Zlib { compression, .. } = &mut self.deflate { + self.filter.clear(); + if let Deflaters::Libdeflater { compression } = &mut self.deflate { compression.clear(); - compression.insert(3); + compression.insert(5); } - self.apply_preset_1() + self.use_heuristics = true; + self } fn apply_preset_1(mut self) -> Self { self.filter.clear(); - if let Deflaters::Zlib { strategies, .. } = &mut self.deflate { - strategies.clear(); + if let Deflaters::Libdeflater { compression } = &mut self.deflate { + compression.clear(); + compression.insert(10); } self.use_heuristics = true; self @@ -254,34 +253,38 @@ impl Options { } fn apply_preset_3(mut self) -> Self { - for i in 1..5 { + for i in 1..=4 { self.filter.insert(i); } self } - fn apply_preset_4(self) -> Self { + fn apply_preset_4(mut self) -> Self { + if let Deflaters::Libdeflater { compression } = &mut self.deflate { + compression.clear(); + compression.insert(12); + } self.apply_preset_3() } fn apply_preset_5(mut self) -> Self { - if let Deflaters::Zlib { compression, .. } = &mut self.deflate { + if let Deflaters::Libdeflater { compression } = &mut self.deflate { compression.clear(); - for i in 3..10 { + for i in 9..=12 { compression.insert(i); } } - self.apply_preset_4() + self.apply_preset_3() } fn apply_preset_6(mut self) -> Self { - if let Deflaters::Zlib { compression, .. } = &mut self.deflate { + if let Deflaters::Libdeflater { compression } = &mut self.deflate { compression.clear(); - for i in 1..10 { + for i in 1..=12 { compression.insert(i); } } - self.apply_preset_4() + self.apply_preset_3() } } @@ -292,11 +295,7 @@ impl Default for Options { filter.insert(0); filter.insert(5); let mut compression = IndexSet::new(); - compression.insert(9); - let mut strategies = IndexSet::new(); - for i in 0..4 { - strategies.insert(i); - } + compression.insert(11); // We always need NoOp to be present let mut alphas = IndexSet::new(); alphas.insert(AlphaOptim::NoOp); @@ -317,11 +316,7 @@ impl Default for Options { grayscale_reduction: true, idat_recoding: true, strip: Headers::None, - deflate: Deflaters::Zlib { - compression, - strategies, - window: 15, - }, + deflate: Deflaters::Libdeflater { compression }, use_heuristics: false, timeout: None, } @@ -472,7 +467,6 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult> { struct TrialOptions { pub filter: u8, pub compression: u8, - pub strategy: u8, } /// Perform optimization on the input PNG object using the options provided @@ -511,28 +505,19 @@ fn optimize_png( info!(" File size = {} bytes", file_original_size); let mut filter = opts.filter.clone(); - let mut strategies = match &opts.deflate { - Deflaters::Zlib { strategies, .. } => Some(strategies.clone()), - _ => None, - }; if opts.use_heuristics { // Heuristically determine which set of options to use - let (use_filter, use_strategy) = if png.raw.ihdr.bit_depth.as_u8() >= 8 + let use_filter = if png.raw.ihdr.bit_depth.as_u8() >= 8 && png.raw.ihdr.color_type != colors::ColorType::Indexed { - (5, 1) + 5 } else { - (0, 0) + 0 }; if filter.is_empty() { filter.insert(use_filter); } - if let Some(strategies) = &mut strategies { - if strategies.is_empty() { - strategies.insert(use_strategy); - } - } } // This will collect all versions of images and pick one that compresses best @@ -552,51 +537,30 @@ fn optimize_png( if opts.idat_recoding || reduction_occurred { // Go through selected permutations and determine the best - let combinations = if let Deflaters::Zlib { compression, .. } = &opts.deflate { - filter.len() * compression.len() * strategies.as_ref().unwrap().len() + let combinations = if let Deflaters::Libdeflater { compression } = &opts.deflate { + filter.len() * compression.len() } else { filter.len() }; let mut results: Vec = Vec::with_capacity(combinations); for f in &filter { - match &opts.deflate { - Deflaters::Zlib { compression, .. } => { - for zc in compression { - for zs in strategies.as_ref().unwrap() { - results.push(TrialOptions { - filter: *f, - compression: *zc, - strategy: *zs, - }); - } - if deadline.passed() { - break; - } - } - } - #[cfg(feature = "zopfli")] - Deflaters::Zopfli { .. } => { - // Zopfli has no additional options. + if let Deflaters::Libdeflater { compression } = &opts.deflate { + for zc in compression { results.push(TrialOptions { filter: *f, - compression: 0, - strategy: 0, + compression: *zc, }); - } - #[cfg(feature = "libdeflater")] - Deflaters::Libdeflater { compression } => { - for zc in compression { - results.push(TrialOptions { - filter: *f, - compression: *zc, - strategy: 0, - }); - if deadline.passed() { - break; - } + if deadline.passed() { + break; } } + } else { + // Zopfli has no additional options. + results.push(TrialOptions { + filter: *f, + compression: 0, + }); } if deadline.passed() { @@ -626,28 +590,19 @@ fn optimize_png( } let filtered = &filters[&trial.filter]; let new_idat = match opts.deflate { - Deflaters::Zlib { window, .. } => deflate::deflate( - filtered, - trial.compression, - trial.strategy, - window, - &best_size, - &deadline, - ), + Deflaters::Libdeflater { .. } => { + deflate::deflate(filtered, trial.compression, &best_size) + } #[cfg(feature = "zopfli")] Deflaters::Zopfli { iterations } => deflate::zopfli_deflate(filtered, iterations), - #[cfg(feature = "libdeflater")] - Deflaters::Libdeflater { .. } => { - deflate::libdeflater_deflate(filtered, trial.compression, &best_size) - } }; let new_idat = match new_idat { Ok(n) => n, Err(PngError::DeflatedDataTooLong(max)) => { debug!( - " zc = {} zs = {} f = {} >{} bytes", - trial.compression, trial.strategy, trial.filter, max, + " zc = {} f = {} >{} bytes", + trial.compression, trial.filter, max, ); return None; } @@ -659,9 +614,8 @@ fn optimize_png( best_size.set_min(new_size); debug!( - " zc = {} zs = {} f = {} {} bytes", + " zc = {} f = {} {} bytes", trial.compression, - trial.strategy, trial.filter, new_idat.len() ); @@ -684,9 +638,8 @@ fn optimize_png( png.idat_data = idat_data; info!("Found better combination:"); info!( - " zc = {} zs = {} f = {} {} bytes", + " zc = {} f = {} {} bytes", opts.compression, - opts.strategy, opts.filter, png.idat_data.len() ); @@ -952,7 +905,9 @@ fn srgb_rendering_intent(mut iccp: &[u8]) -> Option { if compression_method != 0 { return None; // The profile is supposed to be compressed (method 0) } - let icc_data = inflate(compressed_data).ok()?; + // The decompressed size is unknown so we have to guess the required buffer size + let max_size = (compressed_data.len() * 2).max(1000); + let icc_data = inflate(compressed_data, max_size).ok()?; let rendering_intent = *icc_data.get(67)?; @@ -968,10 +923,7 @@ fn srgb_rendering_intent(mut iccp: &[u8]) -> Option { } b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => { // Known-bad profiles are identified by their CRC - match ( - Crc::::new(&CRC_32_ISO_HDLC).checksum(&icc_data), - icc_data.len(), - ) { + match (crc32(&icc_data), icc_data.len()) { (0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => { Some(rendering_intent) } diff --git a/src/main.rs b/src/main.rs index d503b585..6969f2f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ #![allow(clippy::cognitive_complexity)] use clap::{AppSettings, Arg, ArgMatches, Command}; -use indexmap::{indexset, IndexSet}; +use indexmap::IndexSet; use log::{error, warn}; use oxipng::AlphaOptim; use oxipng::Deflaters; @@ -22,6 +22,7 @@ use oxipng::Headers; use oxipng::Options; use oxipng::{InFile, OutFile}; use std::fs::DirBuilder; +#[cfg(feature = "zopfli")] use std::num::NonZeroU8; use std::path::PathBuf; use std::process::exit; @@ -33,199 +34,214 @@ fn main() { .author("Joshua Holmer ") .about("Losslessly improves compression of PNG files") .setting(AppSettings::DeriveDisplayOrder) - .arg(Arg::new("files") - .help("File(s) to compress (use \"-\" for stdin)") - .index(1) - .multiple_values(true) - .use_value_delimiter(false) - .required(true)) - .arg(Arg::new("optimization") - .help("Optimization level - Default: 2") - .short('o') - .long("opt") - .takes_value(true) - .value_name("level") - .possible_value("0") - .possible_value("1") - .possible_value("2") - .possible_value("3") - .possible_value("4") - .possible_value("5") - .possible_value("6") - .possible_value("max")) - .arg(Arg::new("backup") - .help("Back up modified files") - .short('b') - .long("backup")) - .arg(Arg::new("recursive") - .help("Recurse into subdirectories") - .short('r') - .long("recursive")) - .arg(Arg::new("output_dir") - .help("Write output file(s) to ") - .long("dir") - .takes_value(true) - .value_name("directory") - .conflicts_with("output_file") - .conflicts_with("stdout")) - .arg(Arg::new("output_file") - .help("Write output file to ") - .long("out") - .takes_value(true) - .value_name("file") - .conflicts_with("output_dir") - .conflicts_with("stdout")) - .arg(Arg::new("stdout") - .help("Write output to stdout") - .long("stdout") - .conflicts_with("output_dir") - .conflicts_with("output_file")) - .arg(Arg::new("preserve") - .help("Preserve file attributes if possible") - .short('p') - .long("preserve")) - .arg(Arg::new("check") - .help("Do not run any optimization passes") - .short('c') - .long("check")) - .arg(Arg::new("pretend") - .help("Do not write any files, only calculate compression gains") - .short('P') - .long("pretend")) - .arg(Arg::new("strip-safe") - .help("Strip safely-removable metadata objects") - .short('s') - .conflicts_with("strip")) - .arg(Arg::new("strip") - .help("Strip metadata objects ['safe', 'all', or comma-separated list]") - .long("strip") - .takes_value(true) - .value_name("mode") - .conflicts_with("strip-safe")) - .arg(Arg::new("keep") - .help("Strip all optional metadata except objects in the comma-separated list") - .long("keep") - .takes_value(true) - .value_name("list") - .conflicts_with("strip") - .conflicts_with("strip-safe")) - .arg(Arg::new("alpha") - .help("Perform additional alpha optimizations") - .short('a') - .long("alpha")) - .arg(Arg::new("interlace") - .help("PNG interlace type") - .short('i') - .long("interlace") - .takes_value(true) - .value_name("0/1") - .possible_value("0") - .possible_value("1")) - .arg(Arg::new("verbose") - .help("Run in verbose mode") - .short('v') - .long("verbose") - .conflicts_with("quiet")) - .arg(Arg::new("quiet") - .help("Run in quiet mode") - .short('q') - .long("quiet") - .conflicts_with("verbose")) - .arg(Arg::new("filters") - .help("PNG delta filters (0-5) - Default: 0,5") - .short('f') - .long("filters") - .takes_value(true) - .validator(|x| { - match parse_numeric_range_opts(x, 0, 5) { + .arg( + Arg::new("files") + .help("File(s) to compress (use \"-\" for stdin)") + .index(1) + .multiple_values(true) + .use_value_delimiter(false) + .required(true), + ) + .arg( + Arg::new("optimization") + .help("Optimization level - Default: 2") + .short('o') + .long("opt") + .takes_value(true) + .value_name("level") + .possible_value("0") + .possible_value("1") + .possible_value("2") + .possible_value("3") + .possible_value("4") + .possible_value("5") + .possible_value("6") + .possible_value("max"), + ) + .arg( + Arg::new("backup") + .help("Back up modified files") + .short('b') + .long("backup"), + ) + .arg( + Arg::new("recursive") + .help("Recurse into subdirectories") + .short('r') + .long("recursive"), + ) + .arg( + Arg::new("output_dir") + .help("Write output file(s) to ") + .long("dir") + .takes_value(true) + .value_name("directory") + .conflicts_with("output_file") + .conflicts_with("stdout"), + ) + .arg( + Arg::new("output_file") + .help("Write output file to ") + .long("out") + .takes_value(true) + .value_name("file") + .conflicts_with("output_dir") + .conflicts_with("stdout"), + ) + .arg( + Arg::new("stdout") + .help("Write output to stdout") + .long("stdout") + .conflicts_with("output_dir") + .conflicts_with("output_file"), + ) + .arg( + Arg::new("preserve") + .help("Preserve file attributes if possible") + .short('p') + .long("preserve"), + ) + .arg( + Arg::new("check") + .help("Do not run any optimization passes") + .short('c') + .long("check"), + ) + .arg( + Arg::new("pretend") + .help("Do not write any files, only calculate compression gains") + .short('P') + .long("pretend"), + ) + .arg( + Arg::new("strip-safe") + .help("Strip safely-removable metadata objects") + .short('s') + .conflicts_with("strip"), + ) + .arg( + Arg::new("strip") + .help("Strip metadata objects ['safe', 'all', or comma-separated list]") + .long("strip") + .takes_value(true) + .value_name("mode") + .conflicts_with("strip-safe"), + ) + .arg( + Arg::new("keep") + .help("Strip all optional metadata except objects in the comma-separated list") + .long("keep") + .takes_value(true) + .value_name("list") + .conflicts_with("strip") + .conflicts_with("strip-safe"), + ) + .arg( + Arg::new("alpha") + .help("Perform additional alpha optimizations") + .short('a') + .long("alpha"), + ) + .arg( + Arg::new("interlace") + .help("PNG interlace type") + .short('i') + .long("interlace") + .takes_value(true) + .value_name("0/1") + .possible_value("0") + .possible_value("1"), + ) + .arg( + Arg::new("verbose") + .help("Run in verbose mode") + .short('v') + .long("verbose") + .conflicts_with("quiet"), + ) + .arg( + Arg::new("quiet") + .help("Run in quiet mode") + .short('q') + .long("quiet") + .conflicts_with("verbose"), + ) + .arg( + Arg::new("filters") + .help("PNG delta filters (0-5) - Default: 0,5") + .short('f') + .long("filters") + .takes_value(true) + .validator(|x| match parse_numeric_range_opts(x, 0, 5) { Ok(_) => Ok(()), Err(_) => Err("Invalid option for filters".to_owned()), - } - })) - .arg(Arg::new("compression") - .help("compression levels (zlib: 1-9, libdeflater: 1-12) - Default: 9 or 12") - .long("zc") - .takes_value(true) - .value_name("levels") - .validator(|x| { - match parse_numeric_range_opts(x, 1, 12) { + }), + ) + .arg( + Arg::new("compression") + .help("zlib compression levels (1-12) - Default: 12") + .long("zc") + .takes_value(true) + .value_name("levels") + .validator(|x| match parse_numeric_range_opts(x, 1, 12) { Ok(_) => Ok(()), Err(_) => Err("Invalid option for compression".to_owned()), - } - }) - .conflicts_with("zopfli")) - .arg(Arg::new("strategies") - .help("zlib compression strategies (0-3) - Default: 0-3") - .long("zs") - .takes_value(true) - .validator(|x| { - match parse_numeric_range_opts(x, 0, 3) { - Ok(_) => Ok(()), - Err(_) => Err("Invalid option for strategies".to_owned()), - } - }) - .conflicts_with_all(&["zopfli", "libdeflater"])) - .arg(Arg::new("window") - .help("zlib window size - Default: 32k") - .long("zw") - .takes_value(true) - .value_name("size") - .possible_value("256") - .possible_value("512") - .possible_value("1k") - .possible_value("2k") - .possible_value("4k") - .possible_value("8k") - .possible_value("16k") - .possible_value("32k") - .conflicts_with_all(&["zopfli", "libdeflater"])) - .arg(Arg::new("no-bit-reduction") - .help("No bit depth reduction") - .long("nb")) - .arg(Arg::new("no-color-reduction") - .help("No color type reduction") - .long("nc")) - .arg(Arg::new("no-palette-reduction") - .help("No palette reduction") - .long("np")) - .arg(Arg::new("no-grayscale-reduction") - .help("No grayscale reduction") - .long("ng")) - .arg(Arg::new("no-reductions") - .help("No reductions") - .long("nx")) - .arg(Arg::new("no-recoding") - .help("No IDAT recoding unless necessary") - .long("nz")) - .arg(Arg::new("fix") - .help("Enable error recovery") - .long("fix")) - .arg(Arg::new("force") - .help("Write the output even if it is larger than the input") - .long("force")) - .arg(Arg::new("zopfli") - .help("Use the slower but better compressing Zopfli algorithm, overrides zlib-specific options") - .short('Z') - .long("zopfli") - .conflicts_with("libdeflater")) - .arg(Arg::new("libdeflater") - .help("Use an alternative Libdeflater algorithm, overrides zlib-specific options") - .short('D') - .long("libdeflater") - .conflicts_with("zopfli")) - .arg(Arg::new("timeout") - .help("Maximum amount of time, in seconds, to spend on optimizations") - .takes_value(true) - .value_name("secs") - .long("timeout")) - .arg(Arg::new("threads") - .help("Set number of threads to use - default 1.5x CPU cores") - .long("threads") - .short('t') - .takes_value(true) - .value_name("num") - .validator(|x| { - match x.parse::() { + }) + .conflicts_with("zopfli"), + ) + .arg( + Arg::new("no-bit-reduction") + .help("No bit depth reduction") + .long("nb"), + ) + .arg( + Arg::new("no-color-reduction") + .help("No color type reduction") + .long("nc"), + ) + .arg( + Arg::new("no-palette-reduction") + .help("No palette reduction") + .long("np"), + ) + .arg( + Arg::new("no-grayscale-reduction") + .help("No grayscale reduction") + .long("ng"), + ) + .arg(Arg::new("no-reductions").help("No reductions").long("nx")) + .arg( + Arg::new("no-recoding") + .help("No IDAT recoding unless necessary") + .long("nz"), + ) + .arg(Arg::new("fix").help("Enable error recovery").long("fix")) + .arg( + Arg::new("force") + .help("Write the output even if it is larger than the input") + .long("force"), + ) + .arg( + Arg::new("zopfli") + .help("Use the slower but better compressing Zopfli algorithm") + .short('Z') + .long("zopfli"), + ) + .arg( + Arg::new("timeout") + .help("Maximum amount of time, in seconds, to spend on optimizations") + .takes_value(true) + .value_name("secs") + .long("timeout"), + ) + .arg( + Arg::new("threads") + .help("Set number of threads to use - default 1.5x CPU cores") + .long("threads") + .short('t') + .takes_value(true) + .value_name("num") + .validator(|x| match x.parse::() { Ok(val) => { if val > 0 { Ok(()) @@ -234,20 +250,22 @@ fn main() { } } Err(_) => Err("Thread count must be >= 1".to_owned()), - } - })) - .after_help("Optimization levels: - -o 0 => --zc 3 --nz (0 or 1 trials) - -o 1 => --zc 9 (1 trial, determined heuristically) - -o 2 => --zc 9 --zs 0-3 -f 0,5 (8 trials with zlib or 2 trials with other compressors) - -o 3 => --zc 9 --zs 0-3 -f 0-5 (24 trials with zlib or 6 trials with other compressors) - -o 4 => (deprecated; same as `-o 3`) - -o 5 => --zc 3-9 --zs 0-3 -f 0-5 (168 trials with zlib; same as `-o 3` for other compressors) - -o 6 => --zc 1-9 --zs 0-3 -f 0-5 (216 trials with zlib; same as `-o 3` for other compressors) - -o max => (stable alias for the max compression) + }), + ) + .after_help( + "Optimization levels: + -o 0 => --zc 6 --nz (0 or 1 trials) + -o 1 => --zc 10 (1 trial, determined heuristically) + -o 2 => --zc 11 -f 0,5 (2 trials) + -o 3 => --zc 11 -f 0-5 (6 trials) + -o 4 => --zc 12 -f 0-5 (6 trials; same as `-o 3` for zopfli) + -o 5 => --zc 9-12 -f 0-5 (24 trials; same as `-o 3` for zopfli) + -o 6 => --zc 1-12 -f 0-5 (72 trials; same as `-o 3` for zopfli) + -o max => (stable alias for the max compression) - Manually specifying a compression option (zc, zs, etc.) will override the optimization preset, - regardless of the order you write the arguments.") + Manually specifying a compression option (zc, f, etc.) will override the optimization preset, + regardless of the order you write the arguments.", + ) .get_matches_from(wild::args()); let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) { @@ -496,55 +514,20 @@ fn parse_opts_into_struct( } if matches.is_present("zopfli") { - opts.deflate = Deflaters::Zopfli { - iterations: NonZeroU8::new(15).unwrap(), - }; - } else if matches.is_present("libdeflater") { - opts.deflate = Deflaters::Libdeflater { - compression: match matches.value_of("compression") { - Some(x) => parse_numeric_range_opts(x, 1, 12).unwrap(), - _ => indexset! { 12 }, - }, - }; - } else if let Deflaters::Zlib { - compression, - strategies, - window, - } = &mut opts.deflate - { + if explicit_level > Some(3) { + warn!("Level 4 and above are equivalent to level 3 for zopfli"); + } + #[cfg(feature = "zopfli")] + if let Some(iterations) = NonZeroU8::new(15) { + opts.deflate = Deflaters::Zopfli { iterations }; + } + } else if let Deflaters::Libdeflater { compression } = &mut opts.deflate { if let Some(x) = matches.value_of("compression") { - *compression = parse_numeric_range_opts(x, 1, 9) - .map_err(|_| "Compression levels 10-12 are only valid for libdeflater".to_owned())? - } - - if let Some(x) = matches.value_of("strategies") { - *strategies = parse_numeric_range_opts(x, 0, 3).unwrap(); - } - - match matches.value_of("window") { - Some("256") => *window = 8, - Some("512") => *window = 9, - Some("1k") => *window = 10, - Some("2k") => *window = 11, - Some("4k") => *window = 12, - Some("8k") => *window = 13, - Some("16k") => *window = 14, - // 32k is default - _ => (), - } - } - - if explicit_level > Some(3) { - match opts.deflate { - Deflaters::Zlib { .. } => {} - _ => { - warn!( - "Level 4 and above are equivalent to level 3 for compressors other than zlib" - ); - } + *compression = parse_numeric_range_opts(x, 1, 12).unwrap(); } } + #[cfg(feature = "parallel")] if let Some(x) = matches.value_of("threads") { let threads = x.parse::().unwrap(); diff --git a/src/png/mod.rs b/src/png/mod.rs index b625de9c..2d1adace 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -4,7 +4,6 @@ use crate::error::PngError; use crate::filters::*; use crate::headers::*; use crate::interlace::{deinterlace_image, interlace_image}; -use crc::{Crc, CRC_32_ISO_HDLC}; use indexmap::IndexMap; use rgb::ComponentSlice; use rgb::RGBA8; @@ -14,11 +13,8 @@ use std::iter::Iterator; use std::path::Path; use std::sync::Arc; -pub(crate) const STD_COMPRESSION: u8 = 6; -/// Must use normal compression, as faster ones (Huffman/RLE-only) are not representative -pub(crate) const STD_STRATEGY: u8 = 0; -/// OK to use a bit smalller window for evaluation -pub(crate) const STD_WINDOW: u8 = 13; +/// Must use normal (lazy) compression, as faster ones (greedy) are not representative +pub(crate) const STD_COMPRESSION: u8 = 5; pub(crate) const STD_FILTERS: [u8; 2] = [0, 5]; pub(crate) mod scan_lines; @@ -115,7 +111,7 @@ impl PngData { None => return Err(PngError::ChunkMissing("IHDR")), }; let ihdr_header = parse_ihdr_header(&ihdr)?; - let raw_data = deflate::inflate(idat_headers.as_ref())?; + let raw_data = deflate::inflate(idat_headers.as_ref(), ihdr_header.raw_data_size())?; // Reject files with incorrect width/height or truncated data if raw_data.len() != ihdr_header.raw_data_size() { @@ -373,7 +369,7 @@ fn write_png_block(key: &[u8], header: &[u8], output: &mut Vec) { header_data.extend_from_slice(header); output.reserve(header_data.len() + 8); output.extend_from_slice(&(header_data.len() as u32 - 4).to_be_bytes()); - let crc = Crc::::new(&CRC_32_ISO_HDLC).checksum(&header_data); + let crc = deflate::crc32(&header_data); output.append(&mut header_data); output.extend_from_slice(&crc.to_be_bytes()); } diff --git a/tests/flags.rs b/tests/flags.rs index 11f117f1..9bacf8e0 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -170,10 +170,10 @@ fn verbose_mode() { }); let mut logs: Vec<_> = receiver.into_iter().collect(); - assert_eq!(logs.len(), 4); + assert_eq!(logs.len(), 1); logs.sort(); for (i, log) in logs.into_iter().enumerate() { - let expected_prefix = format!(" zc = 9 zs = {} f = 0 ", i); + let expected_prefix = format!(" zc = 11 f = 0 "); assert!( log.starts_with(&expected_prefix), "logs[{}] = {:?} doesn't start with {:?}", @@ -597,23 +597,3 @@ fn zopfli_mode() { BitDepth::Eight, ); } - -#[test] -#[cfg(feature = "libdeflater")] -fn libdeflater_mode() { - let input = PathBuf::from("tests/files/zopfli_mode.png"); - let (output, mut opts) = get_opts(&input); - let mut compression = IndexSet::new(); - compression.insert(0); - opts.deflate = Deflaters::Libdeflater { compression }; - - test_it_converts( - input, - &output, - &opts, - ColorType::RGB, - BitDepth::Eight, - ColorType::RGB, - BitDepth::Eight, - ); -} diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 65cc0ac2..80610c94 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -443,10 +443,6 @@ fn interlaced_palette_8_should_be_palette_8() { #[test] fn interlaced_palette_8_should_be_palette_4() { - // miniz doesn't estimate compression that well - if !oxipng::internal_tests::cfzlib::is_supported() { - return; - } test_it_converts( "tests/files/interlaced_palette_8_should_be_palette_4.png", ColorType::Indexed,