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 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 \
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")

View file

@ -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<RowFilter>),
}
@ -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),
}
}

View file

@ -199,6 +199,23 @@ fn parse_opts_into_struct(
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") {
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();

View file

@ -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
}
}

View file

@ -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;

View file

@ -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,