oxipng/src/evaluate.rs
andrews05 bbde68ddce
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
```
2025-01-29 20:53:11 +01:00

193 lines
6.8 KiB
Rust

//! Check if a reduction makes file smaller, and keep best reductions.
//! Works asynchronously when possible
#[cfg(not(feature = "parallel"))]
use std::cell::RefCell;
use std::sync::{
atomic::{AtomicUsize, Ordering::*},
Arc,
};
#[cfg(feature = "parallel")]
use crossbeam_channel::{unbounded, Receiver, Sender};
use deflate::Deflaters;
use indexmap::IndexSet;
use log::trace;
use rayon::prelude::*;
#[cfg(not(feature = "parallel"))]
use crate::rayon;
use crate::{atomicmin::AtomicMin, deflate, filters::RowFilter, png::PngImage, Deadline, PngError};
pub(crate) struct Candidate {
pub image: Arc<PngImage>,
pub idat_data: Vec<u8>,
pub filtered: Vec<u8>,
pub filter: RowFilter,
// For determining tie-breaker
nth: usize,
}
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 {
(
self.estimated_output_size(),
self.image.data.len(),
self.filter,
// Prefer the later image added (e.g. baseline, which is always added last)
usize::MAX - self.nth,
)
}
}
/// Collect image versions and pick one that compresses best
pub(crate) struct Evaluator {
deadline: Arc<Deadline>,
filters: IndexSet<RowFilter>,
deflater: Deflaters,
optimize_alpha: bool,
nth: AtomicUsize,
executed: Arc<AtomicUsize>,
best_candidate_size: Arc<AtomicMin>,
/// images are sent to the caller thread for evaluation
#[cfg(feature = "parallel")]
eval_channel: (Sender<Candidate>, Receiver<Candidate>),
// in non-parallel mode, images are evaluated synchronously
#[cfg(not(feature = "parallel"))]
eval_best_candidate: RefCell<Option<Candidate>>,
}
impl Evaluator {
pub fn new(
deadline: Arc<Deadline>,
filters: IndexSet<RowFilter>,
deflater: Deflaters,
optimize_alpha: bool,
) -> Self {
#[cfg(feature = "parallel")]
let eval_channel = unbounded();
Self {
deadline,
filters,
deflater,
optimize_alpha,
nth: AtomicUsize::new(0),
executed: Arc::new(AtomicUsize::new(0)),
best_candidate_size: Arc::new(AtomicMin::new(None)),
#[cfg(feature = "parallel")]
eval_channel,
#[cfg(not(feature = "parallel"))]
eval_best_candidate: RefCell::new(None),
}
}
/// Wait for all evaluations to finish and return smallest reduction
/// Or `None` if the queue is empty.
#[cfg(feature = "parallel")]
pub fn get_best_candidate(self) -> Option<Candidate> {
let (eval_send, eval_recv) = self.eval_channel;
// Disconnect the sender, breaking the loop in the thread
drop(eval_send);
let nth = self.nth.load(SeqCst);
// Yield to ensure all evaluations are executed
// This can prevent deadlocks when run within an existing rayon thread pool
while self.executed.load(Relaxed) < nth {
rayon::yield_local();
}
eval_recv.into_iter().min_by_key(Candidate::cmp_key)
}
#[cfg(not(feature = "parallel"))]
pub fn get_best_candidate(self) -> Option<Candidate> {
self.eval_best_candidate.into_inner()
}
/// Set best size, if known in advance
pub fn set_best_size(&self, size: usize) {
self.best_candidate_size.set_min(size);
}
/// Check if the image is smaller than others
pub fn try_image(&self, image: Arc<PngImage>) {
let description = format!("{}", image.ihdr.color_type);
self.try_image_with_description(image, &description);
}
/// Check if the image is smaller than others, with a description for verbose mode
pub fn try_image_with_description(&self, image: Arc<PngImage>, description: &str) {
let nth = self.nth.fetch_add(1, SeqCst);
// These clones are only cheap refcounts
let deadline = self.deadline.clone();
let filters = self.filters.clone();
let deflater = self.deflater;
let optimize_alpha = self.optimize_alpha;
let executed = self.executed.clone();
let best_candidate_size = self.best_candidate_size.clone();
let description = description.to_string();
// sends it off asynchronously for compression,
// but results will be collected via the message queue
#[cfg(feature = "parallel")]
let eval_send = self.eval_channel.0.clone();
rayon::spawn(move || {
executed.fetch_add(1, Relaxed);
let filters_iter = filters.par_iter().with_max_len(1);
// Updating of best result inside the parallel loop would require locks,
// which are dangerous to do in side Rayon's loop.
// Instead, only update (atomic) best size in real time,
// and the best result later without need for locks.
filters_iter.for_each(|&filter| {
if deadline.passed() {
return;
}
let filtered = image.filter_image(filter, optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
if let Ok(idat_data) = idat_data {
let new = Candidate {
image: image.clone(),
idat_data,
filtered,
filter,
nth,
};
let size = new.estimated_output_size();
best_candidate_size.set_min(size);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth,
description,
filter,
size
);
#[cfg(feature = "parallel")]
{
eval_send.send(new).expect("send");
}
#[cfg(not(feature = "parallel"))]
{
match &mut *self.eval_best_candidate.borrow_mut() {
Some(prev) if prev.cmp_key() < new.cmp_key() => {}
best => *best = Some(new),
}
}
} else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data {
trace!(
"Eval: {}-bit {:23} {:8} >{} bytes",
image.ihdr.bit_depth,
description,
filter,
size
);
}
});
});
}
}