From a2a5993abf5d9392c8cf396b498b286db54c6b7e Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 4 May 2023 16:33:33 +1200 Subject: [PATCH] Refactor reduction evaluation sequence --- benches/reductions.rs | 26 +++++---- src/colors.rs | 9 +++ src/lib.rs | 91 ++----------------------------- src/reduction/color.rs | 6 +- src/reduction/mod.rs | 121 ++++++++++++++++++++++++++++++----------- tests/regression.rs | 6 +- 6 files changed, 125 insertions(+), 134 deletions(-) diff --git a/benches/reductions.rs b/benches/reductions.rs index 4db42e64b..c5551bf56 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -140,7 +140,7 @@ fn reductions_rgba_to_rgb_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_16_should_be_rgb_16.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } #[bench] @@ -148,7 +148,7 @@ fn reductions_rgba_to_rgb_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } #[bench] @@ -158,7 +158,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -168,7 +168,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -178,7 +178,10 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| { + color::reduce_rgb_to_grayscale(&png.raw) + .and_then(|r| alpha::reduced_alpha_channel(&r, false)) + }); } #[bench] @@ -188,7 +191,10 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| { + color::reduce_rgb_to_grayscale(&png.raw) + .and_then(|r| alpha::reduced_alpha_channel(&r, false)) + }); } #[bench] @@ -198,7 +204,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -206,7 +212,7 @@ fn reductions_rgb_to_grayscale_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -214,7 +220,7 @@ fn reductions_rgba_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_to_palette(&png.raw)); } #[bench] @@ -222,7 +228,7 @@ fn reductions_rgb_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_to_palette(&png.raw)); } #[bench] diff --git a/src/colors.rs b/src/colors.rs index 329998494..474b0ad25 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -76,6 +76,15 @@ impl ColorType { pub fn has_alpha(&self) -> bool { matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) } + + #[inline] + pub fn has_trns(&self) -> bool { + match self { + ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(), + ColorType::RGB { transparent_color } => transparent_color.is_some(), + _ => false, + } + } } #[repr(u8)] diff --git a/src/lib.rs b/src/lib.rs index 7d9845990..b33d57c45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ extern crate rayon; mod rayon; use crate::atomicmin::AtomicMin; -use crate::colors::{BitDepth, ColorType}; +use crate::colors::ColorType; use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; use crate::png::PngData; @@ -477,23 +477,6 @@ fn optimize_png( perform_strip(png, opts); let stripped_png = png.clone(); - // Interlacing is not part of the evaluator trials but must be done first to evaluate the rest correctly - let mut reduction_occurred = false; - if let Some(interlacing) = opts.interlace { - if let Some(reduced) = png.raw.change_interlacing(interlacing) { - png.raw = Arc::new(reduced); - reduction_occurred = true; - } - } - - // If alpha optimization is enabled, perform a black alpha reduction before evaluating reductions - // This can allow reductions from alpha to indexed which may not have been possible otherwise - if opts.optimize_alpha { - if let Some(reduced) = cleaned_alpha_channel(&png.raw) { - png.raw = Arc::new(reduced); - } - } - // Must use normal (lazy) compression, as faster ones (greedy) are not representative let eval_compression = 5; // None and Bigrams work well together, especially for alpha reductions @@ -505,7 +488,9 @@ fn optimize_png( eval_compression, false, ); - perform_reductions(png.raw.clone(), opts, &deadline, &eval); + let (baseline, mut reduction_occurred) = + perform_reductions(png.raw.clone(), opts, &deadline, &eval); + png.raw = baseline; let mut eval_filter = if let Some(result) = eval.get_best_candidate() { *png = result.image; if result.is_reduction { @@ -664,74 +649,6 @@ fn optimize_png( Ok(output) } -fn perform_reductions( - mut png: Arc, - opts: &Options, - deadline: &Deadline, - eval: &Evaluator, -) { - // The eval baseline will be set from the original png only if we attempt any reductions - let baseline = png.clone(); - let mut reduction_occurred = false; - - if opts.palette_reduction { - if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) { - png = Arc::new(reduced); - eval.try_image(png.clone()); - reduction_occurred = true; - } - if deadline.passed() { - return; - } - } - - if opts.bit_depth_reduction { - if let Some(reduced) = reduce_bit_depth_16_to_8(&png) { - png = Arc::new(reduced); - eval.try_image(png.clone()); - reduction_occurred = true; - } - if deadline.passed() { - return; - } - } - - if opts.color_type_reduction { - if let Some(reduced) = - reduce_color_type(&png, opts.grayscale_reduction, opts.optimize_alpha) - { - png = Arc::new(reduced); - eval.try_image(png.clone()); - reduction_occurred = true; - } - if deadline.passed() { - return; - } - } - - if opts.bit_depth_reduction { - if let Some(reduced) = reduce_bit_depth_8_or_less(&png, 1) { - let previous = png; - png = Arc::new(reduced); - eval.try_image(png.clone()); - if png.ihdr.bit_depth < BitDepth::Four && previous.ihdr.bit_depth == BitDepth::Eight { - // Also try 16-color mode for all lower bits images, since that may compress better - if let Some(reduced) = reduce_bit_depth_8_or_less(&previous, 4) { - eval.try_image(Arc::new(reduced)); - } - } - reduction_occurred = true; - } - if deadline.passed() { - return; - } - } - - if reduction_occurred { - eval.set_baseline(baseline); - } -} - /// Execute a compression trial fn perform_trial( filtered: &[u8], diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 1dbc68ceb..6eb9700a3 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -35,7 +35,7 @@ where #[must_use] pub fn reduce_to_palette(png: &PngImage) -> Option { - if png.ihdr.bit_depth != BitDepth::Eight { + if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 { return None; } let mut raw_data = Vec::with_capacity(png.data.len()); @@ -154,6 +154,10 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { #[must_use] pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option { + if !png.ihdr.color_type.is_rgb() { + return None; + } + let mut reduced = Vec::with_capacity(png.data.len()); let byte_depth = png.bytes_per_channel(); let bpp = png.channels_per_pixel() * byte_depth; diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 8e6b6d6dc..1970d2200 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,19 +1,21 @@ use crate::colors::{BitDepth, ColorType}; +use crate::evaluate::Evaluator; use crate::headers::IhdrData; use crate::png::PngImage; +use crate::Deadline; +use crate::Options; use indexmap::map::{Entry::*, IndexMap}; use rgb::RGBA8; -use std::borrow::Cow; +use std::sync::Arc; pub mod alpha; -use crate::alpha::reduced_alpha_channel; +use crate::alpha::*; pub mod bit_depth; +use crate::bit_depth::*; pub mod color; use crate::color::*; pub(crate) use crate::alpha::cleaned_alpha_channel; -pub(crate) use crate::bit_depth::reduce_bit_depth_16_to_8; -pub(crate) use crate::bit_depth::reduce_bit_depth_8_or_less; /// Attempt to reduce the number of colors in the palette /// Returns `None` if palette hasn't changed @@ -190,50 +192,103 @@ fn reordered_palette(palette: &[RGBA8], palette_map: &[Option; 256]) -> Vec< new_palette } -/// Attempt to reduce the color type of the image, returning the reduced image if successful -pub fn reduce_color_type( - png: &PngImage, - grayscale_reduction: bool, - optimize_alpha: bool, -) -> Option { - let mut reduced = Cow::Borrowed(png); +pub(crate) fn perform_reductions( + mut png: Arc, + opts: &Options, + deadline: &Deadline, + eval: &Evaluator, +) -> (Arc, bool) { + let mut reduction_occurred = false; + let mut evaluation_added = false; + + // Interlacing must be processed first in order to evaluate the rest correctly + if let Some(interlacing) = opts.interlace { + if let Some(reduced) = png.change_interlacing(interlacing) { + png = Arc::new(reduced); + reduction_occurred = true; + } + } + + // If alpha optimization is enabled, clean the alpha channel before continuing + // This can allow some color type reductions which may not have been possible otherwise + if opts.optimize_alpha && !deadline.passed() { + if let Some(reduced) = cleaned_alpha_channel(&png) { + png = Arc::new(reduced); + // This does not count as a reduction + } + } + + // Attempt to reduce 16-bit to 8-bit + // This is just removal of bytes and does not need to be evaluated + if opts.bit_depth_reduction && !deadline.passed() { + if let Some(reduced) = reduce_bit_depth_16_to_8(&png) { + png = Arc::new(reduced); + reduction_occurred = true; + } + } - // Go down one step at a time - maybe not the most efficient, but it's safe // Attempt to reduce RGB to grayscale - if grayscale_reduction && reduced.ihdr.color_type.is_rgb() { - if let Some(r) = reduce_rgb_to_grayscale(&reduced) { - reduced = Cow::Owned(r); + // This is just removal of bytes and does not need to be evaluated + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduce_rgb_to_grayscale(&png) { + png = Arc::new(reduced); + reduction_occurred = true; } } - // Attempt grayscale alpha reduction before palette, as grayscale will typically be smaller than indexed - if reduced.ihdr.color_type == ColorType::GrayscaleAlpha { - if let Some(r) = reduced_alpha_channel(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); + // Now retain the current png for the evaluator baseline + // It will only be entered into the evaluator if there are also others to evaluate + let mut baseline = png.clone(); + + // Attempt alpha removal + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + // If the reduction requires a tRNS chunk, enter this into the evaluator + // Otherwise it is just removal of bytes and should become the baseline + if png.ihdr.color_type.has_trns() { + eval.try_image(png.clone()); + evaluation_added = true; + } else { + baseline = png.clone(); + reduction_occurred = true; + } } } - // Attempt to reduce to palette, if not already a single channel - if reduced.channels_per_pixel() != 1 { - if let Some(r) = reduce_to_palette(&reduced) { - reduced = Cow::Owned(r); + // Attempt to reduce the palette size + if opts.palette_reduction && !deadline.passed() { + if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + eval.try_image(png.clone()); + evaluation_added = true; + } + } - // Make sure that palette gets sorted. Ideally, this should be done within reduce_to_palette. - if let Some(r) = reduced_palette(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); + // Attempt to reduce to palette + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduce_to_palette(&png) { + png = Arc::new(reduced); + // Make sure the palette gets sorted (ideally, this should be done within reduce_to_palette) + if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) { + png = Arc::new(reduced); } + eval.try_image(png.clone()); + evaluation_added = true; } } - // Attempt RGBA alpha reduction after palette, so it can be skipped if palette was successful - if reduced.ihdr.color_type == ColorType::RGBA { - if let Some(r) = reduced_alpha_channel(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); + // Attempt to reduce to a lower bit depth + if opts.bit_depth_reduction && !deadline.passed() { + if let Some(reduced) = reduce_bit_depth_8_or_less(&png, 1) { + png = Arc::new(reduced); + eval.try_image(png.clone()); + evaluation_added = true; } } - match reduced { - Cow::Owned(r) => Some(r), - _ => None, + if evaluation_added { + eval.set_baseline(baseline.clone()); } + (baseline, reduction_occurred) } diff --git a/tests/regression.rs b/tests/regression.rs index 17f041a4f..c24c83732 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -171,8 +171,8 @@ fn issue_52_04() { None, RGBA, BitDepth::Eight, - INDEXED, - BitDepth::One, + RGB, + BitDepth::Eight, ); } @@ -243,7 +243,7 @@ fn issue_60() { None, RGBA, BitDepth::Eight, - RGBA, + GRAYSCALE_ALPHA, BitDepth::Eight, ); }