use image::imageops::FilterType; use image::{DynamicImage, Pixel, Rgba}; use ratatui::buffer::{Buffer, Cell}; use ratatui::layout::{Position, Rect}; use ratatui::prelude::Color; use ratatui::widgets::Widget; use std::fmt::Debug; use std::hash::Hash; static PIXEL_STRING: &str = "▀"; const FILTER_TYPE: FilterType = FilterType::Lanczos3; // use to reduce the size of the image before storing it const DEFAULT_CACHED_WIDTH: u32 = 50; const DEFAULT_CACHED_HEIGHT: u32 = 100; const GRAY: Rgba = Rgba([242, 242, 242, 255]); const WHITE: Rgba = Rgba([255, 255, 255, 255]); #[derive(Clone, Debug, Hash, PartialEq)] pub struct ImagePreviewWidget { cells: Vec>, } impl Widget for &ImagePreviewWidget { fn render(self, area: Rect, buf: &mut Buffer) { let height = self.height(); let width = self.width(); // offset of the left top corner where the image is centered let total_width = usize::from(area.width) + 2 * usize::from(area.x); let x_offset = total_width.saturating_sub(width) / 2 + 1; let total_height = usize::from(area.height) + 2 * usize::from(area.y); let y_offset = total_height.saturating_sub(height) / 2; let (area_border_up, area_border_down) = (area.y, area.y + area.height); let (area_border_left, area_border_right) = (area.x, area.x + area.width); for (y, row) in self.cells.iter().enumerate() { let pos_y = u16::try_from(y_offset + y).unwrap_or(u16::MAX); if pos_y >= area_border_up && pos_y < area_border_down { for (x, cell) in row.iter().enumerate() { let pos_x = u16::try_from(x_offset + x).unwrap_or(u16::MAX); if pos_x >= area_border_left && pos_x <= area_border_right { if let Some(buf_cell) = buf.cell_mut(Position::new(pos_x, pos_y)) { *buf_cell = cell.clone(); } } } } } } } impl ImagePreviewWidget { pub fn new(cells: Vec>) -> ImagePreviewWidget { ImagePreviewWidget { cells } } pub fn height(&self) -> usize { self.cells.len() } pub fn width(&self) -> usize { if self.height() > 0 { self.cells[0].len() } else { 0 } } pub fn from_dynamic_image( dynamic_image: DynamicImage, dimension: Option<(u32, u32)>, ) -> Self { let (window_width, window_height) = dimension.unwrap_or((DEFAULT_CACHED_WIDTH, DEFAULT_CACHED_HEIGHT)); let (max_width, max_height) = (window_width, window_height * 2 - 2); // -2 to have some space with the title // first quick resize let big_resized_image = if dynamic_image.width() > max_width * 4 || dynamic_image.height() > max_height * 4 { dynamic_image.resize( max_width * 4, max_height * 4, FilterType::Nearest, ) } else { dynamic_image }; // this time resize with the filter let resized_image = if big_resized_image.width() > max_width || big_resized_image.height() > max_height { big_resized_image.resize(max_width, max_height, FILTER_TYPE) } else { big_resized_image }; let cells = Self::cells_from_dynamic_image(resized_image); ImagePreviewWidget::new(cells) } fn cells_from_dynamic_image(image: DynamicImage) -> Vec> { let image_rgba = image.into_rgba8(); //creation of the grid of cell image_rgba // iter over pair of rows .rows() .step_by(2) .zip(image_rgba.rows().skip(1).step_by(2)) .enumerate() .map(|(double_row_y, (row_1, row_2))| { // create rows of cells row_1 .into_iter() .zip(row_2) .enumerate() .map(|(x, (color_up, color_down))| { let position = (x, double_row_y); DoublePixel::new(*color_up, *color_down) .add_grid_background(position) .into_cell() }) .collect::>() }) .collect::>>() } } // util to convert Rgba into ratatui's Cell struct DoublePixel { color_up: Rgba, color_down: Rgba, } impl DoublePixel { pub fn new(color_up: Rgba, color_down: Rgba) -> Self { Self { color_up, color_down, } } pub fn add_grid_background(mut self, position: (usize, usize)) -> Self { let color_up = self.color_up.0; let color_down = self.color_down.0; self.color_up = Self::blend_with_background(color_up, position, 0); self.color_down = Self::blend_with_background(color_down, position, 1); self } fn blend_with_background( color: impl Into>, position: (usize, usize), offset: usize, ) -> Rgba { let color = color.into(); if color[3] == 255 { color } else { let is_white = (position.0 + position.1 * 2 + offset) % 2 == 0; let mut base = if is_white { WHITE } else { GRAY }; base.blend(&color); base } } pub fn into_cell(self) -> Cell { let mut cell = Cell::new(PIXEL_STRING); cell.set_bg(Self::convert_image_color_to_ratatui_color( self.color_down, )) .set_fg(Self::convert_image_color_to_ratatui_color(self.color_up)); cell } fn convert_image_color_to_ratatui_color(color: Rgba) -> Color { Color::Rgb(color[0], color[1], color[2]) } }