Indexed to channels

This commit is contained in:
Andrew 2023-05-08 11:54:38 +12:00
parent 21d6d955f6
commit 762e6ba788
2 changed files with 83 additions and 9 deletions

View file

@ -2,7 +2,7 @@ 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, FromSlice, RGBA, RGBA8}; use rgb::{ComponentMap, ComponentSlice, FromSlice, RGBA, RGBA8};
use rustc_hash::FxHasher; use rustc_hash::FxHasher;
use std::hash::{BuildHasherDefault, Hash}; use std::hash::{BuildHasherDefault, Hash};
@ -34,7 +34,7 @@ where
} }
#[must_use] #[must_use]
pub fn reduce_to_palette(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 || png.channels_per_pixel() == 1 {
return None; return None;
} }
@ -153,7 +153,7 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
} }
#[must_use] #[must_use]
pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> { pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
if !png.ihdr.color_type.is_rgb() { if !png.ihdr.color_type.is_rgb() {
return None; return None;
} }
@ -204,3 +204,67 @@ pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
aux_headers, aux_headers,
}) })
} }
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
#[must_use]
pub fn indexed_to_channels(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette,
_ => return None,
};
// Determine which channels are required
let is_gray = palette.iter().all(|c| c.r == c.g && c.g == c.b);
let has_alpha = palette.iter().any(|c| c.a != 255);
let color_type = match (is_gray, has_alpha) {
(false, true) => ColorType::RGBA,
(false, false) => ColorType::RGB {
transparent_color: None,
},
(true, true) => ColorType::GrayscaleAlpha,
(true, false) => ColorType::Grayscale {
transparent_shade: None,
},
};
// 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 {
return None;
}
// Construct the new data
let black = RGBA::new(0, 0, 0, 255);
let ch_start = if is_gray { 2 } else { 0 };
let ch_end = if has_alpha { 3 } else { 2 };
let mut data = Vec::with_capacity(out_size);
for b in &png.data {
let color = palette.get(*b as usize).unwrap_or(&black);
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]);
}
// Update bKGD if it exists
let mut aux_headers = png.aux_headers.clone();
if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) {
if let Some(color) = palette.get(idx as usize) {
let bkgd = if is_gray {
vec![0, color.r]
} else {
vec![0, color.r, 0, color.g, 0, color.b]
};
aux_headers.insert(*b"bKGD", bkgd);
}
}
Some(PngImage {
ihdr: IhdrData {
color_type,
..png.ihdr
},
data,
aux_headers,
})
}

View file

@ -51,7 +51,7 @@ pub(crate) fn perform_reductions(
// Attempt to reduce RGB to grayscale // Attempt to reduce RGB to grayscale
// This is just removal of bytes and does not need to be evaluated // This is just removal of bytes and does not need to be evaluated
if opts.color_type_reduction && !deadline.passed() { if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduce_rgb_to_grayscale(&png) { if let Some(reduced) = reduced_rgb_to_grayscale(&png) {
png = Arc::new(reduced); png = Arc::new(reduced);
reduction_occurred = true; reduction_occurred = true;
} }
@ -74,9 +74,9 @@ pub(crate) fn perform_reductions(
if opts.color_type_reduction && !deadline.passed() { if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) { if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) {
png = Arc::new(reduced); png = Arc::new(reduced);
// If the reduction requires a tRNS chunk, enter this into the evaluator // For small differences, if a tRNS chunk is required then enter this into the evaluator
// Otherwise it is just removal of bytes and should become the baseline // Otherwise it is mostly just removal of bytes and should become the baseline
if png.ihdr.color_type.has_trns() { if png.ihdr.color_type.has_trns() && baseline.data.len() - png.data.len() <= 1000 {
eval.try_image(png.clone()); eval.try_image(png.clone());
evaluation_added = true; evaluation_added = true;
} else { } else {
@ -95,9 +95,19 @@ pub(crate) fn perform_reductions(
} }
} }
// Attempt to reduce to palette // Attempt to convert from indexed to channels
// This may give a better result due to dropping the PLTE chunk
if opts.color_type_reduction && !deadline.passed() { if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduce_to_palette(&png) { if let Some(reduced) = indexed_to_channels(&png) {
// This result should not be passed on to subsequent reductions
eval.try_image(Arc::new(reduced));
evaluation_added = true;
}
}
// Attempt to reduce to indexed
if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduced_to_indexed(&png) {
png = Arc::new(reduced); 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) { if let Some(reduced) = sorted_palette(&png) {