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() {
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,

View file

@ -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<RowFilter>,
/// 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<TrialWithData> = 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<TrialWithData> {
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")]

View file

@ -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::<u8>().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::<u8>().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 };

View file

@ -47,6 +47,8 @@ pub struct PngData {
pub raw: Arc<PngImage>,
/// The filtered and compressed data of the IDAT chunk
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>>);
@ -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),
})
}