diff --git a/src/evaluate.rs b/src/evaluate.rs index adeef359..5260bc2d 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -19,7 +19,7 @@ use std::thread; /// Collect image versions and pick one that compresses best pub struct Evaluator { /// images are sent to the thread for evaluation - eval_send: Option, bool)>>, + eval_send: Option, f32, bool)>>, // the thread helps evaluate images asynchronously eval_thread: thread::JoinHandle>, } @@ -43,24 +43,26 @@ impl Evaluator { /// Set baseline image. It will be used only to measure minimum compression level required pub fn set_baseline(&self, image: Arc) { - self.try_image_inner(image, false) + self.try_image_inner(image, 1.0, false) } /// Check if the image is smaller than others - pub fn try_image(&self, image: Arc) { - self.try_image_inner(image, true) + /// Bias is a value in 0..=1 range. Compressed size is multiplied by + /// this fraction when comparing to the best, so 0.95 allows 5% larger size. + pub fn try_image(&self, image: Arc, bias: f32) { + self.try_image_inner(image, bias, true) } - fn try_image_inner(&self, image: Arc, is_reduction: bool) { - self.eval_send.as_ref().expect("not finished yet").send((image, is_reduction)).expect("send") + fn try_image_inner(&self, image: Arc, bias: f32, is_reduction: bool) { + self.eval_send.as_ref().expect("not finished yet").send((image, bias, is_reduction)).expect("send") } /// Main loop of evaluation thread - fn evaluate_images(from_channel: Receiver<(Arc, bool)>) -> Option { + fn evaluate_images(from_channel: Receiver<(Arc, f32, bool)>) -> Option { let best_candidate_size = AtomicMin::new(None); let best_result: Mutex> = Mutex::new(None); // ends when sender is dropped - for (nth, (image, is_reduction)) in from_channel.iter().enumerate() { + for (nth, (image, bias, is_reduction)) in from_channel.iter().enumerate() { #[cfg(feature = "parallel")] let filters_iter = STD_FILTERS.par_iter().with_max_len(1); #[cfg(not(feature = "parallel"))] @@ -75,15 +77,16 @@ impl Evaluator { &best_candidate_size, ) { let mut res = best_result.lock().unwrap(); - if best_candidate_size.get().map_or(true, |best_len| { + if best_candidate_size.get().map_or(true, |old_best_len| { + let new_len = (idat_data.len() as f64 * bias as f64) as usize; // a tie-breaker is required to make evaluation deterministic if let Some(res) = res.as_ref() { // choose smallest compressed, or if compresses the same, smallest uncompressed, or cheaper filter let old_img = &res.0.raw; - let new = (idat_data.len(), image.data.len(), image.ihdr.bit_depth, f, nth); - let old = (best_len, old_img.data.len(), old_img.ihdr.bit_depth, res.1, res.2); + let new = (new_len, image.data.len(), image.ihdr.bit_depth, f, nth); + let old = (old_best_len, old_img.data.len(), old_img.ihdr.bit_depth, res.1, res.2); new < old - } else if best_len > idat_data.len() { + } else if new_len < old_best_len { true } else { false diff --git a/src/lib.rs b/src/lib.rs index be18e74d..1a54cf82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -747,7 +747,7 @@ fn perform_reductions(mut png: Arc, opts: &Options, deadline: &Deadlin if let Some(interlacing) = opts.interlace { if let Some(reduced) = png.change_interlacing(interlacing) { png = Arc::new(reduced); - eval.try_image(png.clone()); + eval.try_image(png.clone(), 0.); } if deadline.passed() { return; @@ -757,7 +757,7 @@ fn perform_reductions(mut png: Arc, opts: &Options, deadline: &Deadlin if opts.palette_reduction { if let Some(reduced) = reduced_palette(&png) { png = Arc::new(reduced); - eval.try_image(png.clone()); + eval.try_image(png.clone(), 0.95); if opts.verbosity == Some(1) { report_reduction(&png); } @@ -772,11 +772,11 @@ fn perform_reductions(mut png: Arc, opts: &Options, deadline: &Deadlin let previous = png.clone(); let bits = reduced.ihdr.bit_depth; png = Arc::new(reduced); - eval.try_image(png.clone()); + eval.try_image(png.clone(), 1.0); if (bits == BitDepth::One || bits == BitDepth::Two) && previous.ihdr.bit_depth != BitDepth::Four { // Also try 16-color mode for all lower bits images, since that may compress better if let Some(reduced) = reduce_bit_depth(&previous, 4) { - eval.try_image(Arc::new(reduced)); + eval.try_image(Arc::new(reduced), 0.98); } } if opts.verbosity == Some(1) { @@ -791,7 +791,7 @@ fn perform_reductions(mut png: Arc, opts: &Options, deadline: &Deadlin if opts.color_type_reduction { if let Some(reduced) = reduce_color_type(&png) { png = Arc::new(reduced); - eval.try_image(png.clone()); + eval.try_image(png.clone(), 0.96); if opts.verbosity == Some(1) { report_reduction(&png); } diff --git a/src/reduction/alpha.rs b/src/reduction/alpha.rs index b013ab23..e2bba245 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -19,7 +19,7 @@ pub fn try_alpha_reductions(png: Arc, alphas: &HashSet, ev let alphas_iter = alphas.iter(); alphas_iter .filter_map(|&alpha| filtered_alpha_channel(&png, *alpha)) - .for_each(|image| eval.try_image(Arc::new(image))); + .for_each(|image| eval.try_image(Arc::new(image), 0.99)); } pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option {