Refactor optimize_raw (#670)
Code always tends to get messy over time. I've found the `optimize_raw`
function increasingly harder to read, particularly after the addition of
fast mode, so I've taken some time to refactor and simplify it.
One change of note here is the main compression trials now use the
Evaluator. This means verbose output is a little different which is
shown below.
There is no change to performance or output size.
`-vvo2`: master
```
Processing: tests/files/rgba_8_should_be_palette_4.png
500x400 pixels, PNG format
8-bit RGB + Alpha, non-interlaced
IDAT size = 2757 bytes
File size = 18109 bytes
Eval: 4-bit Indexed (5 colors) None 1837 bytes
Eval: 8-bit Indexed (5 colors) None 1988 bytes
Eval: 4-bit Indexed (5 colors) Bigrams >1837 bytes
Eval: 8-bit Indexed (5 colors) Bigrams >1837 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Evaluating: 2 filters
Eval: 4-bit Indexed (5 colors) Sub >1810 bytes
Eval: 4-bit Indexed (5 colors) Entropy >1810 bytes
Trying: None
zc = 11 f = None 1583 bytes
Found better combination:
zc = 11 f = None 1583 bytes
IDAT size = 1583 bytes (1174 bytes decrease)
file size = 16962 bytes (1147 bytes = 6.33% decrease)
16962 bytes (6.33% smaller): Running in pretend mode, no output
```
`-vvo2`: PR
```
Processing: tests/files/rgba_8_should_be_palette_4.png
500x400 pixels, PNG format
8-bit RGB + Alpha, non-interlaced
IDAT size = 2757 bytes
File size = 18109 bytes
Eval: 4-bit Indexed (5 colors) None 1837 bytes
Eval: 8-bit Indexed (5 colors) None 1988 bytes
Eval: 4-bit Indexed (5 colors) Bigrams >1837 bytes
Eval: 8-bit Indexed (5 colors) Bigrams >1837 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Evaluating 2 filters
Eval: 4-bit Indexed (5 colors) Sub >1810 bytes
Eval: 4-bit Indexed (5 colors) Entropy >1810 bytes
Trying filter None with zc = 11
1610 bytes
Found better result:
zc = 11, f = None
IDAT size = 1583 bytes (1174 bytes decrease)
file size = 16962 bytes (1147 bytes = 6.33% decrease)
16962 bytes (6.33% smaller): Running in pretend mode, no output
```
`-vvZo5`: master
```
Processing: tests/files/rgba_8_should_be_palette_4.png
500x400 pixels, PNG format
8-bit RGB + Alpha, non-interlaced
IDAT size = 2757 bytes
File size = 18109 bytes
Eval: 8-bit Indexed (battiato sort) None 1821 bytes
Eval: 4-bit Indexed (5 colors) None 1657 bytes
Eval: 8-bit Indexed (mzeng sort) None 1821 bytes
Eval: 8-bit Indexed (5 colors) None 1821 bytes
Eval: 8-bit Indexed (battiato sort) Bigrams >1821 bytes
Eval: 4-bit Indexed (5 colors) Bigrams >1657 bytes
Eval: 8-bit Indexed (mzeng sort) Bigrams >1657 bytes
Eval: 8-bit Indexed (5 colors) Bigrams >1657 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Trying: 8 filters
zc = zopfli f = Brute 1562 bytes
zc = zopfli f = Sub >1562 bytes
zc = zopfli f = Bigrams >1562 bytes
zc = zopfli f = None 1407 bytes
zc = zopfli f = Up >1407 bytes
zc = zopfli f = MinSum >1407 bytes
zc = zopfli f = BigEnt >1407 bytes
zc = zopfli f = Entropy >1407 bytes
Found better combination:
zc = zopfli f = None 1407 bytes
IDAT size = 1407 bytes (1350 bytes decrease)
file size = 16786 bytes (1323 bytes = 7.31% decrease)
16786 bytes (7.31% smaller): Running in pretend mode, no output
```
`-vvZo5`: PR
```
Processing: tests/files/rgba_8_should_be_palette_4.png
500x400 pixels, PNG format
8-bit RGB + Alpha, non-interlaced
IDAT size = 2757 bytes
File size = 18109 bytes
Eval: 8-bit Indexed (battiato sort) None 1821 bytes
Eval: 4-bit Indexed (5 colors) None 1657 bytes
Eval: 8-bit Indexed (mzeng sort) None 1821 bytes
Eval: 8-bit Indexed (5 colors) None 1821 bytes
Eval: 8-bit Indexed (battiato sort) Bigrams >1657 bytes
Eval: 4-bit Indexed (5 colors) Bigrams >1657 bytes
Eval: 8-bit Indexed (mzeng sort) Bigrams >1657 bytes
Eval: 8-bit Indexed (5 colors) Bigrams >1657 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Trying 8 filters with zopfli, zi = 15
Eval: 4-bit Indexed (5 colors) Brute 1589 bytes
Eval: 4-bit Indexed (5 colors) Bigrams 1641 bytes
Eval: 4-bit Indexed (5 colors) Sub 1711 bytes
Eval: 4-bit Indexed (5 colors) None 1434 bytes
Eval: 4-bit Indexed (5 colors) Up 1764 bytes
Eval: 4-bit Indexed (5 colors) MinSum 1760 bytes
Eval: 4-bit Indexed (5 colors) BigEnt 1742 bytes
Eval: 4-bit Indexed (5 colors) Entropy 1748 bytes
Found better result:
zopfli, zi = 15, f = None
IDAT size = 1407 bytes (1350 bytes decrease)
file size = 16786 bytes (1323 bytes = 7.31% decrease)
16786 bytes (7.31% smaller): Running in pretend mode, no output
```
This commit is contained in:
parent
8a44cdbc84
commit
bbde68ddce
10 changed files with 192 additions and 258 deletions
|
|
@ -13,10 +13,7 @@ fn deflate_16_bits(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();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -24,10 +21,7 @@ fn deflate_8_bits(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();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -37,10 +31,7 @@ fn deflate_4_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -50,10 +41,7 @@ fn deflate_2_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -63,10 +51,7 @@ fn deflate_1_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,6 @@ impl AtomicMin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unset value is `usize_max`
|
|
||||||
pub const fn as_atomic_usize(&self) -> &AtomicUsize {
|
|
||||||
&self.val
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try a new value, returning true if it is the new minimum
|
/// Try a new value, returning true if it is the new minimum
|
||||||
pub fn set_min(&self, new_val: usize) -> bool {
|
pub fn set_min(&self, new_val: usize) -> bool {
|
||||||
new_val < self.val.fetch_min(new_val, SeqCst)
|
new_val < self.val.fetch_min(new_val, SeqCst)
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,11 @@ impl Display for ColorType {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Grayscale { .. } => Display::fmt("Grayscale", f),
|
Self::Grayscale { .. } => write!(f, "Grayscale"),
|
||||||
Self::RGB { .. } => Display::fmt("RGB", f),
|
Self::RGB { .. } => write!(f, "RGB"),
|
||||||
Self::Indexed { palette } => {
|
Self::Indexed { palette } => write!(f, "Indexed ({} colors)", palette.len()),
|
||||||
Display::fmt(&format!("Indexed ({} colors)", palette.len()), f)
|
Self::GrayscaleAlpha => write!(f, "Grayscale + Alpha"),
|
||||||
}
|
Self::RGBA => write!(f, "RGB + Alpha"),
|
||||||
Self::GrayscaleAlpha => Display::fmt("Grayscale + Alpha", f),
|
|
||||||
Self::RGBA => Display::fmt("RGB + Alpha", f),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
use libdeflater::*;
|
use libdeflater::*;
|
||||||
|
|
||||||
use crate::{atomicmin::AtomicMin, PngError, PngResult};
|
use crate::{PngError, PngResult};
|
||||||
|
|
||||||
pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
pub fn deflate(data: &[u8], level: u8, max_size: Option<usize>) -> PngResult<Vec<u8>> {
|
||||||
let mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap());
|
let mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap());
|
||||||
let capacity = max_size
|
let capacity = max_size.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
|
||||||
.get()
|
|
||||||
.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
|
|
||||||
let mut dest = vec![0; capacity];
|
let mut dest = vec![0; capacity];
|
||||||
let len = compressor
|
let len = compressor
|
||||||
.zlib_compress(data, &mut dest)
|
.zlib_compress(data, &mut dest)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::{fmt, fmt::Display};
|
||||||
|
|
||||||
pub use deflater::{crc32, deflate, inflate};
|
pub use deflater::{crc32, deflate, inflate};
|
||||||
|
|
||||||
use crate::{AtomicMin, PngError, PngResult};
|
use crate::{PngError, PngResult};
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
mod zopfli_oxipng;
|
mod zopfli_oxipng;
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
|
|
@ -30,13 +30,13 @@ pub enum Deflaters {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deflaters {
|
impl Deflaters {
|
||||||
pub(crate) fn deflate(self, data: &[u8], max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> 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 { iterations } => zopfli_deflate(data, iterations)?,
|
||||||
};
|
};
|
||||||
if let Some(max) = max_size.get() {
|
if let Some(max) = max_size {
|
||||||
if compressed.len() > max {
|
if compressed.len() > max {
|
||||||
return Err(PngError::DeflatedDataTooLong(max));
|
return Err(PngError::DeflatedDataTooLong(max));
|
||||||
}
|
}
|
||||||
|
|
@ -49,9 +49,9 @@ impl Display for Deflaters {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Libdeflater { compression } => Display::fmt(compression, f),
|
Self::Libdeflater { compression } => write!(f, "zc = {compression}"),
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
Self::Zopfli { .. } => Display::fmt("zopfli", f),
|
Self::Zopfli { iterations } => write!(f, "zopfli, zi = {iterations}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use std::sync::{
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||||
|
use deflate::Deflaters;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
@ -28,9 +29,15 @@ pub(crate) struct Candidate {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Candidate {
|
impl Candidate {
|
||||||
|
/// Return an estimate of the output size which can help with evaluation of very small data
|
||||||
|
#[must_use]
|
||||||
|
pub fn estimated_output_size(&self) -> usize {
|
||||||
|
self.idat_data.len() + self.image.key_chunks_size()
|
||||||
|
}
|
||||||
|
|
||||||
fn cmp_key(&self) -> impl Ord {
|
fn cmp_key(&self) -> impl Ord {
|
||||||
(
|
(
|
||||||
self.idat_data.len() + self.image.key_chunks_size(),
|
self.estimated_output_size(),
|
||||||
self.image.data.len(),
|
self.image.data.len(),
|
||||||
self.filter,
|
self.filter,
|
||||||
// Prefer the later image added (e.g. baseline, which is always added last)
|
// Prefer the later image added (e.g. baseline, which is always added last)
|
||||||
|
|
@ -43,7 +50,7 @@ impl Candidate {
|
||||||
pub(crate) struct Evaluator {
|
pub(crate) struct Evaluator {
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
filters: IndexSet<RowFilter>,
|
filters: IndexSet<RowFilter>,
|
||||||
compression: u8,
|
deflater: Deflaters,
|
||||||
optimize_alpha: bool,
|
optimize_alpha: bool,
|
||||||
nth: AtomicUsize,
|
nth: AtomicUsize,
|
||||||
executed: Arc<AtomicUsize>,
|
executed: Arc<AtomicUsize>,
|
||||||
|
|
@ -60,7 +67,7 @@ impl Evaluator {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
filters: IndexSet<RowFilter>,
|
filters: IndexSet<RowFilter>,
|
||||||
compression: u8,
|
deflater: Deflaters,
|
||||||
optimize_alpha: bool,
|
optimize_alpha: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
|
|
@ -68,7 +75,7 @@ impl Evaluator {
|
||||||
Self {
|
Self {
|
||||||
deadline,
|
deadline,
|
||||||
filters,
|
filters,
|
||||||
compression,
|
deflater,
|
||||||
optimize_alpha,
|
optimize_alpha,
|
||||||
nth: AtomicUsize::new(0),
|
nth: AtomicUsize::new(0),
|
||||||
executed: Arc::new(AtomicUsize::new(0)),
|
executed: Arc::new(AtomicUsize::new(0)),
|
||||||
|
|
@ -118,7 +125,7 @@ impl Evaluator {
|
||||||
// These clones are only cheap refcounts
|
// These clones are only cheap refcounts
|
||||||
let deadline = self.deadline.clone();
|
let deadline = self.deadline.clone();
|
||||||
let filters = self.filters.clone();
|
let filters = self.filters.clone();
|
||||||
let compression = self.compression;
|
let deflater = self.deflater;
|
||||||
let optimize_alpha = self.optimize_alpha;
|
let optimize_alpha = self.optimize_alpha;
|
||||||
let executed = self.executed.clone();
|
let executed = self.executed.clone();
|
||||||
let best_candidate_size = self.best_candidate_size.clone();
|
let best_candidate_size = self.best_candidate_size.clone();
|
||||||
|
|
@ -140,9 +147,16 @@ impl Evaluator {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let filtered = image.filter_image(filter, optimize_alpha);
|
let filtered = image.filter_image(filter, optimize_alpha);
|
||||||
let idat_data = deflate::deflate(&filtered, compression, &best_candidate_size);
|
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
|
||||||
if let Ok(idat_data) = idat_data {
|
if let Ok(idat_data) = idat_data {
|
||||||
let size = idat_data.len() + image.key_chunks_size();
|
let new = Candidate {
|
||||||
|
image: image.clone(),
|
||||||
|
idat_data,
|
||||||
|
filtered,
|
||||||
|
filter,
|
||||||
|
nth,
|
||||||
|
};
|
||||||
|
let size = new.estimated_output_size();
|
||||||
best_candidate_size.set_min(size);
|
best_candidate_size.set_min(size);
|
||||||
trace!(
|
trace!(
|
||||||
"Eval: {}-bit {:23} {:8} {} bytes",
|
"Eval: {}-bit {:23} {:8} {} bytes",
|
||||||
|
|
@ -151,13 +165,6 @@ impl Evaluator {
|
||||||
filter,
|
filter,
|
||||||
size
|
size
|
||||||
);
|
);
|
||||||
let new = Candidate {
|
|
||||||
image: image.clone(),
|
|
||||||
idat_data,
|
|
||||||
filtered,
|
|
||||||
filter,
|
|
||||||
nth,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
display_chunks::DISPLAY_CHUNKS,
|
display_chunks::DISPLAY_CHUNKS,
|
||||||
error::PngError,
|
error::PngError,
|
||||||
interlace::Interlacing,
|
interlace::Interlacing,
|
||||||
AtomicMin, Deflaters, PngResult,
|
Deflaters, PngResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -275,9 +275,9 @@ pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct an iCCP chunk by compressing the ICC profile
|
/// Make an iCCP chunk by compressing the ICC profile
|
||||||
pub fn construct_iccp(icc: &[u8], deflater: Deflaters) -> PngResult<Chunk> {
|
pub fn make_iccp(icc: &[u8], deflater: Deflaters, max_size: Option<usize>) -> PngResult<Chunk> {
|
||||||
let mut compressed = deflater.deflate(icc, &AtomicMin::new(None))?;
|
let mut compressed = deflater.deflate(icc, max_size)?;
|
||||||
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
|
||||||
data.extend([0, 0]); // Null separator, zlib compression method
|
data.extend([0, 0]); // Null separator, zlib compression method
|
||||||
|
|
|
||||||
338
src/lib.rs
338
src/lib.rs
|
|
@ -42,13 +42,6 @@ use log::{debug, info, trace, warn};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
pub use rgb::{RGB16, RGBA8};
|
pub use rgb::{RGB16, RGBA8};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
atomicmin::AtomicMin,
|
|
||||||
evaluate::Evaluator,
|
|
||||||
headers::*,
|
|
||||||
png::{PngData, PngImage},
|
|
||||||
reduction::*,
|
|
||||||
};
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
deflate::Deflaters,
|
deflate::Deflaters,
|
||||||
|
|
@ -58,6 +51,12 @@ pub use crate::{
|
||||||
interlace::Interlacing,
|
interlace::Interlacing,
|
||||||
options::{InFile, Options, OutFile},
|
options::{InFile, Options, OutFile},
|
||||||
};
|
};
|
||||||
|
use crate::{
|
||||||
|
evaluate::{Candidate, Evaluator},
|
||||||
|
headers::*,
|
||||||
|
png::{PngData, PngImage},
|
||||||
|
reduction::*,
|
||||||
|
};
|
||||||
|
|
||||||
mod apng;
|
mod apng;
|
||||||
mod atomicmin;
|
mod atomicmin;
|
||||||
|
|
@ -80,7 +79,7 @@ mod sanity_checks;
|
||||||
pub mod internal_tests {
|
pub mod internal_tests {
|
||||||
#[cfg(feature = "sanity-checks")]
|
#[cfg(feature = "sanity-checks")]
|
||||||
pub use crate::sanity_checks::*;
|
pub use crate::sanity_checks::*;
|
||||||
pub use crate::{atomicmin::*, deflate::*, png::*, reduction::*};
|
pub use crate::{deflate::*, png::*, reduction::*};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PngResult<T> = Result<T, PngError>;
|
pub type PngResult<T> = Result<T, PngError>;
|
||||||
|
|
@ -149,7 +148,7 @@ 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) = make_iccp(data, deflater, None) {
|
||||||
self.aux_chunks.push(iccp);
|
self.aux_chunks.push(iccp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,16 +156,21 @@ impl RawImage {
|
||||||
/// 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(&self, opts: &Options) -> 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 Some(result) = optimize_raw(self.png.clone(), opts, deadline, None) else {
|
||||||
.ok_or_else(|| PngError::new("Failed to optimize input data"))?;
|
return Err(PngError::new("Failed to optimize input data"));
|
||||||
|
};
|
||||||
|
|
||||||
// Process aux chunks
|
let mut png = PngData {
|
||||||
png.aux_chunks = self
|
raw: result.image,
|
||||||
.aux_chunks
|
idat_data: result.idat_data,
|
||||||
.iter()
|
aux_chunks: self
|
||||||
.filter(|c| opts.strip.keep(&c.name))
|
.aux_chunks
|
||||||
.cloned()
|
.iter()
|
||||||
.collect();
|
.filter(|c| opts.strip.keep(&c.name))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
frames: Vec::new(),
|
||||||
|
};
|
||||||
postprocess_chunks(&mut png, opts, &self.png.ihdr);
|
postprocess_chunks(&mut png, opts, &self.png.ihdr);
|
||||||
|
|
||||||
Ok(png.output())
|
Ok(png.output())
|
||||||
|
|
@ -323,8 +327,6 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult<Vec<u8>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrialResult = (RowFilter, Vec<u8>);
|
|
||||||
|
|
||||||
/// Perform optimization on the input PNG object using the options provided
|
/// Perform optimization on the input PNG object using the options provided
|
||||||
fn optimize_png(
|
fn optimize_png(
|
||||||
png: &mut PngData,
|
png: &mut PngData,
|
||||||
|
|
@ -362,14 +364,13 @@ 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(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
|
||||||
png.raw = new_png.raw;
|
png.raw = result.image;
|
||||||
png.idat_data = new_png.idat_data;
|
png.idat_data = result.idat_data;
|
||||||
png.filter = new_png.filter;
|
recompress_frames(png, &opts, deadline, result.filter)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
postprocess_chunks(png, &opts, &raw.ihdr);
|
postprocess_chunks(png, &opts, &raw.ihdr);
|
||||||
recompress_frames(png, &opts, deadline)?;
|
|
||||||
|
|
||||||
let output = png.output();
|
let output = png.output();
|
||||||
|
|
||||||
|
|
@ -418,19 +419,20 @@ fn optimize_raw(
|
||||||
opts: &Options,
|
opts: &Options,
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
max_size: Option<usize>,
|
max_size: Option<usize>,
|
||||||
) -> Option<PngData> {
|
) -> Option<Candidate> {
|
||||||
// Libdeflate has four algorithms: 0 = 'uncompressed', 1-4 = 'greedy', 5-7 = 'lazy', 8-9 = 'lazy2', 10-12 = 'near-optimal'
|
// Libdeflate has four algorithms: 0 = 'uncompressed', 1-4 = 'greedy', 5-7 = 'lazy', 8-9 = 'lazy2', 10-12 = 'near-optimal'
|
||||||
// 5 is the minimumm required for a decent evaluation result
|
// 5 is the minimumm required for a decent evaluation result
|
||||||
// 7 is not noticeably slower than 5 and improves evaluation of filters in 'fast' mode (o2 and lower)
|
// 7 is not noticeably slower than 5 and improves evaluation of filters in 'fast' mode (o2 and lower)
|
||||||
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
|
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
|
||||||
// 9 is not appreciably better than 8
|
// 9 is not appreciably better than 8
|
||||||
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
|
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
|
||||||
let eval_compression = match opts.deflate {
|
let compression = match opts.deflate {
|
||||||
Deflaters::Libdeflater { compression } => {
|
Deflaters::Libdeflater { compression } => {
|
||||||
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
|
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
|
||||||
}
|
}
|
||||||
_ => 8,
|
_ => 8,
|
||||||
};
|
};
|
||||||
|
let eval_deflater = Deflaters::Libdeflater { compression };
|
||||||
// If only one filter is selected, use this for evaluations
|
// If only one filter is selected, use this for evaluations
|
||||||
let eval_filters = if opts.filter.len() == 1 {
|
let eval_filters = if opts.filter.len() == 1 {
|
||||||
opts.filter.clone()
|
opts.filter.clone()
|
||||||
|
|
@ -439,169 +441,124 @@ fn optimize_raw(
|
||||||
indexset! {RowFilter::None, RowFilter::Bigrams}
|
indexset! {RowFilter::None, RowFilter::Bigrams}
|
||||||
};
|
};
|
||||||
// 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
|
||||||
let eval = Evaluator::new(
|
let eval = Evaluator::new(deadline.clone(), eval_filters.clone(), eval_deflater, false);
|
||||||
deadline.clone(),
|
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
|
||||||
eval_filters.clone(),
|
let eval_result = eval.get_best_candidate();
|
||||||
eval_compression,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
let mut png = perform_reductions(image.clone(), opts, &deadline, &eval);
|
|
||||||
let mut eval_result = eval.get_best_candidate();
|
|
||||||
if let Some(ref result) = eval_result {
|
if let Some(ref result) = eval_result {
|
||||||
png = result.image.clone();
|
new_image = result.image.clone();
|
||||||
}
|
}
|
||||||
let reduction_occurred = png.ihdr.color_type != image.ihdr.color_type
|
let reduction_occurred = new_image.ihdr.color_type != image.ihdr.color_type
|
||||||
|| png.ihdr.bit_depth != image.ihdr.bit_depth
|
|| new_image.ihdr.bit_depth != image.ihdr.bit_depth
|
||||||
|| png.ihdr.interlaced != image.ihdr.interlaced;
|
|| new_image.ihdr.interlaced != image.ihdr.interlaced;
|
||||||
|
|
||||||
if reduction_occurred {
|
if reduction_occurred {
|
||||||
report_format("Transformed image to ", &png);
|
report_format("Transformed image to ", &new_image);
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.idat_recoding || reduction_occurred {
|
let (result, deflater) = if opts.idat_recoding || reduction_occurred {
|
||||||
let mut filters = opts.filter.clone();
|
let result = perform_trials(
|
||||||
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
|
new_image.clone(),
|
||||||
let best: Option<TrialResult> = if fast_eval {
|
opts,
|
||||||
// Perform a fast evaluation of selected filters followed by a single main compression trial
|
deadline.clone(),
|
||||||
|
max_size,
|
||||||
if eval_result.is_some() {
|
eval_result,
|
||||||
// Some filters have already been evaluated, we don't need to try them again
|
eval_filters,
|
||||||
filters = filters.difference(&eval_filters).copied().collect();
|
eval_deflater,
|
||||||
}
|
);
|
||||||
|
(result?, opts.deflate)
|
||||||
if !filters.is_empty() {
|
} else {
|
||||||
trace!("Evaluating: {} filters", filters.len());
|
|
||||||
let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha);
|
|
||||||
if let Some(ref result) = eval_result {
|
|
||||||
eval.set_best_size(result.idat_data.len());
|
|
||||||
}
|
|
||||||
eval.try_image(png.clone());
|
|
||||||
if let Some(result) = eval.get_best_candidate() {
|
|
||||||
eval_result = Some(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We should have a result here - fail if not (e.g. deadline passed)
|
|
||||||
let result = eval_result?;
|
|
||||||
|
|
||||||
match opts.deflate {
|
|
||||||
Deflaters::Libdeflater { compression } if compression <= eval_compression => {
|
|
||||||
// No further compression required
|
|
||||||
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 {
|
|
||||||
// Perform full compression trials of selected filters and determine the best
|
|
||||||
|
|
||||||
if filters.is_empty() {
|
|
||||||
// Pick a filter automatically
|
|
||||||
if png.ihdr.bit_depth as u8 >= 8 {
|
|
||||||
// Bigrams is the best all-rounder when there's at least one byte per pixel
|
|
||||||
filters.insert(RowFilter::Bigrams);
|
|
||||||
} else {
|
|
||||||
// Otherwise delta filters generally don't work well, so just stick with None
|
|
||||||
filters.insert(RowFilter::None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Trying: {} filters", filters.len());
|
|
||||||
|
|
||||||
let best_size = AtomicMin::new(max_size);
|
|
||||||
let results_iter = filters.into_par_iter().with_max_len(1);
|
|
||||||
let best = results_iter.filter_map(|filter| {
|
|
||||||
if deadline.passed() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let filtered = &png.filter_image(filter, opts.optimize_alpha);
|
|
||||||
perform_trial(filtered, opts, filter, &best_size)
|
|
||||||
});
|
|
||||||
best.reduce_with(|i, j| {
|
|
||||||
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
|
|
||||||
i
|
|
||||||
} else {
|
|
||||||
j
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((filter, idat_data)) = best {
|
|
||||||
let image = PngData {
|
|
||||||
raw: png,
|
|
||||||
idat_data,
|
|
||||||
aux_chunks: Vec::new(),
|
|
||||||
frames: Vec::new(),
|
|
||||||
filter: Some(filter),
|
|
||||||
};
|
|
||||||
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
|
||||||
debug!("Found better combination:");
|
|
||||||
debug!(
|
|
||||||
" zc = {} f = {:8} {} bytes",
|
|
||||||
opts.deflate,
|
|
||||||
filter,
|
|
||||||
image.idat_data.len()
|
|
||||||
);
|
|
||||||
return Some(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Some(result) = eval_result {
|
|
||||||
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
|
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
|
||||||
// we should still check if the evaluator compressed the baseline smaller than the original.
|
// we should still check if the evaluator compressed the baseline smaller than the original.
|
||||||
let image = PngData {
|
(eval_result?, eval_deflater)
|
||||||
raw: result.image,
|
};
|
||||||
idat_data: result.idat_data,
|
|
||||||
aux_chunks: Vec::new(),
|
|
||||||
frames: Vec::new(),
|
|
||||||
filter: Some(result.filter),
|
|
||||||
};
|
|
||||||
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
|
||||||
debug!("Found better combination:");
|
|
||||||
debug!(
|
|
||||||
" zc = {} f = {:8} {} bytes",
|
|
||||||
eval_compression,
|
|
||||||
result.filter,
|
|
||||||
image.idat_data.len()
|
|
||||||
);
|
|
||||||
return Some(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if max_size.map_or(true, |max_size| result.estimated_output_size() < max_size) {
|
||||||
|
debug!("Found better result:");
|
||||||
|
debug!(" {}, f = {}", deflater, result.filter);
|
||||||
|
return Some(result);
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a compression trial
|
/// Perform compression trials
|
||||||
fn perform_trial(
|
fn perform_trials(
|
||||||
filtered: &[u8],
|
image: Arc<PngImage>,
|
||||||
opts: &Options,
|
opts: &Options,
|
||||||
filter: RowFilter,
|
deadline: Arc<Deadline>,
|
||||||
best_size: &AtomicMin,
|
max_size: Option<usize>,
|
||||||
) -> Option<TrialResult> {
|
mut eval_result: Option<Candidate>,
|
||||||
match opts.deflate.deflate(filtered, best_size) {
|
eval_filters: IndexSet<RowFilter>,
|
||||||
Ok(new_idat) => {
|
eval_deflater: Deflaters,
|
||||||
let bytes = new_idat.len();
|
) -> Option<Candidate> {
|
||||||
best_size.set_min(bytes);
|
let mut filters = opts.filter.clone();
|
||||||
trace!(
|
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
|
||||||
" zc = {} f = {:8} {} bytes",
|
if fast_eval {
|
||||||
opts.deflate,
|
// Perform a fast evaluation of selected filters followed by a single main compression trial
|
||||||
filter,
|
|
||||||
bytes
|
if eval_result.is_some() {
|
||||||
);
|
// Some filters have already been evaluated, we don't need to try them again
|
||||||
Some((filter, new_idat))
|
filters = filters.difference(&eval_filters).copied().collect();
|
||||||
}
|
}
|
||||||
Err(PngError::DeflatedDataTooLong(bytes)) => {
|
|
||||||
trace!(
|
if !filters.is_empty() {
|
||||||
" zc = {} f = {:8} >{} bytes",
|
trace!("Evaluating {} filters", filters.len());
|
||||||
opts.deflate,
|
let eval = Evaluator::new(
|
||||||
filter,
|
deadline.clone(),
|
||||||
bytes,
|
filters,
|
||||||
|
eval_deflater,
|
||||||
|
opts.optimize_alpha,
|
||||||
);
|
);
|
||||||
None
|
if let Some(result) = &eval_result {
|
||||||
|
eval.set_best_size(result.idat_data.len());
|
||||||
|
}
|
||||||
|
eval.try_image(image.clone());
|
||||||
|
if let Some(result) = eval.get_best_candidate() {
|
||||||
|
eval_result = Some(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
if opts.deflate == eval_deflater {
|
||||||
|
// No further compression required
|
||||||
|
return eval_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have a result here - fail if not (e.g. deadline passed)
|
||||||
|
let mut result = eval_result?;
|
||||||
|
|
||||||
|
// Recompress with the main deflater
|
||||||
|
debug!("Trying filter {} with {}", result.filter, opts.deflate);
|
||||||
|
match opts.deflate.deflate(&result.filtered, max_size) {
|
||||||
|
Ok(idat_data) => {
|
||||||
|
result.idat_data = idat_data;
|
||||||
|
trace!("{} bytes", result.estimated_output_size());
|
||||||
|
}
|
||||||
|
Err(PngError::DeflatedDataTooLong(bytes)) => {
|
||||||
|
trace!(">{bytes} bytes");
|
||||||
|
}
|
||||||
|
Err(_) => (),
|
||||||
|
};
|
||||||
|
return Some(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Perform full compression trials of selected filters and determine the best
|
||||||
|
|
||||||
|
if filters.is_empty() {
|
||||||
|
// Pick a filter automatically
|
||||||
|
if image.ihdr.bit_depth as u8 >= 8 {
|
||||||
|
// Bigrams is the best all-rounder when there's at least one byte per pixel
|
||||||
|
filters.insert(RowFilter::Bigrams);
|
||||||
|
} else {
|
||||||
|
// Otherwise delta filters generally don't work well, so just stick with None
|
||||||
|
filters.insert(RowFilter::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Trying {} filters with {}", filters.len(), opts.deflate);
|
||||||
|
let eval = Evaluator::new(deadline, filters, opts.deflate, opts.optimize_alpha);
|
||||||
|
if let Some(max_size) = max_size {
|
||||||
|
eval.set_best_size(max_size);
|
||||||
|
}
|
||||||
|
eval.try_image(image);
|
||||||
|
eval.get_best_candidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -686,17 +643,14 @@ fn postprocess_chunks(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData) {
|
||||||
};
|
};
|
||||||
} else if opts.idat_recoding {
|
} else if opts.idat_recoding {
|
||||||
// Try recompressing the profile
|
// Try recompressing the profile
|
||||||
if let Ok(iccp) = construct_iccp(&icc, opts.deflate) {
|
let cur_len = png.aux_chunks[iccp_idx].data.len();
|
||||||
let cur_len = png.aux_chunks[iccp_idx].data.len();
|
if let Ok(iccp) = make_iccp(&icc, opts.deflate, Some(cur_len - 1)) {
|
||||||
let new_len = iccp.data.len();
|
debug!(
|
||||||
if new_len < cur_len {
|
"Recompressed iCCP chunk: {} ({} bytes decrease)",
|
||||||
debug!(
|
iccp.data.len(),
|
||||||
"Recompressed iCCP chunk: {} ({} bytes decrease)",
|
cur_len - iccp.data.len()
|
||||||
new_len,
|
);
|
||||||
cur_len - new_len
|
png.aux_chunks[iccp_idx] = iccp;
|
||||||
);
|
|
||||||
png.aux_chunks[iccp_idx] = iccp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -721,15 +675,15 @@ fn postprocess_chunks(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recompress the additional frames of an APNG
|
/// Recompress the additional frames of an APNG
|
||||||
fn recompress_frames(png: &mut PngData, opts: &Options, deadline: Arc<Deadline>) -> PngResult<()> {
|
fn recompress_frames(
|
||||||
|
png: &mut PngData,
|
||||||
|
opts: &Options,
|
||||||
|
deadline: Arc<Deadline>,
|
||||||
|
filter: RowFilter,
|
||||||
|
) -> PngResult<()> {
|
||||||
if !opts.idat_recoding || png.frames.is_empty() {
|
if !opts.idat_recoding || png.frames.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// Use the same filter chosen for the main image
|
|
||||||
// No filter means we failed to optimise the main image and we shouldn't bother trying here
|
|
||||||
let Some(filter) = png.filter else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
png.frames
|
png.frames
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.with_max_len(1)
|
.with_max_len(1)
|
||||||
|
|
@ -743,8 +697,8 @@ fn recompress_frames(png: &mut PngData, opts: &Options, deadline: Arc<Deadline>)
|
||||||
ihdr.height = frame.height;
|
ihdr.height = frame.height;
|
||||||
let image = PngImage::new(ihdr, &frame.data)?;
|
let image = PngImage::new(ihdr, &frame.data)?;
|
||||||
let filtered = image.filter_image(filter, opts.optimize_alpha);
|
let filtered = image.filter_image(filter, opts.optimize_alpha);
|
||||||
let max_size = AtomicMin::new(Some(frame.data.len() - 1));
|
let max_size = Some(frame.data.len() - 1);
|
||||||
if let Ok(data) = opts.deflate.deflate(&filtered, &max_size) {
|
if let Ok(data) = opts.deflate.deflate(&filtered, max_size) {
|
||||||
debug!(
|
debug!(
|
||||||
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
|
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
|
||||||
i,
|
i,
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,6 @@ pub struct PngData {
|
||||||
pub aux_chunks: Vec<Chunk>,
|
pub aux_chunks: Vec<Chunk>,
|
||||||
/// APNG frames
|
/// APNG frames
|
||||||
pub frames: Vec<Frame>,
|
pub frames: Vec<Frame>,
|
||||||
/// The filter strategy applied to the idat_data (initially unknown)
|
|
||||||
pub filter: Option<RowFilter>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PngData {
|
impl PngData {
|
||||||
|
|
@ -184,7 +182,6 @@ impl PngData {
|
||||||
raw: Arc::new(raw),
|
raw: Arc::new(raw),
|
||||||
aux_chunks,
|
aux_chunks,
|
||||||
frames,
|
frames,
|
||||||
filter: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,9 +181,9 @@ fn verbose_mode() {
|
||||||
" 8-bit RGB, non-interlaced",
|
" 8-bit RGB, non-interlaced",
|
||||||
" IDAT size = 113794 bytes",
|
" IDAT size = 113794 bytes",
|
||||||
" File size = 114708 bytes",
|
" File size = 114708 bytes",
|
||||||
"Trying: 1 filters",
|
"Trying 1 filters with zc = ",
|
||||||
"Found better combination:",
|
"Found better result:",
|
||||||
" zc = 11 f = None ",
|
" zc = 11, f = None",
|
||||||
" IDAT size = ",
|
" IDAT size = ",
|
||||||
" file size = ",
|
" file size = ",
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue