From 1934587253789ceccd67c86e63f3b563abd78339 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Thu, 8 Dec 2022 02:09:19 +1300 Subject: [PATCH] Rebalance presets (#467) * Add --fast option * Rebalance presets * Retain filtered idat --- src/evaluate.rs | 10 ++++---- src/lib.rs | 68 +++++++++++++++++++++++++++++-------------------- src/main.rs | 39 +++++++++++++++------------- src/png/mod.rs | 5 +++- 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/src/evaluate.rs b/src/evaluate.rs index 15222f7a..13cc0473 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -122,15 +122,15 @@ impl Evaluator { if deadline.passed() { return; } - if let Ok(idat_data) = deflate::deflate( - &image.filter_image(filter), - compression, - &best_candidate_size, - ) { + let filtered = image.filter_image(filter); + if let Ok(idat_data) = + deflate::deflate(&filtered, compression, &best_candidate_size) + { best_candidate_size.set_min(idat_data.len()); let new = Candidate { image: PngData { idat_data, + filtered, raw: Arc::clone(&image), }, filter, diff --git a/src/lib.rs b/src/lib.rs index 09fa4100..9728245e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,9 +147,9 @@ pub struct Options { /// /// Default: `false` pub preserve_attrs: bool, - /// Which filters to try on the file (0-5) + /// Which RowFilters to try on the file /// - /// Default: `0,5` + /// Default: `None,Sub,Entropy,Bigrams` pub filter: IndexSet, /// Whether to change the interlacing type of the file. /// @@ -194,9 +194,7 @@ pub struct Options { pub deflate: Deflaters, /// Whether to use fast evaluation to pick the best filter /// - /// Intended for use with `-o 1` from the CLI interface - /// - /// Default: `false` + /// Default: `true` pub fast_evaluation: bool, /// Maximum amount of time to spend on optimizations. @@ -229,12 +227,10 @@ impl Options { // The following methods make assumptions that they are operating // on an `Options` struct generated by the `default` method. fn apply_preset_0(mut self) -> Self { - self.idat_recoding = false; self.filter.clear(); if let Deflaters::Libdeflater { compression } = &mut self.deflate { *compression = 5; } - self.fast_evaluation = true; self } @@ -243,7 +239,6 @@ impl Options { if let Deflaters::Libdeflater { compression } = &mut self.deflate { *compression = 10; } - self.fast_evaluation = true; self } @@ -252,10 +247,13 @@ impl Options { } fn apply_preset_3(mut self) -> Self { - self.filter.insert(RowFilter::Sub); - self.filter.insert(RowFilter::Up); - self.filter.insert(RowFilter::Average); - self.filter.insert(RowFilter::Paeth); + self.fast_evaluation = false; + self.filter = indexset! { + RowFilter::None, + RowFilter::Bigrams, + RowFilter::BigEnt, + RowFilter::Brute + }; self } @@ -266,12 +264,22 @@ impl Options { self.apply_preset_3() } - fn apply_preset_5(self) -> Self { - self.apply_preset_4() + fn apply_preset_5(mut self) -> Self { + self.fast_evaluation = false; + self.filter.insert(RowFilter::Up); + self.filter.insert(RowFilter::MinSum); + self.filter.insert(RowFilter::BigEnt); + self.filter.insert(RowFilter::Brute); + if let Deflaters::Libdeflater { compression } = &mut self.deflate { + *compression = 12; + } + self } - fn apply_preset_6(self) -> Self { - self.apply_preset_4() + fn apply_preset_6(mut self) -> Self { + self.filter.insert(RowFilter::Average); + self.filter.insert(RowFilter::Paeth); + self.apply_preset_5() } } @@ -285,7 +293,7 @@ impl Default for Options { fix_errors: false, force: false, preserve_attrs: false, - filter: indexset! {RowFilter::None, RowFilter::MinSum}, + filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams}, interlace: None, alphas: IndexSet::new(), bit_depth_reduction: true, @@ -295,7 +303,7 @@ impl Default for Options { idat_recoding: true, strip: Headers::None, deflate: Deflaters::Libdeflater { compression: 11 }, - fast_evaluation: false, + fast_evaluation: true, timeout: None, } } @@ -484,8 +492,10 @@ fn optimize_png( let stripped_png = png.clone(); // Must use normal (lazy) compression, as faster ones (greedy) are not representative + // Alpha reductions can benefit from higher compression but otherwise it's not beneficial let eval_compression = 5; - let eval_filters = indexset! {RowFilter::None, RowFilter::MinSum}; + // 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); perform_reductions(png.raw.clone(), opts, &deadline, &eval); @@ -501,6 +511,7 @@ fn optimize_png( 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(); @@ -537,17 +548,18 @@ fn optimize_png( info!("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.raw, opts, trial, &best_size) + perform_trial(&png.filtered, opts, trial, &best_size) } } else { // Perform full compression trials of selected filters and determine the best + if filters.is_empty() { - // Heuristically determine which filter to use - if png.raw.ihdr.bit_depth.as_u8() >= 8 - && png.raw.ihdr.color_type != colors::ColorType::Indexed - { - filters.insert(RowFilter::MinSum); + // 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); } } @@ -573,7 +585,8 @@ fn optimize_png( if deadline.passed() { return None; } - perform_trial(&png.raw, opts, trial, &best_size) + let filtered = &png.raw.filter_image(trial.filter); + 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) { @@ -737,12 +750,11 @@ fn perform_reductions( /// Execute a compression trial fn perform_trial( - png: &PngImage, + filtered: &[u8], opts: &Options, trial: TrialOptions, best_size: &AtomicMin, ) -> Option { - let filtered = &png.filter_image(trial.filter); let new_idat = match opts.deflate { Deflaters::Libdeflater { .. } => deflate::deflate(filtered, trial.compression, best_size), #[cfg(feature = "zopfli")] diff --git a/src/main.rs b/src/main.rs index 5b9f41d4..61eaed71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,6 +182,11 @@ fn main() { Err(_) => Err("Invalid option for filters".to_owned()), }), ) + .arg( + Arg::new("fast") + .help("Use fast filter evaluation") + .long("fast"), + ) .arg( Arg::new("compression") .help("zlib compression level (1-12) - Default: 11") @@ -256,14 +261,14 @@ fn main() { ) .after_help( "Optimization levels: - -o 0 => --zc 6 --nz (0 or 1 trials) - -o 1 => --zc 10 (1 trial, determined heuristically) - -o 2 => --zc 11 -f 0,5 (2 trials) - -o 3 => --zc 11 -f 0-5 (6 trials) - -o 4 => --zc 12 -f 0-5 (6 trials; same as `-o 3` for zopfli) - -o 5 => --zc 9-12 -f 0-5 (24 trials; same as `-o 3` for zopfli) - -o 6 => --zc 1-12 -f 0-5 (72 trials; same as `-o 3` for zopfli) - -o max => (stable alias for the max compression) + -o 0 => --zc 5 --fast (1 trial, determined heuristically) + -o 1 => --zc 10 --fast (1 trial, determined heuristically) + -o 2 => --zc 11 -f 0,1,6,7 --fast (1 trial, determined by fast evaluation) + -o 3 => --zc 11 -f 0,7,8,9 (4 trials) + -o 4 => --zc 12 -f 0,7,8,9 (4 trials; same as `-o 3` for zopfli) + -o 5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials) + -o 6 => --zc 12 -f 0-9 (10 trials) + -o max => (stable alias for the max compression) Manually specifying a compression option (zc, f, etc.) will override the optimization preset, regardless of the order you write the arguments. @@ -382,13 +387,10 @@ fn parse_opts_into_struct( .init() .unwrap(); - let (explicit_level, mut opts) = match matches.value_of("optimization") { - None => (None, Options::default()), - Some("max") => (None, Options::max_compression()), - Some(level) => { - let level = level.parse::().unwrap(); - (Some(level), Options::from_preset(level)) - } + let mut opts = match matches.value_of("optimization") { + None => Options::default(), + Some("max") => Options::max_compression(), + Some(level) => Options::from_preset(level.parse::().unwrap()), }; if let Some(x) = matches.value_of("interlace") { @@ -442,6 +444,10 @@ fn parse_opts_into_struct( opts.alphas.insert(AlphaOptim::Left); } + if matches.is_present("fast") { + opts.fast_evaluation = true; + } + if matches.is_present("backup") { opts.backup = true; } @@ -530,9 +536,6 @@ fn parse_opts_into_struct( } if matches.is_present("zopfli") { - if explicit_level > Some(3) { - warn!("Level 4 and above are equivalent to level 3 for zopfli"); - } #[cfg(feature = "zopfli")] if let Some(iterations) = NonZeroU8::new(15) { opts.deflate = Deflaters::Zopfli { iterations }; diff --git a/src/png/mod.rs b/src/png/mod.rs index 58039083..3ef33846 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -47,6 +47,8 @@ pub struct PngData { pub raw: Arc, /// The filtered and compressed data of the IDAT chunk pub idat_data: Vec, + /// The filtered, uncompressed data of the IDAT chunk + pub filtered: Vec, } type PaletteWithTrns = (Option>, Option>); @@ -135,10 +137,11 @@ impl PngData { transparency_pixel, aux_headers, }; - raw.data = raw.unfilter_image()?; + let unfiltered = raw.unfilter_image()?; // Return the PngData Ok(Self { idat_data: idat_headers, + filtered: std::mem::replace(&mut raw.data, unfiltered), raw: Arc::new(raw), }) }