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:
parent
7cc91b1308
commit
aebddf443a
7 changed files with 124 additions and 44 deletions
|
|
@ -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,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
|
|
|||
36
src/cli.rs
36
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")
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
src/main.rs
22
src/main.rs
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue