Allow brute filter configuration (#720)

This PR allows the Brute filter parameters to be configured rather than
using fixed constants. It also tweaks the presets to take advantage of
these new options:
o3: level=1, lines=3 (fractionally worse/faster)
o4: level=1, lines=4 (same as before)
o5: level=4, lines=4 (fractionally better/slower)
o6: level=5, lines=8 (slightly better/slower)

The parameters can be explicitly set in both the API and the CLI.
This commit is contained in:
andrews05 2025-09-26 09:20:29 +12:00 committed by GitHub
parent 7cc91b1308
commit aebddf443a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 124 additions and 44 deletions

View file

@ -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 input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap(); 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,
)
});
} }

View file

@ -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 \ compression. Lower levels are faster, higher levels provide better compression, though \
with increasingly diminishing returns. with increasingly diminishing returns.
0 => --zc 5 --fast (1 trial, determined heuristically) 0 => --zc 5 --fast (filter chosen heuristically)
1 => --zc 10 --fast (1 trial, determined heuristically) 1 => --zc 10 --fast (filter chosen heuristically)
2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial) 2 => --zc 11 -f 0,1,6,7 --fast
3 => --zc 11 -f 0,7,8,9 (4 trials) 3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
4 => --zc 12 -f 0,7,8,9 (4 trials) 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 (8 trials) 5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
6 => --zc 12 -f 0-9 (10 trials) 6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
max => (stable alias for the max level) max => (stable alias for the maximum level)
Manually specifying a compression option (zc, f, etc.) will override the optimization \ Manually specifying a compression option (zc, f, etc.) will override the optimization \
preset, regardless of the order you write the arguments.") 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)) .value_parser(value_parser!(NonZeroU64))
.requires("zopfli"), .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(
Arg::new("timeout") Arg::new("timeout")
.help("Maximum amount of time to spend on optimizations") .help("Maximum amount of time to spend on optimizations")

View file

@ -14,7 +14,12 @@ pub enum FilterStrategy {
/// Shannon entropy of bigrams /// Shannon entropy of bigrams
BigEnt, BigEnt,
/// Deflate compression /// 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 filter for each row
Predefined(Vec<RowFilter>), Predefined(Vec<RowFilter>),
} }
@ -35,7 +40,7 @@ impl Display for FilterStrategy {
Self::Entropy => "Entropy".fmt(f), Self::Entropy => "Entropy".fmt(f),
Self::Bigrams => "Bigrams".fmt(f), Self::Bigrams => "Bigrams".fmt(f),
Self::BigEnt => "BigEnt".fmt(f), Self::BigEnt => "BigEnt".fmt(f),
Self::Brute => "Brute".fmt(f), Self::Brute { .. } => "Brute".fmt(f),
Self::Predefined(_) => "Predefined".fmt(f), Self::Predefined(_) => "Predefined".fmt(f),
} }
} }

View file

@ -199,6 +199,23 @@ fn parse_opts_into_struct(
Some(level) => Options::from_preset(level.parse::<u8>().unwrap()), Some(level) => Options::from_preset(level.parse::<u8>().unwrap()),
}; };
// Get custom brute settings and rebuild the filter set to apply them
let mut brute_lines = matches.get_one::<usize>("brute-lines").cloned();
let mut brute_level = matches.get_one::<i64>("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::<IndexSet<u8>>("filters") { if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
opts.filters = x opts.filters = x
.iter() .iter()
@ -208,7 +225,10 @@ fn parse_opts_into_struct(
6 => FilterStrategy::Entropy, 6 => FilterStrategy::Entropy,
7 => FilterStrategy::Bigrams, 7 => FilterStrategy::Bigrams,
8 => FilterStrategy::BigEnt, 8 => FilterStrategy::BigEnt,
9 => FilterStrategy::Brute, 9 => FilterStrategy::Brute {
num_lines: brute_lines.unwrap_or(3),
level: brute_level.unwrap_or(1),
},
_ => unreachable!(), _ => unreachable!(),
}) })
.collect(); .collect();

View file

@ -189,17 +189,13 @@ impl Options {
// 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.filters.clear(); self.filters.clear();
if let Deflater::Libdeflater { compression } = &mut self.deflater { self.deflater = Deflater::Libdeflater { compression: 5 };
*compression = 5;
}
self self
} }
fn apply_preset_1(mut self) -> Self { fn apply_preset_1(mut self) -> Self {
self.filters.clear(); self.filters.clear();
if let Deflater::Libdeflater { compression } = &mut self.deflater { self.deflater = Deflater::Libdeflater { compression: 10 };
*compression = 10;
}
self self
} }
@ -213,34 +209,67 @@ impl Options {
FilterStrategy::NONE, FilterStrategy::NONE,
FilterStrategy::Bigrams, FilterStrategy::Bigrams,
FilterStrategy::BigEnt, FilterStrategy::BigEnt,
FilterStrategy::Brute FilterStrategy::Brute {
num_lines: 3,
level: 1,
},
}; };
self self
} }
fn apply_preset_4(mut self) -> Self { fn apply_preset_4(mut self) -> Self {
if let Deflater::Libdeflater { compression } = &mut self.deflater { self.fast_evaluation = false;
*compression = 12; self.filters = indexset! {
} FilterStrategy::NONE,
self.apply_preset_3() 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 { fn apply_preset_5(mut self) -> Self {
self.fast_evaluation = false; self.fast_evaluation = false;
self.filters.insert(FilterStrategy::UP); self.filters = indexset! {
self.filters.insert(FilterStrategy::MinSum); FilterStrategy::NONE,
self.filters.insert(FilterStrategy::BigEnt); FilterStrategy::SUB,
self.filters.insert(FilterStrategy::Brute); FilterStrategy::UP,
if let Deflater::Libdeflater { compression } = &mut self.deflater { FilterStrategy::MinSum,
*compression = 12; FilterStrategy::Entropy,
} FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 4,
level: 4,
},
};
self.deflater = Deflater::Libdeflater { compression: 12 };
self self
} }
fn apply_preset_6(mut self) -> Self { fn apply_preset_6(mut self) -> Self {
self.filters.insert(FilterStrategy::AVERAGE); self.fast_evaluation = false;
self.filters.insert(FilterStrategy::PAETH); self.filters = indexset! {
self.apply_preset_5() 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
} }
} }

View file

@ -21,11 +21,6 @@ pub(crate) mod scan_lines;
use self::scan_lines::ScanLines; 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)] #[derive(Debug, Clone)]
pub struct PngImage { pub struct PngImage {
/// The headers stored in the IHDR chunk /// 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 // Brute force by compressing each filter attempt
// Similar to that of LodePNG but includes some previous lines for context // Similar to that of LodePNG but includes some previous lines for context
let mut best_size = usize::MAX; let mut best_size = usize::MAX;
let line_start = filtered.len(); let line_start = filtered.len();
filtered.resize(filtered.len() + line.data.len() + 1, 0); filtered.resize(filtered.len() + line.data.len() + 1, 0);
let mut compressor = let mut compressor =
Compressor::new(CompressionLvl::new(BRUTE_LEVEL).unwrap()); Compressor::new(CompressionLvl::new(level.into()).unwrap());
let limit = filtered.len().min((line.data.len() + 1) * BRUTE_LINES); let limit = filtered.len().min((line.data.len() + 1) * num_lines);
let capacity = compressor.zlib_compress_bound(limit); let capacity = compressor.deflate_compress_bound(limit);
let mut dest = vec![0; capacity]; let mut dest = vec![0; capacity];
for f in try_filters { for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes); f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered[line_start..].copy_from_slice(&f_buf); filtered[line_start..].copy_from_slice(&f_buf);
let size = compressor let size = compressor
.zlib_compress(&filtered[filtered.len() - limit..], &mut dest) .deflate_compress(&filtered[filtered.len() - limit..], &mut dest)
.unwrap_or(usize::MAX); .unwrap_or(usize::MAX);
if size < best_size { if size < best_size {
best_size = size; best_size = size;

View file

@ -110,7 +110,10 @@ fn filter_bigent() {
fn filter_brute() { fn filter_brute() {
test_it_converts( test_it_converts(
"tests/files/palette_8_should_be_palette_8.png", "tests/files/palette_8_should_be_palette_8.png",
FilterStrategy::Brute, FilterStrategy::Brute {
num_lines: 4,
level: 1,
},
INDEXED, INDEXED,
BitDepth::Eight, BitDepth::Eight,
INDEXED, INDEXED,