mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-03 01:50:12 +00:00

I initially didn't notice that an image previewer was already implemented but commented out—still, I wanted to finish mine! :) It works almost instantly on my side. I tested it in different terminals and only noticed some slowness in the RustRover-integrated terminal. So far, I’ve tested it with PNG, JPEG, ICO, GIF, and TIFF formats, and it works well. In theory, it should support all formats that the image crate can handle. I included them in the file list but commented out the ones I haven’t tested yet. To optimize memory usage, images are resized to a maximum of 128x128 before being cached. I’m not really sure what the best size is, since the image gets resized again when rendered to fit the preview window. Let me know if you have any feedback! 🚀    --------- Co-authored-by: Alexandre Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Co-authored-by: alexpasmantier <alex.pasmant@gmail.com>
186 lines
5.9 KiB
Rust
186 lines
5.9 KiB
Rust
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<u8> = Rgba([242, 242, 242, 255]);
|
|
const WHITE: Rgba<u8> = Rgba([255, 255, 255, 255]);
|
|
|
|
#[derive(Clone, Debug, Hash, PartialEq)]
|
|
pub struct ImagePreviewWidget {
|
|
cells: Vec<Vec<Cell>>,
|
|
}
|
|
|
|
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<Vec<Cell>>) -> 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<Vec<Cell>> {
|
|
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::<Vec<Cell>>()
|
|
})
|
|
.collect::<Vec<Vec<Cell>>>()
|
|
}
|
|
}
|
|
|
|
// util to convert Rgba into ratatui's Cell
|
|
struct DoublePixel {
|
|
color_up: Rgba<u8>,
|
|
color_down: Rgba<u8>,
|
|
}
|
|
impl DoublePixel {
|
|
pub fn new(color_up: Rgba<u8>, color_down: Rgba<u8>) -> 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<Rgba<u8>>,
|
|
position: (usize, usize),
|
|
offset: usize,
|
|
) -> Rgba<u8> {
|
|
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<u8>) -> Color {
|
|
Color::Rgb(color[0], color[1], color[2])
|
|
}
|
|
}
|