Attempt grayscale to indexed

This commit is contained in:
Andrew 2023-05-10 16:07:52 +12:00
parent 762e6ba788
commit a0e979d009
3 changed files with 54 additions and 38 deletions

View file

@ -70,13 +70,21 @@ impl ColorType {
matches!(self, ColorType::RGB { .. } | ColorType::RGBA) matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
} }
#[inline]
pub(crate) fn is_grayscale(&self) -> bool {
matches!(
self,
ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha
)
}
#[inline] #[inline]
pub(crate) fn has_alpha(&self) -> bool { pub(crate) fn has_alpha(&self) -> bool {
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
} }
#[inline] #[inline]
pub fn has_trns(&self) -> bool { pub(crate) fn has_trns(&self) -> bool {
match self { match self {
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(), ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
ColorType::RGB { transparent_color } => transparent_color.is_some(), ColorType::RGB { transparent_color } => transparent_color.is_some(),

View file

@ -2,12 +2,16 @@ use crate::colors::{BitDepth, ColorType};
use crate::headers::IhdrData; use crate::headers::IhdrData;
use crate::png::PngImage; use crate::png::PngImage;
use indexmap::IndexMap; 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 rustc_hash::FxHasher;
use std::hash::{BuildHasherDefault, Hash}; use std::hash::{BuildHasherDefault, Hash};
type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>; type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
/// 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<T>( fn reduce_scanline_to_palette<T>(
iter: impl IntoIterator<Item = T>, iter: impl IntoIterator<Item = T>,
palette: &mut FxIndexMap<T, u8>, palette: &mut FxIndexMap<T, u8>,
@ -35,13 +39,31 @@ where
#[must_use] #[must_use]
pub fn reduced_to_indexed(png: &PngImage) -> Option<PngImage> { pub fn reduced_to_indexed(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 { if png.ihdr.bit_depth != BitDepth::Eight {
return None; return None;
} }
if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) {
return None;
}
let mut raw_data = Vec::with_capacity(png.data.len()); let mut raw_data = Vec::with_capacity(png.data.len());
let mut palette = FxIndexMap::default(); let mut palette = FxIndexMap::default();
palette.reserve(257); 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 // Convert the RGB16 transparency to RGB8
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8)); let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
reduce_scanline_to_palette( reduce_scanline_to_palette(
@ -57,12 +79,7 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option<PngImage> {
) )
} else if png.ihdr.color_type == ColorType::GrayscaleAlpha { } else if png.ihdr.color_type == ColorType::GrayscaleAlpha {
reduce_scanline_to_palette( reduce_scanline_to_palette(
png.data.as_gray_alpha().iter().cloned().map(|px| RGBA { png.data.as_gray_alpha().iter().cloned().map(RGBA::from),
r: px.0,
g: px.0,
b: px.0,
a: px.1,
}),
&mut palette, &mut palette,
&mut raw_data, &mut raw_data,
) )
@ -78,24 +95,6 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option<PngImage> {
return None; 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(); let mut aux_headers = png.aux_headers.clone();
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 { 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<PngImage> {
bkgd_header[5], bkgd_header[5],
255, 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( Some(RGBA8::new(
bkgd_header[1], bkgd_header[1],
bkgd_header[1], bkgd_header[1],
@ -232,7 +231,7 @@ pub fn indexed_to_channels(png: &PngImage) -> Option<PngImage> {
// Don't proceed if output would be too much larger // Don't proceed if output would be too much larger
let out_size = color_type.channels_per_pixel() as usize * png.data.len(); 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; return None;
} }

View file

@ -106,23 +106,32 @@ pub(crate) fn perform_reductions(
} }
// Attempt to reduce to indexed // Attempt to reduce to indexed
let mut indexed = None;
if opts.color_type_reduction && !deadline.passed() { if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduced_to_indexed(&png) { 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) // Make sure the palette gets sorted (but don't bother evaluating both results)
if let Some(reduced) = sorted_palette(&png) { let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
png = Arc::new(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()); indexed = Some(new);
evaluation_added = true;
} }
} }
// Attempt to reduce to a lower bit depth // Attempt to reduce to a lower bit depth
if opts.bit_depth_reduction && !deadline.passed() { if opts.bit_depth_reduction && !deadline.passed() {
if let Some(reduced) = reduced_bit_depth_8_or_less(&png, 1) { // Try reducing the previous png, falling back to the indexed one if it exists
png = Arc::new(reduced); // This allows a grayscale depth reduction to be preferred over an indexed depth reduction
eval.try_image(png.clone()); 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; evaluation_added = true;
} }
} }