Add Predefined filter strategy

Use this to retain filters during evaluation rather than the complete filtered data
This commit is contained in:
Andrew 2025-05-20 15:17:42 +12:00
parent 682b5d9ad0
commit 8c3758b9f4
4 changed files with 59 additions and 30 deletions

View file

@ -23,11 +23,14 @@ use crate::{
pub(crate) struct Candidate {
pub image: Arc<PngImage>,
pub data: Vec<u8>,
pub data_is_compressed: bool,
pub idat_data: Option<Vec<u8>>,
pub estimated_output_size: usize,
/// The input filter, which is retained for printing and for APNG frames.
pub filter: FilterStrategy,
// For determining tie-breaker
/// The filter returned by the filter function, which may be Predefined.
/// Use this for the next round to avoid recomputing the filter.
pub filter_used: FilterStrategy,
/// For determining tie-breaker
nth: usize,
}
@ -36,7 +39,7 @@ impl Candidate {
(
self.estimated_output_size,
self.image.data.len(),
self.filter,
self.filter.clone(),
// Prefer the later image added (e.g. baseline, which is always added last)
usize::MAX - self.nth,
)
@ -143,21 +146,21 @@ impl Evaluator {
// 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| {
filters_iter.for_each(|filter| {
if deadline.passed() {
return;
}
let filtered = image.filter_image(filter, optimize_alpha);
let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
if let Ok(idat_data) = idat_data {
let estimated_output_size = image.estimated_output_size(&idat_data);
// For the final round we need the IDAT data, otherwise the filtered data
// We only need to retain the IDAT data in the final round
let new = Candidate {
image: image.clone(),
data: if final_round { idat_data } else { filtered },
data_is_compressed: final_round,
idat_data: if final_round { Some(idat_data) } else { None },
estimated_output_size,
filter,
filter: filter.clone(),
filter_used,
nth,
};
best_candidate_size.set_min(estimated_output_size);

View file

@ -1,7 +1,7 @@
use std::{fmt, fmt::Display, mem::transmute};
/// Filtering strategy for use in [`Options`][crate::Options]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
pub enum FilterStrategy {
/// Same filter for all rows
Basic(RowFilter),
@ -15,6 +15,8 @@ pub enum FilterStrategy {
BigEnt,
/// Deflate compression
Brute,
/// Predefined filter for each row
Predefined(Vec<RowFilter>),
}
impl Display for FilterStrategy {
@ -26,6 +28,7 @@ impl Display for FilterStrategy {
Self::Bigrams => "Bigrams".fmt(f),
Self::BigEnt => "BigEnt".fmt(f),
Self::Brute => "Brute".fmt(f),
Self::Predefined(_) => "Predefined".fmt(f),
}
}
}

View file

@ -170,7 +170,7 @@ impl RawImage {
let mut png = PngData {
raw: result.image,
idat_data: result.data,
idat_data: result.idat_data.unwrap(),
aux_chunks,
frames: Vec::new(),
};
@ -359,7 +359,7 @@ fn optimize_png(
};
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
png.raw = result.image;
png.idat_data = result.data;
png.idat_data = result.idat_data.unwrap();
recompress_frames(png, &opts, deadline, result.filter)?;
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
}
@ -472,7 +472,7 @@ fn optimize_raw(
(eval_result?, eval_deflater)
};
if result.data_is_compressed
if result.idat_data.is_some()
&& max_size.is_none_or(|max_size| result.estimated_output_size < max_size)
{
debug!("Found better result:");
@ -499,7 +499,7 @@ fn perform_trials(
if eval_result.is_some() {
// Some filters have already been evaluated, we don't need to try them again
filters = filters.difference(&eval_filters).copied().collect();
filters = filters.difference(&eval_filters).cloned().collect();
}
if !filters.is_empty() {
@ -523,14 +523,14 @@ fn perform_trials(
// We should have a result here - fail if not (e.g. deadline passed)
let mut result = eval_result?;
if !result.data_is_compressed {
if result.idat_data.is_none() {
// Compress with the main deflater
debug!("Trying filter {} with {}", result.filter, opts.deflate);
match opts.deflate.deflate(&result.data, max_size) {
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha);
match opts.deflate.deflate(&data, max_size) {
Ok(idat_data) => {
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
result.data = idat_data;
result.data_is_compressed = true;
result.idat_data = Some(idat_data);
trace!("{} bytes", result.estimated_output_size);
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
@ -644,7 +644,7 @@ fn recompress_frames(
ihdr.width = frame.width;
ihdr.height = frame.height;
let image = PngImage::new(ihdr, &frame.data)?;
let filtered = image.filter_image(filter, opts.optimize_alpha);
let (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha);
let max_size = Some(frame.data.len() - 1);
if let Ok(data) = opts.deflate.deflate(&filtered, max_size) {
debug!(

View file

@ -386,7 +386,11 @@ impl PngImage {
/// Apply the specified filter type to all rows in the image
#[must_use]
pub fn filter_image(&self, strategy: FilterStrategy, optimize_alpha: bool) -> Vec<u8> {
pub fn filter_image(
&self,
strategy: FilterStrategy,
optimize_alpha: bool,
) -> (Vec<u8>, FilterStrategy) {
let mut filtered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
// If alpha optimization is enabled, determine how many bytes of alpha there are per pixel
@ -399,31 +403,39 @@ impl PngImage {
let mut prev_line = Vec::new();
let mut prev_pass: Option<u8> = None;
let mut f_buf = Vec::new();
for line in self.scan_lines(false) {
// For heuristic strategies, keep track of the actual filter used for each line
let mut filters_used = Vec::new();
for (i, line) in self.scan_lines(false).enumerate() {
if prev_pass != line.pass || line.data.len() != prev_line.len() {
prev_line = vec![0; line.data.len()];
}
// Alpha optimisation may alter the line data, so we need a mutable copy of it
let mut line_data = line.data.to_vec();
if let FilterStrategy::Basic(filter) = strategy {
if let FilterStrategy::Basic(mut filter) = strategy {
// Standard filters
let filter = if prev_pass == line.pass || filter <= RowFilter::Sub {
filter
} else {
RowFilter::None
};
if prev_pass != line.pass && filter > RowFilter::Sub {
filter = RowFilter::None;
}
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered.extend_from_slice(&f_buf);
prev_line = line_data;
} else if let FilterStrategy::Predefined(lines) = &strategy {
// Predefined filter for each line
let filter = lines.get(i).unwrap_or(&RowFilter::None);
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered.extend_from_slice(&f_buf);
prev_line = line_data;
} else {
// Heuristic filter selection strategies
let mut best_filter = RowFilter::None;
if line_data.iter().all(|&x| x == 0) {
// Assume None if the line is all zeros
filtered.push(RowFilter::None as u8);
filtered.push(best_filter as u8);
filtered.extend_from_slice(&line_data);
prev_line = line_data;
filters_used.push(best_filter);
continue;
}
@ -450,6 +462,7 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
@ -473,6 +486,7 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
@ -492,6 +506,7 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
@ -512,6 +527,7 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
@ -537,6 +553,7 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
filtered.resize(line_start, 0);
@ -545,11 +562,17 @@ impl PngImage {
}
filtered.extend_from_slice(&best_line);
prev_line = best_line_raw;
filters_used.push(best_filter);
}
prev_pass = line.pass;
}
filtered
if filters_used.is_empty() {
(filtered, strategy)
} else {
(filtered, FilterStrategy::Predefined(filters_used))
}
}
}