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 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,
|
||||||
|
)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 \
|
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")
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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()),
|
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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue