use alloc::boxed::Box;
use alloc::format;
use alloc::vec;
use alloc::vec::Vec;
use crate::error::PngError;
#[allow(unused_imports)]
use whereat::at;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct QuantizeOutput {
pub palette_rgba: Vec<[u8; 4]>,
pub indices: Vec<u8>,
pub mpe_score: Option<f32>,
pub ssim2_estimate: Option<f32>,
pub butteraugli_estimate: Option<f32>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MultiFrameOutput {
pub palette_rgba: Vec<[u8; 4]>,
pub frame_indices: Vec<Vec<u8>>,
pub mpe_scores: Vec<Option<f32>>,
pub ssim2_estimates: Vec<Option<f32>>,
pub butteraugli_estimates: Vec<Option<f32>>,
}
pub trait Quantizer: Send + Sync {
fn quantize_rgba(
&self,
pixels: &[[u8; 4]],
width: usize,
height: usize,
) -> Result<QuantizeOutput, PngError>;
fn quantize_multi_frame(
&self,
frames: &[&[[u8; 4]]],
width: usize,
height: usize,
) -> Result<MultiFrameOutput, PngError> {
let pixels_per_frame = width * height;
let mut concat: Vec<[u8; 4]> = Vec::with_capacity(pixels_per_frame * frames.len());
for frame in frames {
if frame.len() < pixels_per_frame {
return Err(PngError::InvalidInput(format!(
"frame has {} pixels, expected {}",
frame.len(),
pixels_per_frame
)));
}
concat.extend_from_slice(&frame[..pixels_per_frame]);
}
let total_height = height * frames.len();
let result = self.quantize_rgba(&concat, width, total_height)?;
let expected_indices = concat.len();
if result.indices.len() < expected_indices {
return Err(PngError::InvalidInput(format!(
"quantize_rgba returned {} indices, expected {}",
result.indices.len(),
expected_indices
)));
}
let n = frames.len();
let mut frame_indices = Vec::with_capacity(n);
for i in 0..n {
let start = i * pixels_per_frame;
frame_indices.push(result.indices[start..start + pixels_per_frame].to_vec());
}
Ok(MultiFrameOutput {
palette_rgba: result.palette_rgba,
frame_indices,
mpe_scores: vec![None; n],
ssim2_estimates: vec![None; n],
butteraugli_estimates: vec![None; n],
})
}
fn with_quality_metrics(&self) -> Option<Box<dyn Quantizer>> {
None
}
fn name(&self) -> &str;
}
#[cfg(any(feature = "quantize", feature = "imagequant", feature = "quantette"))]
pub fn default_quantizer() -> Box<dyn Quantizer> {
#[cfg(feature = "quantize")]
{
Box::new(ZenquantQuantizer::new().with_compute_quality_metric(true))
}
#[cfg(all(not(feature = "quantize"), feature = "imagequant"))]
{
Box::new(ImagequantQuantizer::default())
}
#[cfg(all(
not(feature = "quantize"),
not(feature = "imagequant"),
feature = "quantette"
))]
{
Box::new(QuantetteQuantizer::default())
}
}
pub fn quantizer_by_name(name: &str) -> crate::error::Result<Box<dyn Quantizer>> {
match name {
#[cfg(feature = "quantize")]
"zenquant" => Ok(Box::new(
ZenquantQuantizer::new().with_compute_quality_metric(true),
)),
#[cfg(feature = "imagequant")]
"imagequant" => Ok(Box::new(ImagequantQuantizer::default())),
#[cfg(feature = "quantette")]
"quantette" => Ok(Box::new(QuantetteQuantizer::default())),
_ => Err(at!(PngError::InvalidInput(format!(
"unknown or disabled quantizer backend: {name:?}. Available: {:?}",
available_backends()
)))),
}
}
pub fn available_backends() -> &'static [&'static str] {
&[
#[cfg(feature = "quantize")]
"zenquant",
#[cfg(feature = "imagequant")]
"imagequant",
#[cfg(feature = "quantette")]
"quantette",
]
}
#[cfg(feature = "quantize")]
pub use self::zenquant_backend::ZenquantQuantizer;
#[cfg(feature = "quantize")]
mod zenquant_backend {
use super::*;
#[derive(Debug, Clone)]
pub struct ZenquantQuantizer {
config: zenquant::QuantizeConfig,
}
impl Default for ZenquantQuantizer {
fn default() -> Self {
Self::new()
}
}
impl ZenquantQuantizer {
#[must_use]
pub fn new() -> Self {
Self {
config: zenquant::QuantizeConfig::new(zenquant::OutputFormat::Png),
}
}
#[must_use]
pub fn with_format(format: zenquant::OutputFormat) -> Self {
Self {
config: zenquant::QuantizeConfig::new(format),
}
}
#[must_use]
pub fn from_config(config: zenquant::QuantizeConfig) -> Self {
Self { config }
}
#[must_use]
pub fn with_quality(mut self, quality: zenquant::Quality) -> Self {
self.config = self.config.with_quality(quality);
self
}
#[must_use]
pub fn with_max_colors(mut self, n: u16) -> Self {
self.config = self.config.with_max_colors(n.into());
self
}
#[must_use]
pub fn with_compute_quality_metric(mut self, compute: bool) -> Self {
self.config = self.config.with_compute_quality_metric(compute);
self
}
pub fn config(&self) -> &zenquant::QuantizeConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut zenquant::QuantizeConfig {
&mut self.config
}
}
impl Quantizer for ZenquantQuantizer {
fn quantize_rgba(
&self,
pixels: &[[u8; 4]],
width: usize,
height: usize,
) -> Result<QuantizeOutput, PngError> {
let rgba: &[zenquant::RGBA<u8>] = bytemuck::cast_slice(pixels);
let result = zenquant::quantize_rgba(rgba, width, height, &self.config)?;
Ok(QuantizeOutput {
palette_rgba: result.palette_rgba().to_vec(),
indices: result.indices().to_vec(),
mpe_score: result.mpe_score(),
ssim2_estimate: result.ssimulacra2_estimate(),
butteraugli_estimate: result.butteraugli_estimate(),
})
}
fn quantize_multi_frame(
&self,
frames: &[&[[u8; 4]]],
width: usize,
height: usize,
) -> Result<MultiFrameOutput, PngError> {
use imgref::ImgRef;
let pixels_per_frame = width * height;
let mut frame_refs: Vec<ImgRef<'_, zenquant::RGBA<u8>>> =
Vec::with_capacity(frames.len());
for (i, f) in frames.iter().enumerate() {
if f.len() < pixels_per_frame {
return Err(PngError::InvalidInput(format!(
"frame {} has {} pixels, expected {}",
i,
f.len(),
pixels_per_frame
)));
}
let pixels: &[zenquant::RGBA<u8>] = bytemuck::cast_slice(&f[..pixels_per_frame]);
frame_refs.push(ImgRef::new(pixels, width, height));
}
let palette_result = zenquant::build_palette_rgba(&frame_refs, &self.config)?;
let palette_rgba = palette_result.palette_rgba().to_vec();
let mut frame_indices = Vec::with_capacity(frames.len());
let mut mpe_scores = Vec::with_capacity(frames.len());
let mut ssim2_estimates = Vec::with_capacity(frames.len());
let mut butteraugli_estimates = Vec::with_capacity(frames.len());
let mut prev_indices: Option<Vec<u8>> = None;
for frame_ref in &frame_refs {
let (frame_buf, fw, fh) = frame_ref.to_contiguous_buf();
let remap_result = if let Some(prev) = &prev_indices {
palette_result.remap_rgba_with_prev(
frame_buf.as_ref(),
fw,
fh,
&self.config,
prev,
)?
} else {
palette_result.remap_rgba(frame_buf.as_ref(), fw, fh, &self.config)?
};
mpe_scores.push(remap_result.mpe_score());
ssim2_estimates.push(remap_result.ssimulacra2_estimate());
butteraugli_estimates.push(remap_result.butteraugli_estimate());
let indices = remap_result.indices().to_vec();
prev_indices = Some(indices.clone());
frame_indices.push(indices);
}
Ok(MultiFrameOutput {
palette_rgba,
frame_indices,
mpe_scores,
ssim2_estimates,
butteraugli_estimates,
})
}
fn with_quality_metrics(&self) -> Option<Box<dyn Quantizer>> {
Some(Box::new(self.clone().with_compute_quality_metric(true)))
}
fn name(&self) -> &str {
"zenquant"
}
}
}
#[cfg(feature = "imagequant")]
pub use self::imagequant_backend::ImagequantQuantizer;
#[cfg(feature = "imagequant")]
mod imagequant_backend {
use super::*;
#[derive(Debug, Clone)]
pub struct ImagequantQuantizer {
pub speed: i32,
pub max_quality: u8,
pub dithering: f32,
pub max_colors: u16,
}
impl Default for ImagequantQuantizer {
fn default() -> Self {
Self {
speed: 4,
max_quality: 100,
dithering: 1.0,
max_colors: 256,
}
}
}
impl ImagequantQuantizer {
#[must_use]
pub fn with_speed(mut self, speed: i32) -> Self {
self.speed = speed;
self
}
#[must_use]
pub fn with_max_quality(mut self, q: u8) -> Self {
self.max_quality = q;
self
}
#[must_use]
pub fn with_dithering(mut self, d: f32) -> Self {
self.dithering = d;
self
}
#[must_use]
pub fn with_max_colors(mut self, n: u16) -> Self {
self.max_colors = n;
self
}
}
impl Quantizer for ImagequantQuantizer {
fn quantize_rgba(
&self,
pixels: &[[u8; 4]],
width: usize,
height: usize,
) -> Result<QuantizeOutput, PngError> {
let mut attr = imagequant::Attributes::new();
attr.set_quality(0, self.max_quality)
.map_err(|e| PngError::InvalidInput(format!("imagequant quality: {e}")))?;
attr.set_speed(self.speed)
.map_err(|e| PngError::InvalidInput(format!("imagequant speed: {e}")))?;
attr.set_max_colors(self.max_colors as u32)
.map_err(|e| PngError::InvalidInput(format!("imagequant max_colors: {e}")))?;
let rgba_pixels: Vec<imagequant::RGBA> = pixels
.iter()
.map(|p| imagequant::RGBA::new(p[0], p[1], p[2], p[3]))
.collect();
let mut img = attr
.new_image(rgba_pixels, width, height, 0.0)
.map_err(|e| PngError::InvalidInput(format!("imagequant image: {e}")))?;
let mut result = attr
.quantize(&mut img)
.map_err(|e| PngError::InvalidInput(format!("imagequant quantize: {e}")))?;
result
.set_dithering_level(self.dithering)
.map_err(|e| PngError::InvalidInput(format!("imagequant dithering: {e}")))?;
let (palette, indices) = result
.remapped(&mut img)
.map_err(|e| PngError::InvalidInput(format!("imagequant remap: {e}")))?;
let palette_rgba: Vec<[u8; 4]> = palette.iter().map(|c| [c.r, c.g, c.b, c.a]).collect();
Ok(QuantizeOutput {
palette_rgba,
indices,
mpe_score: None,
ssim2_estimate: None,
butteraugli_estimate: None,
})
}
fn name(&self) -> &str {
"imagequant"
}
}
}
#[cfg(feature = "quantette")]
pub use self::quantette_backend::QuantetteQuantizer;
#[cfg(feature = "quantette")]
mod quantette_backend {
use super::*;
#[derive(Debug, Clone)]
pub struct QuantetteQuantizer {
pub kmeans: bool,
pub dithering: bool,
pub max_colors: u16,
pub sampling_factor: f32,
}
impl Default for QuantetteQuantizer {
fn default() -> Self {
Self {
kmeans: true,
dithering: true,
max_colors: 256,
sampling_factor: 1.0,
}
}
}
impl QuantetteQuantizer {
#[must_use]
pub fn with_kmeans(mut self, kmeans: bool) -> Self {
self.kmeans = kmeans;
self
}
#[must_use]
pub fn with_dithering(mut self, dithering: bool) -> Self {
self.dithering = dithering;
self
}
#[must_use]
pub fn with_max_colors(mut self, n: u16) -> Self {
self.max_colors = n;
self
}
#[must_use]
pub fn with_sampling_factor(mut self, f: f32) -> Self {
self.sampling_factor = f;
self
}
}
impl Quantizer for QuantetteQuantizer {
fn quantize_rgba(
&self,
pixels: &[[u8; 4]],
width: usize,
height: usize,
) -> Result<QuantizeOutput, PngError> {
use quantette::deps::palette::Srgb;
use quantette::{ImageBuf, Pipeline, QuantizeMethod};
let srgb_pixels: Vec<Srgb<u8>> =
pixels.iter().map(|p| Srgb::new(p[0], p[1], p[2])).collect();
let image = ImageBuf::new(width as u32, height as u32, srgb_pixels)
.map_err(|e| PngError::InvalidInput(format!("quantette image: {e}")))?;
let method = if self.kmeans {
use quantette::kmeans::KmeansOptions;
QuantizeMethod::Kmeans(KmeansOptions::new().sampling_factor(self.sampling_factor))
} else {
QuantizeMethod::Wu
};
let palette_size = self
.max_colors
.try_into()
.map_err(|_| PngError::InvalidInput("max_colors out of range".into()))?;
let mut pipeline = Pipeline::new()
.palette_size(palette_size)
.quantize_method(method);
if self.dithering {
use quantette::dither::FloydSteinberg;
pipeline = pipeline.ditherer(Some(FloydSteinberg::new()));
}
let indexed = pipeline
.input_image(image.as_ref())
.output_srgb8_indexed_image();
let palette_rgba: Vec<[u8; 4]> = indexed
.palette()
.iter()
.map(|c| [c.red, c.green, c.blue, 255])
.collect();
let indices = indexed.indices().to_vec();
Ok(QuantizeOutput {
palette_rgba,
indices,
mpe_score: None,
ssim2_estimate: None,
butteraugli_estimate: None,
})
}
fn name(&self) -> &str {
"quantette"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "quantize")]
fn available_backends_includes_zenquant() {
let backends = available_backends();
assert!(
backends.contains(&"zenquant"),
"zenquant should be available"
);
}
#[test]
#[cfg(feature = "quantize")]
fn quantizer_by_name_zenquant() {
let q = quantizer_by_name("zenquant").unwrap();
assert_eq!(q.name(), "zenquant");
}
#[test]
fn quantizer_by_name_unknown_fails() {
let result = quantizer_by_name("nonexistent");
assert!(result.is_err());
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!(err.contains("unknown or disabled"), "got: {err}");
}
#[cfg(any(feature = "quantize", feature = "imagequant", feature = "quantette"))]
#[test]
fn default_quantizer_works() {
let q = default_quantizer();
assert!(!q.name().is_empty());
}
struct DummyQuantizer;
impl Quantizer for DummyQuantizer {
fn quantize_rgba(
&self,
pixels: &[[u8; 4]],
_width: usize,
_height: usize,
) -> Result<QuantizeOutput, crate::error::PngError> {
Ok(QuantizeOutput {
palette_rgba: vec![[0, 0, 0, 255], [255, 255, 255, 255]],
indices: vec![0; pixels.len()],
mpe_score: None,
ssim2_estimate: None,
butteraugli_estimate: None,
})
}
fn name(&self) -> &str {
"dummy"
}
}
#[test]
fn default_with_quality_metrics_returns_none() {
let q = DummyQuantizer;
assert!(q.with_quality_metrics().is_none());
}
#[test]
fn default_quantize_multi_frame_splits_correctly() {
let q = DummyQuantizer;
let frame0: Vec<[u8; 4]> = vec![[255, 0, 0, 255]; 4];
let frame1: Vec<[u8; 4]> = vec![[0, 255, 0, 255]; 4];
let frames: Vec<&[[u8; 4]]> = vec![&frame0, &frame1];
let result = q.quantize_multi_frame(&frames, 2, 2).unwrap();
assert_eq!(result.frame_indices.len(), 2);
assert_eq!(result.frame_indices[0].len(), 4);
assert_eq!(result.frame_indices[1].len(), 4);
assert_eq!(result.palette_rgba.len(), 2);
assert_eq!(result.mpe_scores, vec![None, None]);
assert_eq!(result.ssim2_estimates, vec![None, None]);
}
#[test]
fn default_quantize_multi_frame_rejects_short_frame() {
let q = DummyQuantizer;
let frame0: Vec<[u8; 4]> = vec![[255, 0, 0, 255]; 2]; let frames: Vec<&[[u8; 4]]> = vec![&frame0];
let result = q.quantize_multi_frame(&frames, 2, 2);
assert!(result.is_err());
}
#[cfg(feature = "quantize")]
#[test]
fn zenquant_basic_quantize() {
let q = ZenquantQuantizer::new();
assert_eq!(q.name(), "zenquant");
let pixels: Vec<[u8; 4]> = (0..64)
.map(|i| [(i * 4) as u8, (i * 3) as u8, (i * 2) as u8, 255])
.collect();
let result = q.quantize_rgba(&pixels, 8, 8).unwrap();
assert!(!result.palette_rgba.is_empty());
assert!(result.palette_rgba.len() <= 256);
assert_eq!(result.indices.len(), 64);
}
#[cfg(feature = "quantize")]
#[test]
fn zenquant_with_quality_metrics() {
let q = ZenquantQuantizer::new();
let q2 = q.with_quality_metrics();
assert!(q2.is_some(), "zenquant should support quality metrics");
let q2 = q2.unwrap();
assert_eq!(q2.name(), "zenquant");
}
#[cfg(feature = "quantize")]
#[test]
fn zenquant_multi_frame() {
let q = ZenquantQuantizer::new();
let frame0: Vec<[u8; 4]> = vec![[255, 0, 0, 255]; 16];
let frame1: Vec<[u8; 4]> = vec![[0, 255, 0, 255]; 16];
let frames: Vec<&[[u8; 4]]> = vec![&frame0, &frame1];
let result = q.quantize_multi_frame(&frames, 4, 4).unwrap();
assert_eq!(result.frame_indices.len(), 2);
assert_eq!(result.frame_indices[0].len(), 16);
}
#[cfg(feature = "quantize")]
#[test]
fn zenquant_builder_methods() {
let q = ZenquantQuantizer::new()
.with_max_colors(64)
.with_compute_quality_metric(true);
let pixels: Vec<[u8; 4]> = (0..16).map(|i| [(i * 16) as u8, 0, 0, 255]).collect();
let result = q.quantize_rgba(&pixels, 4, 4).unwrap();
assert!(result.palette_rgba.len() <= 64);
assert!(result.mpe_score.is_some());
}
#[cfg(feature = "quantize")]
#[test]
fn zenquant_config_access() {
let mut q = ZenquantQuantizer::new();
let _config = q.config();
let _config_mut = q.config_mut();
}
}