use std::hash::{BuildHasherDefault, Hash};
use indexmap::IndexSet;
use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
use rustc_hash::FxHasher;
use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::PngImage,
};
type FxIndexSet<V> = IndexSet<V, BuildHasherDefault<FxHasher>>;
pub const INDEXED_MAX_DIFF: usize = 20000;
fn build_palette<T>(
iter: impl IntoIterator<Item = T>,
reduced: &mut Vec<u8>,
) -> Option<FxIndexSet<T>>
where
T: Eq + Hash,
{
let mut palette = FxIndexSet::default();
palette.reserve(257);
for pixel in iter {
let (idx, _) = palette.insert_full(pixel);
if idx == 256 {
return None;
}
reduced.push(idx as u8);
}
Some(palette)
}
#[must_use]
pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) {
return None;
}
if !allow_grayscale && png.ihdr.color_type.is_gray() {
return None;
}
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
let palette: Vec<_> = match png.ihdr.color_type {
ColorType::Grayscale { transparent_shade } => {
let pmap = build_palette(png.data.as_gray().iter().copied(), &mut raw_data)?;
let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8));
pmap.into_iter()
.map(|px| {
RGB::from(px).with_alpha(if Some(px) == transparency_pixel {
0
} else {
255
})
})
.collect()
}
ColorType::RGB { transparent_color } => {
let pmap = build_palette(png.data.as_rgb().iter().copied(), &mut raw_data)?;
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
pmap.into_iter()
.map(|px| {
px.with_alpha(if Some(px) == transparency_pixel {
0
} else {
255
})
})
.collect()
}
ColorType::GrayscaleAlpha => {
let pmap = build_palette(png.data.as_gray_alpha().iter().copied(), &mut raw_data)?;
pmap.into_iter().map(RGBA::from).collect()
}
ColorType::RGBA => {
let pmap = build_palette(png.data.as_rgba().iter().copied(), &mut raw_data)?;
pmap.into_iter().collect()
}
_ => return None,
};
Some(PngImage {
data: raw_data,
ihdr: IhdrData {
color_type: ColorType::Indexed { palette },
..png.ihdr
},
})
}
#[must_use]
pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
if !png.ihdr.color_type.is_rgb() {
return None;
}
let mut reduced = Vec::with_capacity(png.data.len());
let byte_depth = png.bytes_per_channel();
let bpp = png.channels_per_pixel() * byte_depth;
let last_color = 2 * byte_depth;
for pixel in png.data.chunks_exact(bpp) {
if byte_depth == 1 {
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
return None;
}
} else if pixel[0..2] != pixel[2..4] || pixel[2..4] != pixel[4..6] {
return None;
}
reduced.extend_from_slice(&pixel[last_color..]);
}
let color_type = match png.ihdr.color_type {
ColorType::RGB { transparent_color } => ColorType::Grayscale {
transparent_shade: transparent_color
.filter(|t| t.r == t.g && t.g == t.b)
.map(|t| t.r),
},
_ => ColorType::GrayscaleAlpha,
};
Some(PngImage {
data: reduced,
ihdr: IhdrData {
color_type,
..png.ihdr
},
})
}
#[must_use]
pub fn indexed_to_channels(
png: &PngImage,
allow_grayscale: bool,
optimize_alpha: bool,
) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let mut palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette.clone(),
_ => return None,
};
if optimize_alpha {
for color in &mut palette {
if color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
}
}
let is_gray = if allow_grayscale {
palette.iter().all(|c| c.r == c.g && c.g == c.b)
} else {
false
};
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,
},
};
let out_size = color_type.channels_per_pixel() as usize * png.data.len();
if out_size - png.data.len() > INDEXED_MAX_DIFF {
return None;
}
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_ref()[ch_start..=ch_end]);
}
Some(PngImage {
ihdr: IhdrData {
color_type,
..png.ihdr
},
data,
})
}