use iascii::render::ColorDepth;
pub(crate) type Rgb8 = (u8, u8, u8);
pub(crate) static ANSI16_PALETTE: [Rgb8; 16] = [
(0, 0, 0),
(128, 0, 0),
(0, 128, 0),
(128, 128, 0),
(0, 0, 128),
(128, 0, 128),
(0, 128, 128),
(192, 192, 192),
(128, 128, 128),
(255, 0, 0),
(0, 255, 0),
(255, 255, 0),
(0, 0, 255),
(255, 0, 255),
(0, 255, 255),
(255, 255, 255),
];
const CUBE_LEVELS: [u8; 6] = [0, 95, 135, 175, 215, 255];
fn dist2(a: Rgb8, b: Rgb8) -> u32 {
let dr = a.0 as i32 - b.0 as i32;
let dg = a.1 as i32 - b.1 as i32;
let db = a.2 as i32 - b.2 as i32;
(dr * dr + dg * dg + db * db) as u32
}
pub(crate) fn nearest_ansi16(c: Rgb8) -> u8 {
ANSI16_PALETTE
.iter()
.enumerate()
.min_by_key(|(_, &p)| dist2(c, p))
.map(|(i, _)| i as u8)
.unwrap_or(0)
}
pub(crate) fn ansi16_rgb(idx: u8) -> Rgb8 {
ANSI16_PALETTE[(idx as usize).min(15)]
}
pub(crate) fn nearest_ansi256(c: Rgb8) -> u8 {
let closest_level = |v: u8| -> (usize, u8) {
CUBE_LEVELS
.iter()
.enumerate()
.min_by_key(|(_, &l)| (v as i32 - l as i32).unsigned_abs())
.map(|(i, &l)| (i, l))
.unwrap_or((0, 0))
};
let (ri, rv) = closest_level(c.0);
let (gi, gv) = closest_level(c.1);
let (bi, bv) = closest_level(c.2);
let cube_dist = dist2(c, (rv, gv, bv));
let gray_avg = ((c.0 as u32 + c.1 as u32 + c.2 as u32) / 3) as u8;
let gray_idx: u8 = if gray_avg < 8 {
0
} else if gray_avg > 238 {
23
} else {
((gray_avg - 8) as f32 / 10.0).round() as u8
};
let gray_val = 8 + gray_idx * 10;
let gray_dist = dist2(c, (gray_val, gray_val, gray_val));
if gray_dist < cube_dist {
232 + gray_idx
} else {
16 + (36 * ri + 6 * gi + bi) as u8
}
}
pub(crate) fn ansi256_rgb(idx: u8) -> Rgb8 {
if idx >= 232 {
let v = 8 + (idx - 232) * 10;
(v, v, v)
} else if idx >= 16 {
let i = idx - 16;
let r = CUBE_LEVELS[(i / 36) as usize];
let g = CUBE_LEVELS[((i / 6) % 6) as usize];
let b = CUBE_LEVELS[(i % 6) as usize];
(r, g, b)
} else {
ansi16_rgb(idx)
}
}
pub(crate) fn posterize_channel(v: u8, levels: u8) -> u8 {
let levels = levels.max(2) as f32;
let step = 255.0 / (levels - 1.0);
((v as f32 / step).round() * step).clamp(0.0, 255.0) as u8
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Quantized {
TrueColor(Rgb8),
Ansi256(u8),
Ansi16(u8),
}
impl Quantized {
pub(crate) fn rgb(&self) -> Rgb8 {
match *self {
Quantized::TrueColor(c) => c,
Quantized::Ansi256(i) => ansi256_rgb(i),
Quantized::Ansi16(i) => ansi16_rgb(i),
}
}
pub(crate) fn escape(&self) -> String {
match *self {
Quantized::TrueColor((r, g, b)) => format!("\x1b[38;2;{r};{g};{b}m"),
Quantized::Ansi256(i) => format!("\x1b[38;5;{i}m"),
Quantized::Ansi16(i) => {
let code = if i < 8 { 30 + i } else { 90 + (i - 8) };
format!("\x1b[{code}m")
}
}
}
}
pub(crate) fn quantize_for_depth(c: Rgb8, depth: ColorDepth, levels: Option<u8>) -> Quantized {
match depth {
ColorDepth::TrueColor => match levels {
Some(l) => Quantized::TrueColor((
posterize_channel(c.0, l),
posterize_channel(c.1, l),
posterize_channel(c.2, l),
)),
None => Quantized::TrueColor(c),
},
ColorDepth::Ansi256 => Quantized::Ansi256(nearest_ansi256(c)),
ColorDepth::Ansi16 => Quantized::Ansi16(nearest_ansi16(c)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nearest_ansi16_matches_exact_palette_entries() {
for (i, &c) in ANSI16_PALETTE.iter().enumerate() {
assert_eq!(nearest_ansi16(c), i as u8);
}
}
#[test]
fn ansi256_roundtrips_cube_corners() {
let idx = nearest_ansi256((255, 0, 0));
let rgb = ansi256_rgb(idx);
assert!(rgb.0 > 200 && rgb.1 < 50 && rgb.2 < 50);
}
#[test]
fn posterize_channel_clamps_to_levels() {
assert_eq!(posterize_channel(0, 2), 0);
assert_eq!(posterize_channel(255, 2), 255);
assert_eq!(posterize_channel(130, 2), 255);
assert_eq!(posterize_channel(120, 2), 0);
}
#[test]
fn quantize_truecolor_without_levels_is_identity() {
let q = quantize_for_depth((10, 20, 30), ColorDepth::TrueColor, None);
assert_eq!(q.rgb(), (10, 20, 30));
}
}