use crate::error::ImageError;
use crate::grid::{Cell, Grid, Rgb};
use crate::render::ColorDepth;
use std::io::Write as IoWrite;
static ANSI16_PALETTE: [(u8, u8, u8); 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), ];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ActiveColor {
Reset,
TrueColor(Rgb),
Ansi256(u8),
Ansi16(u8),
}
fn rgb_to_ansi16(rgb: Rgb) -> u8 {
let mut min_dist = u32::MAX;
let mut best_idx = 0;
for (i, &(r, g, b)) in ANSI16_PALETTE.iter().enumerate() {
let dr = rgb.r as i32 - r as i32;
let dg = rgb.g as i32 - g as i32;
let db = rgb.b as i32 - b as i32;
let dist = (dr * dr + dg * dg + db * db) as u32;
if dist < min_dist {
min_dist = dist;
best_idx = i as u8;
}
}
best_idx
}
fn rgb_to_ansi256(rgb: Rgb) -> u8 {
static CUBE_LEVELS: [u8; 6] = [0, 95, 135, 175, 215, 255];
let closest_level = |val: u8| -> (usize, u8) {
let mut min_d = u32::MAX;
let mut best_idx = 0;
for (i, &l) in CUBE_LEVELS.iter().enumerate() {
let d = (val as i32 - l as i32).unsigned_abs();
if d < min_d {
min_d = d;
best_idx = i;
}
}
(best_idx, CUBE_LEVELS[best_idx])
};
let (r_idx, r_val) = closest_level(rgb.r);
let (g_idx, g_val) = closest_level(rgb.g);
let (b_idx, b_val) = closest_level(rgb.b);
let cube_dist = {
let dr = rgb.r as i32 - r_val as i32;
let dg = rgb.g as i32 - g_val as i32;
let db = rgb.b as i32 - b_val as i32;
(dr * dr + dg * dg + db * db) as u32
};
let gray_avg = ((rgb.r as u32 + rgb.g as u32 + rgb.b as u32) / 3) as u8;
let gray_idx = 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 = {
let dr = rgb.r as i32 - gray_val as i32;
let dg = rgb.g as i32 - gray_val as i32;
let db = rgb.b as i32 - gray_val as i32;
(dr * dr + dg * dg + db * db) as u32
};
if gray_dist < cube_dist {
232 + gray_idx
} else {
16 + (36 * r_idx + 6 * g_idx + b_idx) as u8
}
}
#[inline]
fn push_decimal(output: &mut String, value: u32) {
if value < 10 {
output.push((b'0' + value as u8) as char);
return;
}
let mut digits = [0u8; 10];
let mut cursor = digits.len();
let mut remaining = value;
while remaining > 0 {
cursor -= 1;
digits[cursor] = b'0' + (remaining % 10) as u8;
remaining /= 10;
}
output.push_str(std::str::from_utf8(&digits[cursor..]).expect("ASCII digits"));
}
fn ansi_capacity(grid: &Grid, depth: ColorDepth) -> usize {
let escape_bytes = match depth {
ColorDepth::TrueColor => 19, ColorDepth::Ansi256 => 11, ColorDepth::Ansi16 => 5, };
let cells = grid.width() as usize * grid.height() as usize;
let glyph_bytes: usize = grid.as_slice().iter().map(|cell| cell.ch.len_utf8()).sum();
glyph_bytes + cells * escape_bytes + grid.height() as usize * 5 }
fn write_ansi_row(row: &[Cell], depth: ColorDepth, output: &mut String) {
let mut current_color = ActiveColor::Reset;
for cell in row {
let target_color = match depth {
ColorDepth::TrueColor => ActiveColor::TrueColor(cell.color),
ColorDepth::Ansi256 => ActiveColor::Ansi256(rgb_to_ansi256(cell.color)),
ColorDepth::Ansi16 => ActiveColor::Ansi16(rgb_to_ansi16(cell.color)),
};
if target_color != current_color {
match target_color {
ActiveColor::TrueColor(c) => {
output.push_str("\x1b[38;2;");
push_decimal(output, c.r as u32);
output.push(';');
push_decimal(output, c.g as u32);
output.push(';');
push_decimal(output, c.b as u32);
output.push('m');
}
ActiveColor::Ansi256(code) => {
output.push_str("\x1b[38;5;");
push_decimal(output, code as u32);
output.push('m');
}
ActiveColor::Ansi16(code) => {
let escape_code = if code < 8 { 30 + code } else { 90 + (code - 8) };
output.push_str("\x1b[");
push_decimal(output, escape_code as u32);
output.push('m');
}
ActiveColor::Reset => {
output.push_str("\x1b[0m");
}
}
current_color = target_color;
}
output.push(cell.ch);
}
output.push_str("\x1b[0m\n");
}
pub fn render_ansi(grid: &Grid, depth: ColorDepth) -> String {
let mut output = String::with_capacity(ansi_capacity(grid, depth));
for row in grid.rows() {
write_ansi_row(row, depth, &mut output);
}
output
}
pub fn render_ansi_to<W: IoWrite + ?Sized>(
grid: &Grid,
depth: ColorDepth,
out: &mut W,
) -> Result<(), ImageError> {
let mut scratch = String::with_capacity(grid.width() as usize * 24 + 8);
for row in grid.rows() {
scratch.clear();
write_ansi_row(row, depth, &mut scratch);
out.write_all(scratch.as_bytes())?;
}
Ok(())
}