Provide BufferedZopfliDeflater and allow user to pass in a custom Deflater (#530)
* Add .whitesource configuration file * Experimental: allow Zopfli to use any size BufWriter * Allow user to specify the output buffer size as well * Allow user to specify maximum block splits * Reformat and fix warnings * Use deflater on iCCP chunk as well * Bug fix: need to implement Zlib format * Make functions const when possible * Switch to using zopfli::Options in prep for https://github.com/zopfli-rs/zopfli/pull/21 * Switch to using zopfli::Options in prep for https://github.com/zopfli-rs/zopfli/pull/21 * Cargo fmt * Fix compilation * Fix tests * Fix more lints * Fix more lints * Fix compilation more --------- Co-authored-by: mend-bolt-for-github[bot] <42819689+mend-bolt-for-github[bot]@users.noreply.github.com> Co-authored-by: Chris Hennick <hennickc@amazon.com> Co-authored-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com>
This commit is contained in:
parent
b846a2e909
commit
2a59419bdf
14 changed files with 242 additions and 77 deletions
14
.whitesource
Normal file
14
.whitesource
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"scanSettings": {
|
||||||
|
"baseBranches": []
|
||||||
|
},
|
||||||
|
"checkRunSettings": {
|
||||||
|
"vulnerableCheckRunConclusionLevel": "failure",
|
||||||
|
"displayMode": "diff",
|
||||||
|
"useMendCheckNames": true
|
||||||
|
},
|
||||||
|
"issueSettings": {
|
||||||
|
"minSeverityLevel": "LOW",
|
||||||
|
"issueType": "DEPENDENCY"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -479,6 +479,7 @@ dependencies = [
|
||||||
"rgb",
|
"rgb",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
|
"simd-adler32",
|
||||||
"stderrlog",
|
"stderrlog",
|
||||||
"wild",
|
"wild",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ required-features = ["zopfli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zopfli = { version = "0.7.4", optional = true, default-features = false, features = ["std", "zlib"] }
|
zopfli = { version = "0.7.4", optional = true, default-features = false, features = ["std", "zlib"] }
|
||||||
|
simd-adler32 = { version = "0.3.5", optional = true, default-features = false }
|
||||||
rgb = "0.8.36"
|
rgb = "0.8.36"
|
||||||
indexmap = "2.0.0"
|
indexmap = "2.0.0"
|
||||||
libdeflater = "0.14.0"
|
libdeflater = "0.14.0"
|
||||||
|
|
@ -66,6 +67,7 @@ version = "0.24.6"
|
||||||
rustc_version = "0.4.0"
|
rustc_version = "0.4.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
zopfli = ["zopfli/std", "zopfli/zlib", "simd-adler32"]
|
||||||
binary = ["clap", "wild", "stderrlog"]
|
binary = ["clap", "wild", "stderrlog"]
|
||||||
default = ["binary", "filetime", "parallel", "zopfli"]
|
default = ["binary", "filetime", "parallel", "zopfli"]
|
||||||
parallel = ["rayon", "indexmap/rayon", "crossbeam-channel"]
|
parallel = ["rayon", "indexmap/rayon", "crossbeam-channel"]
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,19 @@ extern crate test;
|
||||||
|
|
||||||
use oxipng::internal_tests::*;
|
use oxipng::internal_tests::*;
|
||||||
use oxipng::*;
|
use oxipng::*;
|
||||||
use std::num::NonZeroU8;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
// SAFETY: trivially safe. Stopgap solution until const unwrap is stabilized.
|
|
||||||
const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(15) };
|
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn zopfli_16_bits_strategy_0(b: &mut Bencher) {
|
fn zopfli_16_bits_strategy_0(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, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
let max_size = AtomicMin::new(Some(png.idat_data.len()));
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
BufferedZopfliDeflater::default()
|
||||||
|
.deflate(png.raw.data.as_ref(), &max_size)
|
||||||
|
.ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,9 +25,12 @@ fn zopfli_16_bits_strategy_0(b: &mut Bencher) {
|
||||||
fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
|
fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
let max_size = AtomicMin::new(Some(png.idat_data.len()));
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
BufferedZopfliDeflater::default()
|
||||||
|
.deflate(png.raw.data.as_ref(), &max_size)
|
||||||
|
.ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,9 +40,12 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
let max_size = AtomicMin::new(Some(png.idat_data.len()));
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
BufferedZopfliDeflater::default()
|
||||||
|
.deflate(png.raw.data.as_ref(), &max_size)
|
||||||
|
.ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,9 +55,12 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
let max_size = AtomicMin::new(Some(png.idat_data.len()));
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
BufferedZopfliDeflater::default()
|
||||||
|
.deflate(png.raw.data.as_ref(), &max_size)
|
||||||
|
.ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,8 +70,11 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
let max_size = AtomicMin::new(Some(png.idat_data.len()));
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
BufferedZopfliDeflater::default()
|
||||||
|
.deflate(png.raw.data.as_ref(), &max_size)
|
||||||
|
.ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ impl AtomicMin {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unset value is usize_max
|
/// Unset value is usize_max
|
||||||
pub fn as_atomic_usize(&self) -> &AtomicUsize {
|
pub const fn as_atomic_usize(&self) -> &AtomicUsize {
|
||||||
&self.val
|
&self.val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ impl Display for ColorType {
|
||||||
impl ColorType {
|
impl ColorType {
|
||||||
/// Get the code used by the PNG specification to denote this color type
|
/// Get the code used by the PNG specification to denote this color type
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn png_header_code(&self) -> u8 {
|
pub const fn png_header_code(&self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
ColorType::Grayscale { .. } => 0,
|
ColorType::Grayscale { .. } => 0,
|
||||||
ColorType::RGB { .. } => 2,
|
ColorType::RGB { .. } => 2,
|
||||||
|
|
@ -56,7 +56,7 @@ impl ColorType {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn channels_per_pixel(&self) -> u8 {
|
pub(crate) const fn channels_per_pixel(&self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1,
|
ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1,
|
||||||
ColorType::GrayscaleAlpha => 2,
|
ColorType::GrayscaleAlpha => 2,
|
||||||
|
|
@ -66,12 +66,12 @@ impl ColorType {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn is_rgb(&self) -> bool {
|
pub(crate) const fn is_rgb(&self) -> bool {
|
||||||
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
|
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn is_gray(&self) -> bool {
|
pub(crate) const fn is_gray(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha
|
ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha
|
||||||
|
|
@ -79,12 +79,12 @@ impl ColorType {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn has_alpha(&self) -> bool {
|
pub(crate) const fn has_alpha(&self) -> bool {
|
||||||
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
|
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn has_trns(&self) -> bool {
|
pub(crate) const fn has_trns(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
|
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
|
||||||
ColorType::RGB { transparent_color } => transparent_color.is_some(),
|
ColorType::RGB { transparent_color } => transparent_color.is_some(),
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,15 @@ pub use deflater::inflate;
|
||||||
use std::{fmt, fmt::Display};
|
use std::{fmt, fmt::Display};
|
||||||
|
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
use std::num::NonZeroU8;
|
use std::io::{self, copy, BufWriter, Cursor, Write};
|
||||||
|
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
|
use zopfli::{DeflateEncoder, Options};
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
mod zopfli_oxipng;
|
mod zopfli_oxipng;
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
|
use simd_adler32::Adler32;
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
pub use zopfli_oxipng::deflate as zopfli_deflate;
|
pub use zopfli_oxipng::deflate as zopfli_deflate;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -24,19 +29,21 @@ pub enum Deflaters {
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
/// Use the better but slower Zopfli implementation
|
/// Use the better but slower Zopfli implementation
|
||||||
Zopfli {
|
Zopfli {
|
||||||
/// The number of compression iterations to do. 15 iterations are fine
|
/// Zopfli compression options
|
||||||
/// for small files, but bigger files will need to be compressed with
|
options: Options,
|
||||||
/// less iterations, or else they will be too slow.
|
|
||||||
iterations: NonZeroU8,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deflaters {
|
pub trait Deflater: Sync + Send {
|
||||||
pub(crate) fn deflate(self, data: &[u8], max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
fn deflate(&self, data: &[u8], max_size: &AtomicMin) -> PngResult<Vec<u8>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deflater for Deflaters {
|
||||||
|
fn deflate(&self, data: &[u8], max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
||||||
let compressed = match self {
|
let compressed = match self {
|
||||||
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
|
Self::Libdeflater { compression } => deflate(data, *compression, max_size)?,
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?,
|
Self::Zopfli { options } => zopfli_deflate(data, options)?,
|
||||||
};
|
};
|
||||||
if let Some(max) = max_size.get() {
|
if let Some(max) = max_size.get() {
|
||||||
if compressed.len() > max {
|
if compressed.len() > max {
|
||||||
|
|
@ -47,6 +54,75 @@ impl Deflaters {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct BufferedZopfliDeflater {
|
||||||
|
input_buffer_size: usize,
|
||||||
|
output_buffer_size: usize,
|
||||||
|
options: Options,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
|
impl BufferedZopfliDeflater {
|
||||||
|
pub const fn new(
|
||||||
|
input_buffer_size: usize,
|
||||||
|
output_buffer_size: usize,
|
||||||
|
options: Options,
|
||||||
|
) -> Self {
|
||||||
|
BufferedZopfliDeflater {
|
||||||
|
input_buffer_size,
|
||||||
|
output_buffer_size,
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
|
impl Default for BufferedZopfliDeflater {
|
||||||
|
fn default() -> Self {
|
||||||
|
BufferedZopfliDeflater {
|
||||||
|
input_buffer_size: 1024 * 1024,
|
||||||
|
output_buffer_size: 64 * 1024,
|
||||||
|
options: Options::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
|
impl Deflater for BufferedZopfliDeflater {
|
||||||
|
/// Fork of the zlib_compress function in Zopfli.
|
||||||
|
fn deflate(&self, data: &[u8], max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
||||||
|
let mut out = Cursor::new(Vec::with_capacity(self.output_buffer_size));
|
||||||
|
let cmf = 120; /* CM 8, CINFO 7. See zlib spec.*/
|
||||||
|
let flevel = 3;
|
||||||
|
let fdict = 0;
|
||||||
|
let mut cmfflg: u16 = 256 * cmf + fdict * 32 + flevel * 64;
|
||||||
|
let fcheck = 31 - cmfflg % 31;
|
||||||
|
cmfflg += fcheck;
|
||||||
|
|
||||||
|
let out = (|| -> io::Result<Vec<u8>> {
|
||||||
|
let mut rolling_adler = Adler32::new();
|
||||||
|
let mut in_data =
|
||||||
|
zopfli_oxipng::HashingAndCountingRead::new(data, &mut rolling_adler, None);
|
||||||
|
out.write_all(&cmfflg.to_be_bytes())?;
|
||||||
|
let mut buffer = BufWriter::with_capacity(
|
||||||
|
self.input_buffer_size,
|
||||||
|
DeflateEncoder::new(self.options, Default::default(), &mut out),
|
||||||
|
);
|
||||||
|
copy(&mut in_data, &mut buffer)?;
|
||||||
|
buffer.into_inner()?.finish()?;
|
||||||
|
out.write_all(&rolling_adler.finish().to_be_bytes())?;
|
||||||
|
Ok(out.into_inner())
|
||||||
|
})();
|
||||||
|
let out = out.map_err(|e| PngError::new(&e.to_string()))?;
|
||||||
|
if max_size.get().map(|max| max < out.len()).unwrap_or(false) {
|
||||||
|
Err(PngError::DeflatedDataTooLong(out.len()))
|
||||||
|
} else {
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for Deflaters {
|
impl Display for Deflaters {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,64 @@
|
||||||
use crate::{PngError, PngResult};
|
use crate::{PngError, PngResult};
|
||||||
use std::num::NonZeroU8;
|
use simd_adler32::Adler32;
|
||||||
|
use std::io::{Error, ErrorKind, Read};
|
||||||
|
|
||||||
pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult<Vec<u8>> {
|
pub fn deflate(data: &[u8], options: &zopfli::Options) -> PngResult<Vec<u8>> {
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
|
|
||||||
let mut output = Vec::with_capacity(max(1024, data.len() / 20));
|
let mut output = Vec::with_capacity(max(1024, data.len() / 20));
|
||||||
let options = zopfli::Options {
|
match zopfli::compress(options, &zopfli::Format::Zlib, data, &mut output) {
|
||||||
iteration_count: iterations,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
match zopfli::compress(&options, &zopfli::Format::Zlib, data, &mut output) {
|
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
|
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
|
||||||
};
|
};
|
||||||
output.shrink_to_fit();
|
output.shrink_to_fit();
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forked from zopfli crate
|
||||||
|
pub trait Hasher {
|
||||||
|
fn update(&mut self, data: &[u8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hasher for &mut Adler32 {
|
||||||
|
fn update(&mut self, data: &[u8]) {
|
||||||
|
Adler32::write(self, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reader that wraps another reader, a hasher and an optional counter,
|
||||||
|
/// updating the hasher state and incrementing a counter of bytes read so
|
||||||
|
/// far for each block of data read.
|
||||||
|
pub struct HashingAndCountingRead<'counter, R: Read, H: Hasher> {
|
||||||
|
inner: R,
|
||||||
|
hasher: H,
|
||||||
|
bytes_read: Option<&'counter mut u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'counter, R: Read, H: Hasher> HashingAndCountingRead<'counter, R, H> {
|
||||||
|
pub fn new(inner: R, hasher: H, bytes_read: Option<&'counter mut u32>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
hasher,
|
||||||
|
bytes_read,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read, H: Hasher> Read for HashingAndCountingRead<'_, R, H> {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
|
||||||
|
match self.inner.read(buf) {
|
||||||
|
Ok(bytes_read) => {
|
||||||
|
self.hasher.update(&buf[..bytes_read]);
|
||||||
|
|
||||||
|
if let Some(total_bytes_read) = &mut self.bytes_read {
|
||||||
|
**total_bytes_read = total_bytes_read
|
||||||
|
.checked_add(bytes_read.try_into().map_err(|_| ErrorKind::Other)?)
|
||||||
|
.ok_or(ErrorKind::Other)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bytes_read)
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
use crate::colors::{BitDepth, ColorType};
|
use crate::colors::{BitDepth, ColorType};
|
||||||
use crate::deflate::{crc32, inflate};
|
use crate::deflate::{crc32, inflate, Deflater};
|
||||||
use crate::error::PngError;
|
use crate::error::PngError;
|
||||||
use crate::interlace::Interlacing;
|
use crate::interlace::Interlacing;
|
||||||
use crate::AtomicMin;
|
use crate::AtomicMin;
|
||||||
use crate::Deflaters;
|
|
||||||
use crate::PngResult;
|
use crate::PngResult;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
@ -28,7 +27,7 @@ impl IhdrData {
|
||||||
/// Bits per pixel
|
/// Bits per pixel
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn bpp(&self) -> usize {
|
pub const fn bpp(&self) -> usize {
|
||||||
self.bit_depth as usize * self.color_type.channels_per_pixel() as usize
|
self.bit_depth as usize * self.color_type.channels_per_pixel() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,7 +38,7 @@ impl IhdrData {
|
||||||
let h = self.height as usize;
|
let h = self.height as usize;
|
||||||
let bpp = self.bpp();
|
let bpp = self.bpp();
|
||||||
|
|
||||||
fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
|
const fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
|
||||||
((w * bpp + 7) / 8) * h
|
((w * bpp + 7) / 8) * h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,7 +248,7 @@ pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct an iCCP chunk by compressing the ICC profile
|
/// Construct an iCCP chunk by compressing the ICC profile
|
||||||
pub fn construct_iccp(icc: &[u8], deflater: Deflaters) -> PngResult<Chunk> {
|
pub fn construct_iccp<T: Deflater>(icc: &[u8], deflater: &T) -> PngResult<Chunk> {
|
||||||
let mut compressed = deflater.deflate(icc, &AtomicMin::new(None))?;
|
let mut compressed = deflater.deflate(icc, &AtomicMin::new(None))?;
|
||||||
let mut data = Vec::with_capacity(compressed.len() + 5);
|
let mut data = Vec::with_capacity(compressed.len() + 5);
|
||||||
data.extend(b"icc"); // Profile name - generally unused, can be anything
|
data.extend(b"icc"); // Profile name - generally unused, can be anything
|
||||||
|
|
|
||||||
70
src/lib.rs
70
src/lib.rs
|
|
@ -30,7 +30,7 @@ use crate::headers::*;
|
||||||
use crate::png::PngData;
|
use crate::png::PngData;
|
||||||
use crate::png::PngImage;
|
use crate::png::PngImage;
|
||||||
use crate::reduction::*;
|
use crate::reduction::*;
|
||||||
use log::{debug, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
@ -42,6 +42,7 @@ use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
pub use crate::colors::{BitDepth, ColorType};
|
pub use crate::colors::{BitDepth, ColorType};
|
||||||
|
use crate::deflate::Deflater;
|
||||||
pub use crate::deflate::Deflaters;
|
pub use crate::deflate::Deflaters;
|
||||||
pub use crate::error::PngError;
|
pub use crate::error::PngError;
|
||||||
pub use crate::filters::RowFilter;
|
pub use crate::filters::RowFilter;
|
||||||
|
|
@ -249,7 +250,7 @@ impl Options {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_2(self) -> Self {
|
const fn apply_preset_2(self) -> Self {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -381,15 +382,19 @@ impl RawImage {
|
||||||
pub fn add_icc_profile(&mut self, data: &[u8]) {
|
pub fn add_icc_profile(&mut self, data: &[u8]) {
|
||||||
// Compress with fastest compression level - will be recompressed during optimization
|
// Compress with fastest compression level - will be recompressed during optimization
|
||||||
let deflater = Deflaters::Libdeflater { compression: 1 };
|
let deflater = Deflaters::Libdeflater { compression: 1 };
|
||||||
if let Ok(iccp) = construct_iccp(data, deflater) {
|
if let Ok(iccp) = construct_iccp(data, &deflater) {
|
||||||
self.aux_chunks.push(iccp);
|
self.aux_chunks.push(iccp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an optimized png from the raw image data using the options provided
|
/// Create an optimized png from the raw image data using the options provided
|
||||||
pub fn create_optimized_png(&self, opts: &Options) -> PngResult<Vec<u8>> {
|
pub fn create_optimized_png<T: Deflater>(
|
||||||
|
&self,
|
||||||
|
opts: &Options,
|
||||||
|
deflater: &T,
|
||||||
|
) -> PngResult<Vec<u8>> {
|
||||||
let deadline = Arc::new(Deadline::new(opts.timeout));
|
let deadline = Arc::new(Deadline::new(opts.timeout));
|
||||||
let mut png = optimize_raw(self.png.clone(), opts, deadline.clone(), None)
|
let mut png = optimize_raw(self.png.clone(), opts, deadline.clone(), None, deflater)
|
||||||
.ok_or_else(|| PngError::new("Failed to optimize input data"))?;
|
.ok_or_else(|| PngError::new("Failed to optimize input data"))?;
|
||||||
|
|
||||||
// Process aux chunks
|
// Process aux chunks
|
||||||
|
|
@ -399,7 +404,7 @@ impl RawImage {
|
||||||
.filter(|c| opts.strip.keep(&c.name))
|
.filter(|c| opts.strip.keep(&c.name))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
postprocess_chunks(&mut png, opts, deadline, &self.png.ihdr);
|
postprocess_chunks(&mut png, opts, deadline, &self.png.ihdr, deflater);
|
||||||
|
|
||||||
Ok(png.output())
|
Ok(png.output())
|
||||||
}
|
}
|
||||||
|
|
@ -512,7 +517,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
// force drop and thereby closing of file handle before modifying any timestamp
|
// force drop and thereby closing of file handle before modifying any timestamp
|
||||||
std::mem::drop(buffer);
|
drop(buffer);
|
||||||
if let Some(metadata_input) = &opt_metadata_preserved {
|
if let Some(metadata_input) = &opt_metadata_preserved {
|
||||||
copy_times(metadata_input, output_path)?;
|
copy_times(metadata_input, output_path)?;
|
||||||
}
|
}
|
||||||
|
|
@ -583,12 +588,18 @@ fn optimize_png(
|
||||||
} else {
|
} else {
|
||||||
Some(png.estimated_output_size())
|
Some(png.estimated_output_size())
|
||||||
};
|
};
|
||||||
if let Some(new_png) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
|
if let Some(new_png) = optimize_raw(
|
||||||
|
raw.clone(),
|
||||||
|
&opts,
|
||||||
|
deadline.clone(),
|
||||||
|
max_size,
|
||||||
|
&opts.deflate,
|
||||||
|
) {
|
||||||
png.raw = new_png.raw;
|
png.raw = new_png.raw;
|
||||||
png.idat_data = new_png.idat_data;
|
png.idat_data = new_png.idat_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
postprocess_chunks(png, &opts, deadline, &raw.ihdr);
|
postprocess_chunks(png, &opts, deadline, &raw.ihdr, &opts.deflate);
|
||||||
|
|
||||||
let output = png.output();
|
let output = png.output();
|
||||||
|
|
||||||
|
|
@ -628,11 +639,12 @@ fn optimize_png(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform optimization on the input image data using the options provided
|
/// Perform optimization on the input image data using the options provided
|
||||||
fn optimize_raw(
|
fn optimize_raw<T: Deflater>(
|
||||||
image: Arc<PngImage>,
|
image: Arc<PngImage>,
|
||||||
opts: &Options,
|
opts: &Options,
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
max_size: Option<usize>,
|
max_size: Option<usize>,
|
||||||
|
deflater: &T,
|
||||||
) -> Option<PngData> {
|
) -> Option<PngData> {
|
||||||
// Must use normal (lazy) compression, as faster ones (greedy) are not representative
|
// Must use normal (lazy) compression, as faster ones (greedy) are not representative
|
||||||
let eval_compression = 5;
|
let eval_compression = 5;
|
||||||
|
|
@ -683,17 +695,9 @@ fn optimize_raw(
|
||||||
// We should have a result here - fail if not (e.g. deadline passed)
|
// We should have a result here - fail if not (e.g. deadline passed)
|
||||||
let result = eval_result?;
|
let result = eval_result?;
|
||||||
|
|
||||||
match opts.deflate {
|
debug!("Trying: {}", result.filter);
|
||||||
Deflaters::Libdeflater { compression } if compression <= eval_compression => {
|
let best_size = AtomicMin::new(max_size);
|
||||||
// No further compression required
|
perform_trial(&result.filtered, opts, result.filter, &best_size, deflater)
|
||||||
Some((result.filter, result.idat_data))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
debug!("Trying: {}", result.filter);
|
|
||||||
let best_size = AtomicMin::new(max_size);
|
|
||||||
perform_trial(&result.filtered, opts, result.filter, &best_size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Perform full compression trials of selected filters and determine the best
|
// Perform full compression trials of selected filters and determine the best
|
||||||
|
|
||||||
|
|
@ -717,7 +721,7 @@ fn optimize_raw(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let filtered = &png.filter_image(filter, opts.optimize_alpha);
|
let filtered = &png.filter_image(filter, opts.optimize_alpha);
|
||||||
perform_trial(filtered, opts, filter, &best_size)
|
perform_trial(filtered, opts, filter, &best_size, deflater)
|
||||||
});
|
});
|
||||||
best.reduce_with(|i, j| {
|
best.reduce_with(|i, j| {
|
||||||
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
|
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
|
||||||
|
|
@ -769,13 +773,15 @@ fn optimize_raw(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a compression trial
|
/// Execute a compression trial
|
||||||
fn perform_trial(
|
fn perform_trial<T: Deflater>(
|
||||||
filtered: &[u8],
|
filtered: &[u8],
|
||||||
opts: &Options,
|
opts: &Options,
|
||||||
filter: RowFilter,
|
filter: RowFilter,
|
||||||
best_size: &AtomicMin,
|
best_size: &AtomicMin,
|
||||||
|
deflater: &T,
|
||||||
) -> Option<TrialResult> {
|
) -> Option<TrialResult> {
|
||||||
match opts.deflate.deflate(filtered, best_size) {
|
let result = deflater.deflate(filtered, best_size);
|
||||||
|
match result {
|
||||||
Ok(new_idat) => {
|
Ok(new_idat) => {
|
||||||
let bytes = new_idat.len();
|
let bytes = new_idat.len();
|
||||||
best_size.set_min(bytes);
|
best_size.set_min(bytes);
|
||||||
|
|
@ -796,7 +802,10 @@ fn perform_trial(
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(e) => {
|
||||||
|
error!("I/O error: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -858,12 +867,15 @@ fn report_format(prefix: &str, png: &PngImage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed
|
/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed
|
||||||
fn postprocess_chunks(
|
fn postprocess_chunks<T>(
|
||||||
png: &mut PngData,
|
png: &mut PngData,
|
||||||
opts: &Options,
|
opts: &Options,
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
orig_ihdr: &IhdrData,
|
orig_ihdr: &IhdrData,
|
||||||
) {
|
deflater: &T,
|
||||||
|
) where
|
||||||
|
T: Deflater,
|
||||||
|
{
|
||||||
if let Some(iccp_idx) = png.aux_chunks.iter().position(|c| &c.name == b"iCCP") {
|
if let Some(iccp_idx) = png.aux_chunks.iter().position(|c| &c.name == b"iCCP") {
|
||||||
// See if we can replace an iCCP chunk with an sRGB chunk
|
// See if we can replace an iCCP chunk with an sRGB chunk
|
||||||
let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB");
|
let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB");
|
||||||
|
|
@ -885,7 +897,7 @@ fn postprocess_chunks(
|
||||||
name: *b"sRGB",
|
name: *b"sRGB",
|
||||||
data: vec![intent],
|
data: vec![intent],
|
||||||
};
|
};
|
||||||
} else if let Ok(iccp) = construct_iccp(&icc, opts.deflate) {
|
} else if let Ok(iccp) = construct_iccp(&icc, deflater) {
|
||||||
let cur_len = png.aux_chunks[iccp_idx].data.len();
|
let cur_len = png.aux_chunks[iccp_idx].data.len();
|
||||||
let new_len = iccp.data.len();
|
let new_len = iccp.data.len();
|
||||||
if new_len < cur_len {
|
if new_len < cur_len {
|
||||||
|
|
@ -951,7 +963,7 @@ fn postprocess_chunks(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if an image was already optimized prior to oxipng's operations
|
/// Check if an image was already optimized prior to oxipng's operations
|
||||||
fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
|
const fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
|
||||||
original_size <= optimized_size && !opts.force
|
original_size <= optimized_size && !opts.force
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ use oxipng::RowFilter;
|
||||||
use oxipng::StripChunks;
|
use oxipng::StripChunks;
|
||||||
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::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -517,9 +515,10 @@ fn parse_opts_into_struct(
|
||||||
|
|
||||||
if matches.get_flag("zopfli") {
|
if matches.get_flag("zopfli") {
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
if let Some(iterations) = NonZeroU8::new(15) {
|
let zopfli_opts = zopfli::Options::default();
|
||||||
opts.deflate = Deflaters::Zopfli { iterations };
|
opts.deflate = Deflaters::Zopfli {
|
||||||
}
|
options: zopfli_opts,
|
||||||
|
};
|
||||||
} else if let Deflaters::Libdeflater { compression } = &mut opts.deflate {
|
} else if let Deflaters::Libdeflater { compression } = &mut opts.deflate {
|
||||||
if let Some(x) = matches.get_one::<i64>("compression") {
|
if let Some(x) = matches.get_one::<i64>("compression") {
|
||||||
*compression = *x as u8;
|
*compression = *x as u8;
|
||||||
|
|
|
||||||
|
|
@ -260,13 +260,13 @@ impl PngImage {
|
||||||
|
|
||||||
/// Return the number of channels in the image, based on color type
|
/// Return the number of channels in the image, based on color type
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn channels_per_pixel(&self) -> usize {
|
pub const fn channels_per_pixel(&self) -> usize {
|
||||||
self.ihdr.color_type.channels_per_pixel() as usize
|
self.ihdr.color_type.channels_per_pixel() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the number of bytes per channel in the image
|
/// Return the number of bytes per channel in the image
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn bytes_per_channel(&self) -> usize {
|
pub const fn bytes_per_channel(&self) -> usize {
|
||||||
match self.ihdr.bit_depth {
|
match self.ihdr.bit_depth {
|
||||||
BitDepth::Sixteen => 2,
|
BitDepth::Sixteen => 2,
|
||||||
// Depths lower than 8 will round up to 1 byte
|
// Depths lower than 8 will round up to 1 byte
|
||||||
|
|
@ -491,7 +491,7 @@ fn write_png_block(key: &[u8], chunk: &[u8], output: &mut Vec<u8>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Integer approximation for i * log2(i) - much faster than float calculations
|
// Integer approximation for i * log2(i) - much faster than float calculations
|
||||||
fn ilog2i(i: u32) -> u32 {
|
const fn ilog2i(i: u32) -> u32 {
|
||||||
let log = 32 - i.leading_zeros() - 1;
|
let log = 32 - i.leading_zeros() - 1;
|
||||||
i * log + ((i - (1 << log)) << 1)
|
i * log + ((i - (1 << log)) << 1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -678,7 +678,10 @@ fn zopfli_mode() {
|
||||||
let input = PathBuf::from("tests/files/zopfli_mode.png");
|
let input = PathBuf::from("tests/files/zopfli_mode.png");
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
opts.deflate = Deflaters::Zopfli {
|
opts.deflate = Deflaters::Zopfli {
|
||||||
iterations: NonZeroU8::new(15).unwrap(),
|
options: zopfli::Options {
|
||||||
|
iteration_count: NonZeroU8::new(15).unwrap(),
|
||||||
|
maximum_block_splits: 15,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ fn get_opts() -> Options {
|
||||||
fn test_it_converts(input: &str) {
|
fn test_it_converts(input: &str) {
|
||||||
let input = PathBuf::from(input);
|
let input = PathBuf::from(input);
|
||||||
let opts = get_opts();
|
let opts = get_opts();
|
||||||
|
let deflater = BufferedZopfliDeflater::default();
|
||||||
|
|
||||||
let original_data = PngData::read_file(&PathBuf::from(input)).unwrap();
|
let original_data = PngData::read_file(&PathBuf::from(input)).unwrap();
|
||||||
let image = PngData::from_slice(&original_data, &opts).unwrap();
|
let image = PngData::from_slice(&original_data, &opts).unwrap();
|
||||||
|
|
@ -35,7 +36,7 @@ fn test_it_converts(input: &str) {
|
||||||
raw.add_png_chunk(chunk.name, chunk.data);
|
raw.add_png_chunk(chunk.name, chunk.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = raw.create_optimized_png(&opts).unwrap();
|
let output = raw.create_optimized_png(&opts, &deflater).unwrap();
|
||||||
|
|
||||||
let new = PngData::from_slice(&output, &opts).unwrap();
|
let new = PngData::from_slice(&output, &opts).unwrap();
|
||||||
assert!(new.aux_chunks.len() == num_chunks);
|
assert!(new.aux_chunks.len() == num_chunks);
|
||||||
|
|
@ -52,6 +53,7 @@ fn from_file() {
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_indexed() {
|
fn custom_indexed() {
|
||||||
let opts = get_opts();
|
let opts = get_opts();
|
||||||
|
let deflater = BufferedZopfliDeflater::default();
|
||||||
|
|
||||||
let raw = RawImage::new(
|
let raw = RawImage::new(
|
||||||
4,
|
4,
|
||||||
|
|
@ -69,7 +71,7 @@ fn custom_indexed() {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
raw.create_optimized_png(&opts).unwrap();
|
raw.create_optimized_png(&opts, &deflater).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue