diff --git a/benches/strategies.rs b/benches/strategies.rs index 72d19e90..2f0136be 100644 --- a/benches/strategies.rs +++ b/benches/strategies.rs @@ -45,5 +45,13 @@ fn filters_brute(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(FilterStrategy::Brute, false)); + b.iter(|| { + png.raw.filter_image( + FilterStrategy::Brute { + num_lines: 4, + level: 1, + }, + false, + ) + }); } diff --git a/src/cli.rs b/src/cli.rs index 108c89bd..f10d8af3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,14 +32,14 @@ Set the optimization level preset. The default level 2 is quite fast and provide compression. Lower levels are faster, higher levels provide better compression, though \ with increasingly diminishing returns. - 0 => --zc 5 --fast (1 trial, determined heuristically) - 1 => --zc 10 --fast (1 trial, determined heuristically) - 2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial) - 3 => --zc 11 -f 0,7,8,9 (4 trials) - 4 => --zc 12 -f 0,7,8,9 (4 trials) - 5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials) - 6 => --zc 12 -f 0-9 (10 trials) - max => (stable alias for the max level) + 0 => --zc 5 --fast (filter chosen heuristically) + 1 => --zc 10 --fast (filter chosen heuristically) + 2 => --zc 11 -f 0,1,6,7 --fast + 3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3 + 4 => --zc 12 -f 0,7,8,9 --brute-level 1 --brute-lines 4 + 5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4 + 6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8 + max => (stable alias for the maximum level) Manually specifying a compression option (zc, f, etc.) will override the optimization \ preset, regardless of the order you write the arguments.") @@ -353,6 +353,26 @@ conjunction with a high value for '--zi' to achieve better compression in reason .value_parser(value_parser!(NonZeroU64)) .requires("zopfli"), ) + .arg( + Arg::new("brute-level") + .hide_short_help(true) + .long_help("\ +Set the libdeflate compression level to use with the Brute filter strategy. Sane values \ +are 1-5. Higher values are not necessarily better.") + .long("brute-level") + .value_name("level") + .value_parser(1..=12), + ) + .arg( + Arg::new("brute-lines") + .hide_short_help(true) + .long_help("\ +Set the number of lines to compress at once with the Brute filter strategy. Sane values \ +are 2-16. Higher values are not necessarily better.") + .long("brute-lines") + .value_name("lines") + .value_parser(value_parser!(usize)), + ) .arg( Arg::new("timeout") .help("Maximum amount of time to spend on optimizations") diff --git a/src/filters.rs b/src/filters.rs index cf2a9978..a7922596 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -14,7 +14,12 @@ pub enum FilterStrategy { /// Shannon entropy of bigrams BigEnt, /// Deflate compression - Brute, + Brute { + /// The number of lines to compress at once + num_lines: usize, + /// The compression level to use (1-12) + level: u8, + }, /// Predefined filter for each row Predefined(Vec), } @@ -35,7 +40,7 @@ impl Display for FilterStrategy { Self::Entropy => "Entropy".fmt(f), Self::Bigrams => "Bigrams".fmt(f), Self::BigEnt => "BigEnt".fmt(f), - Self::Brute => "Brute".fmt(f), + Self::Brute { .. } => "Brute".fmt(f), Self::Predefined(_) => "Predefined".fmt(f), } } diff --git a/src/main.rs b/src/main.rs index 2823d232..052244df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -199,6 +199,23 @@ fn parse_opts_into_struct( Some(level) => Options::from_preset(level.parse::().unwrap()), }; + // Get custom brute settings and rebuild the filter set to apply them + let mut brute_lines = matches.get_one::("brute-lines").cloned(); + let mut brute_level = matches.get_one::("brute-level").map(|x| *x as u8); + let mut new_filters = IndexSet::new(); + for mut f in opts.filters.drain(..) { + if let FilterStrategy::Brute { num_lines, level } = &mut f { + *num_lines = brute_lines.unwrap_or(*num_lines); + *level = brute_level.unwrap_or(*level); + // If custom settings were not given, we still need to retain the default values + // from the preset so we can re-apply them if the filters are overridden below + brute_lines = Some(*num_lines); + brute_level = Some(*level); + } + new_filters.insert(f); + } + opts.filters = new_filters; + if let Some(x) = matches.get_one::>("filters") { opts.filters = x .iter() @@ -208,7 +225,10 @@ fn parse_opts_into_struct( 6 => FilterStrategy::Entropy, 7 => FilterStrategy::Bigrams, 8 => FilterStrategy::BigEnt, - 9 => FilterStrategy::Brute, + 9 => FilterStrategy::Brute { + num_lines: brute_lines.unwrap_or(3), + level: brute_level.unwrap_or(1), + }, _ => unreachable!(), }) .collect(); diff --git a/src/options.rs b/src/options.rs index 476673cf..5a7e23d0 100644 --- a/src/options.rs +++ b/src/options.rs @@ -189,17 +189,13 @@ impl Options { // on an `Options` struct generated by the `default` method. fn apply_preset_0(mut self) -> Self { self.filters.clear(); - if let Deflater::Libdeflater { compression } = &mut self.deflater { - *compression = 5; - } + self.deflater = Deflater::Libdeflater { compression: 5 }; self } fn apply_preset_1(mut self) -> Self { self.filters.clear(); - if let Deflater::Libdeflater { compression } = &mut self.deflater { - *compression = 10; - } + self.deflater = Deflater::Libdeflater { compression: 10 }; self } @@ -213,34 +209,67 @@ impl Options { FilterStrategy::NONE, FilterStrategy::Bigrams, FilterStrategy::BigEnt, - FilterStrategy::Brute + FilterStrategy::Brute { + num_lines: 3, + level: 1, + }, }; self } fn apply_preset_4(mut self) -> Self { - if let Deflater::Libdeflater { compression } = &mut self.deflater { - *compression = 12; - } - self.apply_preset_3() + self.fast_evaluation = false; + self.filters = indexset! { + FilterStrategy::NONE, + FilterStrategy::Bigrams, + FilterStrategy::BigEnt, + FilterStrategy::Brute { + num_lines: 4, + level: 1, + }, + }; + self.deflater = Deflater::Libdeflater { compression: 12 }; + self } fn apply_preset_5(mut self) -> Self { self.fast_evaluation = false; - self.filters.insert(FilterStrategy::UP); - self.filters.insert(FilterStrategy::MinSum); - self.filters.insert(FilterStrategy::BigEnt); - self.filters.insert(FilterStrategy::Brute); - if let Deflater::Libdeflater { compression } = &mut self.deflater { - *compression = 12; - } + self.filters = indexset! { + FilterStrategy::NONE, + FilterStrategy::SUB, + FilterStrategy::UP, + FilterStrategy::MinSum, + FilterStrategy::Entropy, + FilterStrategy::Bigrams, + FilterStrategy::BigEnt, + FilterStrategy::Brute { + num_lines: 4, + level: 4, + }, + }; + self.deflater = Deflater::Libdeflater { compression: 12 }; self } fn apply_preset_6(mut self) -> Self { - self.filters.insert(FilterStrategy::AVERAGE); - self.filters.insert(FilterStrategy::PAETH); - self.apply_preset_5() + self.fast_evaluation = false; + self.filters = indexset! { + FilterStrategy::NONE, + FilterStrategy::SUB, + FilterStrategy::UP, + FilterStrategy::AVERAGE, + FilterStrategy::PAETH, + FilterStrategy::MinSum, + FilterStrategy::Entropy, + FilterStrategy::Bigrams, + FilterStrategy::BigEnt, + FilterStrategy::Brute { + num_lines: 8, + level: 5, + }, + }; + self.deflater = Deflater::Libdeflater { compression: 12 }; + self } } diff --git a/src/png/mod.rs b/src/png/mod.rs index e5129b91..913c2ab9 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -21,11 +21,6 @@ pub(crate) mod scan_lines; use self::scan_lines::ScanLines; -/// Compression level to use for the Brute filter strategy -const BRUTE_LEVEL: i32 = 1; // 1 is fastest, 2-4 are not useful, 5 is slower but more effective -/// Number of lines to compress with the Brute filter strategy -const BRUTE_LINES: usize = 4; // Values over 8 are generally not useful - #[derive(Debug, Clone)] pub struct PngImage { /// The headers stored in the IHDR chunk @@ -509,23 +504,23 @@ impl PngImage { } } } - FilterStrategy::Brute => { + FilterStrategy::Brute { num_lines, level } => { // Brute force by compressing each filter attempt // Similar to that of LodePNG but includes some previous lines for context let mut best_size = usize::MAX; let line_start = filtered.len(); filtered.resize(filtered.len() + line.data.len() + 1, 0); let mut compressor = - Compressor::new(CompressionLvl::new(BRUTE_LEVEL).unwrap()); - let limit = filtered.len().min((line.data.len() + 1) * BRUTE_LINES); - let capacity = compressor.zlib_compress_bound(limit); + Compressor::new(CompressionLvl::new(level.into()).unwrap()); + let limit = filtered.len().min((line.data.len() + 1) * num_lines); + let capacity = compressor.deflate_compress_bound(limit); let mut dest = vec![0; capacity]; for f in try_filters { f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes); filtered[line_start..].copy_from_slice(&f_buf); let size = compressor - .zlib_compress(&filtered[filtered.len() - limit..], &mut dest) + .deflate_compress(&filtered[filtered.len() - limit..], &mut dest) .unwrap_or(usize::MAX); if size < best_size { best_size = size; diff --git a/tests/strategies.rs b/tests/strategies.rs index de4db2c3..e935780f 100644 --- a/tests/strategies.rs +++ b/tests/strategies.rs @@ -110,7 +110,10 @@ fn filter_bigent() { fn filter_brute() { test_it_converts( "tests/files/palette_8_should_be_palette_8.png", - FilterStrategy::Brute, + FilterStrategy::Brute { + num_lines: 4, + level: 1, + }, INDEXED, BitDepth::Eight, INDEXED,