diff --git a/src/evaluate.rs b/src/evaluate.rs index 833f41c6..15763a2c 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -81,7 +81,7 @@ impl Evaluator { } /// Wait for all evaluations to finish and return smallest reduction - /// Or `None` if all reductions were worse than baseline. + /// Or `None` if the queue is empty. #[cfg(feature = "parallel")] pub fn get_best_candidate(self) -> Option { let (eval_send, eval_recv) = self.eval_channel; diff --git a/src/lib.rs b/src/lib.rs index 2b27c3c0..e246fe87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -446,6 +446,19 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult> { } } +/// Perform optimization on the raw image data using the options provided +pub fn optimize_from_raw(raw: PngImage, opts: &Options) -> PngResult> { + info!("Processing from raw image data"); + + let deadline = Arc::new(Deadline::new(opts.timeout)); + + if let Some(png) = optimize_raw(Arc::new(raw), opts, deadline, None) { + return Ok(png.output()); + } + + Err(PngError::new("Failed to optimize input data")) +} + #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] /// Defines options to be used for a single compression trial struct TrialOptions { @@ -474,141 +487,10 @@ fn optimize_png( // Do this first so that reductions can ignore certain chunks such as bKGD perform_strip(png, opts); - let stripped_png = png.clone(); - // Must use normal (lazy) compression, as faster ones (greedy) are not representative - let eval_compression = 5; - // None and Bigrams work well together, especially for alpha reductions - let eval_filters = indexset! {RowFilter::None, RowFilter::Bigrams}; - // This will collect all versions of images and pick one that compresses best - let eval = Evaluator::new( - deadline.clone(), - eval_filters.clone(), - eval_compression, - false, - ); - let (baseline, mut reduction_occurred) = - perform_reductions(png.raw.clone(), opts, &deadline, &eval); - png.raw = baseline; - let mut eval_filter = if let Some(result) = eval.get_best_candidate() { - *png = result.image; - if result.is_reduction { - reduction_occurred = true; - } - Some(result.filter) - } else { - None - }; - - if reduction_occurred { - report_format("Reducing image to ", &png.raw); - } - - if opts.idat_recoding || reduction_occurred { - let mut filters = opts.filter.clone(); - let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_filter.is_some()); - let best: Option = if fast_eval { - // Perform a fast evaluation of selected filters followed by a single main compression trial - - if eval_filter.is_some() { - // Some filters have already been evaluated, we don't need to try them again - filters = filters.difference(&eval_filters).cloned().collect(); - } - - if !filters.is_empty() { - trace!("Evaluating: {} filters", filters.len()); - let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha); - if eval_filter.is_some() { - eval.set_best_size(png.idat_data.len()); - } - eval.try_image(png.raw.clone()); - if let Some(result) = eval.get_best_candidate() { - *png = result.image; - eval_filter = Some(result.filter); - } - } - - let trial = TrialOptions { - filter: eval_filter.unwrap(), - compression: match opts.deflate { - Deflaters::Libdeflater { compression } => compression, - _ => 0, - }, - }; - if trial.compression > 0 && trial.compression <= eval_compression { - // No further compression required - if png.idat_data.len() < idat_original_size || opts.force { - Some((trial, png.idat_data.clone())) - } else { - None - } - } else { - debug!("Trying: {}", trial.filter); - let original_len = idat_original_size; - let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) }); - perform_trial(&png.filtered, opts, trial, &best_size) - } - } else { - // Perform full compression trials of selected filters and determine the best - - if filters.is_empty() { - // Pick a filter automatically - if png.raw.ihdr.bit_depth as u8 >= 8 { - // Bigrams is the best all-rounder when there's at least one byte per pixel - filters.insert(RowFilter::Bigrams); - } else { - // Otherwise delta filters generally don't work well, so just stick with None - filters.insert(RowFilter::None); - } - } - - let mut results: Vec = Vec::with_capacity(filters.len()); - - for f in &filters { - results.push(TrialOptions { - filter: *f, - compression: match opts.deflate { - Deflaters::Libdeflater { compression } => compression, - _ => 0, - }, - }); - } - - debug!("Trying: {} filters", results.len()); - - let original_len = idat_original_size; - let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) }); - let results_iter = results.into_par_iter().with_max_len(1); - let best = results_iter.filter_map(|trial| { - if deadline.passed() { - return None; - } - let filtered = &png.raw.filter_image(trial.filter, opts.optimize_alpha); - perform_trial(filtered, opts, trial, &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 - } - }) - }; - - if let Some((opts, idat_data)) = best { - png.idat_data = idat_data; - debug!("Found better combination:"); - debug!( - " zc = {} f = {:8} {} bytes", - opts.compression, - opts.filter, - png.idat_data.len() - ); - } else { - *png = stripped_png; - } - } else if png.idat_data.len() >= idat_original_size { - *png = stripped_png; + if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, Some(idat_original_size)) { + png.raw = new_png.raw; + png.idat_data = new_png.idat_data; } let output = png.output(); @@ -648,6 +530,164 @@ fn optimize_png( Ok(output) } +/// Perform optimization on the input image data using the options provided +fn optimize_raw( + mut png: Arc, + opts: &Options, + deadline: Arc, + max_idat_size: Option, +) -> Option { + // Must use normal (lazy) compression, as faster ones (greedy) are not representative + let eval_compression = 5; + // None and Bigrams work well together, especially for alpha reductions + let eval_filters = indexset! {RowFilter::None, RowFilter::Bigrams}; + // This will collect all versions of images and pick one that compresses best + let eval = Evaluator::new( + deadline.clone(), + eval_filters.clone(), + eval_compression, + false, + ); + let (baseline, mut reduction_occurred) = + perform_reductions(png.clone(), opts, &deadline, &eval); + png = baseline; + let mut eval_result = eval.get_best_candidate(); + if let Some(ref result) = eval_result { + if result.is_reduction { + png = Arc::clone(&result.image.raw); + reduction_occurred = true; + } + } + + if reduction_occurred { + report_format("Reducing image to ", &png); + } + + if opts.idat_recoding || reduction_occurred { + let mut filters = opts.filter.clone(); + let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some()); + let best: Option = if fast_eval { + // Perform a fast evaluation of selected filters followed by a single main compression trial + + if eval_result.is_some() { + // Some filters have already been evaluated, we don't need to try them again + filters = filters.difference(&eval_filters).cloned().collect(); + } + + if !filters.is_empty() { + trace!("Evaluating: {} filters", filters.len()); + let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha); + if let Some(ref result) = eval_result { + eval.set_best_size(result.image.idat_data.len()); + } + eval.try_image(png.clone()); + if let Some(result) = eval.get_best_candidate() { + eval_result = Some(result); + } + } + // We should have a result here - fail if not (e.g. deadline passed) + let eval_result = eval_result?; + + let trial = TrialOptions { + filter: eval_result.filter, + compression: match opts.deflate { + Deflaters::Libdeflater { compression } => compression, + _ => 0, + }, + }; + if trial.compression > 0 && trial.compression <= eval_compression { + // No further compression required + let idat_data = eval_result.image.idat_data; + if opts.force || idat_data.len() < max_idat_size.unwrap_or(usize::MAX) { + Some((trial, idat_data)) + } else { + None + } + } else { + debug!("Trying: {}", trial.filter); + let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size }); + perform_trial(&eval_result.image.filtered, opts, trial, &best_size) + } + } else { + // Perform full compression trials of selected filters and determine the best + + if filters.is_empty() { + // Pick a filter automatically + if png.ihdr.bit_depth as u8 >= 8 { + // Bigrams is the best all-rounder when there's at least one byte per pixel + filters.insert(RowFilter::Bigrams); + } else { + // Otherwise delta filters generally don't work well, so just stick with None + filters.insert(RowFilter::None); + } + } + + let mut results: Vec = Vec::with_capacity(filters.len()); + + for f in &filters { + results.push(TrialOptions { + filter: *f, + compression: match opts.deflate { + Deflaters::Libdeflater { compression } => compression, + _ => 0, + }, + }); + } + + debug!("Trying: {} filters", results.len()); + + let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size }); + let results_iter = results.into_par_iter().with_max_len(1); + let best = results_iter.filter_map(|trial| { + if deadline.passed() { + return None; + } + let filtered = &png.filter_image(trial.filter, opts.optimize_alpha); + perform_trial(filtered, opts, trial, &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 + } + }) + }; + + if let Some((opts, idat_data)) = best { + debug!("Found better combination:"); + debug!( + " zc = {} f = {:8} {} bytes", + opts.compression, + opts.filter, + idat_data.len() + ); + return Some(PngData { + raw: png, + // The filtered data has not been retained here, but we don't need to return it + filtered: vec![], + idat_data, + }); + } + } else if let Some(result) = eval_result { + // 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. + let idat_data = &result.image.idat_data; + if idat_data.len() < max_idat_size.unwrap_or(usize::MAX) { + debug!("Found better combination:"); + debug!( + " zc = {} f = {:8} {} bytes", + eval_compression, + result.filter, + idat_data.len() + ); + return Some(result.image); + } + } + + None +} + /// Execute a compression trial fn perform_trial( filtered: &[u8],