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:
parent
f688d20fe6
commit
446c788eb3
16 changed files with 367 additions and 733 deletions
34
Cargo.lock
generated
34
Cargo.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
11
Cargo.toml
11
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"
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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<Vec<u8>> {
|
||||
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);
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<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")]
|
||||
pub fn zopfli_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)
|
||||
}
|
||||
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<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")]
|
||||
/// 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<u8>,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
18
src/deflate/zopfli_oxipng.rs
Normal file
18
src/deflate/zopfli_oxipng.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::<u32>::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)
|
||||
|
|
|
|||
154
src/lib.rs
154
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<Vec<u8>> {
|
|||
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<TrialOptions> = 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<u8> {
|
|||
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<u8> {
|
|||
}
|
||||
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::<u32>::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)
|
||||
}
|
||||
|
|
|
|||
479
src/main.rs
479
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 <jholmer.in@gmail.com>")
|
||||
.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 <directory>")
|
||||
.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 <file>")
|
||||
.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 <directory>")
|
||||
.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 <file>")
|
||||
.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::<usize>() {
|
||||
})
|
||||
.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::<usize>() {
|
||||
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::<usize>().unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<u8>) {
|
|||
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::<u32>::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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue