use crate::{
markdown::text_style::{Color, Colors, TextStyle},
terminal::{
image::printer::{ImageProperties, ImageSpec, PrintImage, PrintImageError, PrintOptions, RegisterImageError},
printer::{TerminalCommand, TerminalIo},
},
};
use image::{DynamicImage, GenericImageView, Pixel, Rgba, RgbaImage, imageops::FilterType};
use itertools::Itertools;
use std::{
collections::HashMap,
fs,
sync::{Arc, Mutex},
};
const TOP_CHAR: &str = "▀";
const BOTTOM_CHAR: &str = "▄";
struct Inner {
image: DynamicImage,
cached_sizes: Mutex<HashMap<(u16, u16), RgbaImage>>,
}
#[derive(Clone)]
pub(crate) struct AsciiImage {
inner: Arc<Inner>,
}
impl AsciiImage {
pub(crate) fn cache_scaling(&self, columns: u16, rows: u16) {
let mut cached_sizes = self.inner.cached_sizes.lock().unwrap();
let cache_key = (columns, rows);
if cached_sizes.get(&cache_key).is_none() {
let image = self.inner.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle);
cached_sizes.insert(cache_key, image.into_rgba8());
}
}
}
impl ImageProperties for AsciiImage {
fn dimensions(&self) -> (u32, u32) {
self.inner.image.dimensions()
}
}
impl From<DynamicImage> for AsciiImage {
fn from(image: DynamicImage) -> Self {
let image = image.into_rgba8();
let inner = Inner { image: image.into(), cached_sizes: Default::default() };
Self { inner: Arc::new(inner) }
}
}
#[derive(Default)]
pub struct AsciiPrinter;
impl AsciiPrinter {
fn pixel_color(pixel: &Rgba<u8>, background: Option<Color>) -> Option<Color> {
let [r, g, b, alpha] = pixel.0;
if alpha == 0 {
None
} else if alpha < 255 {
let mut pixel = *pixel;
match background {
Some(Color::Rgb { r, g, b }) => {
pixel.blend(&Rgba([r, g, b, 255 - alpha]));
Some(Color::Rgb { r: pixel[0], g: pixel[1], b: pixel[2] })
}
None | Some(_) => Some(Color::Rgb { r, g, b }),
}
} else {
Some(Color::Rgb { r, g, b })
}
}
}
impl PrintImage for AsciiPrinter {
type Image = AsciiImage;
fn register(&self, spec: ImageSpec) -> Result<Self::Image, RegisterImageError> {
let image = match spec {
ImageSpec::Generated(image) => image,
ImageSpec::Filesystem(path) => {
let contents = fs::read(path)?;
image::load_from_memory(&contents)?
}
};
Ok(AsciiImage::from(image))
}
fn print<T>(&self, image: &Self::Image, options: &PrintOptions, terminal: &mut T) -> Result<(), PrintImageError>
where
T: TerminalIo,
{
let columns = options.columns;
let rows = options.rows * 2;
image.cache_scaling(columns, rows);
let cache_key = (columns, rows);
let cached_sizes = image.inner.cached_sizes.lock().unwrap();
let image = cached_sizes.get(&cache_key).expect("scaled image no longer there");
let default_background = options.background_color;
for mut rows in &image.rows().chunks(2) {
let top_row = rows.next().unwrap();
let mut bottom_row = rows.next();
for top_pixel in top_row {
let bottom_pixel = bottom_row.as_mut().and_then(|pixels| pixels.next());
let background = default_background;
let top = Self::pixel_color(top_pixel, background);
let bottom = bottom_pixel.and_then(|c| Self::pixel_color(c, background));
let command = match (top, bottom) {
(Some(top), Some(bottom)) => TerminalCommand::PrintText {
content: TOP_CHAR,
style: TextStyle::default().fg_color(top).bg_color(bottom),
},
(Some(top), None) => TerminalCommand::PrintText {
content: TOP_CHAR,
style: TextStyle::colored(Colors { foreground: Some(top), background: default_background }),
},
(None, Some(bottom)) => TerminalCommand::PrintText {
content: BOTTOM_CHAR,
style: TextStyle::colored(Colors { foreground: Some(bottom), background: default_background }),
},
(None, None) => TerminalCommand::MoveRight(1),
};
terminal.execute(&command)?;
}
terminal.execute(&TerminalCommand::MoveDown(1))?;
terminal.execute(&TerminalCommand::MoveLeft(options.columns))?;
}
Ok(())
}
}