perf: cache generated images as ascii

This commit is contained in:
Matias Fontanini 2025-04-16 16:28:56 -07:00
parent a97e66fedf
commit 5eee8a9fae
10 changed files with 83 additions and 94 deletions

View File

@ -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);

View File

@ -361,7 +361,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(())
@ -440,14 +440,13 @@ impl<'a> Presenter<'a> {
let Some(config) = self.options.transition.clone() else {
return Ok(());
};
let registry = self.resources.image_registry();
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)
}
@ -456,14 +455,13 @@ impl<'a> Presenter<'a> {
let Some(config) = self.options.transition.clone() else {
return Ok(());
};
let registry = self.resources.image_registry();
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())?;
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,9 +541,8 @@ impl<'a> Presenter<'a> {
slide: &Slide,
dimensions: WindowSize,
options: &RenderEngineOptions,
registry: ImageRegistry,
) -> Result<TerminalGrid, RenderError> {
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())

View File

@ -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<ScalableImage>, registry: ImageRegistry) {
fn scale(images: Vec<ScalableImage>) {
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,12 +83,9 @@ 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 };
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;

View File

@ -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 =

View File

@ -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.

View File

@ -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<Option<AsciiImage>>,
}
/// 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<TerminalImage>,
inner: Arc<Inner>,
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),

View File

@ -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<ImagePrinter>,
images: Arc<Mutex<HashMap<PathBuf, Image>>>,
ascii_images: Arc<Mutex<HashMap<PathBuf, AsciiImage>>>,
}
impl ImageRegistry {
pub fn new(printer: Arc<ImagePrinter>) -> 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
}
}

View File

@ -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<HashMap<(u16, u16), RgbaImage>>,
}
#[derive(Clone)]
pub(crate) struct AsciiImage {
image: Arc<DynamicImage>,
cached_sizes: Arc<Mutex<HashMap<(u16, u16), RgbaImage>>>,
inner: Arc<Inner>,
}
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<DynamicImage> 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);

View File

@ -146,7 +146,7 @@ impl<I: TerminalWrite> Terminal<I> {
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(())
}

View File

@ -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)]