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(crate) struct Candidate {
pub image: Arc<PngImage>, pub image: Arc<PngImage>,
pub data: Vec<u8>, pub idat_data: Option<Vec<u8>>,
pub data_is_compressed: bool,
pub estimated_output_size: usize, pub estimated_output_size: usize,
/// The input filter, which is retained for printing and for APNG frames.
pub filter: FilterStrategy, 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, nth: usize,
} }
@ -36,7 +39,7 @@ impl Candidate {
( (
self.estimated_output_size, self.estimated_output_size,
self.image.data.len(), self.image.data.len(),
self.filter, self.filter.clone(),
// 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)
usize::MAX - self.nth, usize::MAX - self.nth,
) )
@ -143,21 +146,21 @@ impl Evaluator {
// which are dangerous to do in side Rayon's loop. // which are dangerous to do in side Rayon's loop.
// Instead, only update (atomic) best size in real time, // Instead, only update (atomic) best size in real time,
// and the best result later without need for locks. // and the best result later without need for locks.
filters_iter.for_each(|&filter| { filters_iter.for_each(|filter| {
if deadline.passed() { if deadline.passed() {
return; 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()); 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 estimated_output_size = image.estimated_output_size(&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 { let new = Candidate {
image: image.clone(), image: image.clone(),
data: if final_round { idat_data } else { filtered }, idat_data: if final_round { Some(idat_data) } else { None },
data_is_compressed: final_round,
estimated_output_size, estimated_output_size,
filter, filter: filter.clone(),
filter_used,
nth, nth,
}; };
best_candidate_size.set_min(estimated_output_size); best_candidate_size.set_min(estimated_output_size);

View file

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

View file

@ -170,7 +170,7 @@ impl RawImage {
let mut png = PngData { let mut png = PngData {
raw: result.image, raw: result.image,
idat_data: result.data, idat_data: result.idat_data.unwrap(),
aux_chunks, aux_chunks,
frames: Vec::new(), frames: Vec::new(),
}; };
@ -359,7 +359,7 @@ fn optimize_png(
}; };
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) { if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
png.raw = result.image; png.raw = result.image;
png.idat_data = result.data; png.idat_data = result.idat_data.unwrap();
recompress_frames(png, &opts, deadline, result.filter)?; recompress_frames(png, &opts, deadline, result.filter)?;
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr); postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
} }
@ -472,7 +472,7 @@ fn optimize_raw(
(eval_result?, eval_deflater) (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) && max_size.is_none_or(|max_size| result.estimated_output_size < max_size)
{ {
debug!("Found better result:"); debug!("Found better result:");
@ -499,7 +499,7 @@ fn perform_trials(
if eval_result.is_some() { if eval_result.is_some() {
// Some filters have already been evaluated, we don't need to try them again // 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() { if !filters.is_empty() {
@ -523,14 +523,14 @@ fn perform_trials(
// 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 mut result = eval_result?; let mut result = eval_result?;
if !result.data_is_compressed { if result.idat_data.is_none() {
// Compress with the main deflater // Compress with the main deflater
debug!("Trying filter {} with {}", result.filter, opts.deflate); 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) => { Ok(idat_data) => {
result.estimated_output_size = result.image.estimated_output_size(&idat_data); result.estimated_output_size = result.image.estimated_output_size(&idat_data);
result.data = idat_data; result.idat_data = Some(idat_data);
result.data_is_compressed = true;
trace!("{} bytes", result.estimated_output_size); trace!("{} bytes", result.estimated_output_size);
} }
Err(PngError::DeflatedDataTooLong(bytes)) => { Err(PngError::DeflatedDataTooLong(bytes)) => {
@ -644,7 +644,7 @@ fn recompress_frames(
ihdr.width = frame.width; ihdr.width = frame.width;
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.clone(), opts.optimize_alpha);
let max_size = 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!(

View file

@ -386,7 +386,11 @@ impl PngImage {
/// Apply the specified filter type to all rows in the image /// Apply the specified filter type to all rows in the image
#[must_use] #[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 mut filtered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel(); 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 // 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_line = Vec::new();
let mut prev_pass: Option<u8> = None; let mut prev_pass: Option<u8> = None;
let mut f_buf = Vec::new(); 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() { if prev_pass != line.pass || line.data.len() != prev_line.len() {
prev_line = vec![0; line.data.len()]; prev_line = vec![0; line.data.len()];
} }
// Alpha optimisation may alter the line data, so we need a mutable copy of it // Alpha optimisation may alter the line data, so we need a mutable copy of it
let mut line_data = line.data.to_vec(); let mut line_data = line.data.to_vec();
if let FilterStrategy::Basic(filter) = strategy { if let FilterStrategy::Basic(mut filter) = strategy {
// Standard filters // Standard filters
let filter = if prev_pass == line.pass || filter <= RowFilter::Sub { if prev_pass != line.pass && filter > RowFilter::Sub {
filter filter = RowFilter::None;
} else { }
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); filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered.extend_from_slice(&f_buf); filtered.extend_from_slice(&f_buf);
prev_line = line_data; prev_line = line_data;
} else { } else {
// Heuristic filter selection strategies // Heuristic filter selection strategies
let mut best_filter = RowFilter::None;
if line_data.iter().all(|&x| x == 0) { if line_data.iter().all(|&x| x == 0) {
// Assume None if the line is all zeros // 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); filtered.extend_from_slice(&line_data);
prev_line = line_data; prev_line = line_data;
filters_used.push(best_filter);
continue; continue;
} }
@ -450,6 +462,7 @@ impl PngImage {
best_size = size; best_size = size;
std::mem::swap(&mut best_line, &mut f_buf); std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data); best_line_raw.clone_from(&line_data);
best_filter = *f;
} }
} }
} }
@ -473,6 +486,7 @@ impl PngImage {
best_size = size; best_size = size;
std::mem::swap(&mut best_line, &mut f_buf); std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data); best_line_raw.clone_from(&line_data);
best_filter = *f;
} }
} }
} }
@ -492,6 +506,7 @@ impl PngImage {
best_size = size; best_size = size;
std::mem::swap(&mut best_line, &mut f_buf); std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data); best_line_raw.clone_from(&line_data);
best_filter = *f;
} }
} }
} }
@ -512,6 +527,7 @@ impl PngImage {
best_size = size; best_size = size;
std::mem::swap(&mut best_line, &mut f_buf); std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data); best_line_raw.clone_from(&line_data);
best_filter = *f;
} }
} }
} }
@ -537,6 +553,7 @@ impl PngImage {
best_size = size; best_size = size;
std::mem::swap(&mut best_line, &mut f_buf); std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data); best_line_raw.clone_from(&line_data);
best_filter = *f;
} }
} }
filtered.resize(line_start, 0); filtered.resize(line_start, 0);
@ -545,11 +562,17 @@ impl PngImage {
} }
filtered.extend_from_slice(&best_line); filtered.extend_from_slice(&best_line);
prev_line = best_line_raw; prev_line = best_line_raw;
filters_used.push(best_filter);
} }
prev_pass = line.pass; prev_pass = line.pass;
} }
filtered
if filters_used.is_empty() {
(filtered, strategy)
} else {
(filtered, FilterStrategy::Predefined(filters_used))
}
} }
} }