mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 15:32:58 +00:00
perf: pre-scale ascii images when transitions are enabled
This commit is contained in:
parent
9e1f2beca2
commit
eca6ce91bf
@ -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 {
|
||||
|
106
src/render/ascii_scaler.rs
Normal file
106
src/render/ascii_scaler.rs
Normal file
@ -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<ScalableImage>, 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<ScalableImage>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub(crate) mod ascii_scaler;
|
||||
pub(crate) mod engine;
|
||||
pub(crate) mod layout;
|
||||
pub(crate) mod operation;
|
||||
|
@ -23,6 +23,18 @@ pub(crate) struct AsciiImage {
|
||||
cached_sizes: Arc<Mutex<HashMap<(u16, u16), RgbaImage>>>,
|
||||
}
|
||||
|
||||
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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user