#[cfg_attr(not(feature = "quantize"), allow(unused_imports))]
use crate::{Pixel, TrueColor};
use std::collections::HashMap;
#[cfg(feature = "quantize")]
use color_quant::NeuQuant;
#[derive(Clone, Debug)]
pub struct Quantizer {
pub palette_size: usize,
pub gif_optimization: bool,
pub quality: u8,
pub fallback_to_lossless: bool,
}
impl Default for Quantizer {
fn default() -> Self {
Self {
palette_size: 256,
gif_optimization: false,
quality: 20,
fallback_to_lossless: true,
}
}
}
impl Quantizer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_palette_size(mut self, palette_size: usize) -> Self {
self.palette_size = palette_size;
self
}
#[must_use]
pub const fn with_gif_optimization(mut self, gif_optimization: bool) -> Self {
self.gif_optimization = gif_optimization;
self
}
#[must_use]
pub const fn with_quality(mut self, quality: u8) -> Self {
self.quality = quality;
self
}
#[must_use]
pub const fn with_fallback_to_lossless(mut self, fallback_to_lossless: bool) -> Self {
self.fallback_to_lossless = fallback_to_lossless;
self
}
pub fn quantize<P: TrueColor>(
&self,
pixels: impl AsRef<[P]>,
) -> crate::Result<(Vec<P>, Vec<u8>)> {
#[cfg(feature = "quantize")]
{
quantize_lossy(pixels, self)
}
#[cfg(not(feature = "quantize"))]
{
quantize_simple(pixels, self)
}
}
}
pub fn quantize_simple<P: TrueColor>(
pixels: impl AsRef<[P]>,
config: &Quantizer,
) -> crate::Result<(Vec<P>, Vec<u8>)> {
let pixels = pixels.as_ref();
let mut palette = Vec::with_capacity(config.palette_size);
let mut lookup = HashMap::with_capacity(config.palette_size);
let quantized = pixels
.iter()
.map(|pixel| {
let key @ (r, g, b, mut a) = pixel.as_rgba_tuple();
let mut maybe_err = None;
let result = *lookup.entry(key).or_insert_with(|| {
if palette.len() >= config.palette_size {
maybe_err = Some(crate::Error::QuantizationOverflow {
unique_colors: palette.len(),
palette_size: config.palette_size,
});
return 0;
}
if config.gif_optimization {
a = if a == 0 { 0 } else { 255 };
}
palette.push(P::from_rgba_tuple((r, g, b, a)));
palette.len() - 1
}) as u8;
maybe_err.map_or(Ok(result), Err)
})
.collect::<crate::Result<_>>()?;
Ok((palette, quantized))
}
#[cfg(feature = "quantize")]
pub fn quantize_lossy<P: TrueColor>(
pixels: impl AsRef<[P]>,
config: &Quantizer,
) -> crate::Result<(Vec<P>, Vec<u8>)> {
let pixels = pixels.as_ref();
if config.fallback_to_lossless {
let count = pixels.windows(2).filter(|win| win[0] != win[1]).count() + 1;
if count <= config.palette_size {
return quantize_simple::<P>(pixels, config);
}
}
let pixels = pixels
.iter()
.flat_map(|p| p.into_rgba().as_bytes())
.collect::<Vec<_>>();
#[allow(clippy::cast_lossless)]
let quantizer = NeuQuant::new(31 - config.quality as i32, config.palette_size, &pixels);
let palette = quantizer
.color_map_rgba()
.chunks_exact(4)
.map(|chunk| P::from_rgba_tuple((chunk[0], chunk[1], chunk[2], chunk[3])))
.collect();
Ok((
palette,
(0..pixels.len())
.step_by(4)
.map(|i| quantizer.index_of(&pixels[i..i + 4]) as u8)
.collect(),
))
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crate::quantize::Quantizer;
#[test]
fn test_quantization() {
let sample = [
Rgba::new(255, 255, 255, 255),
Rgba::new(255, 255, 255, 255),
Rgba::new(0, 255, 255, 255),
];
let (palette, pixels) = Quantizer::new().quantize(&sample).unwrap();
assert_eq!(
&palette[..],
&[Rgba::new(255, 255, 255, 255), Rgba::new(0, 255, 255, 255)]
);
assert_eq!(pixels, &[0, 0, 1]);
}
}