From 74c5c4173bc2c065aff0ef5bddd3db8baf13b1b9 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 19 Jan 2025 13:55:28 +1300 Subject: [PATCH] Use evaluator for main trials --- src/evaluate.rs | 24 +++++---- src/lib.rs | 140 +++++++++++++++++------------------------------- tests/flags.rs | 4 +- 3 files changed, 67 insertions(+), 101 deletions(-) diff --git a/src/evaluate.rs b/src/evaluate.rs index d148841a..5370a4dc 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -29,9 +29,15 @@ pub(crate) struct 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 { ( - self.idat_data.len() + self.image.key_chunks_size(), + self.estimated_output_size(), self.image.data.len(), self.filter, // Prefer the later image added (e.g. baseline, which is always added last) @@ -143,7 +149,14 @@ impl Evaluator { let filtered = image.filter_image(filter, optimize_alpha); let idat_data = deflater.deflate(&filtered, &best_candidate_size); 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); trace!( "Eval: {}-bit {:23} {:8} {} bytes", @@ -152,13 +165,6 @@ impl Evaluator { filter, size ); - let new = Candidate { - image: image.clone(), - idat_data, - filtered, - filter, - nth, - }; #[cfg(feature = "parallel")] { diff --git a/src/lib.rs b/src/lib.rs index 35b6b8e3..45823ab2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -323,8 +323,6 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult> { } } -type TrialResult = (RowFilter, Vec); - /// Perform optimization on the input PNG object using the options provided fn optimize_png( png: &mut PngData, @@ -454,9 +452,8 @@ fn optimize_raw( report_format("Transformed image to ", &new_image); } - let mut deflater = opts.deflate; - let mut png = if opts.idat_recoding || reduction_occurred { - perform_trials( + let (result, deflater) = if opts.idat_recoding || reduction_occurred { + let result = perform_trials( new_image.clone(), opts, deadline.clone(), @@ -464,38 +461,26 @@ fn optimize_raw( eval_result, eval_filters, eval_deflater, - ) - .map(|(filter, idat_data)| PngData { - raw: new_image, - idat_data, - aux_chunks: Vec::new(), - frames: Vec::new(), - filter: Some(filter), - }) + ); + (result?, opts.deflate) } else { // 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. - deflater = eval_deflater; - eval_result.map(|result| PngData { + (eval_result?, eval_deflater) + }; + + if max_size.map_or(true, |max_size| result.estimated_output_size() < max_size) { + debug!("Found better result:"); + debug!(" zc = {} f = {}", deflater, result.filter); + return Some(PngData { raw: result.image, idat_data: result.idat_data, aux_chunks: Vec::new(), frames: Vec::new(), filter: Some(result.filter), - }) - }; - - png = png.filter(|png| png.estimated_output_size() < max_size.unwrap_or(usize::MAX)); - if let Some(png) = &png { - debug!("Found better result:"); - debug!( - " zc = {} f = {:8} {} bytes", - deflater, - png.filter.unwrap(), - png.idat_data.len() - ); + }); } - png + None } /// Perform compression trials @@ -507,7 +492,7 @@ fn perform_trials( mut eval_result: Option, eval_filters: IndexSet, eval_deflater: Deflaters, -) -> Option { +) -> Option { let mut filters = opts.filter.clone(); let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some()); if fast_eval { @@ -519,9 +504,14 @@ fn perform_trials( } if !filters.is_empty() { - trace!("Evaluating: {} filters", filters.len()); - let eval = Evaluator::new(deadline, filters, eval_deflater, opts.optimize_alpha); - if let Some(ref result) = eval_result { + trace!("Evaluating {} filters", filters.len()); + let eval = Evaluator::new( + deadline.clone(), + filters, + eval_deflater, + opts.optimize_alpha, + ); + if let Some(result) = &eval_result { eval.set_best_size(result.idat_data.len()); } eval.try_image(image.clone()); @@ -529,17 +519,30 @@ fn perform_trials( eval_result = Some(result); } } - // We should have a result here - fail if not (e.g. deadline passed) - let result = eval_result?; - - return if opts.deflate == eval_deflater { + if opts.deflate == eval_deflater { // No further compression required - Some((result.filter, result.idat_data)) - } else { - debug!("Trying: {}", result.filter); - let best_size = AtomicMin::new(max_size); - perform_trial(&result.filtered, opts, result.filter, &best_size) + 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 {}, zc = {}", result.filter, opts.deflate); + match opts + .deflate + .deflate(&result.filtered, &AtomicMin::new(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 @@ -555,56 +558,13 @@ fn perform_trials( } } - 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 = &image.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 - } - }) -} - -/// Execute a compression trial -fn perform_trial( - filtered: &[u8], - opts: &Options, - filter: RowFilter, - best_size: &AtomicMin, -) -> Option { - match opts.deflate.deflate(filtered, best_size) { - Ok(new_idat) => { - let bytes = new_idat.len(); - best_size.set_min(bytes); - trace!( - " zc = {} f = {:8} {} bytes", - opts.deflate, - filter, - bytes - ); - Some((filter, new_idat)) - } - Err(PngError::DeflatedDataTooLong(bytes)) => { - trace!( - " zc = {} f = {:8} >{} bytes", - opts.deflate, - filter, - bytes, - ); - None - } - Err(_) => None, + debug!("Trying {} filters, zc = {}", 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)] diff --git a/tests/flags.rs b/tests/flags.rs index 53eac855..4c8ced95 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -181,9 +181,9 @@ fn verbose_mode() { " 8-bit RGB, non-interlaced", " IDAT size = 113794 bytes", " File size = 114708 bytes", - "Trying: 1 filters", + "Trying 1 filters, zc = ", "Found better result:", - " zc = 11 f = None ", + " zc = 11 f = None", " IDAT size = ", " file size = ", ];