Rebalance presets (#467)

* Add --fast option

* Rebalance presets

* Retain filtered idat
This commit is contained in:
andrews05 2022-12-08 02:09:19 +13:00 committed by GitHub
parent 3365aca12f
commit 1934587253
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 52 deletions

View file

@ -122,15 +122,15 @@ impl Evaluator {
if deadline.passed() { if deadline.passed() {
return; return;
} }
if let Ok(idat_data) = deflate::deflate( let filtered = image.filter_image(filter);
&image.filter_image(filter), if let Ok(idat_data) =
compression, deflate::deflate(&filtered, compression, &best_candidate_size)
&best_candidate_size, {
) {
best_candidate_size.set_min(idat_data.len()); best_candidate_size.set_min(idat_data.len());
let new = Candidate { let new = Candidate {
image: PngData { image: PngData {
idat_data, idat_data,
filtered,
raw: Arc::clone(&image), raw: Arc::clone(&image),
}, },
filter, filter,

View file

@ -147,9 +147,9 @@ pub struct Options {
/// ///
/// Default: `false` /// Default: `false`
pub preserve_attrs: bool, 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<RowFilter>, pub filter: IndexSet<RowFilter>,
/// Whether to change the interlacing type of the file. /// Whether to change the interlacing type of the file.
/// ///
@ -194,9 +194,7 @@ pub struct Options {
pub deflate: Deflaters, pub deflate: Deflaters,
/// Whether to use fast evaluation to pick the best filter /// Whether to use fast evaluation to pick the best filter
/// ///
/// Intended for use with `-o 1` from the CLI interface /// Default: `true`
///
/// Default: `false`
pub fast_evaluation: bool, pub fast_evaluation: bool,
/// Maximum amount of time to spend on optimizations. /// Maximum amount of time to spend on optimizations.
@ -229,12 +227,10 @@ impl Options {
// The following methods make assumptions that they are operating // The following methods make assumptions that they are operating
// on an `Options` struct generated by the `default` method. // on an `Options` struct generated by the `default` method.
fn apply_preset_0(mut self) -> Self { fn apply_preset_0(mut self) -> Self {
self.idat_recoding = false;
self.filter.clear(); self.filter.clear();
if let Deflaters::Libdeflater { compression } = &mut self.deflate { if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 5; *compression = 5;
} }
self.fast_evaluation = true;
self self
} }
@ -243,7 +239,6 @@ impl Options {
if let Deflaters::Libdeflater { compression } = &mut self.deflate { if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 10; *compression = 10;
} }
self.fast_evaluation = true;
self self
} }
@ -252,10 +247,13 @@ impl Options {
} }
fn apply_preset_3(mut self) -> Self { fn apply_preset_3(mut self) -> Self {
self.filter.insert(RowFilter::Sub); self.fast_evaluation = false;
self.filter.insert(RowFilter::Up); self.filter = indexset! {
self.filter.insert(RowFilter::Average); RowFilter::None,
self.filter.insert(RowFilter::Paeth); RowFilter::Bigrams,
RowFilter::BigEnt,
RowFilter::Brute
};
self self
} }
@ -266,12 +264,22 @@ impl Options {
self.apply_preset_3() self.apply_preset_3()
} }
fn apply_preset_5(self) -> Self { fn apply_preset_5(mut self) -> Self {
self.apply_preset_4() 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 { fn apply_preset_6(mut self) -> Self {
self.apply_preset_4() 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, fix_errors: false,
force: false, force: false,
preserve_attrs: false, preserve_attrs: false,
filter: indexset! {RowFilter::None, RowFilter::MinSum}, filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams},
interlace: None, interlace: None,
alphas: IndexSet::new(), alphas: IndexSet::new(),
bit_depth_reduction: true, bit_depth_reduction: true,
@ -295,7 +303,7 @@ impl Default for Options {
idat_recoding: true, idat_recoding: true,
strip: Headers::None, strip: Headers::None,
deflate: Deflaters::Libdeflater { compression: 11 }, deflate: Deflaters::Libdeflater { compression: 11 },
fast_evaluation: false, fast_evaluation: true,
timeout: None, timeout: None,
} }
} }
@ -484,8 +492,10 @@ fn optimize_png(
let stripped_png = png.clone(); let stripped_png = png.clone();
// Must use normal (lazy) compression, as faster ones (greedy) are not representative // 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_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 // This will collect all versions of images and pick one that compresses best
let eval = Evaluator::new(deadline.clone(), eval_filters.clone(), eval_compression); let eval = Evaluator::new(deadline.clone(), eval_filters.clone(), eval_compression);
perform_reductions(png.raw.clone(), opts, &deadline, &eval); 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 fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_filter.is_some());
let best: Option<TrialWithData> = if fast_eval { let best: Option<TrialWithData> = if fast_eval {
// Perform a fast evaluation of selected filters followed by a single main compression trial // Perform a fast evaluation of selected filters followed by a single main compression trial
if eval_filter.is_some() { if eval_filter.is_some() {
// Some filters have already been evaluated, we don't need to try them again // Some filters have already been evaluated, we don't need to try them again
filters = filters.difference(&eval_filters).cloned().collect(); filters = filters.difference(&eval_filters).cloned().collect();
@ -537,17 +548,18 @@ fn optimize_png(
info!("Trying: {}", trial.filter); info!("Trying: {}", trial.filter);
let original_len = idat_original_size; let original_len = idat_original_size;
let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) }); 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 { } else {
// Perform full compression trials of selected filters and determine the best // Perform full compression trials of selected filters and determine the best
if filters.is_empty() { if filters.is_empty() {
// Heuristically determine which filter to use // Pick a filter automatically
if png.raw.ihdr.bit_depth.as_u8() >= 8 if png.raw.ihdr.bit_depth.as_u8() >= 8 {
&& png.raw.ihdr.color_type != colors::ColorType::Indexed // Bigrams is the best all-rounder when there's at least one byte per pixel
{ filters.insert(RowFilter::Bigrams);
filters.insert(RowFilter::MinSum);
} else { } else {
// Otherwise delta filters generally don't work well, so just stick with None
filters.insert(RowFilter::None); filters.insert(RowFilter::None);
} }
} }
@ -573,7 +585,8 @@ fn optimize_png(
if deadline.passed() { if deadline.passed() {
return None; 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| { best.reduce_with(|i, j| {
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) { 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 /// Execute a compression trial
fn perform_trial( fn perform_trial(
png: &PngImage, filtered: &[u8],
opts: &Options, opts: &Options,
trial: TrialOptions, trial: TrialOptions,
best_size: &AtomicMin, best_size: &AtomicMin,
) -> Option<TrialWithData> { ) -> Option<TrialWithData> {
let filtered = &png.filter_image(trial.filter);
let new_idat = match opts.deflate { let new_idat = match opts.deflate {
Deflaters::Libdeflater { .. } => deflate::deflate(filtered, trial.compression, best_size), Deflaters::Libdeflater { .. } => deflate::deflate(filtered, trial.compression, best_size),
#[cfg(feature = "zopfli")] #[cfg(feature = "zopfli")]

View file

@ -182,6 +182,11 @@ fn main() {
Err(_) => Err("Invalid option for filters".to_owned()), Err(_) => Err("Invalid option for filters".to_owned()),
}), }),
) )
.arg(
Arg::new("fast")
.help("Use fast filter evaluation")
.long("fast"),
)
.arg( .arg(
Arg::new("compression") Arg::new("compression")
.help("zlib compression level (1-12) - Default: 11") .help("zlib compression level (1-12) - Default: 11")
@ -256,14 +261,14 @@ fn main() {
) )
.after_help( .after_help(
"Optimization levels: "Optimization levels:
-o 0 => --zc 6 --nz (0 or 1 trials) -o 0 => --zc 5 --fast (1 trial, determined heuristically)
-o 1 => --zc 10 (1 trial, determined heuristically) -o 1 => --zc 10 --fast (1 trial, determined heuristically)
-o 2 => --zc 11 -f 0,5 (2 trials) -o 2 => --zc 11 -f 0,1,6,7 --fast (1 trial, determined by fast evaluation)
-o 3 => --zc 11 -f 0-5 (6 trials) -o 3 => --zc 11 -f 0,7,8,9 (4 trials)
-o 4 => --zc 12 -f 0-5 (6 trials; same as `-o 3` for zopfli) -o 4 => --zc 12 -f 0,7,8,9 (4 trials; same as `-o 3` for zopfli)
-o 5 => --zc 9-12 -f 0-5 (24 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 1-12 -f 0-5 (72 trials; same as `-o 3` for zopfli) -o 6 => --zc 12 -f 0-9 (10 trials)
-o max => (stable alias for the max compression) -o max => (stable alias for the max compression)
Manually specifying a compression option (zc, f, etc.) will override the optimization preset, Manually specifying a compression option (zc, f, etc.) will override the optimization preset,
regardless of the order you write the arguments. regardless of the order you write the arguments.
@ -382,13 +387,10 @@ fn parse_opts_into_struct(
.init() .init()
.unwrap(); .unwrap();
let (explicit_level, mut opts) = match matches.value_of("optimization") { let mut opts = match matches.value_of("optimization") {
None => (None, Options::default()), None => Options::default(),
Some("max") => (None, Options::max_compression()), Some("max") => Options::max_compression(),
Some(level) => { Some(level) => Options::from_preset(level.parse::<u8>().unwrap()),
let level = level.parse::<u8>().unwrap();
(Some(level), Options::from_preset(level))
}
}; };
if let Some(x) = matches.value_of("interlace") { if let Some(x) = matches.value_of("interlace") {
@ -442,6 +444,10 @@ fn parse_opts_into_struct(
opts.alphas.insert(AlphaOptim::Left); opts.alphas.insert(AlphaOptim::Left);
} }
if matches.is_present("fast") {
opts.fast_evaluation = true;
}
if matches.is_present("backup") { if matches.is_present("backup") {
opts.backup = true; opts.backup = true;
} }
@ -530,9 +536,6 @@ fn parse_opts_into_struct(
} }
if matches.is_present("zopfli") { 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")] #[cfg(feature = "zopfli")]
if let Some(iterations) = NonZeroU8::new(15) { if let Some(iterations) = NonZeroU8::new(15) {
opts.deflate = Deflaters::Zopfli { iterations }; opts.deflate = Deflaters::Zopfli { iterations };

View file

@ -47,6 +47,8 @@ pub struct PngData {
pub raw: Arc<PngImage>, pub raw: Arc<PngImage>,
/// The filtered and compressed data of the IDAT chunk /// The filtered and compressed data of the IDAT chunk
pub idat_data: Vec<u8>, pub idat_data: Vec<u8>,
/// The filtered, uncompressed data of the IDAT chunk
pub filtered: Vec<u8>,
} }
type PaletteWithTrns = (Option<Vec<RGBA8>>, Option<Vec<u8>>); type PaletteWithTrns = (Option<Vec<RGBA8>>, Option<Vec<u8>>);
@ -135,10 +137,11 @@ impl PngData {
transparency_pixel, transparency_pixel,
aux_headers, aux_headers,
}; };
raw.data = raw.unfilter_image()?; let unfiltered = raw.unfilter_image()?;
// Return the PngData // Return the PngData
Ok(Self { Ok(Self {
idat_data: idat_headers, idat_data: idat_headers,
filtered: std::mem::replace(&mut raw.data, unfiltered),
raw: Arc::new(raw), raw: Arc::new(raw),
}) })
} }