From 724844ba34d5e590d51b1e901d71757ebdbdffce Mon Sep 17 00:00:00 2001 From: azy Date: Sat, 1 Feb 2025 18:36:31 +0800 Subject: [PATCH] Add the preview of images - New image.rs file in utils to convert images in usable array - new accepted type of files in utils/files.rs - the enum PreviewContent has now an Image entry to handle images - conversion from image file to a usable paragraph into preview --- Cargo.toml | 1 + television/preview/mod.rs | 14 ++++-- television/preview/previewers/files.rs | 50 +++++++++++++++++++-- television/screen/preview.rs | 61 ++++++++++++++++++++++++++ television/television.rs | 7 +-- television/utils/files.rs | 39 ++++++++++++++++ television/utils/image.rs | 56 +++++++++++++++++++++++ television/utils/mod.rs | 1 + 8 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 television/utils/image.rs diff --git a/Cargo.toml b/Cargo.toml index c8ff09f..2dd98bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ bat = { version = "0.24", default-features = false, features = ["regex-onig"] } gag = "1.0" nucleo = "0.5" toml = "0.8" +image = "0.25.5" [target.'cfg(windows)'.dependencies] winapi-util = "0.1.9" diff --git a/television/preview/mod.rs b/television/preview/mod.rs index ad9342b..a873550 100644 --- a/television/preview/mod.rs +++ b/television/preview/mod.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use crate::channels::entry::{Entry, PreviewType}; use devicons::FileIcon; +use ratatui::layout::Rect; pub mod ansi; pub mod cache; @@ -10,6 +11,7 @@ pub mod previewers; // previewer types use crate::utils::cache::RingSet; use crate::utils::syntax::HighlightedLines; +use crate::utils::image::Image; pub use previewers::basic::BasicPreviewer; pub use previewers::basic::BasicPreviewerConfig; pub use previewers::command::CommandPreviewer; @@ -30,6 +32,7 @@ pub enum PreviewContent { PlainText(Vec), PlainTextWrapped(String), AnsiText(String), + Image(Image) } impl PreviewContent { @@ -44,6 +47,9 @@ impl PreviewContent { PreviewContent::AnsiText(text) => { text.lines().count().try_into().unwrap_or(u16::MAX) } + PreviewContent::Image(image) => { + image.pixel_grid.len().try_into().unwrap_or(u16::MAX) + } _ => 0, } } @@ -164,11 +170,11 @@ impl Previewer { } } - fn dispatch_request(&mut self, entry: &Entry) -> Option> { + fn dispatch_request(&mut self, entry: &Entry, preview_window: Option) -> Option> { match &entry.preview_type { PreviewType::Basic => Some(self.basic.preview(entry)), PreviewType::EnvVar => Some(self.env_var.preview(entry)), - PreviewType::Files => self.file.preview(entry), + PreviewType::Files => self.file.preview(entry, preview_window), PreviewType::Command(cmd) => self.command.preview(entry, cmd), PreviewType::None => Some(Arc::new(Preview::default())), } @@ -183,11 +189,11 @@ impl Previewer { } } - pub fn preview(&mut self, entry: &Entry) -> Option> { + pub fn preview(&mut self, entry: &Entry, preview_window: Option) -> Option> { // if we haven't acknowledged the request yet, acknowledge it self.requests.push(entry.clone()); - if let Some(preview) = self.dispatch_request(entry) { + if let Some(preview) = self.dispatch_request(entry, preview_window) { return Some(preview); } // lookup request stack and return the most recent preview available diff --git a/television/preview/previewers/files.rs b/television/preview/previewers/files.rs index fb6a828..d7d2e56 100644 --- a/television/preview/previewers/files.rs +++ b/television/preview/previewers/files.rs @@ -10,14 +10,16 @@ use std::sync::{ atomic::{AtomicU8, Ordering}, Arc, }; - +use ratatui::layout::Rect; use syntect::{highlighting::Theme, parsing::SyntaxSet}; use tracing::{debug, warn}; +use image::ImageReader; use crate::channels::entry; use crate::preview::cache::PreviewCache; use crate::preview::{previewers::meta, Preview, PreviewContent}; use crate::utils::{ + image::Image, files::FileType, strings::preprocess_line, syntax::{self, load_highlighting_assets, HighlightingAssetsExt}, @@ -78,20 +80,20 @@ impl FilePreviewer { self.cache.lock().get(&entry.name) } - pub fn preview(&mut self, entry: &entry::Entry) -> Option> { + pub fn preview(&mut self, entry: &entry::Entry, preview_window: Option) -> Option> { if let Some(preview) = self.cached(entry) { debug!("Preview cache hit for {:?}", entry.name); if preview.partial_offset.is_some() { // preview is partial, spawn a task to compute the next chunk // and return the partial preview debug!("Spawning partial preview task for {:?}", entry.name); - self.handle_preview_request(entry, Some(preview.clone())); + self.handle_preview_request(entry, Some(preview.clone()), preview_window); } Some(preview) } else { // preview is not in cache, spawn a task to compute the preview debug!("Preview cache miss for {:?}", entry.name); - self.handle_preview_request(entry, None); + self.handle_preview_request(entry, None, preview_window); None } } @@ -100,6 +102,7 @@ impl FilePreviewer { &mut self, entry: &entry::Entry, partial_preview: Option>, + preview_window: Option, ) { if self.in_flight_previews.lock().contains(&entry.name) { debug!("Preview already in flight for {:?}", entry.name); @@ -126,6 +129,7 @@ impl FilePreviewer { &syntax_theme, &concurrent_tasks, &in_flight_previews, + preview_window, ); }); } @@ -149,6 +153,7 @@ pub fn try_preview( syntax_theme: &Arc, concurrent_tasks: &Arc, in_flight_previews: &Arc>>, + preview_window: Option, ) { debug!("Computing preview for {:?}", entry.name); let path = PathBuf::from(&entry.name); @@ -236,6 +241,43 @@ pub fn try_preview( cache.lock().insert(entry.name.clone(), &p); } } + + } else if matches!(FileType::from(&path), FileType::Image) + { + debug!("File is an image: {:?}", entry.name); + let (window_height, window_width) = if let Some(preview_window) = preview_window{ + // it should be a better way to know the size of the border to remove than this magic number + let padding = 5; + ((preview_window.height - padding /2 ) * 2, preview_window.width - padding) + }else{ + warn!("Error opening image, impossible to display without information about the size of the preview window"); + let p = meta::not_supported(&entry.name); + cache.lock().insert(entry.name.clone(), &p); + return; + }; + match ImageReader::open(path).unwrap().decode() { + Ok(image) => { + debug!("Width: {:}", window_width); + + let image = Image::from_dynamic_image(image, window_height as u32, window_width as u32); + let total_lines = image.pixel_grid.len().try_into().unwrap_or(u16::MAX); + let content = PreviewContent::Image(image); + let preview = Arc::new(Preview::new( + entry.name.clone(), + content, + entry.icon, + None, + total_lines, + )); + cache.lock().insert(entry.name.clone(), &preview); + } + Err(e) => { + warn!("Error opening file: {:?}", e); + let p = meta::not_supported(&entry.name); + cache.lock().insert(entry.name.clone(), &p); + } + } + } else { debug!("File isn't text-based: {:?}", entry.name); let preview = meta::not_supported(&entry.name); diff --git a/television/screen/preview.rs b/television/screen/preview.rs index 8005337..1c4ed36 100644 --- a/television/screen/preview.rs +++ b/television/screen/preview.rs @@ -21,6 +21,7 @@ use ratatui::{ }; use std::str::FromStr; use std::sync::{Arc, Mutex}; +use crate::utils::image::{Image, ImageColor, PIXEL}; #[allow(dead_code)] const FILL_CHAR_SLANTED: char = '╱'; @@ -68,6 +69,10 @@ pub fn build_preview_paragraph<'a>( colorscheme.preview, ) } + PreviewContent::Image(image) => { + build_image_paragraph(image, preview_block,colorscheme.preview) + } + // meta PreviewContent::Loading => { build_meta_preview_paragraph(inner, LOADING_MSG, FILL_CHAR_EMPTY) @@ -216,6 +221,23 @@ fn build_syntect_highlighted_paragraph( .scroll((preview_scroll, 0)) } +fn build_image_paragraph( + image: Image, + preview_block: Block<'_>, + colorscheme: PreviewColorscheme, +) -> Paragraph<'_> { + let lines = image.pixel_grid.iter().map(|double_pixel_line| + Line::from_iter( + double_pixel_line.iter().map(|(color_up, color_down)| + convert_pixel_to_span(*color_up, *color_down, Some(colorscheme.highlight_bg))) + ) + ).collect::>(); + let text = Text::from(lines); + Paragraph::new(text) + .block(preview_block) + .wrap(Wrap { trim: true }) +} + pub fn build_meta_preview_paragraph<'a>( inner: Rect, message: &str, @@ -509,3 +531,42 @@ fn compute_cache_key(entry: &Entry) -> String { } cache_key } + +pub fn convert_pixel_to_span<'a>( + color_up: ImageColor, + color_down: ImageColor, + background: Option, +) -> Span<'a> { + let bg_color = match background { + Some(Color::Rgb(r, g, b)) => Some((r,g,b)), + _ => {None} + }; + let (color_up, color_down) = if let Some(bg_color) = bg_color { + let color_up_with_alpha = Color::Rgb( + (color_up.r * color_up.a + bg_color.0 * 255 - color_up.a) / 255, + (color_up.b * color_up.a + bg_color.1 * 255 - color_up.a) / 255, + (color_up.b * color_up.a + bg_color.2 * 255 - color_up.a) / 255, + ); + let color_down_with_alpha = Color::Rgb( + (color_down.r * color_down.a + bg_color.0 * 255 - color_down.a) / 255, + (color_down.b * color_down.a + bg_color.1 * 255 - color_down.a) / 255, + (color_down.b * color_down.a + bg_color.2 * 255 - color_down.a) / 255, + ); + + (color_up_with_alpha,color_down_with_alpha) + }else{ + (convert_image_color_to_ratatui_color(color_up), + convert_image_color_to_ratatui_color(color_down)) + }; + let style = Style::default() + .fg(color_up) + .bg(color_down); + + Span::styled(String::from(PIXEL), style) +} + +fn convert_image_color_to_ratatui_color( + color: ImageColor, +) -> Color { + Color::Rgb(color.r, color.g, color.b) +} \ No newline at end of file diff --git a/television/television.rs b/television/television.rs index 61d91b6..7f043a9 100644 --- a/television/television.rs +++ b/television/television.rs @@ -587,10 +587,11 @@ impl Television { if self.config.ui.show_preview_panel && !matches!(selected_entry.preview_type, PreviewType::None) { - // preview content - let maybe_preview = self.previewer.preview(&selected_entry); - let _ = self.previewer.preview(&selected_entry); + // preview content + let maybe_preview = self.previewer.preview(&selected_entry, layout.preview_window); + + let _ = self.previewer.preview(&selected_entry, layout.preview_window); if let Some(preview) = &maybe_preview { self.current_preview_total_lines = preview.total_lines; diff --git a/television/utils/files.rs b/television/utils/files.rs index fb81fc4..e462f53 100644 --- a/television/utils/files.rs +++ b/television/utils/files.rs @@ -104,6 +104,7 @@ pub fn get_file_size(path: &Path) -> Option { #[derive(Debug)] pub enum FileType { Text, + Image, Other, Unknown, } @@ -118,6 +119,9 @@ where if is_known_text_extension(p) { return FileType::Text; } + if is_accepted_image_extension(p){ + return FileType::Image; + } if let Ok(mut f) = File::open(p) { let mut buffer = [0u8; 256]; if let Ok(bytes_read) = f.read(&mut buffer) { @@ -480,3 +484,38 @@ lazy_static! { .copied() .collect(); } + +pub fn is_accepted_image_extension

(path: P) -> bool +where + P: AsRef, +{ + path.as_ref() + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| KNOWN_IMAGE_FILE_EXTENSIONS.contains(ext)) +} + +lazy_static! { + static ref KNOWN_IMAGE_FILE_EXTENSIONS: FxHashSet<&'static str> = [ + //"avif", + //"bmp", + //"dds", + //"farbfeld", + "gif", + //"hdr", + //"ico", + "jpeg", + "jpg", + //"exr", + "png", + //"pnm", + //"qoi", + //"tga", + //"tiff", + "webp", + + ] + .iter() + .copied() + .collect(); +} \ No newline at end of file diff --git a/television/utils/image.rs b/television/utils/image.rs new file mode 100644 index 0000000..65f0c1a --- /dev/null +++ b/television/utils/image.rs @@ -0,0 +1,56 @@ +use image::DynamicImage; +use image::imageops::FilterType; + +pub const PIXEL: char = '▀' ; +const FILTER: FilterType = FilterType::Triangle; +#[derive(Clone, Debug)] +pub struct Image { + pub pixel_grid: Vec> +} +impl Image { + pub fn new(pixel_grid: Vec>) -> Self { + Image { pixel_grid} + } + pub fn from_dynamic_image(dynamic_image: DynamicImage, height: u32, width: u32) -> Self { + + let image = if dynamic_image.height() > height || dynamic_image.width() > width { + println!("{}", dynamic_image.height()); + dynamic_image.resize(width, height, FILTER) + }else{ + dynamic_image + }; + + let image = image.into_rgba8(); + let pixel_grid = image.rows() + .step_by(2) + .zip(image.rows().skip(1).step_by(2)) + .map(|(row_1, row_2)| { + row_1.zip(row_2) + .map(|(pixel_1, pixel_2)| + ( + ImageColor { + r: pixel_1.0[0], + g: pixel_1.0[1], + b: pixel_1.0[2], + a: pixel_1.0[3], + }, + ImageColor { + r: pixel_2.0[0], + g: pixel_2.0[1], + b: pixel_2.0[2], + a: pixel_1.0[3], + })) + .collect::>() + }) + .collect::>>(); + Image::new(pixel_grid) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct ImageColor{ + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8 +} \ No newline at end of file diff --git a/television/utils/mod.rs b/television/utils/mod.rs index 8328621..254ef5c 100644 --- a/television/utils/mod.rs +++ b/television/utils/mod.rs @@ -9,3 +9,4 @@ pub mod stdin; pub mod strings; pub mod syntax; pub mod threads; +pub mod image;