From a0e979d009f9c3c910bf93f4180f0e2ae65d57ec Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 10 May 2023 16:07:52 +1200 Subject: [PATCH] Attempt grayscale to indexed --- src/colors.rs | 10 +++++++- src/reduction/color.rs | 57 +++++++++++++++++++++--------------------- src/reduction/mod.rs | 25 ++++++++++++------ 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/colors.rs b/src/colors.rs index 22f24f19..59cd589e 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -70,13 +70,21 @@ impl ColorType { matches!(self, ColorType::RGB { .. } | ColorType::RGBA) } + #[inline] + pub(crate) fn is_grayscale(&self) -> bool { + matches!( + self, + ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha + ) + } + #[inline] pub(crate) fn has_alpha(&self) -> bool { matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) } #[inline] - pub fn has_trns(&self) -> bool { + pub(crate) fn has_trns(&self) -> bool { match self { ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(), ColorType::RGB { transparent_color } => transparent_color.is_some(), diff --git a/src/reduction/color.rs b/src/reduction/color.rs index a011f55f..8e3b2cec 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -2,12 +2,16 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; use indexmap::IndexMap; -use rgb::{ComponentMap, ComponentSlice, FromSlice, RGBA, RGBA8}; +use rgb::alt::Gray; +use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA, RGBA8}; use rustc_hash::FxHasher; use std::hash::{BuildHasherDefault, Hash}; type FxIndexMap = IndexMap>; +/// Maximum size difference between indexed and channels to consider a candidate for evaluation +pub const INDEXED_MAX_DIFF: usize = 20000; + fn reduce_scanline_to_palette( iter: impl IntoIterator, palette: &mut FxIndexMap, @@ -35,13 +39,31 @@ where #[must_use] pub fn reduced_to_indexed(png: &PngImage) -> Option { - if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 { + if png.ihdr.bit_depth != BitDepth::Eight { return None; } + if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) { + return None; + } + let mut raw_data = Vec::with_capacity(png.data.len()); let mut palette = FxIndexMap::default(); palette.reserve(257); - let ok = if let ColorType::RGB { transparent_color } = png.ihdr.color_type { + let ok = if let ColorType::Grayscale { transparent_shade } = png.ihdr.color_type { + // Convert the Gray16 transparency to Gray8 + let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8)); + reduce_scanline_to_palette( + png.data.as_gray().iter().cloned().map(|px| { + RGB::from(px).alpha(if Some(px) != transparency_pixel { + 255 + } else { + 0 + }) + }), + &mut palette, + &mut raw_data, + ) + } else if let ColorType::RGB { transparent_color } = png.ihdr.color_type { // Convert the RGB16 transparency to RGB8 let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8)); reduce_scanline_to_palette( @@ -57,12 +79,7 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option { ) } else if png.ihdr.color_type == ColorType::GrayscaleAlpha { reduce_scanline_to_palette( - png.data.as_gray_alpha().iter().cloned().map(|px| RGBA { - r: px.0, - g: px.0, - b: px.0, - a: px.1, - }), + png.data.as_gray_alpha().iter().cloned().map(RGBA::from), &mut palette, &mut raw_data, ) @@ -78,24 +95,6 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option { return None; } - let num_transparent = palette - .iter() - .filter_map(|(px, &idx)| { - if px.a != 255 { - Some(idx as usize + 1) - } else { - None - } - }) - .max(); - let trns_size = num_transparent.map_or(0, |n| n + 8); - - let headers_size = palette.len() * 3 + 8 + trns_size; - if raw_data.len() + headers_size > png.data.len() { - // Reduction would result in a larger image - return None; - } - let mut aux_headers = png.aux_headers.clone(); if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 { @@ -106,7 +105,7 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option { bkgd_header[5], 255, )) - } else if png.ihdr.color_type == ColorType::GrayscaleAlpha && bkgd_header.len() == 2 { + } else if png.ihdr.color_type.is_grayscale() && bkgd_header.len() == 2 { Some(RGBA8::new( bkgd_header[1], bkgd_header[1], @@ -232,7 +231,7 @@ pub fn indexed_to_channels(png: &PngImage) -> Option { // Don't proceed if output would be too much larger let out_size = color_type.channels_per_pixel() as usize * png.data.len(); - if out_size - png.data.len() > 10000 { + if out_size - png.data.len() > INDEXED_MAX_DIFF { return None; } diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index e9bfba9f..f5539096 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -106,23 +106,32 @@ pub(crate) fn perform_reductions( } // Attempt to reduce to indexed + let mut indexed = None; if opts.color_type_reduction && !deadline.passed() { if let Some(reduced) = reduced_to_indexed(&png) { - png = Arc::new(reduced); // Make sure the palette gets sorted (but don't bother evaluating both results) - if let Some(reduced) = sorted_palette(&png) { - png = Arc::new(reduced); + let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced)); + // For relatively small differences, enter this into the evaluator + // Otherwise we're confident enough for it to become the baseline + if png.data.len() - new.data.len() <= INDEXED_MAX_DIFF { + eval.try_image(new.clone()); + evaluation_added = true; + } else { + baseline = new.clone(); + reduction_occurred = true; } - eval.try_image(png.clone()); - evaluation_added = true; + indexed = Some(new); } } // Attempt to reduce to a lower bit depth if opts.bit_depth_reduction && !deadline.passed() { - if let Some(reduced) = reduced_bit_depth_8_or_less(&png, 1) { - png = Arc::new(reduced); - eval.try_image(png.clone()); + // Try reducing the previous png, falling back to the indexed one if it exists + // This allows a grayscale depth reduction to be preferred over an indexed depth reduction + let reduced = reduced_bit_depth_8_or_less(&png, 1) + .or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png, 1))); + if let Some(reduced) = reduced { + eval.try_image(Arc::new(reduced)); evaluation_added = true; } }