use std::fmt::Write as _;
use image::{Rgb, RgbImage};
use palette::{Luv, Srgb, convert::FromColorUnclamped, white_point::D65};
use crate::{
BLACK_LUV,
settings::{CharacterMode, ColorMode, ColorPalette, Settings, UnicodeCharSet},
};
pub type LuvColor = Luv<D65, f32>;
type RGB8 = (u8, u8, u8);
#[inline]
fn pixel_to_luv(p: Rgb<u8>) -> LuvColor {
let srgb = Srgb::new(
p.0[0] as f32 / 255.0,
p.0[1] as f32 / 255.0,
p.0[2] as f32 / 255.0,
);
LuvColor::from_color_unclamped(srgb)
}
#[inline]
fn luv_to_rgb(luv: LuvColor) -> RGB8 {
let srgb = Srgb::from_color_unclamped(luv);
(
(srgb.red * 255.0).round().clamp(0.0, 255.0) as u8,
(srgb.green * 255.0).round().clamp(0.0, 255.0) as u8,
(srgb.blue * 255.0).round().clamp(0.0, 255.0) as u8,
)
}
pub fn process_row(
y_char: usize,
width_char: usize,
img: &RgbImage,
settings: &Settings,
) -> String {
let mut row_str = String::with_capacity(width_char * 15);
let y_px = y_char * 2;
let paletted_colors = if settings.colors.is_truecolor {
None
} else {
Some(
settings
.colors
.palette
.iter()
.map(|&c| Srgb::new(c.0[0], c.0[1], c.0[2]).into_format())
.map(LuvColor::from_color_unclamped)
.collect::<Vec<_>>(),
)
};
let mut last_fg: Option<RGB8> = None;
let mut last_bg: Option<RGB8> = None;
for x_char in 0..width_char {
let x_px = x_char * 2;
let colors = [
pixel_to_luv(*img.get_pixel(x_px as u32, y_px as u32)),
pixel_to_luv(*img.get_pixel(x_px as u32 + 1, y_px as u32)),
pixel_to_luv(*img.get_pixel(x_px as u32, y_px as u32 + 1)),
pixel_to_luv(*img.get_pixel(x_px as u32 + 1, y_px as u32 + 1)),
];
let (character, fg, bg) = if let CharacterMode::Unicode(charset) = settings.characters.mode
{
process_unicode(
&colors,
charset,
settings.characters.color_mode,
paletted_colors.as_ref(),
)
} else {
let char_set: &[char] = match &settings.characters.mode {
CharacterMode::Ascii(cs) => cs.as_slice(),
CharacterMode::Custom(v) => v,
CharacterMode::Unicode(_) => unreachable!(),
};
process_ascii(
&colors,
char_set,
settings.characters.color_mode,
paletted_colors.as_ref(),
)
};
let write_fg = fg != last_fg || !settings.advanced.compression;
let write_bg = bg != last_bg || !settings.advanced.compression;
if write_fg {
match fg {
Some(c) => write!(row_str, "\x1b[38;2;{};{};{}m", c.0, c.1, c.2).unwrap(),
None => write!(row_str, "\x1b[39m").unwrap(),
}
last_fg = fg;
}
if write_bg {
match bg {
Some(c) => write!(row_str, "\x1b[48;2;{};{};{}m", c.0, c.1, c.2).unwrap(),
None => write!(row_str, "\x1b[49m").unwrap(),
}
last_bg = bg;
}
row_str.push(character);
}
row_str.push_str("\x1b[0m");
row_str
}
fn process_ascii(
colors: &[LuvColor; 4],
char_set: &[char],
color_mode: ColorMode,
palette: Option<&ColorPalette<LuvColor>>,
) -> (char, Option<RGB8>, Option<RGB8>) {
if color_mode == ColorMode::TwoColor {
let (lightest, darkest) = find_lightest_darkest(colors);
let (fg_luv, bg_luv) = palette.map_or((lightest, darkest), |p| {
find_closest_pair(lightest, darkest, p, true)
});
let avg = average_color(colors);
let total_dist = luv_distance(lightest, darkest);
let avg_dist = luv_distance(avg, darkest);
let brightness = if total_dist < 1e-5 {
0.0
} else {
(avg_dist / total_dist).min(1.0)
};
let index = brightness_to_char_index(brightness, char_set.len());
(
char_set[index],
Some(luv_to_rgb(fg_luv)),
Some(luv_to_rgb(bg_luv)),
)
} else {
let avg_color = average_color(colors);
let fg_luv = palette.map_or(avg_color, |p| find_closest(avg_color, p));
let brightness = 1.0 - (luv_distance(fg_luv, BLACK_LUV) / 100.0).min(1.0);
let index = brightness_to_char_index(brightness, char_set.len());
(
char_set[index],
Some(luv_to_rgb(fg_luv)),
None, )
}
}
fn process_unicode(
colors: &[LuvColor; 4],
charset: UnicodeCharSet,
color_mode: ColorMode,
palette: Option<&ColorPalette<LuvColor>>,
) -> (char, Option<RGB8>, Option<RGB8>) {
if charset == UnicodeCharSet::Full {
let avg_color = average_color(colors);
let final_color = palette.map_or(avg_color, |p| find_closest(avg_color, p));
return ('█', Some(luv_to_rgb(final_color)), None);
}
let candidates = match charset {
UnicodeCharSet::Full => vec![('█', average_color(colors), BLACK_LUV)],
UnicodeCharSet::Half => {
vec![(
'▀',
average_color(&colors[0..2]),
average_color(&colors[2..4]),
)]
}
UnicodeCharSet::Quarter => vec![
(
'▀',
average_color(&colors[0..2]),
average_color(&colors[2..4]),
), (
'▐',
average_color(&[colors[1], colors[3]]),
average_color(&[colors[0], colors[2]]),
), (
'▞',
average_color(&[colors[1], colors[2]]),
average_color(&[colors[0], colors[3]]),
), (
'▖',
colors[2],
average_color(&[colors[0], colors[1], colors[3]]),
), (
'▘',
colors[0],
average_color(&[colors[1], colors[2], colors[3]]),
), (
'▝',
colors[1],
average_color(&[colors[0], colors[2], colors[3]]),
), (
'▗',
colors[3],
average_color(&[colors[0], colors[1], colors[2]]),
), ],
UnicodeCharSet::Shade => vec![
(' ', BLACK_LUV, BLACK_LUV),
('░', average_color(colors), BLACK_LUV), ('▒', average_color(colors), BLACK_LUV), ('▓', average_color(colors), BLACK_LUV), ],
};
let (best_char, best_fg, best_bg) = candidates
.into_iter()
.map(|(char_candidate, fg_candidate, bg_candidate)| {
let (fg, bg) = palette.map_or((fg_candidate, bg_candidate), |p| {
find_closest_pair(fg_candidate, bg_candidate, p, false)
});
let dist = calculate_block_distance(colors, fg, bg, char_candidate);
(dist, char_candidate, fg, bg)
})
.min_by(|a, b| a.0.total_cmp(&b.0))
.map_or((' ', BLACK_LUV, BLACK_LUV), |(_, c, fg, bg)| (c, fg, bg));
let fg = Some(luv_to_rgb(best_fg));
let bg = if color_mode == ColorMode::TwoColor {
Some(luv_to_rgb(best_bg))
} else {
None
};
(best_char, fg, bg)
}
#[inline]
fn luv_distance(c1: LuvColor, c2: LuvColor) -> f32 {
let (l1, u1, v1) = c1.into_components();
let (l2, u2, v2) = c2.into_components();
let dl = l1 - l2;
let du = u1 - u2;
let dv = v1 - v2;
dv.mul_add(dv, dl.mul_add(dl, du * du)).sqrt()
}
fn calculate_block_distance(
original: &[LuvColor; 4],
fg: LuvColor,
bg: LuvColor,
character: char,
) -> f32 {
let (c1, c2, c3, c4) = (original[0], original[1], original[2], original[3]);
let (t1, t2, t3, t4) = match character {
'▀' => (fg, fg, bg, bg),
'▐' => (bg, fg, bg, fg),
'▞' => (bg, fg, fg, bg),
'▖' => (bg, bg, fg, bg),
'▘' => (fg, bg, bg, bg),
'▝' => (bg, fg, bg, bg),
'▗' => (bg, bg, bg, fg),
'█' => (fg, fg, fg, fg),
'░' => (
blend(fg, bg, 0.25),
blend(fg, bg, 0.25),
blend(fg, bg, 0.25),
blend(fg, bg, 0.25),
),
'▒' => (
blend(fg, bg, 0.50),
blend(fg, bg, 0.50),
blend(fg, bg, 0.50),
blend(fg, bg, 0.50),
),
'▓' => (
blend(fg, bg, 0.75),
blend(fg, bg, 0.75),
blend(fg, bg, 0.75),
blend(fg, bg, 0.75),
),
_ => (bg, bg, bg, bg), };
let d1 = luv_distance(c1, t1);
let d2 = luv_distance(c2, t2);
let d3 = luv_distance(c3, t3);
let d4 = luv_distance(c4, t4);
d4.mul_add(d4, d3.mul_add(d3, d1.mul_add(d1, d2 * d2)))
}
#[inline]
fn blend(a: LuvColor, b: LuvColor, ratio: f32) -> LuvColor {
Luv::new(
a.l.mul_add(ratio, b.l * (1.0 - ratio)),
a.u.mul_add(ratio, b.u * (1.0 - ratio)),
a.v.mul_add(ratio, b.v * (1.0 - ratio)),
)
}
#[inline]
fn average_color(colors: &[LuvColor]) -> LuvColor {
let count = colors.len() as f32;
if count == 0.0 {
return BLACK_LUV;
}
let (l_sum, u_sum, v_sum) = colors
.iter()
.fold((0.0, 0.0, 0.0), |(l, u, v), c| (l + c.l, u + c.u, v + c.v));
Luv::new(l_sum / count, u_sum / count, v_sum / count)
}
#[inline]
fn find_lightest_darkest(colors: &[LuvColor]) -> (LuvColor, LuvColor) {
let mut lightest = colors[0];
let mut darkest = colors[0];
for &c in colors.iter().skip(1) {
if c.l > lightest.l {
lightest = c;
}
if c.l < darkest.l {
darkest = c;
}
}
(lightest, darkest)
}
fn find_closest(color: LuvColor, palette: &ColorPalette<LuvColor>) -> LuvColor {
palette
.iter()
.min_by(|&&c1, &&c2| {
let d1 = luv_distance(color, c1);
let d2 = luv_distance(color, c2);
d1.total_cmp(&d2)
})
.copied()
.unwrap_or(color)
}
fn find_closest_pair(
color1: LuvColor,
color2: LuvColor,
palette: &ColorPalette<LuvColor>,
order_by_brightness: bool,
) -> (LuvColor, LuvColor) {
if palette.is_empty() {
return (BLACK_LUV, BLACK_LUV);
}
if palette.len() == 1 {
return (palette[0], palette[0]);
}
if !order_by_brightness {
return (find_closest(color1, palette), find_closest(color2, palette));
}
let (mut closest1, mut min_dist1, mut idx1) = (palette[0], f32::MAX, 0);
for (i, &p_color) in palette.iter().enumerate() {
let dist = luv_distance(color1, p_color);
if dist < min_dist1 {
min_dist1 = dist;
closest1 = p_color;
idx1 = i;
}
}
let mut closest2 = if idx1 == 0 { palette[1] } else { palette[0] };
let mut min_dist2 = f32::MAX;
for (i, &p_color) in palette.iter().enumerate() {
if i == idx1 {
continue; }
let dist = luv_distance(color2, p_color);
if dist < min_dist2 {
min_dist2 = dist;
closest2 = p_color;
}
}
if luv_distance(closest1, BLACK_LUV) < luv_distance(closest2, BLACK_LUV) {
(closest2, closest1)
} else {
(closest1, closest2)
}
}
#[inline]
fn brightness_to_char_index(brightness: f32, char_set_len: usize) -> usize {
let len_f = (char_set_len - 1) as f32;
let index = (brightness * len_f).round() as usize;
index.min(char_set_len - 1)
}
#[cfg(test)]
mod tests {
use super::brightness_to_char_index;
#[test]
fn brightness_index_bounds() {
assert_eq!(brightness_to_char_index(0.0, 10), 0);
assert_eq!(brightness_to_char_index(1.0, 10), 9);
assert_eq!(brightness_to_char_index(-0.1, 10), 0);
assert_eq!(brightness_to_char_index(1.1, 10), 9);
}
}