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)
|
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(),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
eval.try_image(png.clone());
|
if png.data.len() - new.data.len() <= INDEXED_MAX_DIFF {
|
||||||
|
eval.try_image(new.clone());
|
||||||
evaluation_added = true;
|
evaluation_added = true;
|
||||||
|
} else {
|
||||||
|
baseline = new.clone();
|
||||||
|
reduction_occurred = true;
|
||||||
|
}
|
||||||
|
indexed = Some(new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue