diff --git a/src/presenter.rs b/src/presenter.rs index d0c75df..259f2e4 100644 --- a/src/presenter.rs +++ b/src/presenter.rs @@ -13,6 +13,7 @@ use crate::{ }, render::{ ErrorSource, RenderError, RenderResult, TerminalDrawer, TerminalDrawerOptions, + ascii_scaler::AsciiScaler, engine::{MaxSize, RenderEngine, RenderEngineOptions}, operation::RenderAsyncState, properties::WindowSize, @@ -109,7 +110,7 @@ impl<'a> Presenter<'a> { self.resources.watch_presentation_file(path.to_path_buf()); } self.state = PresenterState::Presenting(Presentation::from(vec![])); - self.try_reload(path, true); + self.try_reload(path, true)?; let drawer_options = TerminalDrawerOptions { font_size_fallback: self.options.font_size_fallback, @@ -148,10 +149,11 @@ impl<'a> Presenter<'a> { break; } CommandSideEffect::Reload => { - self.try_reload(path, false); + self.try_reload(path, false)?; break; } CommandSideEffect::Redraw => { + self.try_scale_transition_images()?; break; } CommandSideEffect::NextSlide => { @@ -326,9 +328,9 @@ impl<'a> Presenter<'a> { if needs_redraw { CommandSideEffect::Redraw } else { CommandSideEffect::None } } - fn try_reload(&mut self, path: &Path, force: bool) { + fn try_reload(&mut self, path: &Path, force: bool) -> RenderResult { if matches!(self.options.mode, PresentMode::Presentation) && !force { - return; + return Ok(()); } self.slides_with_pending_async_renders.clear(); self.resources.clear_watches(); @@ -344,12 +346,25 @@ impl<'a> Presenter<'a> { } self.slides_with_pending_async_renders = presentation.slides_with_async_renders().into_iter().collect(); self.state = self.validate_overflows(presentation); + self.try_scale_transition_images()?; } Err(e) => { let presentation = mem::take(&mut self.state).into_presentation(); self.state = PresenterState::failure(e, presentation, ErrorSource::Presentation, FailureMode::Other); } }; + Ok(()) + } + + fn try_scale_transition_images(&self) -> RenderResult { + if self.options.transition.is_none() { + return Ok(()); + } + let options = RenderEngineOptions { max_size: self.options.max_size.clone(), ..Default::default() }; + let scaler = AsciiScaler::new(options, self.resources.image_registry()); + let dimensions = WindowSize::current(self.options.font_size_fallback)?; + scaler.process(self.state.presentation(), &dimensions)?; + Ok(()) } fn is_displaying_other_error(&self) -> bool { diff --git a/src/render/ascii_scaler.rs b/src/render/ascii_scaler.rs new file mode 100644 index 0000000..cd3f2b6 --- /dev/null +++ b/src/render/ascii_scaler.rs @@ -0,0 +1,106 @@ +use super::{ + RenderError, + engine::{RenderEngine, RenderEngineOptions}, +}; +use crate::{ + ImageRegistry, WindowSize, + presentation::Presentation, + terminal::{ + image::{Image, ImageSource}, + printer::{TerminalCommand, TerminalError, TerminalIo}, + }, +}; +use std::thread; +use unicode_width::UnicodeWidthStr; + +pub(crate) struct AsciiScaler { + options: RenderEngineOptions, + registry: ImageRegistry, +} + +impl AsciiScaler { + pub(crate) fn new(options: RenderEngineOptions, registry: ImageRegistry) -> Self { + Self { options, registry } + } + + pub(crate) fn process(self, presentation: &Presentation, dimensions: &WindowSize) -> Result<(), RenderError> { + let mut collector = ImageCollector::default(); + for slide in presentation.iter_slides() { + let engine = RenderEngine::new(&mut collector, dimensions.clone(), self.options.clone()); + engine.render(slide.iter_operations())?; + } + thread::spawn(move || Self::scale(collector.images, self.registry)); + Ok(()) + } + + fn scale(images: Vec, registry: ImageRegistry) { + for image in images { + let ascii_image = registry.as_ascii(&image.image); + ascii_image.cache_scaling(image.columns, image.rows); + } + } +} + +struct ScalableImage { + image: Image, + rows: u16, + columns: u16, +} + +struct ImageCollector { + current_column: u16, + current_row: u16, + current_row_height: u16, + images: Vec, +} + +impl Default for ImageCollector { + fn default() -> Self { + Self { current_row: 0, current_column: 0, current_row_height: 1, images: Default::default() } + } +} + +impl TerminalIo for ImageCollector { + fn execute(&mut self, command: &TerminalCommand<'_>) -> Result<(), TerminalError> { + use TerminalCommand::*; + match command { + MoveTo { column, row } => { + self.current_column = *column; + self.current_row = *row; + } + MoveToRow(row) => self.current_row = *row, + MoveToColumn(column) => self.current_column = *column, + MoveDown(amount) => self.current_row = self.current_row.saturating_add(*amount), + MoveRight(amount) => self.current_column = self.current_column.saturating_add(*amount), + MoveLeft(amount) => self.current_column = self.current_column.saturating_sub(*amount), + MoveToNextLine => { + self.current_row = self.current_row.saturating_add(1); + self.current_column = 0; + self.current_row_height = 1; + } + PrintText { content, style } => { + self.current_column = self.current_column.saturating_add(content.width() as u16); + self.current_row_height = self.current_row_height.max(style.size as u16); + } + PrintImage { image, options } => { + // we can only really cache filesystem images for now + if matches!(image.source, ImageSource::Filesystem(_)) { + let image = + ScalableImage { image: image.clone(), rows: options.rows * 2, columns: options.columns }; + self.images.push(image); + } + } + ClearScreen => { + self.current_column = 0; + self.current_row = 0; + self.current_row_height = 1; + } + BeginUpdate | EndUpdate | Flush | SetColors(_) | SetBackgroundColor(_) => (), + }; + Ok(()) + } + + fn cursor_row(&self) -> u16 { + self.current_row + } +} diff --git a/src/render/mod.rs b/src/render/mod.rs index cc892de..b4654ad 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod ascii_scaler; pub(crate) mod engine; pub(crate) mod layout; pub(crate) mod operation; diff --git a/src/terminal/image/protocols/ascii.rs b/src/terminal/image/protocols/ascii.rs index 355f22e..dbe0dc9 100644 --- a/src/terminal/image/protocols/ascii.rs +++ b/src/terminal/image/protocols/ascii.rs @@ -23,6 +23,18 @@ pub(crate) struct AsciiImage { cached_sizes: Arc>>, } +impl AsciiImage { + pub(crate) fn cache_scaling(&self, columns: u16, rows: u16) { + let mut cached_sizes = self.cached_sizes.lock().unwrap(); + // lookup on cache/resize the image and store it in cache + let cache_key = (columns, rows); + if cached_sizes.get(&cache_key).is_none() { + let image = self.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle); + cached_sizes.insert(cache_key, image.into_rgba8()); + } + } +} + impl ImageProperties for AsciiImage { fn dimensions(&self) -> (u32, u32) { self.image.dimensions() @@ -91,20 +103,14 @@ impl PrintImage for AsciiPrinter { { let columns = options.columns; let rows = options.rows * 2; - let mut cached_sizes = image.cached_sizes.lock().unwrap(); + // Scale it first + image.cache_scaling(columns, rows); + // lookup on cache/resize the image and store it in cache let cache_key = (columns, rows); - let image = match cached_sizes.get(&cache_key) { - Some(image) => image, - None => { - let image = image.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle); - cached_sizes.insert(cache_key, image.into_rgba8()); - cached_sizes.get(&cache_key).unwrap() - } - }; - // The strategy here is taken from viuer: use half vertical ascii blocks in combination - // with foreground/background colors to fit 2 vertical pixels per cell. That is, cell (x, y) - // will contain the pixels at (x, y) and (x, y + 1) combined. + let cached_sizes = image.cached_sizes.lock().unwrap(); + let image = cached_sizes.get(&cache_key).expect("scaled image no longer there"); + let default_background = options.background_color.map(Color::from); // Iterate pixel rows in pairs to be able to merge both pixels in a single iteration.