diff --git a/src/evaluate.rs b/src/evaluate.rs index 69c36544..9463cf6b 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -23,11 +23,14 @@ use crate::{ pub(crate) struct Candidate { pub image: Arc, - pub data: Vec, - pub data_is_compressed: bool, + pub idat_data: Option>, 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); diff --git a/src/filters.rs b/src/filters.rs index 42575b47..ace81197 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -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), } 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), } } } diff --git a/src/lib.rs b/src/lib.rs index fd9c30a9..60666a2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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!( diff --git a/src/png/mod.rs b/src/png/mod.rs index 8a86f31d..c13c761e 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -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 { + pub fn filter_image( + &self, + strategy: FilterStrategy, + optimize_alpha: bool, + ) -> (Vec, 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 = 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)) + } } }