diff --git a/src/export/pdf.rs b/src/export/pdf.rs index d03fa0d..7aa76eb 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -97,13 +97,14 @@ impl ContentManager { ImageSource::Filesystem(path) => Ok(path), ImageSource::Generated => { let mut buffer = Vec::new(); - let dimensions = image.dimensions(); - let TerminalImage::Ascii(resource) = image.image.as_ref() else { panic!("not in ascii mode") }; + let dimensions = image.image().dimensions(); + let TerminalImage::Ascii(image) = image.image() else { panic!("not in ascii mode") }; + let image = image.image(); PngEncoder::new(&mut buffer).write_image( - resource.as_bytes(), + image.as_bytes(), dimensions.0, dimensions.1, - resource.color().into(), + image.color().into(), )?; let name = format!("img-{}.png", self.image_count); let path = self.output_directory.path().join(name); diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index b49573d..6c110c1 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -173,8 +173,8 @@ impl Presentation { } /// Poll every async render operation in the current slide and check whether they're completed. - pub(crate) fn poll_slide_async_renders(&mut self) -> RenderAsyncState { - let slide = self.current_slide_mut(); + pub(crate) fn poll_slide_async_renders(&mut self, slide: usize) -> RenderAsyncState { + let slide = &mut self.slides[slide]; let mut slide_state = RenderAsyncState::Rendered; for operation in slide.iter_operations_mut() { if let RenderOperation::RenderAsync(operation) = operation { diff --git a/src/presenter.rs b/src/presenter.rs index 259f2e4..5f0641f 100644 --- a/src/presenter.rs +++ b/src/presenter.rs @@ -156,12 +156,12 @@ impl<'a> Presenter<'a> { self.try_scale_transition_images()?; break; } - CommandSideEffect::NextSlide => { - self.next_slide(&mut drawer)?; + CommandSideEffect::AnimateNextSlide => { + self.animate_next_slide(&mut drawer)?; break; } - CommandSideEffect::PreviousSlide => { - self.previous_slide(&mut drawer)?; + CommandSideEffect::AnimatePreviousSlide => { + self.animate_previous_slide(&mut drawer)?; break; } CommandSideEffect::None => (), @@ -203,15 +203,19 @@ impl<'a> Presenter<'a> { return Ok(false); } let current_index = self.state.presentation().current_slide_index(); - if self.slides_with_pending_async_renders.contains(¤t_index) { - let state = self.state.presentation_mut().poll_slide_async_renders(); + self.poll_slide_async_renders(current_index) + } + + fn poll_slide_async_renders(&mut self, slide: usize) -> Result { + if self.slides_with_pending_async_renders.contains(&slide) { + let state = self.state.presentation_mut().poll_slide_async_renders(slide); match state { RenderAsyncState::NotStarted | RenderAsyncState::Rendering { modified: false } => (), RenderAsyncState::Rendering { modified: true } => { return Ok(true); } RenderAsyncState::Rendered | RenderAsyncState::JustFinishedRendering => { - self.slides_with_pending_async_renders.remove(¤t_index); + self.slides_with_pending_async_renders.remove(&slide); return Ok(true); } }; @@ -279,7 +283,7 @@ impl<'a> Presenter<'a> { if !presentation.jump_next() { false } else if presentation.current_slide_index() != current_slide { - return CommandSideEffect::NextSlide; + return CommandSideEffect::AnimateNextSlide; } else { true } @@ -290,7 +294,7 @@ impl<'a> Presenter<'a> { if !presentation.jump_previous() { false } else if presentation.current_slide_index() != current_slide { - return CommandSideEffect::PreviousSlide; + return CommandSideEffect::AnimatePreviousSlide; } else { true } @@ -361,7 +365,7 @@ impl<'a> Presenter<'a> { return Ok(()); } let options = RenderEngineOptions { max_size: self.options.max_size.clone(), ..Default::default() }; - let scaler = AsciiScaler::new(options, self.resources.image_registry()); + let scaler = AsciiScaler::new(options); let dimensions = WindowSize::current(self.options.font_size_fallback)?; scaler.process(self.state.presentation(), &dimensions)?; Ok(()) @@ -436,34 +440,40 @@ impl<'a> Presenter<'a> { } } - fn next_slide(&mut self, drawer: &mut TerminalDrawer) -> RenderResult { + fn animate_next_slide(&mut self, drawer: &mut TerminalDrawer) -> RenderResult { let Some(config) = self.options.transition.clone() else { return Ok(()); }; - let registry = self.resources.image_registry(); + self.poll_and_scale_images()?; + let options = drawer.render_engine_options(); let presentation = self.state.presentation_mut(); let dimensions = WindowSize::current(self.options.font_size_fallback)?; presentation.jump_previous(); - let left = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options, registry.clone())?; + let left = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options)?; presentation.jump_next(); - let right = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options, registry)?; + let right = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options)?; let direction = TransitionDirection::Next; self.animate_transition(drawer, left, right, direction, dimensions, config) } - fn previous_slide(&mut self, drawer: &mut TerminalDrawer) -> RenderResult { + fn animate_previous_slide(&mut self, drawer: &mut TerminalDrawer) -> RenderResult { let Some(config) = self.options.transition.clone() else { return Ok(()); }; - let registry = self.resources.image_registry(); + self.poll_and_scale_images()?; + let options = drawer.render_engine_options(); let presentation = self.state.presentation_mut(); let dimensions = WindowSize::current(self.options.font_size_fallback)?; presentation.jump_next(); - let right = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options, registry.clone())?; + + // Re-borrow to avoid calling fns above while mutably borrowing + let presentation = self.state.presentation_mut(); + + let right = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options)?; presentation.jump_previous(); - let left = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options, registry)?; + let left = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options)?; let direction = TransitionDirection::Previous; self.animate_transition(drawer, left, right, direction, dimensions, config) } @@ -543,13 +553,23 @@ impl<'a> Presenter<'a> { slide: &Slide, dimensions: WindowSize, options: &RenderEngineOptions, - registry: ImageRegistry, ) -> Result { - let mut term = VirtualTerminal::new(dimensions.clone(), ImageBehavior::PrintAscii(registry)); + let mut term = VirtualTerminal::new(dimensions.clone(), ImageBehavior::PrintAscii); let engine = RenderEngine::new(&mut term, dimensions.clone(), options.clone()); engine.render(slide.iter_visible_operations())?; Ok(term.into_contents()) } + + fn poll_and_scale_images(&mut self) -> RenderResult { + let mut needs_scaling = false; + for index in 0..self.state.presentation().iter_slides().count() { + needs_scaling = self.poll_slide_async_renders(index)? || needs_scaling; + } + if needs_scaling { + self.try_scale_transition_images()?; + } + Ok(()) + } } enum CommandSideEffect { @@ -557,8 +577,8 @@ enum CommandSideEffect { Suspend, Redraw, Reload, - NextSlide, - PreviousSlide, + AnimateNextSlide, + AnimatePreviousSlide, None, } diff --git a/src/render/ascii_scaler.rs b/src/render/ascii_scaler.rs index cd3f2b6..9d5b6bf 100644 --- a/src/render/ascii_scaler.rs +++ b/src/render/ascii_scaler.rs @@ -3,10 +3,10 @@ use super::{ engine::{RenderEngine, RenderEngineOptions}, }; use crate::{ - ImageRegistry, WindowSize, + WindowSize, presentation::Presentation, terminal::{ - image::{Image, ImageSource}, + image::Image, printer::{TerminalCommand, TerminalError, TerminalIo}, }, }; @@ -15,12 +15,11 @@ 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 new(options: RenderEngineOptions) -> Self { + Self { options } } pub(crate) fn process(self, presentation: &Presentation, dimensions: &WindowSize) -> Result<(), RenderError> { @@ -29,13 +28,13 @@ impl AsciiScaler { 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)); + thread::spawn(move || Self::scale(collector.images)); Ok(()) } - fn scale(images: Vec, registry: ImageRegistry) { + fn scale(images: Vec) { for image in images { - let ascii_image = registry.as_ascii(&image.image); + let ascii_image = image.image.to_ascii(); ascii_image.cache_scaling(image.columns, image.rows); } } @@ -84,11 +83,8 @@ impl TerminalIo for ImageCollector { } 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); - } + let image = ScalableImage { image: image.clone(), rows: options.rows * 2, columns: options.columns }; + self.images.push(image); } ClearScreen => { self.current_column = 0; diff --git a/src/render/engine.rs b/src/render/engine.rs index c1bd237..3c6dfc1 100644 --- a/src/render/engine.rs +++ b/src/render/engine.rs @@ -258,7 +258,7 @@ where let starting_cursor = CursorPosition { row: starting_row.saturating_sub(rect.start_row), column: rect.start_column }; - let (width, height) = image.dimensions(); + let (width, height) = image.image().dimensions(); let (columns, rows) = match properties.size { ImageSize::ShrinkIfNeeded => { let image_scale = diff --git a/src/resource.rs b/src/resource.rs index 53d9965..35b059c 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -133,10 +133,6 @@ impl Resources { inner.image_registry.clear(); inner.themes.clear(); } - - pub(crate) fn image_registry(&self) -> ImageRegistry { - self.inner.borrow().image_registry.clone() - } } /// Watches for file changes. diff --git a/src/terminal/image/mod.rs b/src/terminal/image/mod.rs index e544935..4467c9c 100644 --- a/src/terminal/image/mod.rs +++ b/src/terminal/image/mod.rs @@ -1,23 +1,59 @@ +use image::DynamicImage; +use protocols::ascii::AsciiImage; + use self::printer::{ImageProperties, TerminalImage}; -use std::{fmt::Debug, ops::Deref, path::PathBuf, sync::Arc}; +use std::{ + fmt::Debug, + ops::Deref, + path::PathBuf, + sync::{Arc, Mutex}, +}; pub(crate) mod printer; pub(crate) mod protocols; pub(crate) mod scale; +struct Inner { + image: TerminalImage, + ascii_image: Mutex>, +} + /// An image. /// /// This stores the image in an [std::sync::Arc] so it's cheap to clone. #[derive(Clone)] pub(crate) struct Image { - pub(crate) image: Arc, + inner: Arc, pub(crate) source: ImageSource, } impl Image { /// Constructs a new image. pub(crate) fn new(image: TerminalImage, source: ImageSource) -> Self { - Self { image: Arc::new(image), source } + let inner = Inner { image, ascii_image: Default::default() }; + Self { inner: Arc::new(inner), source } + } + + pub(crate) fn to_ascii(&self) -> AsciiImage { + let mut ascii_image = self.inner.ascii_image.lock().unwrap(); + match ascii_image.deref() { + Some(image) => image.clone(), + None => { + let image = match &self.inner.image { + TerminalImage::Ascii(image) => image.clone(), + TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(), + TerminalImage::Iterm(image) => DynamicImage::from(image.as_rgba8()).into(), + #[cfg(feature = "sixel")] + TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(), + }; + *ascii_image = Some(image.clone()); + image + } + } + } + + pub(crate) fn image(&self) -> &TerminalImage { + &self.inner.image } } @@ -29,19 +65,11 @@ impl PartialEq for Image { impl Debug for Image { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let (width, height) = self.image.dimensions(); + let (width, height) = self.inner.image.dimensions(); write!(f, "Image<{width}x{height}>") } } -impl Deref for Image { - type Target = TerminalImage; - - fn deref(&self) -> &Self::Target { - &self.image - } -} - #[derive(Clone, Debug, PartialEq)] pub(crate) enum ImageSource { Filesystem(PathBuf), diff --git a/src/terminal/image/printer.rs b/src/terminal/image/printer.rs index 7d1f4fe..fe66813 100644 --- a/src/terminal/image/printer.rs +++ b/src/terminal/image/printer.rs @@ -18,7 +18,6 @@ use std::{ borrow::Cow, collections::HashMap, fmt, io, - ops::Deref, path::PathBuf, sync::{Arc, Mutex}, }; @@ -151,12 +150,11 @@ impl PrintImage for ImagePrinter { pub(crate) struct ImageRegistry { printer: Arc, images: Arc>>, - ascii_images: Arc>>, } impl ImageRegistry { pub fn new(printer: Arc) -> Self { - Self { printer, images: Default::default(), ascii_images: Default::default() } + Self { printer, images: Default::default() } } } @@ -191,39 +189,12 @@ impl ImageRegistry { let image = Image::new(resource, source); if let Some(key) = cache_key { images.insert(key.clone(), image.clone()); - drop(images); - if let TerminalImage::Ascii(image) = image.image.as_ref() { - self.ascii_images.lock().unwrap().insert(key, image.clone()); - } } Ok(image) } pub(crate) fn clear(&self) { self.images.lock().unwrap().clear(); - self.ascii_images.lock().unwrap().clear(); - } - - pub(crate) fn as_ascii(&self, image: &Image) -> AsciiImage { - if let ImageSource::Filesystem(path) = &image.source { - if let Some(image) = self.ascii_images.lock().unwrap().get(path) { - return image.clone(); - } - if let Some(TerminalImage::Ascii(image)) = self.images.lock().unwrap().get(path).map(|i| i.image.as_ref()) { - return image.clone(); - } - } - let ascii_image = match image.image.deref() { - TerminalImage::Ascii(image) => image.clone(), - TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(), - TerminalImage::Iterm(image) => DynamicImage::from(image.as_rgba8()).into(), - #[cfg(feature = "sixel")] - TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(), - }; - if let ImageSource::Filesystem(path) = &image.source { - self.ascii_images.lock().unwrap().insert(path.clone(), ascii_image.clone()); - } - ascii_image } } diff --git a/src/terminal/image/protocols/ascii.rs b/src/terminal/image/protocols/ascii.rs index dbe0dc9..145aa26 100644 --- a/src/terminal/image/protocols/ascii.rs +++ b/src/terminal/image/protocols/ascii.rs @@ -10,49 +10,49 @@ use itertools::Itertools; use std::{ collections::HashMap, fs, - ops::Deref, sync::{Arc, Mutex}, }; const TOP_CHAR: &str = "▀"; const BOTTOM_CHAR: &str = "▄"; +struct Inner { + image: DynamicImage, + cached_sizes: Mutex>, +} + #[derive(Clone)] pub(crate) struct AsciiImage { - image: Arc, - cached_sizes: Arc>>, + inner: Arc, } impl AsciiImage { pub(crate) fn cache_scaling(&self, columns: u16, rows: u16) { - let mut cached_sizes = self.cached_sizes.lock().unwrap(); + let mut cached_sizes = self.inner.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); + let image = self.inner.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle); cached_sizes.insert(cache_key, image.into_rgba8()); } } + + pub(crate) fn image(&self) -> &DynamicImage { + &self.inner.image + } } impl ImageProperties for AsciiImage { fn dimensions(&self) -> (u32, u32) { - self.image.dimensions() + self.inner.image.dimensions() } } impl From for AsciiImage { fn from(image: DynamicImage) -> Self { let image = image.into_rgba8(); - Self { image: Arc::new(image.into()), cached_sizes: Default::default() } - } -} - -impl Deref for AsciiImage { - type Target = DynamicImage; - - fn deref(&self) -> &Self::Target { - &self.image + let inner = Inner { image: image.into(), cached_sizes: Default::default() }; + Self { inner: Arc::new(inner) } } } @@ -108,7 +108,7 @@ impl PrintImage for AsciiPrinter { // lookup on cache/resize the image and store it in cache let cache_key = (columns, rows); - let cached_sizes = image.cached_sizes.lock().unwrap(); + let cached_sizes = image.inner.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); diff --git a/src/terminal/printer.rs b/src/terminal/printer.rs index f0e3333..4e82cea 100644 --- a/src/terminal/printer.rs +++ b/src/terminal/printer.rs @@ -146,7 +146,7 @@ impl Terminal { fn print_image(&mut self, image: &Image, options: &PrintOptions) -> Result<(), PrintImageError> { let image_printer = self.image_printer.clone(); - image_printer.print(&image.image, options, self)?; + image_printer.print(image.image(), options, self)?; self.cursor_row += options.rows; Ok(()) } diff --git a/src/terminal/virt.rs b/src/terminal/virt.rs index c73629c..767754a 100644 --- a/src/terminal/virt.rs +++ b/src/terminal/virt.rs @@ -7,7 +7,7 @@ use super::{ printer::{TerminalError, TerminalIo}, }; use crate::{ - ImageRegistry, WindowSize, + WindowSize, markdown::{ elements::Text, text_style::{Color, Colors, TextStyle}, @@ -187,8 +187,8 @@ impl VirtualTerminal { let image = PrintedImage { image: image.clone(), width_columns: options.columns }; self.images.insert(key, image); } - ImageBehavior::PrintAscii(registry) => { - let image = registry.as_ascii(image); + ImageBehavior::PrintAscii => { + let image = image.to_ascii(); let image_printer = AsciiPrinter; image_printer.print(&image, options, self)? } @@ -228,7 +228,7 @@ impl TerminalIo for VirtualTerminal { pub(crate) enum ImageBehavior { #[default] Store, - PrintAscii(ImageRegistry), + PrintAscii, } #[derive(Clone, Copy, Debug, PartialEq)]