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
This commit is contained in:
azy 2025-02-01 18:46:59 +08:00
parent 724844ba34
commit 9e1d9baaa4
6 changed files with 86 additions and 50 deletions

View File

@ -10,8 +10,8 @@ pub mod previewers;
// previewer types // previewer types
use crate::utils::cache::RingSet; use crate::utils::cache::RingSet;
use crate::utils::syntax::HighlightedLines;
use crate::utils::image::Image; use crate::utils::image::Image;
use crate::utils::syntax::HighlightedLines;
pub use previewers::basic::BasicPreviewer; pub use previewers::basic::BasicPreviewer;
pub use previewers::basic::BasicPreviewerConfig; pub use previewers::basic::BasicPreviewerConfig;
pub use previewers::command::CommandPreviewer; pub use previewers::command::CommandPreviewer;
@ -32,7 +32,7 @@ pub enum PreviewContent {
PlainText(Vec<String>), PlainText(Vec<String>),
PlainTextWrapped(String), PlainTextWrapped(String),
AnsiText(String), AnsiText(String),
Image(Image) Image(Image),
} }
impl PreviewContent { impl PreviewContent {
@ -170,7 +170,11 @@ impl Previewer {
} }
} }
fn dispatch_request(&mut self, entry: &Entry, preview_window: Option<Rect>) -> Option<Arc<Preview>> { fn dispatch_request(
&mut self,
entry: &Entry,
preview_window: Option<Rect>,
) -> Option<Arc<Preview>> {
match &entry.preview_type { match &entry.preview_type {
PreviewType::Basic => Some(self.basic.preview(entry)), PreviewType::Basic => Some(self.basic.preview(entry)),
PreviewType::EnvVar => Some(self.env_var.preview(entry)), PreviewType::EnvVar => Some(self.env_var.preview(entry)),
@ -189,7 +193,11 @@ impl Previewer {
} }
} }
pub fn preview(&mut self, entry: &Entry, preview_window: Option<Rect>) -> Option<Arc<Preview>> { pub fn preview(
&mut self,
entry: &Entry,
preview_window: Option<Rect>,
) -> Option<Arc<Preview>> {
// if we haven't acknowledged the request yet, acknowledge it // if we haven't acknowledged the request yet, acknowledge it
self.requests.push(entry.clone()); self.requests.push(entry.clone());

View File

@ -1,6 +1,8 @@
use crate::utils::files::{read_into_lines_capped, ReadResult}; use crate::utils::files::{read_into_lines_capped, ReadResult};
use crate::utils::syntax::HighlightedLines; use crate::utils::syntax::HighlightedLines;
use image::ImageReader;
use parking_lot::Mutex; use parking_lot::Mutex;
use ratatui::layout::Rect;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use std::collections::HashSet; use std::collections::HashSet;
use std::fs::File; use std::fs::File;
@ -10,17 +12,15 @@ use std::sync::{
atomic::{AtomicU8, Ordering}, atomic::{AtomicU8, Ordering},
Arc, Arc,
}; };
use ratatui::layout::Rect;
use syntect::{highlighting::Theme, parsing::SyntaxSet}; use syntect::{highlighting::Theme, parsing::SyntaxSet};
use tracing::{debug, warn}; use tracing::{debug, warn};
use image::ImageReader;
use crate::channels::entry; use crate::channels::entry;
use crate::preview::cache::PreviewCache; use crate::preview::cache::PreviewCache;
use crate::preview::{previewers::meta, Preview, PreviewContent}; use crate::preview::{previewers::meta, Preview, PreviewContent};
use crate::utils::{ use crate::utils::{
image::Image,
files::FileType, files::FileType,
image::Image,
strings::preprocess_line, strings::preprocess_line,
syntax::{self, load_highlighting_assets, HighlightingAssetsExt}, syntax::{self, load_highlighting_assets, HighlightingAssetsExt},
}; };
@ -80,14 +80,22 @@ impl FilePreviewer {
self.cache.lock().get(&entry.name) self.cache.lock().get(&entry.name)
} }
pub fn preview(&mut self, entry: &entry::Entry, preview_window: Option<Rect>) -> Option<Arc<Preview>> { pub fn preview(
&mut self,
entry: &entry::Entry,
preview_window: Option<Rect>,
) -> Option<Arc<Preview>> {
if let Some(preview) = self.cached(entry) { if let Some(preview) = self.cached(entry) {
debug!("Preview cache hit for {:?}", entry.name); debug!("Preview cache hit for {:?}", entry.name);
if preview.partial_offset.is_some() { if preview.partial_offset.is_some() {
// preview is partial, spawn a task to compute the next chunk // preview is partial, spawn a task to compute the next chunk
// and return the partial preview // and return the partial preview
debug!("Spawning partial preview task for {:?}", entry.name); debug!("Spawning partial preview task for {:?}", entry.name);
self.handle_preview_request(entry, Some(preview.clone()), preview_window); self.handle_preview_request(
entry,
Some(preview.clone()),
preview_window,
);
} }
Some(preview) Some(preview)
} else { } else {
@ -241,15 +249,18 @@ pub fn try_preview(
cache.lock().insert(entry.name.clone(), &p); cache.lock().insert(entry.name.clone(), &p);
} }
} }
} else if matches!(FileType::from(&path), FileType::Image) {
} else if matches!(FileType::from(&path), FileType::Image)
{
debug!("File is an image: {:?}", entry.name); debug!("File is an image: {:?}", entry.name);
let (window_height, window_width) = if let Some(preview_window) = preview_window{ 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 // it should be a better way to know the size of the border to remove than this magic number
let padding = 5; let padding = 5;
((preview_window.height - padding /2 ) * 2, preview_window.width - padding) (
}else{ (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"); warn!("Error opening image, impossible to display without information about the size of the preview window");
let p = meta::not_supported(&entry.name); let p = meta::not_supported(&entry.name);
cache.lock().insert(entry.name.clone(), &p); cache.lock().insert(entry.name.clone(), &p);
@ -259,8 +270,13 @@ pub fn try_preview(
Ok(image) => { Ok(image) => {
debug!("Width: {:}", window_width); debug!("Width: {:}", window_width);
let image = Image::from_dynamic_image(image, window_height as u32, window_width as u32); let image = Image::from_dynamic_image(
let total_lines = image.pixel_grid.len().try_into().unwrap_or(u16::MAX); image,
u32::from(window_height),
u32::from(window_width),
);
let total_lines =
image.pixel_grid.len().try_into().unwrap_or(u16::MAX);
let content = PreviewContent::Image(image); let content = PreviewContent::Image(image);
let preview = Arc::new(Preview::new( let preview = Arc::new(Preview::new(
entry.name.clone(), entry.name.clone(),
@ -277,7 +293,6 @@ pub fn try_preview(
cache.lock().insert(entry.name.clone(), &p); cache.lock().insert(entry.name.clone(), &p);
} }
} }
} else { } else {
debug!("File isn't text-based: {:?}", entry.name); debug!("File isn't text-based: {:?}", entry.name);
let preview = meta::not_supported(&entry.name); let preview = meta::not_supported(&entry.name);

View File

@ -7,6 +7,7 @@ use crate::screen::{
cache::RenderedPreviewCache, cache::RenderedPreviewCache,
colors::{Colorscheme, PreviewColorscheme}, colors::{Colorscheme, PreviewColorscheme},
}; };
use crate::utils::image::{Image, ImageColor, PIXEL};
use crate::utils::strings::{ use crate::utils::strings::{
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig, replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
EMPTY_STRING, EMPTY_STRING,
@ -21,7 +22,6 @@ use ratatui::{
}; };
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::utils::image::{Image, ImageColor, PIXEL};
#[allow(dead_code)] #[allow(dead_code)]
const FILL_CHAR_SLANTED: char = ''; const FILL_CHAR_SLANTED: char = '';
@ -70,7 +70,7 @@ pub fn build_preview_paragraph<'a>(
) )
} }
PreviewContent::Image(image) => { PreviewContent::Image(image) => {
build_image_paragraph(image, preview_block,colorscheme.preview) build_image_paragraph(image, preview_block, colorscheme.preview)
} }
// meta // meta
@ -226,16 +226,25 @@ fn build_image_paragraph(
preview_block: Block<'_>, preview_block: Block<'_>,
colorscheme: PreviewColorscheme, colorscheme: PreviewColorscheme,
) -> Paragraph<'_> { ) -> Paragraph<'_> {
let lines = image.pixel_grid.iter().map(|double_pixel_line| let lines = image
Line::from_iter( .pixel_grid
double_pixel_line.iter().map(|(color_up, color_down)| .into_iter()
convert_pixel_to_span(*color_up, *color_down, Some(colorscheme.highlight_bg))) .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::<Vec<Line>>(); },
))
})
.collect::<Vec<Line>>();
let text = Text::from(lines); let text = Text::from(lines);
Paragraph::new(text) Paragraph::new(text)
.block(preview_block) .block(preview_block)
.wrap(Wrap { trim: true }) .wrap(Wrap { trim: false })
} }
pub fn build_meta_preview_paragraph<'a>( pub fn build_meta_preview_paragraph<'a>(
@ -538,8 +547,8 @@ pub fn convert_pixel_to_span<'a>(
background: Option<Color>, background: Option<Color>,
) -> Span<'a> { ) -> Span<'a> {
let bg_color = match background { let bg_color = match background {
Some(Color::Rgb(r, g, b)) => Some((r,g,b)), Some(Color::Rgb(r, g, b)) => Some((r, g, b)),
_ => {None} _ => None,
}; };
let (color_up, color_down) = if let Some(bg_color) = bg_color { let (color_up, color_down) = if let Some(bg_color) = bg_color {
let color_up_with_alpha = Color::Rgb( let color_up_with_alpha = Color::Rgb(
@ -548,25 +557,26 @@ pub fn convert_pixel_to_span<'a>(
(color_up.b * color_up.a + bg_color.2 * 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( let color_down_with_alpha = Color::Rgb(
(color_down.r * color_down.a + bg_color.0 * 255 - color_down.a) / 255, (color_down.r * color_down.a + bg_color.0 * 255 - color_down.a)
(color_down.b * color_down.a + bg_color.1 * 255 - color_down.a) / 255, / 255,
(color_down.b * color_down.a + bg_color.2 * 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) (color_up_with_alpha, color_down_with_alpha)
}else{ } else {
(convert_image_color_to_ratatui_color(color_up), (
convert_image_color_to_ratatui_color(color_down)) convert_image_color_to_ratatui_color(color_up),
convert_image_color_to_ratatui_color(color_down),
)
}; };
let style = Style::default() let style = Style::default().fg(color_up).bg(color_down);
.fg(color_up)
.bg(color_down);
Span::styled(String::from(PIXEL), style) Span::styled(String::from(PIXEL), style)
} }
fn convert_image_color_to_ratatui_color( fn convert_image_color_to_ratatui_color(color: ImageColor) -> Color {
color: ImageColor,
) -> Color {
Color::Rgb(color.r, color.g, color.b) Color::Rgb(color.r, color.g, color.b)
} }

View File

@ -587,11 +587,14 @@ impl Television {
if self.config.ui.show_preview_panel if self.config.ui.show_preview_panel
&& !matches!(selected_entry.preview_type, PreviewType::None) && !matches!(selected_entry.preview_type, PreviewType::None)
{ {
// preview content // preview content
let maybe_preview = self.previewer.preview(&selected_entry, layout.preview_window); let maybe_preview = self
.previewer
.preview(&selected_entry, layout.preview_window);
let _ = self.previewer.preview(&selected_entry, layout.preview_window); let _ = self
.previewer
.preview(&selected_entry, layout.preview_window);
if let Some(preview) = &maybe_preview { if let Some(preview) = &maybe_preview {
self.current_preview_total_lines = preview.total_lines; self.current_preview_total_lines = preview.total_lines;

View File

@ -119,7 +119,7 @@ where
if is_known_text_extension(p) { if is_known_text_extension(p) {
return FileType::Text; return FileType::Text;
} }
if is_accepted_image_extension(p){ if is_accepted_image_extension(p) {
return FileType::Image; return FileType::Image;
} }
if let Ok(mut f) = File::open(p) { if let Ok(mut f) = File::open(p) {
@ -503,7 +503,7 @@ lazy_static! {
//"farbfeld", //"farbfeld",
"gif", "gif",
//"hdr", //"hdr",
//"ico", "ico",
"jpeg", "jpeg",
"jpg", "jpg",
//"exr", //"exr",

View File

@ -1,6 +1,7 @@
pub mod cache; pub mod cache;
pub mod command; pub mod command;
pub mod files; pub mod files;
pub mod image;
pub mod indices; pub mod indices;
pub mod input; pub mod input;
pub mod metadata; pub mod metadata;
@ -9,4 +10,3 @@ pub mod stdin;
pub mod strings; pub mod strings;
pub mod syntax; pub mod syntax;
pub mod threads; pub mod threads;
pub mod image;