From 174f36ad8f35d7135a1bd81d52da3586fe93880b Mon Sep 17 00:00:00 2001 From: andrews05 Date: Mon, 3 Feb 2025 06:26:18 +1300 Subject: [PATCH] Improve indexed to grayscale conversion (#674) If no palette reduction occurred (or palette reductions were off), a fully transparent palette entry may not have been zeroed to black. By ensuring this gets cleaned in the `indexed_to_channels` transformation, we may able to achieve a grayscale conversion that would otherwise have been RGB. This is the final piece of the puzzle in #649 to achieve better file sizes on the first run and avoid further changes on a second run. Results from the images in #649 (combining this PR and #673): 9.1.3 1st run: 2,191,773 9.1.3 2nd run: 2,191,629 PR 1st run: 2,191,380 PR 2nd run: 2,191,380 --- benches/reductions.rs | 2 +- src/reduction/color.rs | 21 +++++++++++++++--- src/reduction/mod.rs | 4 +++- .../palette_2_should_be_grayscale_alpha_8.png | Bin 0 -> 126 bytes tests/reduction.rs | 12 ++++++++++ 5 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 tests/files/palette_2_should_be_grayscale_alpha_8.png diff --git a/benches/reductions.rs b/benches/reductions.rs index e0b3f265..251c165c 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -257,7 +257,7 @@ fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| color::indexed_to_channels(&png.raw, true)); + b.iter(|| color::indexed_to_channels(&png.raw, true, false)); } #[bench] diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 8992d1c7..fe11d048 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -138,15 +138,30 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option { /// Attempt to convert indexed to a different color type, returning the resulting image if successful #[must_use] -pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option { +pub fn indexed_to_channels( + png: &PngImage, + allow_grayscale: bool, + optimize_alpha: bool, +) -> Option { if png.ihdr.bit_depth != BitDepth::Eight { return None; } - let palette = match &png.ihdr.color_type { - ColorType::Indexed { palette } => palette, + let mut palette = match &png.ihdr.color_type { + ColorType::Indexed { palette } => palette.clone(), _ => return None, }; + // Ensure fully transparent colors are black, which can help with grayscale conversion + if optimize_alpha { + for color in &mut palette { + if color.a == 0 { + color.r = 0; + color.g = 0; + color.b = 0; + } + } + } + // Determine which channels are required let is_gray = if allow_grayscale { palette.iter().all(|c| c.r == c.g && c.g == c.b) diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 9b860c5f..dc6202a1 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -106,7 +106,9 @@ pub(crate) fn perform_reductions( // Attempt to convert from indexed to channels // This may give a better result due to dropping the PLTE chunk if !cheap && opts.color_type_reduction && !deadline.passed() { - if let Some(reduced) = indexed_to_channels(&png, opts.grayscale_reduction) { + if let Some(reduced) = + indexed_to_channels(&png, opts.grayscale_reduction, opts.optimize_alpha) + { // This result should not be passed on to subsequent reductions eval.try_image(Arc::new(reduced)); evaluation_added = true; diff --git a/tests/files/palette_2_should_be_grayscale_alpha_8.png b/tests/files/palette_2_should_be_grayscale_alpha_8.png new file mode 100644 index 0000000000000000000000000000000000000000..c83f2543cd240df7f019dc8c749b65f9ace243a0 GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0L3?#3!&-4XSoB=)|uA!?!|APVJ-8p