diff --git a/Cargo.toml b/Cargo.toml index 4ba22b8..ebf69d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,8 @@ gag = "1.0" nucleo = "0.5" toml = "0.8" image = "0.25.5" +fast_image_resize = { version = "5.1.2", features = ["image"]} + [target.'cfg(windows)'.dependencies] winapi-util = "0.1.9" diff --git a/television/preview/mod.rs b/television/preview/mod.rs index cb7a8c5..4e178d0 100644 --- a/television/preview/mod.rs +++ b/television/preview/mod.rs @@ -10,7 +10,7 @@ pub mod previewers; // previewer types use crate::utils::cache::RingSet; -use crate::utils::image::Image; +use crate::utils::image::{CachedImageData}; use crate::utils::syntax::HighlightedLines; pub use previewers::basic::BasicPreviewer; pub use previewers::basic::BasicPreviewerConfig; @@ -32,7 +32,7 @@ pub enum PreviewContent { PlainText(Vec), PlainTextWrapped(String), AnsiText(String), - Image(Image), + Image(CachedImageData), } impl PreviewContent { @@ -48,7 +48,7 @@ impl PreviewContent { text.lines().count().try_into().unwrap_or(u16::MAX) } PreviewContent::Image(image) => { - image.pixel_grid.len().try_into().unwrap_or(u16::MAX) + image.height().try_into().unwrap_or(u16::MAX) } _ => 0, } diff --git a/television/preview/previewers/files.rs b/television/preview/previewers/files.rs index 77d0b00..ed3cbea 100644 --- a/television/preview/previewers/files.rs +++ b/television/preview/previewers/files.rs @@ -21,10 +21,10 @@ use crate::preview::cache::PreviewCache; use crate::preview::{previewers::meta, Preview, PreviewContent}; use crate::utils::{ files::FileType, - image::Image, strings::preprocess_line, syntax::{self, load_highlighting_assets, HighlightingAssetsExt}, }; +use crate::utils::image::CachedImageData; #[derive(Debug, Default)] pub struct FilePreviewer { @@ -138,7 +138,6 @@ impl FilePreviewer { &syntax_theme, &concurrent_tasks, &in_flight_previews, - preview_window, ); }); } @@ -162,7 +161,6 @@ 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); @@ -251,37 +249,17 @@ pub fn try_preview( } } } 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_width = 5; - let padding_height = 3; - ( - (preview_window.height - padding_height) * 2, - preview_window.width - padding_width * 2, - ) - } 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; - }; + debug!("File {:?} is an image", entry.name); match ImageReader::open(path).unwrap().decode() { Ok(image) => { cache.lock().insert( entry.name.clone(), &meta::loading(&format!("Loading {}", entry.name)), ); - let image = Image::from_dynamic_image( - image, - u32::from(window_height), - u32::from(window_width), - ); + let cached_image_data = CachedImageData::from_dynamic_image(image); let total_lines = - image.pixel_grid.len().try_into().unwrap_or(u16::MAX); - let content = PreviewContent::Image(image); + cached_image_data.height().try_into().unwrap_or(u16::MAX); + let content = PreviewContent::Image(cached_image_data); let preview = Arc::new(Preview::new( entry.name.clone(), content, diff --git a/television/screen/preview.rs b/television/screen/preview.rs index 6b26216..54e1551 100644 --- a/television/screen/preview.rs +++ b/television/screen/preview.rs @@ -19,7 +19,7 @@ use ratatui::{ prelude::{Color, Line, Modifier, Span, Style, Stylize, Text}, }; use std::str::FromStr; -use crate::utils::image::{Image, ImageColor, PIXEL}; +use crate::utils::image::{PIXEL, CachedImageData}; #[allow(dead_code)] const FILL_CHAR_SLANTED: char = '╱'; @@ -98,7 +98,7 @@ pub fn build_preview_paragraph<'a>( ) } PreviewContent::Image(image) => { - build_image_paragraph(image, preview_block, colorscheme.preview) + image.paragraph(inner, preview_block) } // meta @@ -248,33 +248,6 @@ fn build_syntect_highlighted_paragraph<'a>( //.scroll((preview_scroll, 0)) } -fn build_image_paragraph<'a>( - image: &'a Image, - preview_block: Block<'a>, - colorscheme: PreviewColorscheme, -) -> Paragraph<'a> { - let lines = image - .pixel_grid - .iter() - .enumerate() - .map(|(y, double_pixel_line)| { - Line::from_iter(double_pixel_line.iter().enumerate().map( - |(x, (color_up, color_down))| { - convert_pixel_to_span( - *color_up, - *color_down, - Some(colorscheme.highlight_bg), - (x, y), - ) - }, - )) - }) - .collect::>(); - let text_image = Text::from(lines); - Paragraph::new(text_image) - .block(preview_block) - .alignment(Alignment::Center) -} pub fn build_meta_preview_paragraph<'a>( inner: Rect, @@ -463,63 +436,4 @@ fn convert_syn_color_to_ratatui_color( Color::Rgb(color.r, color.g, color.b) } -pub fn convert_pixel_to_span<'a>( - color_up: ImageColor, - color_down: ImageColor, - background: Option, - position: (usize, usize), -) -> Span<'a> { - let bg_color = match background { - Some(Color::Rgb(r, g, b)) => Some((r, g, b)), - _ => None, - }; - // mean of background and picture - 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 { - let alpha_threshold = 30; - let color_up = if color_up.a <= alpha_threshold { - if (position.0 + position.1 * 2) % 2 == 0 { - ImageColor::WHITE - } else { - ImageColor::GRAY - } - } else { - color_up - }; - let color_down = if color_down.a <= alpha_threshold { - if (position.0 + position.1 * 2 + 1) % 2 == 0 { - ImageColor::WHITE - } else { - ImageColor::GRAY - } - } else { - color_down - }; - ( - 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) -} diff --git a/television/utils/image.rs b/television/utils/image.rs index b6327c8..f6676c4 100644 --- a/television/utils/image.rs +++ b/television/utils/image.rs @@ -1,38 +1,204 @@ -use image::imageops::FilterType; -use image::DynamicImage; +use std::cmp; +use std::cmp::max; +use std::hash::{Hash, Hasher}; + +use image::{DynamicImage, GenericImageView, ImageBuffer, ImageReader, Luma, Rgb, RgbImage, Rgba, RgbaImage}; + +use fast_image_resize::{ ImageBufferError, ImageView, IntoImageView, PixelType, ResizeAlg, ResizeOptions, Resizer}; +use fast_image_resize::images::Image; +use fast_image_resize::pixels::{Pixel, U8x3}; +use image::buffer::ConvertBuffer; +use image::imageops; +use image::imageops::FilterType; +use ratatui::layout::{Alignment, Rect}; +use ratatui::prelude::{Color, Span, Style, Text}; +use ratatui::text::Line; +use ratatui::widgets::{Block, Paragraph}; +use tracing::{debug, trace, warn}; pub const PIXEL: char = '▀'; -const FILTER: FilterType = FilterType::Triangle; -#[derive(Clone, Debug, Hash, PartialEq)] -pub struct Image { - pub pixel_grid: Vec>, +pub const PIXEL_TYPE: PixelType = PixelType::U8x4; +const RESIZE_ALGORITHM: ResizeAlg = ResizeAlg::Convolution(fast_image_resize::FilterType::Lanczos3); + +// use to reduce the size of the image before storing it +const CACHED_WIDTH: u32 = 256; +const CACHED_HEIGHT: u32 = 256; + +const GRAY: Rgba = Rgba([242, 242, 242, 255]); +const WHITE: Rgba = Rgba([255, 255, 255, 255]); +#[derive(Clone, Debug, PartialEq)] +pub struct CachedImageData { + image: DynamicImage, } -impl Image { - pub fn new(pixel_grid: Vec>) -> Self { - Image { pixel_grid } +impl Hash for CachedImageData { + fn hash(&self, state: &mut H) { + self.image.to_rgb8().hash(state); + } +} + + +impl CachedImageData { + pub fn new(rgba_image: DynamicImage) -> Self{ + CachedImageData{image: rgba_image} + } + + pub fn height(&self) -> u32{ + self.image.height() + } + pub fn width(&self) -> u32{ + self.image.width() } pub fn from_dynamic_image( dynamic_image: DynamicImage, - height: u32, - width: u32, ) -> Self { - let image = if dynamic_image.height() > height - || dynamic_image.width() > width - { - if dynamic_image.height() <= height * 2 - && dynamic_image.width() <= width * 2 - { - dynamic_image.resize(width, height, FILTER) - } else { - dynamic_image - .resize(width * 2, height * 2, FilterType::Nearest) - .resize(width, height, FILTER) + /* + let (new_width, new_height) = calculate_fit_dimensions(dynamic_image.width(), dynamic_image.height(), CACHED_WIDTH,CACHED_HEIGHT); + + // fixme resize even if useless and should just change the type + let mut dst_image = Image::new( + new_width, + new_height, + dynamic_image.pixel_type()?, + ); + + let resize_option = ResizeOptions::new().resize_alg(ResizeAlg::Nearest); + let mut resizer = Resizer::new(); + resizer.resize(&dynamic_image, &mut dst_image, Some(&resize_option)).unwrap(); + + + // convert the resized image into rgba + let rgba_image: RgbaImage = match dst_image.pixel_type() { + PixelType::U8x3 => { + let rgb_image: RgbImage = RgbImage::from_raw(new_width, new_height, dst_image.into_vec()).unwrap(); + rgb_image.convert() } + PixelType::U16 => { + // Convert `Luma` to `Rgba` (downscaling 16-bit to 8-bit) + let rgba_pixels: Vec = dst_image.into_vec().chunks_exact(2).flat_map(|b| { + let luma16 = u16::from_le_bytes([b[0], b[1]]); + let luma8 = (luma16 >> 8) as u8; // Downscale 16-bit to 8-bit + vec![luma8, luma8, luma8, 255] + }).collect(); + ImageBuffer::from_raw(new_width, new_height, rgba_pixels) + .expect("Failed to create Rgba8 ImageBuffer from Luma16") + } + PixelType::U8x4 => { + // Directly use the buffer since it's already RGBA8 + ImageBuffer::from_raw(new_width, new_height, dst_image.into_vec()) + .expect("Failed to create Rgba8 ImageBuffer from U8x4") + } + _ => panic!("Unsupported pixel type"), + }; + */ + let resized_image = if dynamic_image.width() > CACHED_WIDTH || dynamic_image.height() > CACHED_HEIGHT { + dynamic_image.resize(CACHED_WIDTH, CACHED_HEIGHT , FilterType::Nearest) } else { dynamic_image }; + let rgba_image = resized_image.into_rgba8(); + CachedImageData::new(DynamicImage::from(rgba_image)) + } + pub fn paragraph<'a>(&self, inner: Rect, preview_block: Block<'a>) -> Paragraph<'a> { + // resize it for the preview window + //let mut data = self.image.clone().into_raw(); + //let src_image = DynamicImage::from(self.image.clone()); + /* + let src_image = if let Some(src_image) = Image::from_slice_u8(self.image.width(), self.image.height(), &mut data, PixelType::U8x4) + .map_err(|error| warn!("Failed to resize cached image: {error}")) + .ok(){ + src_image + } else { + return Paragraph::new(Text::raw("Failed to resize cached image")); + }; - let image = image.into_rgba8(); + let (new_width, new_height) = calculate_fit_dimensions(self.image.width(), self.image.height(), u32::from(inner.width), u32::from(inner.height*2)); + let mut dst_image = Image::new( + new_width, + new_height, + PixelType::U8x4, + ); + let resize_option = ResizeOptions::new().resize_alg(RESIZE_ALGORITHM); + let mut resizer = Resizer::new(); + resizer.resize(&src_image, &mut dst_image, &Some(resize_option)).unwrap(); + + let image_rgba: RgbaImage = ImageBuffer::from_raw( dst_image.width(), dst_image.height(), dst_image.into_vec()).unwrap(); + */ + let preview_width = u32::from(inner.width); + let preview_height = u32::from(inner.height) * 2; + let image_rgba = if self.image.width() > preview_width || self.image.height() > preview_height { + self.image.resize(preview_width, preview_height, FilterType::Triangle).to_rgba8() + } else{ + self.image.to_rgba8() + }; + // transform it into text + let lines = image_rgba + .rows() + .step_by(2) + .zip(image_rgba.rows().skip(1).step_by(2)).enumerate() + .map(|(double_row_y, (row_1, row_2))| + Line::from_iter(row_1.into_iter().zip(row_2.into_iter()).enumerate().map( + |(x, (color_up, color_down))| { + convert_pixel_to_span( + color_up, + color_down, + (x, double_row_y), + )} + ) + )) + .collect::>(); + let text_image = Text::from(lines); + Paragraph::new(text_image) + .block(preview_block) + .alignment(Alignment::Center) + } + + +} + +/* +#[derive(Clone, Debug, Hash, PartialEq)] +pub struct ImageDoublePixel { + pub pixel_grid: Vec>, +} +impl ImageDoublePixel { + pub fn new(pixel_grid: Vec>) -> Self { + ImageDoublePixel { pixel_grid } + } + pub fn from_cached_image(cached_image_data: &CachedImageData, + new_width: u32, + new_height: u32) -> Option{ + let mut binding = cached_image_data.data.clone(); + let src_image = Image::from_slice_u8(cached_image_data.width, cached_image_data.height, &mut binding, cached_image_data.pixel_type) + .map_err(|error| warn!("Failed to resize cached image: {error}")) + .ok()?; + let (new_width, new_height) = calculate_fit_dimensions(cached_image_data.width, cached_image_data.height, new_width, new_height); + let mut dst_image = Image::new( + new_width, + new_height, + cached_image_data.pixel_type, + ); + let resize_option = ResizeOptions::new().resize_alg(RESIZE_ALGORITHM); + let mut resizer = Resizer::new(); + resizer.resize(&src_image, &mut dst_image, &Some(resize_option)).unwrap(); + + + match cached_image_data.pixel_type { + PixelType::U8x4 => { + let rgba_image = ImageBuffer::from_raw(new_width, new_height, dst_image.into_vec())?; + Some(Self::from_rgba_image(rgba_image)) + }, + _ => { + warn!("Unsupported pixel type: {:?}", cached_image_data.pixel_type); + println!("Unsupported pixel type: {:?}", cached_image_data.pixel_type); + None + } + } + + } + + pub fn from_rgba_image( + image: RgbaImage + ) -> Self { let pixel_grid = image .rows() .step_by(2) @@ -59,10 +225,13 @@ impl Image { .collect::>() }) .collect::>>(); - Image::new(pixel_grid) + ImageDoublePixel::new(pixel_grid) } -} + +} + */ +/* #[derive(Clone, Copy, Debug, Hash, PartialEq)] pub struct ImageColor { pub r: u8, @@ -93,3 +262,66 @@ impl ImageColor { a: 255, }; } +*/ + + +fn calculate_fit_dimensions(original_width: u32, original_height: u32, max_width: u32, max_height: u32) -> (u32, u32) { + if original_width <= max_width && original_height <= max_height { + return (original_width, original_height); + } + + let width_ratio = f64::from(max_width) / f64::from(original_width); + let height_ratio = f64::from(max_height) / f64::from(original_height); + + let scale = width_ratio.min(height_ratio); + + + let new_width = u32::try_from((f64::from(original_width) * scale).round() as u64) + .unwrap_or(u32::MAX); + let new_height = u32::try_from((f64::from(original_height) * scale).round() as u64) + .unwrap_or(u32::MAX); + + (new_width, new_height) +} + +pub fn convert_pixel_to_span<'a>( + color_up: &Rgba, + color_down: &Rgba, + position: (usize, usize), +) -> Span<'a> { + let color_up = color_up.0; + let color_down = color_down.0; + + let alpha_threshold = 30; + let color_up = if color_up[3] <= alpha_threshold { + if (position.0 + position.1 * 2) % 2 == 0 { + WHITE + } else { + GRAY + } + } else { + Rgba::from(color_up) + }; + let color_down = if color_down[3] <= alpha_threshold { + if (position.0 + position.1 * 2 + 1) % 2 == 0 { + WHITE + } else { + GRAY + } + } else { + Rgba::from(color_down) + }; + + let color_up = convert_image_color_to_ratatui_color(color_up); + let color_down = 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: Rgba) -> Color { + Color::Rgb(color[0], color[1], color[2]) +} + +