mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 15:32:58 +00:00
perf: cache generated images as ascii
This commit is contained in:
parent
a97e66fedf
commit
5eee8a9fae
@ -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);
|
||||
|
@ -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())
|
||||
|
@ -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,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;
|
||||
|
@ -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 =
|
||||
|
@ -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.
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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)]
|
||||
|
Loading…
x
Reference in New Issue
Block a user