use alloc::borrow::Cow;
use alloc::vec::Vec;
use std::collections::HashMap;
use crate::common::Frame;
use crate::neuquant::NeuQuant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Route {
Exact,
Histogram { iters: usize },
UniqueStream { cap: u32 },
PixelStream { samplefac: i32 },
}
#[derive(Debug, Clone, Copy)]
enum Policy {
Auto,
Upstream,
Kmeans { iters: usize },
Unique { cap: u32 },
Budget { pixels: usize },
}
const KMEANS_MAX_UNIQUE: usize = 65_536;
const NQ_SAMPLE_BUDGET: usize = 125_000;
fn policy() -> Policy {
match std::env::var("RUSTY_GIF_Q_POLICY") {
Err(_) => Policy::Auto,
Ok(s) if s == "auto" => Policy::Auto,
Ok(s) if s == "upstream" => Policy::Upstream,
Ok(s) if s.starts_with("kmeans:") => Policy::Kmeans {
iters: s["kmeans:".len()..].parse().expect("RUSTY_GIF_Q_POLICY kmeans:<iters>"),
},
Ok(s) if s.starts_with("unique:") => Policy::Unique {
cap: s["unique:".len()..].parse().expect("RUSTY_GIF_Q_POLICY unique:<cap>"),
},
Ok(s) if s.starts_with("budget:") => Policy::Budget {
pixels: s["budget:".len()..].parse().expect("RUSTY_GIF_Q_POLICY budget:<pixels>"),
},
Ok(s) => panic!("RUSTY_GIF_Q_POLICY: unknown policy `{s}` (want auto | upstream | kmeans:<iters> | unique:<cap> | budget:<pixels>)"),
}
}
fn route_auto(unique: usize, pixels: usize, speed: i32) -> Route {
if unique <= KMEANS_MAX_UNIQUE {
let iters = match unique {
u if u <= 32_768 => 3,
u if u <= 49_152 => 2,
_ => 1,
};
Route::Histogram { iters }
} else {
let samplefac = (pixels.div_ceil(NQ_SAMPLE_BUDGET) as i32).clamp(speed, 30);
Route::PixelStream { samplefac }
}
}
fn trace_enabled() -> bool {
std::env::var_os("RUSTY_GIF_TRACE").is_some_and(|v| v != "0")
}
#[inline]
fn key(px: &[u8]) -> u32 {
u32::from_le_bytes([px[0], px[1], px[2], px[3]])
}
pub(crate) fn quantize_frame(
width: u16,
height: u16,
pixels: &[u8],
speed: i32,
transparent: Option<[u8; 4]>,
) -> Frame<'static> {
let mut hist: HashMap<u32, u32> = HashMap::with_capacity(4096);
for px in pixels.chunks_exact(4) {
*hist.entry(key(px)).or_insert(0) += 1;
}
let mut colors: Vec<(u32, u32)> = hist.iter().map(|(&k, &c)| (k, c)).collect();
colors.sort_unstable_by_key(|&(k, _)| {
let [r, g, b, a] = k.to_le_bytes();
(r, g, b, a)
});
let route = match policy() {
_ if colors.len() <= 256 => Route::Exact,
Policy::Auto => route_auto(colors.len(), pixels.len() / 4, speed),
Policy::Upstream => Route::PixelStream { samplefac: speed },
Policy::Kmeans { iters } => Route::Histogram { iters },
Policy::Unique { cap } => Route::UniqueStream { cap },
Policy::Budget { pixels: budget } => {
let n = pixels.len() / 4;
let samplefac = (n.div_ceil(budget) as i32).clamp(speed, 30);
Route::PixelStream { samplefac }
}
};
if trace_enabled() {
std::eprintln!(
"rusty_gif: route={route:?} unique={} px={} speed={speed}",
colors.len(),
pixels.len() / 4
);
}
let frame = match route {
Route::Exact => exact_frame(width, height, pixels, &colors, transparent),
Route::Histogram { iters } => {
let points: Vec<crate::kmeans::WPoint> = colors
.iter()
.map(|&(k, count)| crate::kmeans::WPoint {
c: k.to_le_bytes().map(|v| v as i32),
w: count as u64,
})
.collect();
let pal = crate::kmeans::palette_kmeans(&points, 256, iters);
let palette: Vec<u8> = pal.colors.iter().flat_map(|c| [c[0], c[1], c[2]]).collect();
let lookup: HashMap<u32, u8> = colors
.iter()
.zip(pal.assignment.iter())
.map(|(&(k, _), &idx)| (k, idx))
.collect();
let index_of = |px: &[u8]| {
*lookup
.get(&key(px))
.expect("kmeans palette lookup: color missing from census")
};
Frame {
width,
height,
buffer: Cow::Owned(pixels.chunks_exact(4).map(index_of).collect()),
palette: Some(palette),
transparent: transparent.map(|t| index_of(&t)),
..Frame::default()
}
}
Route::UniqueStream { cap } => {
let mut stream: Vec<u8> = Vec::with_capacity(
colors.iter().map(|&(_, c)| c.min(cap) as usize).sum::<usize>() * 4,
);
for &(k, count) in &colors {
let bytes = k.to_le_bytes();
for _ in 0..count.min(cap) {
stream.extend_from_slice(&bytes);
}
}
if trace_enabled() {
std::eprintln!(
"rusty_gif: training stream {} samples (cap {cap})",
stream.len() / 4
);
}
let nq = NeuQuant::new(speed, 256, &stream);
mapped_frame(width, height, pixels, &colors, &nq, transparent)
}
Route::PixelStream { samplefac } => {
let nq = NeuQuant::new(samplefac, 256, pixels);
mapped_frame(width, height, pixels, &colors, &nq, transparent)
}
};
#[cfg(debug_assertions)]
{
let palette_len = frame.palette.as_ref().map_or(0, |p| p.len());
debug_assert!(palette_len > 0 && palette_len <= 256 * 3, "palette out of range");
debug_assert_eq!(frame.buffer.len(), pixels.len() / 4, "index buffer size");
}
frame
}
fn exact_frame(
width: u16,
height: u16,
pixels: &[u8],
colors: &[(u32, u32)],
transparent: Option<[u8; 4]>,
) -> Frame<'static> {
let palette: Vec<u8> = colors
.iter()
.flat_map(|&(k, _)| {
let [r, g, b, _a] = k.to_le_bytes();
[r, g, b]
})
.collect();
let lookup: HashMap<u32, u8> = colors
.iter()
.zip(0u16..=255)
.map(|(&(k, _), i)| (k, i as u8))
.collect();
let index_of = |px: &[u8]| lookup.get(&key(px)).copied().unwrap_or(0);
Frame {
width,
height,
buffer: Cow::Owned(pixels.chunks_exact(4).map(index_of).collect()),
palette: Some(palette),
transparent: transparent.map(|t| index_of(&t)),
..Frame::default()
}
}
fn mapped_frame(
width: u16,
height: u16,
pixels: &[u8],
colors: &[(u32, u32)],
nq: &NeuQuant,
transparent: Option<[u8; 4]>,
) -> Frame<'static> {
let lookup: HashMap<u32, u8> = colors
.iter()
.map(|&(k, _)| (k, nq.index_of(&k.to_le_bytes()) as u8))
.collect();
let index_of = |px: &[u8]| {
*lookup
.get(&key(px))
.expect("memoized palette lookup: color missing from census")
};
Frame {
width,
height,
buffer: Cow::Owned(pixels.chunks_exact(4).map(index_of).collect()),
palette: Some(nq.color_map_rgb()),
transparent: transparent.map(|t| nq.index_of(&t) as u8),
..Frame::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rgba(colors: &[(u8, u8, u8)], reps: usize) -> Vec<u8> {
let mut v = Vec::new();
for _ in 0..reps {
for &(r, g, b) in colors {
v.extend_from_slice(&[r, g, b, 0xFF]);
}
}
v
}
#[test]
fn exact_route_fires_and_is_lossless() {
let colors: Vec<(u8, u8, u8)> = (0..64u16).map(|i| (i as u8, (i * 2) as u8, 255 - i as u8)).collect();
let px = rgba(&colors, 16); let frame = quantize_frame(32, 32, &px, 1, None);
let pal = frame.palette.as_ref().unwrap();
assert_eq!(pal.len(), 64 * 3);
for (i, p) in px.chunks_exact(4).enumerate() {
let idx = frame.buffer[i] as usize;
assert_eq!(&pal[idx * 3..idx * 3 + 3], &p[..3], "pixel {i} not lossless");
}
}
#[test]
fn auto_route_boundaries() {
assert_eq!(route_auto(257, 100_000, 1), Route::Histogram { iters: 3 });
assert_eq!(route_auto(32_768, 100_000, 1), Route::Histogram { iters: 3 });
assert_eq!(route_auto(32_769, 100_000, 1), Route::Histogram { iters: 2 });
assert_eq!(route_auto(49_152, 100_000, 1), Route::Histogram { iters: 2 });
assert_eq!(route_auto(49_153, 100_000, 1), Route::Histogram { iters: 1 });
assert_eq!(route_auto(65_536, 100_000, 1), Route::Histogram { iters: 1 });
assert_eq!(
route_auto(65_537, 2_073_600, 1),
Route::PixelStream { samplefac: 17 }
);
assert_eq!(route_auto(65_537, 100_000, 1), Route::PixelStream { samplefac: 1 });
}
#[test]
fn all_auto_arms_execute() {
let px = rgba(&[(0, 0, 0), (255, 255, 255)], 64);
let f = quantize_frame(16, 8, &px, 1, None);
assert_eq!(f.palette.as_ref().unwrap().len(), 2 * 3);
let colors: Vec<(u8, u8, u8)> =
(0..1024u32).map(|i| ((i % 256) as u8, (i / 4) as u8, 77)).collect();
let px = rgba(&colors, 1);
let f = quantize_frame(32, 32, &px, 1, None);
assert!(f.palette.as_ref().unwrap().len() <= 256 * 3);
assert_eq!(f.buffer.len(), 1024);
let mut px = Vec::with_capacity(66_000 * 4);
for i in 0..66_000u32 {
px.extend_from_slice(&[(i & 0xFF) as u8, ((i >> 8) & 0xFF) as u8, ((i >> 16) as u8) | 0x40, 0xFF]);
}
let f = quantize_frame(300, 220, &px, 1, None);
assert!(f.palette.as_ref().unwrap().len() <= 256 * 3);
assert_eq!(f.buffer.len(), 66_000);
}
#[test]
fn unique_stream_route_fires() {
let colors: Vec<(u8, u8, u8)> = (0..512u16).map(|i| ((i / 2) as u8, (i % 256) as u8, (i / 3) as u8)).collect();
let px = rgba(&colors, 2); let frame = quantize_frame(32, 32, &px, 1, None);
let pal_len = frame.palette.as_ref().unwrap().len() / 3;
assert!(pal_len <= 256);
assert!(frame.buffer.iter().all(|&i| (i as usize) < pal_len));
}
}