From 71290acc62253ba90565dfcb6c08295281caa30b Mon Sep 17 00:00:00 2001 From: andrews05 Date: Sat, 20 Sep 2025 06:36:17 +1200 Subject: [PATCH] Reduce memory usage of fast evaluation (#713) This is an experiment I started a while ago before life happened. It reduces memory usage of fast evaluation (-o2 and lower), bringing it inline with normal (slow) evaluation. It does this by not retaining the filtered image data of the evaluations, but instead retaining the row filters that were used in each line so it can be quickly re-filtered when required. This does incur a tiny performance penalty, but it's negligible even at `-o0`. Although this is fully functional, there are a few rough spots in the code so I'm just opening it as a draft for now. ``` PR -sao6 70.08 real 614.36 user 2.66 sys 2024243200 maximum resident set size 1839339520 peak memory footprint PR -sao2 11.16 real 60.85 user 1.33 sys 1982562304 maximum resident set size 1842173824 peak memory footprint PR -sao2 -t1 55.11 real 53.85 user 0.76 sys 429457408 maximum resident set size 245008064 peak memory footprint master -sao6 67.70 real 616.07 user 2.72 sys 2043379712 maximum resident set size 1838340416 peak memory footprint master -sao2 11.53 real 60.63 user 1.25 sys 2753396736 maximum resident set size 2283741440 peak memory footprint master -sao2 -t1 54.29 real 53.55 user 0.72 sys 626311168 maximum resident set size 305252544 peak memory footprint ``` Note that this involves some refactoring of `RowFilter` and the new `FilterStrategy`. These are breaking changes so it will ultimately be destined for v10. One advantage to this new structure is it opens the door for future changes such as allowing the Brute strategy to take a parameter for the number of lines. --- benches/filters.rs | 60 +++++++++--------- benches/strategies.rs | 10 +-- src/evaluate.rs | 33 +++++----- src/filters.rs | 68 +++++++++++++------- src/lib.rs | 32 +++++----- src/main.rs | 21 ++++--- src/options.rs | 35 ++++++----- src/png/mod.rs | 59 ++++++++++++------ tests/filters.rs | 141 +++++++++++++++++++++--------------------- tests/flags.rs | 4 +- tests/interlaced.rs | 7 +-- tests/interlacing.rs | 7 +-- tests/raw.rs | 2 +- tests/reduction.rs | 7 +-- tests/regression.rs | 7 +-- tests/strategies.rs | 21 +++---- 16 files changed, 280 insertions(+), 234 deletions(-) diff --git a/benches/filters.rs b/benches/filters.rs index 6cf1e4cb..f010e2e7 100644 --- a/benches/filters.rs +++ b/benches/filters.rs @@ -13,7 +13,7 @@ fn filters_16_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::None, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false)); } #[bench] @@ -21,7 +21,7 @@ fn filters_8_bits_filter_0(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(RowFilter::None, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false)); } #[bench] @@ -31,7 +31,7 @@ fn filters_4_bits_filter_0(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::None, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false)); } #[bench] @@ -41,7 +41,7 @@ fn filters_2_bits_filter_0(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::None, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false)); } #[bench] @@ -51,7 +51,7 @@ fn filters_1_bits_filter_0(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::None, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false)); } #[bench] @@ -59,7 +59,7 @@ fn filters_16_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Sub, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false)); } #[bench] @@ -67,7 +67,7 @@ fn filters_8_bits_filter_1(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(RowFilter::Sub, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false)); } #[bench] @@ -77,7 +77,7 @@ fn filters_4_bits_filter_1(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Sub, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false)); } #[bench] @@ -87,7 +87,7 @@ fn filters_2_bits_filter_1(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Sub, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false)); } #[bench] @@ -97,7 +97,7 @@ fn filters_1_bits_filter_1(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Sub, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false)); } #[bench] @@ -105,7 +105,7 @@ fn filters_16_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Up, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::UP, false)); } #[bench] @@ -113,7 +113,7 @@ fn filters_8_bits_filter_2(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(RowFilter::Up, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::UP, false)); } #[bench] @@ -123,7 +123,7 @@ fn filters_4_bits_filter_2(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Up, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::UP, false)); } #[bench] @@ -133,7 +133,7 @@ fn filters_2_bits_filter_2(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Up, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::UP, false)); } #[bench] @@ -143,7 +143,7 @@ fn filters_1_bits_filter_2(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Up, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::UP, false)); } #[bench] @@ -151,7 +151,7 @@ fn filters_16_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Average, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false)); } #[bench] @@ -159,7 +159,7 @@ fn filters_8_bits_filter_3(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(RowFilter::Average, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false)); } #[bench] @@ -169,7 +169,7 @@ fn filters_4_bits_filter_3(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Average, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false)); } #[bench] @@ -179,7 +179,7 @@ fn filters_2_bits_filter_3(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Average, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false)); } #[bench] @@ -189,7 +189,7 @@ fn filters_1_bits_filter_3(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Average, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false)); } #[bench] @@ -197,7 +197,7 @@ fn filters_16_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Paeth, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false)); } #[bench] @@ -205,7 +205,7 @@ fn filters_8_bits_filter_4(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(RowFilter::Paeth, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false)); } #[bench] @@ -215,7 +215,7 @@ fn filters_4_bits_filter_4(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Paeth, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false)); } #[bench] @@ -225,7 +225,7 @@ fn filters_2_bits_filter_4(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Paeth, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false)); } #[bench] @@ -235,7 +235,7 @@ fn filters_1_bits_filter_4(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::Paeth, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false)); } #[bench] @@ -243,7 +243,7 @@ fn filters_16_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::MinSum, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false)); } #[bench] @@ -251,7 +251,7 @@ fn filters_8_bits_filter_5(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(RowFilter::MinSum, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false)); } #[bench] @@ -261,7 +261,7 @@ fn filters_4_bits_filter_5(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::MinSum, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false)); } #[bench] @@ -271,7 +271,7 @@ fn filters_2_bits_filter_5(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::MinSum, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false)); } #[bench] @@ -281,5 +281,5 @@ fn filters_1_bits_filter_5(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| png.raw.filter_image(RowFilter::MinSum, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false)); } diff --git a/benches/strategies.rs b/benches/strategies.rs index 15f546f1..72d19e90 100644 --- a/benches/strategies.rs +++ b/benches/strategies.rs @@ -13,7 +13,7 @@ fn filters_minsum(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(RowFilter::MinSum, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false)); } #[bench] @@ -21,7 +21,7 @@ fn filters_entropy(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(RowFilter::Entropy, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::Entropy, false)); } #[bench] @@ -29,7 +29,7 @@ fn filters_bigrams(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(RowFilter::Bigrams, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::Bigrams, false)); } #[bench] @@ -37,7 +37,7 @@ fn filters_bigent(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(RowFilter::BigEnt, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::BigEnt, false)); } #[bench] @@ -45,5 +45,5 @@ 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(RowFilter::Brute, false)); + b.iter(|| png.raw.filter_image(FilterStrategy::Brute, false)); } diff --git a/src/evaluate.rs b/src/evaluate.rs index af6c4070..9463cf6b 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -17,15 +17,20 @@ use rayon::prelude::*; #[cfg(not(feature = "parallel"))] use crate::rayon; -use crate::{Deadline, PngError, atomicmin::AtomicMin, deflate, filters::RowFilter, png::PngImage}; +use crate::{ + Deadline, PngError, atomicmin::AtomicMin, deflate, filters::FilterStrategy, png::PngImage, +}; pub(crate) struct Candidate { pub image: Arc, - pub data: Vec, - pub data_is_compressed: bool, + pub idat_data: Option>, pub estimated_output_size: usize, - pub filter: RowFilter, - // For determining tie-breaker + /// The input filter, which is retained for printing and for APNG frames. + pub filter: FilterStrategy, + /// The filter returned by the filter function, which may be Predefined. + /// Use this for the next round to avoid recomputing the filter. + pub filter_used: FilterStrategy, + /// For determining tie-breaker nth: usize, } @@ -34,7 +39,7 @@ impl Candidate { ( self.estimated_output_size, self.image.data.len(), - self.filter, + self.filter.clone(), // Prefer the later image added (e.g. baseline, which is always added last) usize::MAX - self.nth, ) @@ -44,7 +49,7 @@ impl Candidate { /// Collect image versions and pick one that compresses best pub(crate) struct Evaluator { deadline: Arc, - filters: IndexSet, + filters: IndexSet, deflater: Deflaters, optimize_alpha: bool, final_round: bool, @@ -62,7 +67,7 @@ pub(crate) struct Evaluator { impl Evaluator { pub fn new( deadline: Arc, - filters: IndexSet, + filters: IndexSet, deflater: Deflaters, optimize_alpha: bool, final_round: bool, @@ -141,21 +146,21 @@ impl Evaluator { // which are dangerous to do in side Rayon's loop. // Instead, only update (atomic) best size in real time, // and the best result later without need for locks. - filters_iter.for_each(|&filter| { + filters_iter.for_each(|filter| { if deadline.passed() { return; } - let filtered = image.filter_image(filter, optimize_alpha); + let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha); let idat_data = deflater.deflate(&filtered, best_candidate_size.get()); if let Ok(idat_data) = idat_data { let estimated_output_size = image.estimated_output_size(&idat_data); - // For the final round we need the IDAT data, otherwise the filtered data + // We only need to retain the IDAT data in the final round let new = Candidate { image: image.clone(), - data: if final_round { idat_data } else { filtered }, - data_is_compressed: final_round, + idat_data: if final_round { Some(idat_data) } else { None }, estimated_output_size, - filter, + filter: filter.clone(), + filter_used, nth, }; best_candidate_size.set_min(estimated_output_size); diff --git a/src/filters.rs b/src/filters.rs index d0b03b1d..26e93b68 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,30 +1,62 @@ use std::{fmt, fmt::Display, mem::transmute}; -use crate::error::PngError; - /// Filtering strategy for use in [`Options`][crate::Options] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] +pub enum FilterStrategy { + /// Same filter for all rows + Basic(RowFilter), + /// Minimum sum of absolute differences + MinSum, + /// Shannon entropy + Entropy, + /// Count of distinct bigrams + Bigrams, + /// Shannon entropy of bigrams + BigEnt, + /// Deflate compression + Brute, + /// Predefined filter for each row + Predefined(Vec), +} + +impl FilterStrategy { + pub const NONE: Self = Self::Basic(RowFilter::None); + pub const SUB: Self = Self::Basic(RowFilter::Sub); + pub const UP: Self = Self::Basic(RowFilter::Up); + pub const AVERAGE: Self = Self::Basic(RowFilter::Average); + pub const PAETH: Self = Self::Basic(RowFilter::Paeth); +} + +impl Display for FilterStrategy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Basic(filter) => filter.fmt(f), + Self::MinSum => "MinSum".fmt(f), + Self::Entropy => "Entropy".fmt(f), + Self::Bigrams => "Bigrams".fmt(f), + Self::BigEnt => "BigEnt".fmt(f), + Self::Brute => "Brute".fmt(f), + Self::Predefined(_) => "Predefined".fmt(f), + } + } +} + +/// PNG delta filters #[repr(u8)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] pub enum RowFilter { - // Standard filter types None, Sub, Up, Average, Paeth, - // Heuristic strategies - MinSum, - Entropy, - Bigrams, - BigEnt, - Brute, } impl TryFrom for RowFilter { type Error = (); fn try_from(value: u8) -> Result { - if value > Self::LAST { + if value > 4 { return Err(()); } unsafe { transmute(value as i8) } @@ -40,11 +72,6 @@ impl Display for RowFilter { Self::Up => "Up", Self::Average => "Average", Self::Paeth => "Paeth", - Self::MinSum => "MinSum", - Self::Entropy => "Entropy", - Self::Bigrams => "Bigrams", - Self::BigEnt => "BigEnt", - Self::Brute => "Brute", }, f, ) @@ -52,9 +79,7 @@ impl Display for RowFilter { } impl RowFilter { - pub const LAST: u8 = Self::Brute as u8; - pub(crate) const STANDARD: [Self; 5] = - [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth]; + pub(crate) const ALL: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth]; pub(crate) const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub]; pub(crate) fn filter_line( @@ -115,7 +140,6 @@ impl RowFilter { }); } } - _ => unreachable!(), } } @@ -140,6 +164,7 @@ impl RowFilter { _ => i - 1, }; match self { + Self::None => unreachable!(), Self::Sub => { for j in 0..color_bytes { pixels[i][j] = pixels[prev][j]; @@ -171,7 +196,6 @@ impl RowFilter { }; } } - _ => unreachable!(), } } } @@ -183,7 +207,7 @@ impl RowFilter { data: &[u8], prev_line: &[u8], buf: &mut Vec, - ) -> Result<(), PngError> { + ) { buf.clear(); buf.reserve(data.len()); assert!(data.len() >= bpp); @@ -232,9 +256,7 @@ impl RowFilter { ); } } - _ => return Err(PngError::InvalidData), } - Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 1af2e8e3..d11c3ae3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ pub use crate::{ colors::{BitDepth, ColorType}, deflate::Deflaters, error::PngError, - filters::RowFilter, + filters::{FilterStrategy, RowFilter}, headers::StripChunks, interlace::Interlacing, options::{InFile, Options, OutFile}, @@ -170,7 +170,7 @@ impl RawImage { let mut png = PngData { raw: result.image, - idat_data: result.data, + idat_data: result.idat_data.unwrap(), aux_chunks, frames: Vec::new(), }; @@ -359,7 +359,7 @@ fn optimize_png( }; if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) { png.raw = result.image; - png.idat_data = result.data; + png.idat_data = result.idat_data.unwrap(); recompress_frames(png, &opts, deadline, result.filter)?; postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr); } @@ -432,7 +432,7 @@ fn optimize_raw( opts.filter.clone() } else { // None and Bigrams work well together, especially for alpha reductions - indexset! {RowFilter::None, RowFilter::Bigrams} + indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams} }; // This will collect all versions of images and pick one that compresses best let eval = Evaluator::new( @@ -472,7 +472,7 @@ fn optimize_raw( (eval_result?, eval_deflater) }; - if result.data_is_compressed + if result.idat_data.is_some() && max_size.is_none_or(|max_size| result.estimated_output_size < max_size) { debug!("Found better result:"); @@ -489,7 +489,7 @@ fn perform_trials( deadline: Arc, max_size: Option, mut eval_result: Option, - eval_filters: IndexSet, + eval_filters: IndexSet, eval_deflater: Deflaters, ) -> Option { let mut filters = opts.filter.clone(); @@ -499,7 +499,7 @@ fn perform_trials( if eval_result.is_some() { // Some filters have already been evaluated, we don't need to try them again - filters = filters.difference(&eval_filters).copied().collect(); + filters = filters.difference(&eval_filters).cloned().collect(); } if !filters.is_empty() { @@ -523,14 +523,14 @@ fn perform_trials( // We should have a result here - fail if not (e.g. deadline passed) let mut result = eval_result?; - if !result.data_is_compressed { + if result.idat_data.is_none() { // Compress with the main deflater debug!("Trying filter {} with {}", result.filter, opts.deflate); - match opts.deflate.deflate(&result.data, max_size) { + let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha); + match opts.deflate.deflate(&data, max_size) { Ok(idat_data) => { result.estimated_output_size = result.image.estimated_output_size(&idat_data); - result.data = idat_data; - result.data_is_compressed = true; + result.idat_data = Some(idat_data); trace!("{} bytes", result.estimated_output_size); } Err(PngError::DeflatedDataTooLong(bytes)) => { @@ -548,10 +548,10 @@ fn perform_trials( // Pick a filter automatically if image.ihdr.bit_depth as u8 >= 8 { // Bigrams is the best all-rounder when there's at least one byte per pixel - filters.insert(RowFilter::Bigrams); + filters.insert(FilterStrategy::Bigrams); } else { // Otherwise delta filters generally don't work well, so just stick with None - filters.insert(RowFilter::None); + filters.insert(FilterStrategy::NONE); } } @@ -627,11 +627,13 @@ fn recompress_frames( png: &mut PngData, opts: &Options, deadline: Arc, - filter: RowFilter, + filter: FilterStrategy, ) -> PngResult<()> { if !opts.idat_recoding || png.frames.is_empty() { return Ok(()); } + // Ensure we don't try to recompress frames with a predefined filter + debug_assert!(!matches!(filter, FilterStrategy::Predefined { .. })); png.frames .par_iter_mut() .with_max_len(1) @@ -644,7 +646,7 @@ fn recompress_frames( ihdr.width = frame.width; ihdr.height = frame.height; let image = PngImage::new(ihdr, &frame.data)?; - let filtered = image.filter_image(filter, opts.optimize_alpha); + let (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha); let max_size = Some(frame.data.len() - 1); if let Ok(data) = opts.deflate.deflate(&filtered, max_size) { debug!( diff --git a/src/main.rs b/src/main.rs index 12794f44..17909628 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,7 @@ use clap::ArgMatches; mod cli; use indexmap::IndexSet; use log::{Level, LevelFilter, error, warn}; -use oxipng::{Deflaters, InFile, Options, OutFile, PngError, RowFilter, StripChunks}; +use oxipng::{Deflaters, FilterStrategy, InFile, Options, OutFile, PngError, StripChunks}; use rayon::prelude::*; use crate::cli::DISPLAY_CHUNKS; @@ -36,8 +36,7 @@ fn main() -> ExitCode { // Set the value parser for filters which isn't appropriate to do in the build_command function .mut_arg("filters", |arg| { arg.value_parser(|x: &str| { - parse_numeric_range_opts(x, 0, RowFilter::LAST) - .map_err(|_| "Invalid option for filters") + parse_numeric_range_opts(x, 0, 9).map_err(|_| "Invalid option for filters") }) }) .after_help("Run `oxipng --help` to see full details of all options") @@ -199,10 +198,18 @@ fn parse_opts_into_struct( }; if let Some(x) = matches.get_one::>("filters") { - opts.filter.clear(); - for &f in x { - opts.filter.insert(f.try_into().unwrap()); - } + opts.filter = x + .iter() + .map(|&f| match f { + 0..=4 => FilterStrategy::Basic(f.try_into().unwrap()), + 5 => FilterStrategy::MinSum, + 6 => FilterStrategy::Entropy, + 7 => FilterStrategy::Bigrams, + 8 => FilterStrategy::BigEnt, + 9 => FilterStrategy::Brute, + _ => unreachable!(), + }) + .collect(); } if let Some(&num) = matches.get_one::("timeout") { diff --git a/src/options.rs b/src/options.rs index 60353f82..48f30141 100644 --- a/src/options.rs +++ b/src/options.rs @@ -7,7 +7,9 @@ use std::{ use indexmap::{IndexSet, indexset}; use log::warn; -use crate::{deflate::Deflaters, filters::RowFilter, headers::StripChunks, interlace::Interlacing}; +use crate::{ + deflate::Deflaters, filters::FilterStrategy, headers::StripChunks, interlace::Interlacing, +}; /// Write destination for [`optimize`][crate::optimize]. /// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O. @@ -94,10 +96,10 @@ pub struct Options { /// /// Default: `false` pub force: bool, - /// Which `RowFilters` to try on the file + /// Which `FilterStrategy` to try on the file /// /// Default: `None,Sub,Entropy,Bigrams` - pub filter: IndexSet, + pub filter: IndexSet, /// Whether to change the interlacing type of the file. /// /// These are the interlacing types avaliable: @@ -207,10 +209,10 @@ impl Options { fn apply_preset_3(mut self) -> Self { self.fast_evaluation = false; self.filter = indexset! { - RowFilter::None, - RowFilter::Bigrams, - RowFilter::BigEnt, - RowFilter::Brute + FilterStrategy::NONE, + FilterStrategy::Bigrams, + FilterStrategy::BigEnt, + FilterStrategy::Brute }; self } @@ -224,10 +226,10 @@ impl Options { fn apply_preset_5(mut self) -> Self { self.fast_evaluation = false; - self.filter.insert(RowFilter::Up); - self.filter.insert(RowFilter::MinSum); - self.filter.insert(RowFilter::BigEnt); - self.filter.insert(RowFilter::Brute); + self.filter.insert(FilterStrategy::UP); + self.filter.insert(FilterStrategy::MinSum); + self.filter.insert(FilterStrategy::BigEnt); + self.filter.insert(FilterStrategy::Brute); if let Deflaters::Libdeflater { compression } = &mut self.deflate { *compression = 12; } @@ -235,8 +237,8 @@ impl Options { } fn apply_preset_6(mut self) -> Self { - self.filter.insert(RowFilter::Average); - self.filter.insert(RowFilter::Paeth); + self.filter.insert(FilterStrategy::AVERAGE); + self.filter.insert(FilterStrategy::PAETH); self.apply_preset_5() } } @@ -247,7 +249,12 @@ impl Default for Options { Self { fix_errors: false, force: false, - filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams}, + filter: indexset! { + FilterStrategy::NONE, + FilterStrategy::SUB, + FilterStrategy::Entropy, + FilterStrategy::Bigrams + }, interlace: Some(Interlacing::None), optimize_alpha: false, bit_depth_reduction: true, diff --git a/src/png/mod.rs b/src/png/mod.rs index 66ba002d..c13c761e 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -376,7 +376,7 @@ impl PngImage { } last_line.resize(line.data.len(), 0); let filter = RowFilter::try_from(line.filter).map_err(|_| PngError::InvalidData)?; - filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf)?; + filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf); unfiltered.extend_from_slice(&unfiltered_buf); std::mem::swap(&mut last_line, &mut unfiltered_buf); unfiltered_buf.clear(); @@ -386,7 +386,11 @@ impl PngImage { /// Apply the specified filter type to all rows in the image #[must_use] - pub fn filter_image(&self, filter: RowFilter, optimize_alpha: bool) -> Vec { + pub fn filter_image( + &self, + strategy: FilterStrategy, + optimize_alpha: bool, + ) -> (Vec, FilterStrategy) { let mut filtered = Vec::with_capacity(self.data.len()); let bpp = self.bytes_per_channel() * self.channels_per_pixel(); // If alpha optimization is enabled, determine how many bytes of alpha there are per pixel @@ -399,31 +403,39 @@ impl PngImage { let mut prev_line = Vec::new(); let mut prev_pass: Option = None; let mut f_buf = Vec::new(); - for line in self.scan_lines(false) { + // For heuristic strategies, keep track of the actual filter used for each line + let mut filters_used = Vec::new(); + for (i, line) in self.scan_lines(false).enumerate() { if prev_pass != line.pass || line.data.len() != prev_line.len() { prev_line = vec![0; line.data.len()]; } // Alpha optimisation may alter the line data, so we need a mutable copy of it let mut line_data = line.data.to_vec(); - if filter <= RowFilter::Paeth { + if let FilterStrategy::Basic(mut filter) = strategy { // Standard filters - let filter = if prev_pass == line.pass || filter <= RowFilter::Sub { - filter - } else { - RowFilter::None - }; + if prev_pass != line.pass && filter > RowFilter::Sub { + filter = RowFilter::None; + } + filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes); + filtered.extend_from_slice(&f_buf); + prev_line = line_data; + } else if let FilterStrategy::Predefined(lines) = &strategy { + // Predefined filter for each line + let filter = lines.get(i).unwrap_or(&RowFilter::None); filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes); filtered.extend_from_slice(&f_buf); prev_line = line_data; } else { // Heuristic filter selection strategies + let mut best_filter = RowFilter::None; if line_data.iter().all(|&x| x == 0) { // Assume None if the line is all zeros - filtered.push(RowFilter::None as u8); + filtered.push(best_filter as u8); filtered.extend_from_slice(&line_data); prev_line = line_data; + filters_used.push(best_filter); continue; } @@ -431,12 +443,12 @@ impl PngImage { let mut best_line_raw = Vec::new(); // Avoid vertical filtering on first line of each interlacing pass let try_filters = if prev_pass == line.pass { - RowFilter::STANDARD.iter() + RowFilter::ALL.iter() } else { RowFilter::SINGLE_LINE.iter() }; - match filter { - RowFilter::MinSum => { + match strategy { + FilterStrategy::MinSum => { // MSAD algorithm mentioned in libpng reference docs // http://www.libpng.org/pub/png/book/chapter09.html let mut best_size = usize::MAX; @@ -450,10 +462,11 @@ impl PngImage { best_size = size; std::mem::swap(&mut best_line, &mut f_buf); best_line_raw.clone_from(&line_data); + best_filter = *f; } } } - RowFilter::Entropy => { + FilterStrategy::Entropy => { // Shannon entropy algorithm, from LodePNG // https://github.com/lvandeve/lodepng let mut best_size = i32::MIN; @@ -473,10 +486,11 @@ impl PngImage { best_size = size; std::mem::swap(&mut best_line, &mut f_buf); best_line_raw.clone_from(&line_data); + best_filter = *f; } } } - RowFilter::Bigrams => { + FilterStrategy::Bigrams => { // Count distinct bigrams, from pngwolf // https://bjoern.hoehrmann.de/pngwolf/ let mut best_size = usize::MAX; @@ -492,10 +506,11 @@ impl PngImage { best_size = size; std::mem::swap(&mut best_line, &mut f_buf); best_line_raw.clone_from(&line_data); + best_filter = *f; } } } - RowFilter::BigEnt => { + FilterStrategy::BigEnt => { // Bigram entropy, combined from Entropy and Bigrams filters let mut best_size = i32::MIN; // FxHasher is the fastest rust hasher currently available for this purpose @@ -512,10 +527,11 @@ impl PngImage { best_size = size; std::mem::swap(&mut best_line, &mut f_buf); best_line_raw.clone_from(&line_data); + best_filter = *f; } } } - RowFilter::Brute => { + FilterStrategy::Brute => { // 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; @@ -537,6 +553,7 @@ impl PngImage { best_size = size; std::mem::swap(&mut best_line, &mut f_buf); best_line_raw.clone_from(&line_data); + best_filter = *f; } } filtered.resize(line_start, 0); @@ -545,11 +562,17 @@ impl PngImage { } filtered.extend_from_slice(&best_line); prev_line = best_line_raw; + filters_used.push(best_filter); } prev_pass = line.pass; } - filtered + + if filters_used.is_empty() { + (filtered, strategy) + } else { + (filtered, FilterStrategy::Predefined(filters_used)) + } } } diff --git a/tests/filters.rs b/tests/filters.rs index 99984fda..3d696360 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -12,20 +12,17 @@ const GRAYSCALE_ALPHA: u8 = 4; const RGBA: u8 = 6; fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { - let mut options = oxipng::Options { + let options = oxipng::Options { force: true, + filter: indexset! {FilterStrategy::NONE}, ..Default::default() }; - let mut filter = IndexSet::new(); - filter.insert(RowFilter::None); - options.filter = filter; - (OutFile::from_path(input.with_extension("out.png")), options) } fn test_it_converts( input: &str, - filter: RowFilter, + filter: FilterStrategy, color_type_in: u8, bit_depth_in: BitDepth, color_type_out: u8, @@ -68,7 +65,7 @@ fn test_it_converts( fn filter_0_for_rgba_16() { test_it_converts( "tests/files/filter_0_for_rgba_16.png", - RowFilter::None, + FilterStrategy::NONE, RGBA, BitDepth::Sixteen, RGBA, @@ -80,7 +77,7 @@ fn filter_0_for_rgba_16() { fn filter_1_for_rgba_16() { test_it_converts( "tests/files/filter_1_for_rgba_16.png", - RowFilter::Sub, + FilterStrategy::SUB, RGBA, BitDepth::Sixteen, RGBA, @@ -92,7 +89,7 @@ fn filter_1_for_rgba_16() { fn filter_2_for_rgba_16() { test_it_converts( "tests/files/filter_2_for_rgba_16.png", - RowFilter::Up, + FilterStrategy::UP, RGBA, BitDepth::Sixteen, RGBA, @@ -104,7 +101,7 @@ fn filter_2_for_rgba_16() { fn filter_3_for_rgba_16() { test_it_converts( "tests/files/filter_3_for_rgba_16.png", - RowFilter::Average, + FilterStrategy::AVERAGE, RGBA, BitDepth::Sixteen, RGBA, @@ -116,7 +113,7 @@ fn filter_3_for_rgba_16() { fn filter_4_for_rgba_16() { test_it_converts( "tests/files/filter_4_for_rgba_16.png", - RowFilter::Paeth, + FilterStrategy::PAETH, RGBA, BitDepth::Sixteen, RGBA, @@ -128,7 +125,7 @@ fn filter_4_for_rgba_16() { fn filter_5_for_rgba_16() { test_it_converts( "tests/files/filter_5_for_rgba_16.png", - RowFilter::MinSum, + FilterStrategy::MinSum, RGBA, BitDepth::Sixteen, RGBA, @@ -140,7 +137,7 @@ fn filter_5_for_rgba_16() { fn filter_0_for_rgba_8() { test_it_converts( "tests/files/filter_0_for_rgba_8.png", - RowFilter::None, + FilterStrategy::NONE, RGBA, BitDepth::Eight, RGBA, @@ -152,7 +149,7 @@ fn filter_0_for_rgba_8() { fn filter_1_for_rgba_8() { test_it_converts( "tests/files/filter_1_for_rgba_8.png", - RowFilter::Sub, + FilterStrategy::SUB, RGBA, BitDepth::Eight, RGBA, @@ -164,7 +161,7 @@ fn filter_1_for_rgba_8() { fn filter_2_for_rgba_8() { test_it_converts( "tests/files/filter_2_for_rgba_8.png", - RowFilter::Up, + FilterStrategy::UP, RGBA, BitDepth::Eight, RGBA, @@ -176,7 +173,7 @@ fn filter_2_for_rgba_8() { fn filter_3_for_rgba_8() { test_it_converts( "tests/files/filter_3_for_rgba_8.png", - RowFilter::Average, + FilterStrategy::AVERAGE, RGBA, BitDepth::Eight, RGBA, @@ -188,7 +185,7 @@ fn filter_3_for_rgba_8() { fn filter_4_for_rgba_8() { test_it_converts( "tests/files/filter_4_for_rgba_8.png", - RowFilter::Paeth, + FilterStrategy::PAETH, RGBA, BitDepth::Eight, RGBA, @@ -200,7 +197,7 @@ fn filter_4_for_rgba_8() { fn filter_5_for_rgba_8() { test_it_converts( "tests/files/filter_5_for_rgba_8.png", - RowFilter::MinSum, + FilterStrategy::MinSum, RGBA, BitDepth::Eight, RGBA, @@ -212,7 +209,7 @@ fn filter_5_for_rgba_8() { fn filter_0_for_rgb_16() { test_it_converts( "tests/files/filter_0_for_rgb_16.png", - RowFilter::None, + FilterStrategy::NONE, RGB, BitDepth::Sixteen, RGB, @@ -224,7 +221,7 @@ fn filter_0_for_rgb_16() { fn filter_1_for_rgb_16() { test_it_converts( "tests/files/filter_1_for_rgb_16.png", - RowFilter::Sub, + FilterStrategy::SUB, RGB, BitDepth::Sixteen, RGB, @@ -236,7 +233,7 @@ fn filter_1_for_rgb_16() { fn filter_2_for_rgb_16() { test_it_converts( "tests/files/filter_2_for_rgb_16.png", - RowFilter::Up, + FilterStrategy::UP, RGB, BitDepth::Sixteen, RGB, @@ -248,7 +245,7 @@ fn filter_2_for_rgb_16() { fn filter_3_for_rgb_16() { test_it_converts( "tests/files/filter_3_for_rgb_16.png", - RowFilter::Average, + FilterStrategy::AVERAGE, RGB, BitDepth::Sixteen, RGB, @@ -260,7 +257,7 @@ fn filter_3_for_rgb_16() { fn filter_4_for_rgb_16() { test_it_converts( "tests/files/filter_4_for_rgb_16.png", - RowFilter::Paeth, + FilterStrategy::PAETH, RGB, BitDepth::Sixteen, RGB, @@ -272,7 +269,7 @@ fn filter_4_for_rgb_16() { fn filter_5_for_rgb_16() { test_it_converts( "tests/files/filter_5_for_rgb_16.png", - RowFilter::MinSum, + FilterStrategy::MinSum, RGB, BitDepth::Sixteen, RGB, @@ -284,7 +281,7 @@ fn filter_5_for_rgb_16() { fn filter_0_for_rgb_8() { test_it_converts( "tests/files/filter_0_for_rgb_8.png", - RowFilter::None, + FilterStrategy::NONE, RGB, BitDepth::Eight, RGB, @@ -296,7 +293,7 @@ fn filter_0_for_rgb_8() { fn filter_1_for_rgb_8() { test_it_converts( "tests/files/filter_1_for_rgb_8.png", - RowFilter::Sub, + FilterStrategy::SUB, RGB, BitDepth::Eight, RGB, @@ -308,7 +305,7 @@ fn filter_1_for_rgb_8() { fn filter_2_for_rgb_8() { test_it_converts( "tests/files/filter_2_for_rgb_8.png", - RowFilter::Up, + FilterStrategy::UP, RGB, BitDepth::Eight, RGB, @@ -320,7 +317,7 @@ fn filter_2_for_rgb_8() { fn filter_3_for_rgb_8() { test_it_converts( "tests/files/filter_3_for_rgb_8.png", - RowFilter::Average, + FilterStrategy::AVERAGE, RGB, BitDepth::Eight, RGB, @@ -332,7 +329,7 @@ fn filter_3_for_rgb_8() { fn filter_4_for_rgb_8() { test_it_converts( "tests/files/filter_4_for_rgb_8.png", - RowFilter::Paeth, + FilterStrategy::PAETH, RGB, BitDepth::Eight, RGB, @@ -344,7 +341,7 @@ fn filter_4_for_rgb_8() { fn filter_5_for_rgb_8() { test_it_converts( "tests/files/filter_5_for_rgb_8.png", - RowFilter::MinSum, + FilterStrategy::MinSum, RGB, BitDepth::Eight, RGB, @@ -356,7 +353,7 @@ fn filter_5_for_rgb_8() { fn filter_0_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_0_for_grayscale_alpha_16.png", - RowFilter::None, + FilterStrategy::NONE, GRAYSCALE_ALPHA, BitDepth::Sixteen, GRAYSCALE_ALPHA, @@ -368,7 +365,7 @@ fn filter_0_for_grayscale_alpha_16() { fn filter_1_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_1_for_grayscale_alpha_16.png", - RowFilter::Sub, + FilterStrategy::SUB, GRAYSCALE_ALPHA, BitDepth::Sixteen, GRAYSCALE_ALPHA, @@ -380,7 +377,7 @@ fn filter_1_for_grayscale_alpha_16() { fn filter_2_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_2_for_grayscale_alpha_16.png", - RowFilter::Up, + FilterStrategy::UP, GRAYSCALE_ALPHA, BitDepth::Sixteen, GRAYSCALE_ALPHA, @@ -392,7 +389,7 @@ fn filter_2_for_grayscale_alpha_16() { fn filter_3_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_3_for_grayscale_alpha_16.png", - RowFilter::Average, + FilterStrategy::AVERAGE, GRAYSCALE_ALPHA, BitDepth::Sixteen, GRAYSCALE_ALPHA, @@ -404,7 +401,7 @@ fn filter_3_for_grayscale_alpha_16() { fn filter_4_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_4_for_grayscale_alpha_16.png", - RowFilter::Paeth, + FilterStrategy::PAETH, GRAYSCALE_ALPHA, BitDepth::Sixteen, GRAYSCALE_ALPHA, @@ -416,7 +413,7 @@ fn filter_4_for_grayscale_alpha_16() { fn filter_5_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_5_for_grayscale_alpha_16.png", - RowFilter::MinSum, + FilterStrategy::MinSum, GRAYSCALE_ALPHA, BitDepth::Sixteen, GRAYSCALE_ALPHA, @@ -428,7 +425,7 @@ fn filter_5_for_grayscale_alpha_16() { fn filter_0_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_0_for_grayscale_alpha_8.png", - RowFilter::None, + FilterStrategy::NONE, GRAYSCALE_ALPHA, BitDepth::Eight, GRAYSCALE_ALPHA, @@ -440,7 +437,7 @@ fn filter_0_for_grayscale_alpha_8() { fn filter_1_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_1_for_grayscale_alpha_8.png", - RowFilter::Sub, + FilterStrategy::SUB, GRAYSCALE_ALPHA, BitDepth::Eight, GRAYSCALE_ALPHA, @@ -452,7 +449,7 @@ fn filter_1_for_grayscale_alpha_8() { fn filter_2_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_2_for_grayscale_alpha_8.png", - RowFilter::Up, + FilterStrategy::UP, GRAYSCALE_ALPHA, BitDepth::Eight, GRAYSCALE_ALPHA, @@ -464,7 +461,7 @@ fn filter_2_for_grayscale_alpha_8() { fn filter_3_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_3_for_grayscale_alpha_8.png", - RowFilter::Average, + FilterStrategy::AVERAGE, GRAYSCALE_ALPHA, BitDepth::Eight, GRAYSCALE_ALPHA, @@ -476,7 +473,7 @@ fn filter_3_for_grayscale_alpha_8() { fn filter_4_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_4_for_grayscale_alpha_8.png", - RowFilter::Paeth, + FilterStrategy::PAETH, GRAYSCALE_ALPHA, BitDepth::Eight, GRAYSCALE_ALPHA, @@ -488,7 +485,7 @@ fn filter_4_for_grayscale_alpha_8() { fn filter_5_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_5_for_grayscale_alpha_8.png", - RowFilter::MinSum, + FilterStrategy::MinSum, GRAYSCALE_ALPHA, BitDepth::Eight, GRAYSCALE_ALPHA, @@ -500,7 +497,7 @@ fn filter_5_for_grayscale_alpha_8() { fn filter_0_for_grayscale_16() { test_it_converts( "tests/files/filter_0_for_grayscale_16.png", - RowFilter::None, + FilterStrategy::NONE, GRAYSCALE, BitDepth::Sixteen, GRAYSCALE, @@ -512,7 +509,7 @@ fn filter_0_for_grayscale_16() { fn filter_1_for_grayscale_16() { test_it_converts( "tests/files/filter_1_for_grayscale_16.png", - RowFilter::Sub, + FilterStrategy::SUB, GRAYSCALE, BitDepth::Sixteen, GRAYSCALE, @@ -524,7 +521,7 @@ fn filter_1_for_grayscale_16() { fn filter_2_for_grayscale_16() { test_it_converts( "tests/files/filter_2_for_grayscale_16.png", - RowFilter::Up, + FilterStrategy::UP, GRAYSCALE, BitDepth::Sixteen, GRAYSCALE, @@ -536,7 +533,7 @@ fn filter_2_for_grayscale_16() { fn filter_3_for_grayscale_16() { test_it_converts( "tests/files/filter_3_for_grayscale_16.png", - RowFilter::Average, + FilterStrategy::AVERAGE, GRAYSCALE, BitDepth::Sixteen, GRAYSCALE, @@ -548,7 +545,7 @@ fn filter_3_for_grayscale_16() { fn filter_4_for_grayscale_16() { test_it_converts( "tests/files/filter_4_for_grayscale_16.png", - RowFilter::Paeth, + FilterStrategy::PAETH, GRAYSCALE, BitDepth::Sixteen, GRAYSCALE, @@ -560,7 +557,7 @@ fn filter_4_for_grayscale_16() { fn filter_5_for_grayscale_16() { test_it_converts( "tests/files/filter_5_for_grayscale_16.png", - RowFilter::MinSum, + FilterStrategy::MinSum, GRAYSCALE, BitDepth::Sixteen, GRAYSCALE, @@ -572,7 +569,7 @@ fn filter_5_for_grayscale_16() { fn filter_0_for_grayscale_8() { test_it_converts( "tests/files/filter_0_for_grayscale_8.png", - RowFilter::None, + FilterStrategy::NONE, GRAYSCALE, BitDepth::Eight, GRAYSCALE, @@ -584,7 +581,7 @@ fn filter_0_for_grayscale_8() { fn filter_1_for_grayscale_8() { test_it_converts( "tests/files/filter_1_for_grayscale_8.png", - RowFilter::Sub, + FilterStrategy::SUB, GRAYSCALE, BitDepth::Eight, GRAYSCALE, @@ -596,7 +593,7 @@ fn filter_1_for_grayscale_8() { fn filter_2_for_grayscale_8() { test_it_converts( "tests/files/filter_2_for_grayscale_8.png", - RowFilter::Up, + FilterStrategy::UP, GRAYSCALE, BitDepth::Eight, GRAYSCALE, @@ -608,7 +605,7 @@ fn filter_2_for_grayscale_8() { fn filter_3_for_grayscale_8() { test_it_converts( "tests/files/filter_3_for_grayscale_8.png", - RowFilter::Average, + FilterStrategy::AVERAGE, GRAYSCALE, BitDepth::Eight, GRAYSCALE, @@ -620,7 +617,7 @@ fn filter_3_for_grayscale_8() { fn filter_4_for_grayscale_8() { test_it_converts( "tests/files/filter_4_for_grayscale_8.png", - RowFilter::Paeth, + FilterStrategy::PAETH, GRAYSCALE, BitDepth::Eight, GRAYSCALE, @@ -632,7 +629,7 @@ fn filter_4_for_grayscale_8() { fn filter_5_for_grayscale_8() { test_it_converts( "tests/files/filter_5_for_grayscale_8.png", - RowFilter::MinSum, + FilterStrategy::MinSum, GRAYSCALE, BitDepth::Eight, GRAYSCALE, @@ -644,7 +641,7 @@ fn filter_5_for_grayscale_8() { fn filter_0_for_palette_4() { test_it_converts( "tests/files/filter_0_for_palette_4.png", - RowFilter::None, + FilterStrategy::NONE, INDEXED, BitDepth::Four, INDEXED, @@ -656,7 +653,7 @@ fn filter_0_for_palette_4() { fn filter_1_for_palette_4() { test_it_converts( "tests/files/filter_1_for_palette_4.png", - RowFilter::Sub, + FilterStrategy::SUB, INDEXED, BitDepth::Four, INDEXED, @@ -668,7 +665,7 @@ fn filter_1_for_palette_4() { fn filter_2_for_palette_4() { test_it_converts( "tests/files/filter_2_for_palette_4.png", - RowFilter::Up, + FilterStrategy::UP, INDEXED, BitDepth::Four, INDEXED, @@ -680,7 +677,7 @@ fn filter_2_for_palette_4() { fn filter_3_for_palette_4() { test_it_converts( "tests/files/filter_3_for_palette_4.png", - RowFilter::Average, + FilterStrategy::AVERAGE, INDEXED, BitDepth::Four, INDEXED, @@ -692,7 +689,7 @@ fn filter_3_for_palette_4() { fn filter_4_for_palette_4() { test_it_converts( "tests/files/filter_4_for_palette_4.png", - RowFilter::Paeth, + FilterStrategy::PAETH, INDEXED, BitDepth::Four, INDEXED, @@ -704,7 +701,7 @@ fn filter_4_for_palette_4() { fn filter_5_for_palette_4() { test_it_converts( "tests/files/filter_5_for_palette_4.png", - RowFilter::MinSum, + FilterStrategy::MinSum, INDEXED, BitDepth::Four, INDEXED, @@ -716,7 +713,7 @@ fn filter_5_for_palette_4() { fn filter_0_for_palette_2() { test_it_converts( "tests/files/filter_0_for_palette_2.png", - RowFilter::None, + FilterStrategy::NONE, INDEXED, BitDepth::Two, INDEXED, @@ -728,7 +725,7 @@ fn filter_0_for_palette_2() { fn filter_1_for_palette_2() { test_it_converts( "tests/files/filter_1_for_palette_2.png", - RowFilter::Sub, + FilterStrategy::SUB, INDEXED, BitDepth::Two, INDEXED, @@ -740,7 +737,7 @@ fn filter_1_for_palette_2() { fn filter_2_for_palette_2() { test_it_converts( "tests/files/filter_2_for_palette_2.png", - RowFilter::Up, + FilterStrategy::UP, INDEXED, BitDepth::Two, INDEXED, @@ -752,7 +749,7 @@ fn filter_2_for_palette_2() { fn filter_3_for_palette_2() { test_it_converts( "tests/files/filter_3_for_palette_2.png", - RowFilter::Average, + FilterStrategy::AVERAGE, INDEXED, BitDepth::Two, INDEXED, @@ -764,7 +761,7 @@ fn filter_3_for_palette_2() { fn filter_4_for_palette_2() { test_it_converts( "tests/files/filter_4_for_palette_2.png", - RowFilter::Paeth, + FilterStrategy::PAETH, INDEXED, BitDepth::Two, INDEXED, @@ -776,7 +773,7 @@ fn filter_4_for_palette_2() { fn filter_5_for_palette_2() { test_it_converts( "tests/files/filter_5_for_palette_2.png", - RowFilter::MinSum, + FilterStrategy::MinSum, INDEXED, BitDepth::Two, INDEXED, @@ -788,7 +785,7 @@ fn filter_5_for_palette_2() { fn filter_0_for_palette_1() { test_it_converts( "tests/files/filter_0_for_palette_1.png", - RowFilter::None, + FilterStrategy::NONE, INDEXED, BitDepth::One, INDEXED, @@ -800,7 +797,7 @@ fn filter_0_for_palette_1() { fn filter_1_for_palette_1() { test_it_converts( "tests/files/filter_1_for_palette_1.png", - RowFilter::Sub, + FilterStrategy::SUB, INDEXED, BitDepth::One, INDEXED, @@ -812,7 +809,7 @@ fn filter_1_for_palette_1() { fn filter_2_for_palette_1() { test_it_converts( "tests/files/filter_2_for_palette_1.png", - RowFilter::Up, + FilterStrategy::UP, INDEXED, BitDepth::One, INDEXED, @@ -824,7 +821,7 @@ fn filter_2_for_palette_1() { fn filter_3_for_palette_1() { test_it_converts( "tests/files/filter_3_for_palette_1.png", - RowFilter::Average, + FilterStrategy::AVERAGE, INDEXED, BitDepth::One, INDEXED, @@ -836,7 +833,7 @@ fn filter_3_for_palette_1() { fn filter_4_for_palette_1() { test_it_converts( "tests/files/filter_4_for_palette_1.png", - RowFilter::Paeth, + FilterStrategy::PAETH, INDEXED, BitDepth::One, INDEXED, @@ -848,7 +845,7 @@ fn filter_4_for_palette_1() { fn filter_5_for_palette_1() { test_it_converts( "tests/files/filter_5_for_palette_1.png", - RowFilter::MinSum, + FilterStrategy::MinSum, INDEXED, BitDepth::One, INDEXED, diff --git a/tests/flags.rs b/tests/flags.rs index 1ecffe63..4baa619b 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -16,7 +16,7 @@ fn get_opts(input: &Path) -> (OutFile, Options) { let options = Options { force: true, fast_evaluation: false, - filter: indexset! {RowFilter::None}, + filter: indexset! {FilterStrategy::NONE}, ..Default::default() }; (OutFile::from_path(input.with_extension("out.png")), options) @@ -409,7 +409,7 @@ fn interlaced_0_to_1_other_filter_mode() { let input = PathBuf::from("tests/files/interlaced_0_to_1_other_filter_mode.png"); let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::Adam7); - opts.filter = indexset! {RowFilter::Paeth}; + opts.filter = indexset! {FilterStrategy::PAETH}; test_it_converts_callbacks( input, diff --git a/tests/interlaced.rs b/tests/interlaced.rs index a8931419..b9007d9b 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -12,16 +12,13 @@ const GRAYSCALE_ALPHA: u8 = 4; const RGBA: u8 = 6; fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { - let mut options = oxipng::Options { + let options = oxipng::Options { force: true, fast_evaluation: false, + filter: indexset! {FilterStrategy::NONE}, interlace: None, ..Default::default() }; - let mut filter = IndexSet::new(); - filter.insert(RowFilter::None); - options.filter = filter; - (OutFile::from_path(input.with_extension("out.png")), options) } diff --git a/tests/interlacing.rs b/tests/interlacing.rs index fd72df08..ea7fd5a9 100644 --- a/tests/interlacing.rs +++ b/tests/interlacing.rs @@ -9,14 +9,11 @@ const RGB: u8 = 2; const INDEXED: u8 = 3; fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { - let mut options = oxipng::Options { + let options = oxipng::Options { force: true, + filter: indexset! {FilterStrategy::NONE}, ..Default::default() }; - let mut filter = IndexSet::new(); - filter.insert(RowFilter::None); - options.filter = filter; - (OutFile::from_path(input.with_extension("out.png")), options) } diff --git a/tests/raw.rs b/tests/raw.rs index 3bf49591..d50a58e1 100644 --- a/tests/raw.rs +++ b/tests/raw.rs @@ -5,7 +5,7 @@ use oxipng::{internal_tests::*, *}; fn get_opts() -> Options { Options { force: true, - filter: indexset! { RowFilter::None }, + filter: indexset! {FilterStrategy::NONE}, ..Default::default() } } diff --git a/tests/reduction.rs b/tests/reduction.rs index 4e33f7e2..6ba3236d 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -12,15 +12,12 @@ const GRAYSCALE_ALPHA: u8 = 4; const RGBA: u8 = 6; fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { - let mut options = oxipng::Options { + let options = oxipng::Options { force: true, fast_evaluation: false, + filter: indexset! {FilterStrategy::NONE}, ..Default::default() }; - let mut filter = IndexSet::new(); - filter.insert(RowFilter::None); - options.filter = filter; - (OutFile::from_path(input.with_extension("out.png")), options) } diff --git a/tests/regression.rs b/tests/regression.rs index 765a04f5..89756c29 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -12,14 +12,11 @@ const GRAYSCALE_ALPHA: u8 = 4; const RGBA: u8 = 6; fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { - let mut options = oxipng::Options { + let options = oxipng::Options { force: true, + filter: indexset! {FilterStrategy::NONE}, ..Default::default() }; - let mut filter = IndexSet::new(); - filter.insert(RowFilter::None); - options.filter = filter; - (OutFile::from_path(input.with_extension("out.png")), options) } diff --git a/tests/strategies.rs b/tests/strategies.rs index 7f8e0bcc..264e6983 100644 --- a/tests/strategies.rs +++ b/tests/strategies.rs @@ -11,20 +11,16 @@ const INDEXED: u8 = 3; const RGBA: u8 = 6; fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { - let mut options = oxipng::Options { + let options = oxipng::Options { force: true, ..Default::default() }; - let mut filter = IndexSet::new(); - filter.insert(RowFilter::None); - options.filter = filter; - (OutFile::from_path(input.with_extension("out.png")), options) } fn test_it_converts( input: &str, - filter: RowFilter, + filter: FilterStrategy, color_type_in: u8, bit_depth_in: BitDepth, color_type_out: u8, @@ -34,8 +30,7 @@ fn test_it_converts( let (output, mut opts) = get_opts(&input); let png = PngData::new(&input, &opts).unwrap(); - opts.filter = IndexSet::new(); - opts.filter.insert(filter); + opts.filter = indexset! {filter}; assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); @@ -67,7 +62,7 @@ fn test_it_converts( fn filter_minsum() { test_it_converts( "tests/files/rgb_16_should_be_rgb_16.png", - RowFilter::MinSum, + FilterStrategy::MinSum, RGB, BitDepth::Sixteen, RGB, @@ -79,7 +74,7 @@ fn filter_minsum() { fn filter_entropy() { test_it_converts( "tests/files/rgb_8_should_be_rgb_8.png", - RowFilter::Entropy, + FilterStrategy::Entropy, RGB, BitDepth::Eight, RGB, @@ -91,7 +86,7 @@ fn filter_entropy() { fn filter_bigrams() { test_it_converts( "tests/files/rgba_8_should_be_rgba_8.png", - RowFilter::Bigrams, + FilterStrategy::Bigrams, RGBA, BitDepth::Eight, RGBA, @@ -103,7 +98,7 @@ fn filter_bigrams() { fn filter_bigent() { test_it_converts( "tests/files/grayscale_8_should_be_grayscale_8.png", - RowFilter::BigEnt, + FilterStrategy::BigEnt, GRAYSCALE, BitDepth::Eight, GRAYSCALE, @@ -115,7 +110,7 @@ fn filter_bigent() { fn filter_brute() { test_it_converts( "tests/files/palette_8_should_be_palette_8.png", - RowFilter::Brute, + FilterStrategy::Brute, INDEXED, BitDepth::Eight, INDEXED,