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
This commit is contained in:
andrews05 2022-11-02 02:02:08 +13:00 committed by GitHub
parent f688d20fe6
commit 446c788eb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 367 additions and 733 deletions

34
Cargo.lock generated
View file

@ -91,24 +91,6 @@ dependencies = [
"os_str_bytes", "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]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@ -209,7 +191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide 0.5.4", "miniz_oxide",
] ]
[[package]] [[package]]
@ -324,15 +306,6 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -391,8 +364,6 @@ version = "6.0.1"
dependencies = [ dependencies = [
"bit-vec", "bit-vec",
"clap", "clap",
"cloudflare-zlib",
"crc",
"crossbeam-channel", "crossbeam-channel",
"filetime", "filetime",
"image", "image",
@ -400,7 +371,6 @@ dependencies = [
"itertools", "itertools",
"libdeflater", "libdeflater",
"log", "log",
"miniz_oxide 0.6.2",
"rayon", "rayon",
"rgb", "rgb",
"rustc_version", "rustc_version",
@ -418,7 +388,7 @@ dependencies = [
"bitflags", "bitflags",
"crc32fast", "crc32fast",
"flate2", "flate2",
"miniz_oxide 0.5.4", "miniz_oxide",
] ]
[[package]] [[package]]

View file

@ -24,13 +24,11 @@ required-features = ["binary"]
[dependencies] [dependencies]
bit-vec = "0.6.3" bit-vec = "0.6.3"
crc = "3.0.0"
itertools = "0.10.3" itertools = "0.10.3"
zopfli = { version = "0.7.1", optional = true } zopfli = { version = "0.7.1", optional = true }
miniz_oxide = "0.6.2"
rgb = "0.8.33" rgb = "0.8.33"
indexmap = "1.9.1" indexmap = "1.9.1"
libdeflater = { version = "0.11.0", optional = true } libdeflater = "0.11.0"
log = "0.4.17" log = "0.4.17"
stderrlog = { version = "0.5.3", optional = true, default-features = false } stderrlog = { version = "0.5.3", optional = true, default-features = false }
crossbeam-channel = "0.5.6" crossbeam-channel = "0.5.6"
@ -56,17 +54,14 @@ default-features = false
features = ["png"] features = ["png"]
version = "0.24.3" 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] [build-dependencies]
rustc_version = "0.4.0" rustc_version = "0.4.0"
[features] [features]
binary = ["clap", "wild", "stderrlog"] binary = ["clap", "wild", "stderrlog"]
default = ["binary", "filetime", "parallel", "libdeflater", "zopfli"] default = ["binary", "filetime", "parallel", "zopfli"]
parallel = ["rayon", "indexmap/rayon"] parallel = ["rayon", "indexmap/rayon"]
freestanding = ["libdeflater/freestanding"]
[lib] [lib]
name = "oxipng" name = "oxipng"

View file

@ -7,7 +7,6 @@ use std::path::PathBuf;
use test::Bencher; use test::Bencher;
use oxipng::internal_tests::*; use oxipng::internal_tests::*;
use oxipng::Deadline;
#[bench] #[bench]
fn deflate_16_bits_strategy_0(b: &mut Bencher) { fn deflate_16_bits_strategy_0(b: &mut Bencher) {
@ -16,7 +15,7 @@ fn deflate_16_bits_strategy_0(b: &mut Bencher) {
b.iter(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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(|| { b.iter(|| {
let min = AtomicMin::new(None); 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 input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, false).unwrap(); 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()));
} }

View file

@ -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();
});
}

View file

@ -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<ZError> 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<Vec<u8>> {
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");
}

View file

@ -1,6 +1,6 @@
use crate::atomicmin::AtomicMin; use crate::atomicmin::AtomicMin;
use crate::{PngError, PngResult}; use crate::{PngError, PngResult};
use libdeflater::{CompressionError, CompressionLvl, Compressor}; use libdeflater::*;
pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> { pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
let mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap()); 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<Vec<u8
dest.truncate(len); dest.truncate(len);
Ok(dest) Ok(dest)
} }
pub fn inflate(data: &[u8], out_size: usize) -> PngResult<Vec<u8>> {
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()
}

View file

@ -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<Vec<u8>> {
// 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)
}

View file

@ -1,98 +1,24 @@
use crate::atomicmin::AtomicMin;
use crate::error::PngError;
use crate::Deadline;
use crate::PngResult;
use indexmap::IndexSet; use indexmap::IndexSet;
mod deflater;
pub use deflater::crc32;
pub use deflater::deflate;
pub use deflater::inflate;
#[cfg(feature = "zopfli")] #[cfg(feature = "zopfli")]
use std::num::NonZeroU8; 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<Vec<u8>> {
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<Vec<u8>> {
#[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")] #[cfg(feature = "zopfli")]
pub fn zopfli_deflate(data: &[u8], iterations: NonZeroU8) -> PngResult<Vec<u8>> { mod zopfli_oxipng;
use std::cmp::max; #[cfg(feature = "zopfli")]
pub use zopfli_oxipng::deflate as zopfli_deflate;
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)
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
/// DEFLATE algorithms supported by oxipng /// DEFLATE algorithms supported by oxipng
pub enum Deflaters { pub enum Deflaters {
/// Use the Zlib/Miniz DEFLATE implementation /// Use libdeflater.
Zlib { Libdeflater {
/// Which zlib compression levels to try on the file (1-9) /// Which compression levels to try on the file (1-12)
///
/// Default: `9`
compression: IndexSet<u8>, compression: IndexSet<u8>,
/// Which zlib compression strategies to try on the file (0-3)
///
/// Default: `0-3`
strategies: IndexSet<u8>,
/// 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")] #[cfg(feature = "zopfli")]
/// Use the better but slower Zopfli implementation /// Use the better but slower Zopfli implementation
@ -102,10 +28,4 @@ pub enum Deflaters {
/// less iterations, or else they will be too slow. /// less iterations, or else they will be too slow.
iterations: NonZeroU8, iterations: NonZeroU8,
}, },
#[cfg(feature = "libdeflater")]
/// Use libdeflater.
Libdeflater {
/// Which compression levels to try on the file (1-12)
compression: IndexSet<u8>,
},
} }

View file

@ -0,0 +1,18 @@
use crate::{PngError, PngResult};
use std::num::NonZeroU8;
pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult<Vec<u8>> {
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)
}

View file

@ -7,8 +7,6 @@ use crate::png::PngData;
use crate::png::PngImage; use crate::png::PngImage;
use crate::png::STD_COMPRESSION; use crate::png::STD_COMPRESSION;
use crate::png::STD_FILTERS; use crate::png::STD_FILTERS;
use crate::png::STD_STRATEGY;
use crate::png::STD_WINDOW;
#[cfg(not(feature = "parallel"))] #[cfg(not(feature = "parallel"))]
use crate::rayon; use crate::rayon;
use crate::Deadline; use crate::Deadline;
@ -119,10 +117,7 @@ impl Evaluator {
if let Ok(idat_data) = deflate::deflate( if let Ok(idat_data) = deflate::deflate(
&image.filter_image(filter), &image.filter_image(filter),
STD_COMPRESSION, STD_COMPRESSION,
STD_STRATEGY,
STD_WINDOW,
&best_candidate_size, &best_candidate_size,
&deadline,
) { ) {
best_candidate_size.set_min(idat_data.len()); best_candidate_size.set_min(idat_data.len());
// ignore baseline images after this point // ignore baseline images after this point

View file

@ -1,7 +1,7 @@
use crate::colors::{BitDepth, ColorType}; use crate::colors::{BitDepth, ColorType};
use crate::deflate::crc32;
use crate::error::PngError; use crate::error::PngError;
use crate::PngResult; use crate::PngResult;
use crc::{Crc, CRC_32_ISO_HDLC};
use indexmap::IndexSet; use indexmap::IndexSet;
use std::io; use std::io;
use std::io::{Cursor, Read}; use std::io::{Cursor, Read};
@ -130,7 +130,7 @@ pub fn parse_next_header<'a>(
let header_bytes = byte_data let header_bytes = byte_data
.get(header_start..header_start + 4 + length as usize) .get(header_start..header_start + 4 + length as usize)
.ok_or(PngError::TruncatedData)?; .ok_or(PngError::TruncatedData)?;
if !fix_errors && Crc::<u32>::new(&CRC_32_ISO_HDLC).checksum(header_bytes) != crc { if !fix_errors && crc32(header_bytes) != crc {
return Err(PngError::new(&format!( return Err(PngError::new(&format!(
"CRC Mismatch in {} header; May be recoverable by using --fix", "CRC Mismatch in {} header; May be recoverable by using --fix",
String::from_utf8_lossy(chunk_name) String::from_utf8_lossy(chunk_name)

View file

@ -14,7 +14,7 @@
#![allow(clippy::cognitive_complexity)] #![allow(clippy::cognitive_complexity)]
#![allow(clippy::upper_case_acronyms)] #![allow(clippy::upper_case_acronyms)]
#![cfg_attr( #![cfg_attr(
not(any(feature = "libdeflater", feature = "zopfli")), not(feature = "zopfli"),
allow(irrefutable_let_patterns), allow(irrefutable_let_patterns),
allow(unreachable_patterns) allow(unreachable_patterns)
)] )]
@ -26,12 +26,11 @@ mod rayon;
use crate::atomicmin::AtomicMin; use crate::atomicmin::AtomicMin;
use crate::colors::BitDepth; use crate::colors::BitDepth;
use crate::deflate::inflate; use crate::deflate::{crc32, inflate};
use crate::evaluate::Evaluator; use crate::evaluate::Evaluator;
use crate::png::PngData; use crate::png::PngData;
use crate::png::PngImage; use crate::png::PngImage;
use crate::reduction::*; use crate::reduction::*;
use crc::{Crc, CRC_32_ISO_HDLC};
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use rayon::prelude::*; use rayon::prelude::*;
@ -190,7 +189,7 @@ pub struct Options {
pub strip: Headers, pub strip: Headers,
/// Which DEFLATE algorithm to use /// Which DEFLATE algorithm to use
/// ///
/// Default: `Zlib` /// Default: `Libdeflater`
pub deflate: Deflaters, pub deflate: Deflaters,
/// Whether to use heuristics to pick the best filter and compression /// Whether to use heuristics to pick the best filter and compression
/// ///
@ -212,10 +211,7 @@ impl Options {
1 => opts.apply_preset_1(), 1 => opts.apply_preset_1(),
2 => opts.apply_preset_2(), 2 => opts.apply_preset_2(),
3 => opts.apply_preset_3(), 3 => opts.apply_preset_3(),
4 => { 4 => opts.apply_preset_4(),
warn!("Level 4 is deprecated and is identical to level 3");
opts.apply_preset_4()
}
5 => opts.apply_preset_5(), 5 => opts.apply_preset_5(),
6 => opts.apply_preset_6(), 6 => opts.apply_preset_6(),
_ => { _ => {
@ -233,17 +229,20 @@ impl Options {
// on an `Options` struct generated by the `default` method. // on an `Options` struct generated by the `default` method.
fn apply_preset_0(mut self) -> Self { fn apply_preset_0(mut self) -> Self {
self.idat_recoding = false; 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.clear();
compression.insert(3); compression.insert(5);
} }
self.apply_preset_1() self.use_heuristics = true;
self
} }
fn apply_preset_1(mut self) -> Self { fn apply_preset_1(mut self) -> Self {
self.filter.clear(); self.filter.clear();
if let Deflaters::Zlib { strategies, .. } = &mut self.deflate { if let Deflaters::Libdeflater { compression } = &mut self.deflate {
strategies.clear(); compression.clear();
compression.insert(10);
} }
self.use_heuristics = true; self.use_heuristics = true;
self self
@ -254,34 +253,38 @@ impl Options {
} }
fn apply_preset_3(mut self) -> Self { fn apply_preset_3(mut self) -> Self {
for i in 1..5 { for i in 1..=4 {
self.filter.insert(i); self.filter.insert(i);
} }
self 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() self.apply_preset_3()
} }
fn apply_preset_5(mut self) -> Self { 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(); compression.clear();
for i in 3..10 { for i in 9..=12 {
compression.insert(i); compression.insert(i);
} }
} }
self.apply_preset_4() self.apply_preset_3()
} }
fn apply_preset_6(mut self) -> Self { 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(); compression.clear();
for i in 1..10 { for i in 1..=12 {
compression.insert(i); compression.insert(i);
} }
} }
self.apply_preset_4() self.apply_preset_3()
} }
} }
@ -292,11 +295,7 @@ impl Default for Options {
filter.insert(0); filter.insert(0);
filter.insert(5); filter.insert(5);
let mut compression = IndexSet::new(); let mut compression = IndexSet::new();
compression.insert(9); compression.insert(11);
let mut strategies = IndexSet::new();
for i in 0..4 {
strategies.insert(i);
}
// We always need NoOp to be present // We always need NoOp to be present
let mut alphas = IndexSet::new(); let mut alphas = IndexSet::new();
alphas.insert(AlphaOptim::NoOp); alphas.insert(AlphaOptim::NoOp);
@ -317,11 +316,7 @@ impl Default for Options {
grayscale_reduction: true, grayscale_reduction: true,
idat_recoding: true, idat_recoding: true,
strip: Headers::None, strip: Headers::None,
deflate: Deflaters::Zlib { deflate: Deflaters::Libdeflater { compression },
compression,
strategies,
window: 15,
},
use_heuristics: false, use_heuristics: false,
timeout: None, timeout: None,
} }
@ -472,7 +467,6 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult<Vec<u8>> {
struct TrialOptions { struct TrialOptions {
pub filter: u8, pub filter: u8,
pub compression: u8, pub compression: u8,
pub strategy: u8,
} }
/// Perform optimization on the input PNG object using the options provided /// 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); info!(" File size = {} bytes", file_original_size);
let mut filter = opts.filter.clone(); let mut filter = opts.filter.clone();
let mut strategies = match &opts.deflate {
Deflaters::Zlib { strategies, .. } => Some(strategies.clone()),
_ => None,
};
if opts.use_heuristics { if opts.use_heuristics {
// Heuristically determine which set of options to use // 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 && png.raw.ihdr.color_type != colors::ColorType::Indexed
{ {
(5, 1) 5
} else { } else {
(0, 0) 0
}; };
if filter.is_empty() { if filter.is_empty() {
filter.insert(use_filter); 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 // 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 { if opts.idat_recoding || reduction_occurred {
// Go through selected permutations and determine the best // Go through selected permutations and determine the best
let combinations = if let Deflaters::Zlib { compression, .. } = &opts.deflate { let combinations = if let Deflaters::Libdeflater { compression } = &opts.deflate {
filter.len() * compression.len() * strategies.as_ref().unwrap().len() filter.len() * compression.len()
} else { } else {
filter.len() filter.len()
}; };
let mut results: Vec<TrialOptions> = Vec::with_capacity(combinations); let mut results: Vec<TrialOptions> = Vec::with_capacity(combinations);
for f in &filter { for f in &filter {
match &opts.deflate { if let Deflaters::Libdeflater { compression } = &opts.deflate {
Deflaters::Zlib { compression, .. } => { for zc in 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.
results.push(TrialOptions { results.push(TrialOptions {
filter: *f, filter: *f,
compression: 0, compression: *zc,
strategy: 0,
}); });
} if deadline.passed() {
#[cfg(feature = "libdeflater")] break;
Deflaters::Libdeflater { compression } => {
for zc in compression {
results.push(TrialOptions {
filter: *f,
compression: *zc,
strategy: 0,
});
if deadline.passed() {
break;
}
} }
} }
} else {
// Zopfli has no additional options.
results.push(TrialOptions {
filter: *f,
compression: 0,
});
} }
if deadline.passed() { if deadline.passed() {
@ -626,28 +590,19 @@ fn optimize_png(
} }
let filtered = &filters[&trial.filter]; let filtered = &filters[&trial.filter];
let new_idat = match opts.deflate { let new_idat = match opts.deflate {
Deflaters::Zlib { window, .. } => deflate::deflate( Deflaters::Libdeflater { .. } => {
filtered, deflate::deflate(filtered, trial.compression, &best_size)
trial.compression, }
trial.strategy,
window,
&best_size,
&deadline,
),
#[cfg(feature = "zopfli")] #[cfg(feature = "zopfli")]
Deflaters::Zopfli { iterations } => deflate::zopfli_deflate(filtered, iterations), 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 { let new_idat = match new_idat {
Ok(n) => n, Ok(n) => n,
Err(PngError::DeflatedDataTooLong(max)) => { Err(PngError::DeflatedDataTooLong(max)) => {
debug!( debug!(
" zc = {} zs = {} f = {} >{} bytes", " zc = {} f = {} >{} bytes",
trial.compression, trial.strategy, trial.filter, max, trial.compression, trial.filter, max,
); );
return None; return None;
} }
@ -659,9 +614,8 @@ fn optimize_png(
best_size.set_min(new_size); best_size.set_min(new_size);
debug!( debug!(
" zc = {} zs = {} f = {} {} bytes", " zc = {} f = {} {} bytes",
trial.compression, trial.compression,
trial.strategy,
trial.filter, trial.filter,
new_idat.len() new_idat.len()
); );
@ -684,9 +638,8 @@ fn optimize_png(
png.idat_data = idat_data; png.idat_data = idat_data;
info!("Found better combination:"); info!("Found better combination:");
info!( info!(
" zc = {} zs = {} f = {} {} bytes", " zc = {} f = {} {} bytes",
opts.compression, opts.compression,
opts.strategy,
opts.filter, opts.filter,
png.idat_data.len() png.idat_data.len()
); );
@ -952,7 +905,9 @@ fn srgb_rendering_intent(mut iccp: &[u8]) -> Option<u8> {
if compression_method != 0 { if compression_method != 0 {
return None; // The profile is supposed to be compressed (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)?; let rendering_intent = *icc_data.get(67)?;
@ -968,10 +923,7 @@ fn srgb_rendering_intent(mut iccp: &[u8]) -> Option<u8> {
} }
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => { 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 // Known-bad profiles are identified by their CRC
match ( match (crc32(&icc_data), icc_data.len()) {
Crc::<u32>::new(&CRC_32_ISO_HDLC).checksum(&icc_data),
icc_data.len(),
) {
(0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => { (0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => {
Some(rendering_intent) Some(rendering_intent)
} }

View file

@ -14,7 +14,7 @@
#![allow(clippy::cognitive_complexity)] #![allow(clippy::cognitive_complexity)]
use clap::{AppSettings, Arg, ArgMatches, Command}; use clap::{AppSettings, Arg, ArgMatches, Command};
use indexmap::{indexset, IndexSet}; use indexmap::IndexSet;
use log::{error, warn}; use log::{error, warn};
use oxipng::AlphaOptim; use oxipng::AlphaOptim;
use oxipng::Deflaters; use oxipng::Deflaters;
@ -22,6 +22,7 @@ use oxipng::Headers;
use oxipng::Options; use oxipng::Options;
use oxipng::{InFile, OutFile}; use oxipng::{InFile, OutFile};
use std::fs::DirBuilder; use std::fs::DirBuilder;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8; use std::num::NonZeroU8;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit; use std::process::exit;
@ -33,199 +34,214 @@ fn main() {
.author("Joshua Holmer <jholmer.in@gmail.com>") .author("Joshua Holmer <jholmer.in@gmail.com>")
.about("Losslessly improves compression of PNG files") .about("Losslessly improves compression of PNG files")
.setting(AppSettings::DeriveDisplayOrder) .setting(AppSettings::DeriveDisplayOrder)
.arg(Arg::new("files") .arg(
.help("File(s) to compress (use \"-\" for stdin)") Arg::new("files")
.index(1) .help("File(s) to compress (use \"-\" for stdin)")
.multiple_values(true) .index(1)
.use_value_delimiter(false) .multiple_values(true)
.required(true)) .use_value_delimiter(false)
.arg(Arg::new("optimization") .required(true),
.help("Optimization level - Default: 2") )
.short('o') .arg(
.long("opt") Arg::new("optimization")
.takes_value(true) .help("Optimization level - Default: 2")
.value_name("level") .short('o')
.possible_value("0") .long("opt")
.possible_value("1") .takes_value(true)
.possible_value("2") .value_name("level")
.possible_value("3") .possible_value("0")
.possible_value("4") .possible_value("1")
.possible_value("5") .possible_value("2")
.possible_value("6") .possible_value("3")
.possible_value("max")) .possible_value("4")
.arg(Arg::new("backup") .possible_value("5")
.help("Back up modified files") .possible_value("6")
.short('b') .possible_value("max"),
.long("backup")) )
.arg(Arg::new("recursive") .arg(
.help("Recurse into subdirectories") Arg::new("backup")
.short('r') .help("Back up modified files")
.long("recursive")) .short('b')
.arg(Arg::new("output_dir") .long("backup"),
.help("Write output file(s) to <directory>") )
.long("dir") .arg(
.takes_value(true) Arg::new("recursive")
.value_name("directory") .help("Recurse into subdirectories")
.conflicts_with("output_file") .short('r')
.conflicts_with("stdout")) .long("recursive"),
.arg(Arg::new("output_file") )
.help("Write output file to <file>") .arg(
.long("out") Arg::new("output_dir")
.takes_value(true) .help("Write output file(s) to <directory>")
.value_name("file") .long("dir")
.conflicts_with("output_dir") .takes_value(true)
.conflicts_with("stdout")) .value_name("directory")
.arg(Arg::new("stdout") .conflicts_with("output_file")
.help("Write output to stdout") .conflicts_with("stdout"),
.long("stdout") )
.conflicts_with("output_dir") .arg(
.conflicts_with("output_file")) Arg::new("output_file")
.arg(Arg::new("preserve") .help("Write output file to <file>")
.help("Preserve file attributes if possible") .long("out")
.short('p') .takes_value(true)
.long("preserve")) .value_name("file")
.arg(Arg::new("check") .conflicts_with("output_dir")
.help("Do not run any optimization passes") .conflicts_with("stdout"),
.short('c') )
.long("check")) .arg(
.arg(Arg::new("pretend") Arg::new("stdout")
.help("Do not write any files, only calculate compression gains") .help("Write output to stdout")
.short('P') .long("stdout")
.long("pretend")) .conflicts_with("output_dir")
.arg(Arg::new("strip-safe") .conflicts_with("output_file"),
.help("Strip safely-removable metadata objects") )
.short('s') .arg(
.conflicts_with("strip")) Arg::new("preserve")
.arg(Arg::new("strip") .help("Preserve file attributes if possible")
.help("Strip metadata objects ['safe', 'all', or comma-separated list]") .short('p')
.long("strip") .long("preserve"),
.takes_value(true) )
.value_name("mode") .arg(
.conflicts_with("strip-safe")) Arg::new("check")
.arg(Arg::new("keep") .help("Do not run any optimization passes")
.help("Strip all optional metadata except objects in the comma-separated list") .short('c')
.long("keep") .long("check"),
.takes_value(true) )
.value_name("list") .arg(
.conflicts_with("strip") Arg::new("pretend")
.conflicts_with("strip-safe")) .help("Do not write any files, only calculate compression gains")
.arg(Arg::new("alpha") .short('P')
.help("Perform additional alpha optimizations") .long("pretend"),
.short('a') )
.long("alpha")) .arg(
.arg(Arg::new("interlace") Arg::new("strip-safe")
.help("PNG interlace type") .help("Strip safely-removable metadata objects")
.short('i') .short('s')
.long("interlace") .conflicts_with("strip"),
.takes_value(true) )
.value_name("0/1") .arg(
.possible_value("0") Arg::new("strip")
.possible_value("1")) .help("Strip metadata objects ['safe', 'all', or comma-separated list]")
.arg(Arg::new("verbose") .long("strip")
.help("Run in verbose mode") .takes_value(true)
.short('v') .value_name("mode")
.long("verbose") .conflicts_with("strip-safe"),
.conflicts_with("quiet")) )
.arg(Arg::new("quiet") .arg(
.help("Run in quiet mode") Arg::new("keep")
.short('q') .help("Strip all optional metadata except objects in the comma-separated list")
.long("quiet") .long("keep")
.conflicts_with("verbose")) .takes_value(true)
.arg(Arg::new("filters") .value_name("list")
.help("PNG delta filters (0-5) - Default: 0,5") .conflicts_with("strip")
.short('f') .conflicts_with("strip-safe"),
.long("filters") )
.takes_value(true) .arg(
.validator(|x| { Arg::new("alpha")
match parse_numeric_range_opts(x, 0, 5) { .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(()), Ok(_) => Ok(()),
Err(_) => Err("Invalid option for filters".to_owned()), Err(_) => Err("Invalid option for filters".to_owned()),
} }),
})) )
.arg(Arg::new("compression") .arg(
.help("compression levels (zlib: 1-9, libdeflater: 1-12) - Default: 9 or 12") Arg::new("compression")
.long("zc") .help("zlib compression levels (1-12) - Default: 12")
.takes_value(true) .long("zc")
.value_name("levels") .takes_value(true)
.validator(|x| { .value_name("levels")
match parse_numeric_range_opts(x, 1, 12) { .validator(|x| match parse_numeric_range_opts(x, 1, 12) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(_) => Err("Invalid option for compression".to_owned()), Err(_) => Err("Invalid option for compression".to_owned()),
} })
}) .conflicts_with("zopfli"),
.conflicts_with("zopfli")) )
.arg(Arg::new("strategies") .arg(
.help("zlib compression strategies (0-3) - Default: 0-3") Arg::new("no-bit-reduction")
.long("zs") .help("No bit depth reduction")
.takes_value(true) .long("nb"),
.validator(|x| { )
match parse_numeric_range_opts(x, 0, 3) { .arg(
Ok(_) => Ok(()), Arg::new("no-color-reduction")
Err(_) => Err("Invalid option for strategies".to_owned()), .help("No color type reduction")
} .long("nc"),
}) )
.conflicts_with_all(&["zopfli", "libdeflater"])) .arg(
.arg(Arg::new("window") Arg::new("no-palette-reduction")
.help("zlib window size - Default: 32k") .help("No palette reduction")
.long("zw") .long("np"),
.takes_value(true) )
.value_name("size") .arg(
.possible_value("256") Arg::new("no-grayscale-reduction")
.possible_value("512") .help("No grayscale reduction")
.possible_value("1k") .long("ng"),
.possible_value("2k") )
.possible_value("4k") .arg(Arg::new("no-reductions").help("No reductions").long("nx"))
.possible_value("8k") .arg(
.possible_value("16k") Arg::new("no-recoding")
.possible_value("32k") .help("No IDAT recoding unless necessary")
.conflicts_with_all(&["zopfli", "libdeflater"])) .long("nz"),
.arg(Arg::new("no-bit-reduction") )
.help("No bit depth reduction") .arg(Arg::new("fix").help("Enable error recovery").long("fix"))
.long("nb")) .arg(
.arg(Arg::new("no-color-reduction") Arg::new("force")
.help("No color type reduction") .help("Write the output even if it is larger than the input")
.long("nc")) .long("force"),
.arg(Arg::new("no-palette-reduction") )
.help("No palette reduction") .arg(
.long("np")) Arg::new("zopfli")
.arg(Arg::new("no-grayscale-reduction") .help("Use the slower but better compressing Zopfli algorithm")
.help("No grayscale reduction") .short('Z')
.long("ng")) .long("zopfli"),
.arg(Arg::new("no-reductions") )
.help("No reductions") .arg(
.long("nx")) Arg::new("timeout")
.arg(Arg::new("no-recoding") .help("Maximum amount of time, in seconds, to spend on optimizations")
.help("No IDAT recoding unless necessary") .takes_value(true)
.long("nz")) .value_name("secs")
.arg(Arg::new("fix") .long("timeout"),
.help("Enable error recovery") )
.long("fix")) .arg(
.arg(Arg::new("force") Arg::new("threads")
.help("Write the output even if it is larger than the input") .help("Set number of threads to use - default 1.5x CPU cores")
.long("force")) .long("threads")
.arg(Arg::new("zopfli") .short('t')
.help("Use the slower but better compressing Zopfli algorithm, overrides zlib-specific options") .takes_value(true)
.short('Z') .value_name("num")
.long("zopfli") .validator(|x| match x.parse::<usize>() {
.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::<usize>() {
Ok(val) => { Ok(val) => {
if val > 0 { if val > 0 {
Ok(()) Ok(())
@ -234,20 +250,22 @@ fn main() {
} }
} }
Err(_) => Err("Thread count must be >= 1".to_owned()), Err(_) => Err("Thread count must be >= 1".to_owned()),
} }),
})) )
.after_help("Optimization levels: .after_help(
-o 0 => --zc 3 --nz (0 or 1 trials) "Optimization levels:
-o 1 => --zc 9 (1 trial, determined heuristically) -o 0 => --zc 6 --nz (0 or 1 trials)
-o 2 => --zc 9 --zs 0-3 -f 0,5 (8 trials with zlib or 2 trials with other compressors) -o 1 => --zc 10 (1 trial, determined heuristically)
-o 3 => --zc 9 --zs 0-3 -f 0-5 (24 trials with zlib or 6 trials with other compressors) -o 2 => --zc 11 -f 0,5 (2 trials)
-o 4 => (deprecated; same as `-o 3`) -o 3 => --zc 11 -f 0-5 (6 trials)
-o 5 => --zc 3-9 --zs 0-3 -f 0-5 (168 trials with zlib; same as `-o 3` for other compressors) -o 4 => --zc 12 -f 0-5 (6 trials; same as `-o 3` for zopfli)
-o 6 => --zc 1-9 --zs 0-3 -f 0-5 (216 trials with zlib; same as `-o 3` for other compressors) -o 5 => --zc 9-12 -f 0-5 (24 trials; same as `-o 3` for zopfli)
-o max => (stable alias for the max compression) -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, Manually specifying a compression option (zc, f, etc.) will override the optimization preset,
regardless of the order you write the arguments.") regardless of the order you write the arguments.",
)
.get_matches_from(wild::args()); .get_matches_from(wild::args());
let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) { 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") { if matches.is_present("zopfli") {
opts.deflate = Deflaters::Zopfli { if explicit_level > Some(3) {
iterations: NonZeroU8::new(15).unwrap(), warn!("Level 4 and above are equivalent to level 3 for zopfli");
}; }
} else if matches.is_present("libdeflater") { #[cfg(feature = "zopfli")]
opts.deflate = Deflaters::Libdeflater { if let Some(iterations) = NonZeroU8::new(15) {
compression: match matches.value_of("compression") { opts.deflate = Deflaters::Zopfli { iterations };
Some(x) => parse_numeric_range_opts(x, 1, 12).unwrap(), }
_ => indexset! { 12 }, } else if let Deflaters::Libdeflater { compression } = &mut opts.deflate {
},
};
} else if let Deflaters::Zlib {
compression,
strategies,
window,
} = &mut opts.deflate
{
if let Some(x) = matches.value_of("compression") { if let Some(x) = matches.value_of("compression") {
*compression = parse_numeric_range_opts(x, 1, 9) *compression = parse_numeric_range_opts(x, 1, 12).unwrap();
.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"
);
}
} }
} }
#[cfg(feature = "parallel")]
if let Some(x) = matches.value_of("threads") { if let Some(x) = matches.value_of("threads") {
let threads = x.parse::<usize>().unwrap(); let threads = x.parse::<usize>().unwrap();

View file

@ -4,7 +4,6 @@ use crate::error::PngError;
use crate::filters::*; use crate::filters::*;
use crate::headers::*; use crate::headers::*;
use crate::interlace::{deinterlace_image, interlace_image}; use crate::interlace::{deinterlace_image, interlace_image};
use crc::{Crc, CRC_32_ISO_HDLC};
use indexmap::IndexMap; use indexmap::IndexMap;
use rgb::ComponentSlice; use rgb::ComponentSlice;
use rgb::RGBA8; use rgb::RGBA8;
@ -14,11 +13,8 @@ use std::iter::Iterator;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
pub(crate) const STD_COMPRESSION: u8 = 6; /// Must use normal (lazy) compression, as faster ones (greedy) are not representative
/// Must use normal compression, as faster ones (Huffman/RLE-only) are not representative pub(crate) const STD_COMPRESSION: u8 = 5;
pub(crate) const STD_STRATEGY: u8 = 0;
/// OK to use a bit smalller window for evaluation
pub(crate) const STD_WINDOW: u8 = 13;
pub(crate) const STD_FILTERS: [u8; 2] = [0, 5]; pub(crate) const STD_FILTERS: [u8; 2] = [0, 5];
pub(crate) mod scan_lines; pub(crate) mod scan_lines;
@ -115,7 +111,7 @@ impl PngData {
None => return Err(PngError::ChunkMissing("IHDR")), None => return Err(PngError::ChunkMissing("IHDR")),
}; };
let ihdr_header = parse_ihdr_header(&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 // Reject files with incorrect width/height or truncated data
if raw_data.len() != ihdr_header.raw_data_size() { if raw_data.len() != ihdr_header.raw_data_size() {
@ -373,7 +369,7 @@ fn write_png_block(key: &[u8], header: &[u8], output: &mut Vec<u8>) {
header_data.extend_from_slice(header); header_data.extend_from_slice(header);
output.reserve(header_data.len() + 8); output.reserve(header_data.len() + 8);
output.extend_from_slice(&(header_data.len() as u32 - 4).to_be_bytes()); output.extend_from_slice(&(header_data.len() as u32 - 4).to_be_bytes());
let crc = Crc::<u32>::new(&CRC_32_ISO_HDLC).checksum(&header_data); let crc = deflate::crc32(&header_data);
output.append(&mut header_data); output.append(&mut header_data);
output.extend_from_slice(&crc.to_be_bytes()); output.extend_from_slice(&crc.to_be_bytes());
} }

View file

@ -170,10 +170,10 @@ fn verbose_mode() {
}); });
let mut logs: Vec<_> = receiver.into_iter().collect(); let mut logs: Vec<_> = receiver.into_iter().collect();
assert_eq!(logs.len(), 4); assert_eq!(logs.len(), 1);
logs.sort(); logs.sort();
for (i, log) in logs.into_iter().enumerate() { 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!( assert!(
log.starts_with(&expected_prefix), log.starts_with(&expected_prefix),
"logs[{}] = {:?} doesn't start with {:?}", "logs[{}] = {:?} doesn't start with {:?}",
@ -597,23 +597,3 @@ fn zopfli_mode() {
BitDepth::Eight, 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,
);
}

View file

@ -443,10 +443,6 @@ fn interlaced_palette_8_should_be_palette_8() {
#[test] #[test]
fn interlaced_palette_8_should_be_palette_4() { 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( test_it_converts(
"tests/files/interlaced_palette_8_should_be_palette_4.png", "tests/files/interlaced_palette_8_should_be_palette_4.png",
ColorType::Indexed, ColorType::Indexed,