Attempt grayscale to indexed
This commit is contained in:
parent
762e6ba788
commit
a0e979d009
3 changed files with 54 additions and 38 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<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>(
|
||||
iter: impl IntoIterator<Item = T>,
|
||||
palette: &mut FxIndexMap<T, u8>,
|
||||
|
|
@ -35,13 +39,31 @@ where
|
|||
|
||||
#[must_use]
|
||||
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;
|
||||
}
|
||||
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<PngImage> {
|
|||
)
|
||||
} 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<PngImage> {
|
|||
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<PngImage> {
|
|||
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<PngImage> {
|
|||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue