Rebalance presets (#467)
* Add --fast option * Rebalance presets * Retain filtered idat
This commit is contained in:
parent
3365aca12f
commit
1934587253
4 changed files with 70 additions and 52 deletions
|
|
@ -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,
|
||||
|
|
|
|||
68
src/lib.rs
68
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<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")]
|
||||
|
|
|
|||
39
src/main.rs
39
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::<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 };
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue