use image::Rgba;
use crate::{
config::{self, Config},
target,
};
pub fn correlating_char(block: &[Rgba<u8>], config: &Config) -> String {
assert!(!block.is_empty());
assert!(!config.characters.is_empty());
let (red, green, blue) = average_color(block);
let luminosity = luminosity(red, green, blue);
let length = config.characters.chars().count();
let density_index = map_range(
(0f32, 255f32),
if config.invert {
(0f32, length as f32)
} else {
(length as f32, 0f32)
},
luminosity,
)
.floor()
.clamp(0f32, length as f32 - 1.0);
assert!((density_index as usize) < length);
let density_char = config
.characters
.chars()
.nth(density_index as usize)
.expect("Failed to get char");
match config.target {
config::TargetType::Shell | config::TargetType::AnsiFile if config.color() => {
target::ansi::colored_char(red, green, blue, density_char, config.background_color())
}
config::TargetType::HtmlFile => {
if config.color() {
target::html::colored_char(
red,
green,
blue,
density_char,
config.background_color(),
)
} else {
density_char.to_string()
}
}
_ => density_char.to_string(),
}
}
#[cfg(test)]
mod test_pixel_density {
use std::env;
use crate::ConfigBuilder;
use super::*;
#[test]
fn invert_returns_first_instead_of_last_char() {
let pixels = vec![
Rgba::<u8>::from([255, 255, 255, 255]),
Rgba::<u8>::from([255, 255, 255, 255]),
Rgba::<u8>::from([0, 0, 0, 255]),
];
let config = ConfigBuilder::new()
.characters("# ".to_owned())
.invert(true)
.color(false)
.build();
assert_eq!(" ", correlating_char(&pixels, &config));
}
#[test]
fn medium_density_char() {
let pixels = vec![
Rgba::<u8>::from([255, 255, 255, 255]),
Rgba::<u8>::from([0, 0, 0, 255]),
];
let config = ConfigBuilder::new()
.characters("#k. ".to_owned())
.color(false)
.build();
assert_eq!("k", correlating_char(&pixels, &config));
}
#[test]
fn dark_density_char() {
let pixels = vec![
Rgba::<u8>::from([255, 255, 255, 255]),
Rgba::<u8>::from([255, 255, 255, 255]),
Rgba::<u8>::from([0, 0, 0, 255]),
];
let config = ConfigBuilder::new()
.characters("#k. ".to_owned())
.color(false)
.build();
assert_eq!("#", correlating_char(&pixels, &config));
}
#[test]
#[ignore = "Requires truecolor support"]
fn colored_char() {
env::set_var("COLORTERM", "truecolor");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([0, 0, 255, 255])];
let config = ConfigBuilder::new().characters("#k. ".to_owned()).build();
assert_eq!(
"\u{1b}[38;2;0;0;255m \u{1b}[0m", correlating_char(&pixels, &config)
);
}
#[test]
fn ansi_colored_char_shell() {
env::set_var("COLORTERM", "false");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([123, 42, 244, 255])];
let config = ConfigBuilder::new().characters("#k. ".to_owned()).build();
assert_eq!("\u{1b}[35m.\u{1b}[0m", correlating_char(&pixels, &config));
}
#[test]
fn ansi_colored_char_ansi() {
env::set_var("COLORTERM", "false");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([123, 42, 244, 255])];
let config = ConfigBuilder::new()
.characters("#k. ".to_owned())
.target(config::TargetType::AnsiFile)
.build();
assert_eq!("\u{1b}[35m.\u{1b}[0m", correlating_char(&pixels, &config));
}
#[test]
#[ignore = "Requires truecolor support"]
fn colored_background_char_shell() {
env::set_var("COLORTERM", "truecolor");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([0, 0, 255, 255])];
let config = ConfigBuilder::new()
.characters("#k. ".to_owned())
.background_color(true)
.build();
assert_eq!(
"\u{1b}[48;2;0;0;255m \u{1b}[0m",
correlating_char(&pixels, &config)
);
}
#[test]
#[ignore = "Requires truecolor support"]
fn colored_background_char_ansi() {
env::set_var("COLORTERM", "truecolor");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([0, 0, 255, 255])];
let config = ConfigBuilder::new()
.characters("#k. ".to_owned())
.target(config::TargetType::AnsiFile)
.background_color(true)
.build();
assert_eq!(
"\u{1b}[48;2;0;0;255m \u{1b}[0m",
correlating_char(&pixels, &config)
);
}
#[test]
fn target_file_returns_non_colored_string() {
env::set_var("COLORTERM", "truecolor");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([0, 0, 255, 255])];
let config = ConfigBuilder::new()
.characters("#k. ".to_owned())
.target(config::TargetType::File)
.build();
assert_eq!(" ", correlating_char(&pixels, &config));
}
#[test]
fn white_has_no_tag() {
env::set_var("COLORTERM", "truecolor");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([0, 0, 255, 255])];
let config = ConfigBuilder::new()
.characters("#k. ".to_owned())
.target(config::TargetType::HtmlFile)
.build();
assert_eq!(" ", correlating_char(&pixels, &config));
}
#[test]
fn target_html_colored_string() {
env::set_var("COLORTERM", "truecolor");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([0, 0, 255, 255])];
let config = ConfigBuilder::new()
.characters("#k:.".to_owned())
.target(config::TargetType::HtmlFile)
.color(true)
.build();
assert_eq!(
"<span style=\"color: #0000FF\">.</span>",
correlating_char(&pixels, &config)
);
}
#[test]
fn target_html_background_string() {
env::set_var("COLORTERM", "truecolor");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([0, 0, 255, 255])];
let config = ConfigBuilder::new()
.characters("#k:. ".to_owned())
.target(config::TargetType::HtmlFile)
.background_color(true)
.build();
assert_eq!(
"<span style=\"background-color: #0000FF\"> </span>",
correlating_char(&pixels, &config)
);
}
#[test]
fn target_html_no_color() {
env::set_var("COLORTERM", "truecolor");
env::set_var("CLICOLOR_FORCE", "1");
let pixels = vec![Rgba::<u8>::from([0, 0, 255, 255])];
let config = ConfigBuilder::new()
.characters("#k. ".to_owned())
.target(config::TargetType::HtmlFile)
.color(false)
.build();
assert_eq!(" ", correlating_char(&pixels, &config));
}
}
fn map_range(from_range: (f32, f32), to_range: (f32, f32), value: f32) -> f32 {
to_range.0 + (value - from_range.0) * (to_range.1 - to_range.0) / (from_range.1 - from_range.0)
}
#[cfg(test)]
mod test_map_range {
use super::*;
#[test]
fn remap_values() {
assert_eq!(4f32, map_range((0f32, 10f32), (0f32, 20f32), 2f32));
}
#[test]
fn remap_values_above_range() {
assert_eq!(42f32, map_range((0f32, 10f32), (0f32, 20f32), 21f32));
}
#[test]
fn remap_values_below_range() {
assert_eq!(-2f32, map_range((0f32, 10f32), (0f32, 20f32), -1f32));
}
}
fn average_color(block: &[Rgba<u8>]) -> (u8, u8, u8) {
let sum = block
.iter()
.map(|pixel| {
(
pixel.0[0] as f32 * pixel.0[0] as f32,
pixel.0[1] as f32 * pixel.0[1] as f32,
pixel.0[2] as f32 * pixel.0[2] as f32,
)
})
.fold((0f32, 0f32, 0f32), |acc, value| {
(acc.0 + value.0, acc.1 + value.1, acc.2 + value.2)
});
(
(sum.0 / block.len() as f32).sqrt() as u8,
(sum.1 / block.len() as f32).sqrt() as u8,
(sum.2 / block.len() as f32).sqrt() as u8,
)
}
#[cfg(test)]
mod test_avg_color {
use super::*;
#[test]
fn red_green() {
let pixels = vec![
Rgba::<u8>::from([255, 0, 0, 255]),
Rgba::<u8>::from([0, 255, 0, 255]),
];
assert_eq!((180, 180, 0), average_color(&pixels));
}
#[test]
fn green_blue() {
let pixels = vec![
Rgba::<u8>::from([0, 255, 0, 255]),
Rgba::<u8>::from([0, 0, 255, 255]),
];
assert_eq!((0, 180, 180), average_color(&pixels));
}
#[test]
fn empty_input() {
let pixels: Vec<Rgba<u8>> = Vec::new();
let (r, g, b) = average_color(&pixels);
assert_eq!(0, r);
assert_eq!(0, g);
assert_eq!(0, b);
}
}
pub fn luminosity(red: u8, green: u8, blue: u8) -> f32 {
(0.21 * red as f32) + (0.72 * green as f32) + (0.07 * blue as f32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn luminosity_black_is_zero() {
assert_eq!(0f32, luminosity(0, 0, 0))
}
#[test]
fn luminosity_white_is_255() {
assert_eq!(255.00002, luminosity(255, 255, 255))
}
#[test]
fn luminosity_rust_color_is_255() {
assert_eq!(97.32f32, luminosity(154, 85, 54))
}
}