From 8a76579a183f21b6911c3e39d8c6a3ad2346d7f8 Mon Sep 17 00:00:00 2001 From: Stattek Date: Thu, 27 Nov 2025 14:12:41 -0600 Subject: [PATCH 1/3] So far, this saves like 100 ms. --- .../converters/generic_converter.rs | 21 ++++++++++++++++++- src/conversion/render_char_to_png.rs | 6 ++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/conversion/converters/generic_converter.rs b/src/conversion/converters/generic_converter.rs index f1a73ab..1501c9b 100644 --- a/src/conversion/converters/generic_converter.rs +++ b/src/conversion/converters/generic_converter.rs @@ -1,3 +1,5 @@ +use std::collections::{HashMap, HashSet}; + use crate::{ ImgiiOptions, conversion::{image_data::ImageData, render_char_to_png::str_to_png}, @@ -49,6 +51,9 @@ pub(crate) fn render_ascii_generic( // text to know the width let (mut width, height) = (0, ascii_text.lines().count()); + // hold already rendered images so we don't have to render them more than once + let mut rendered_images: HashMap = HashMap::new(); + // read every line in the file for (i, line) in ascii_text.lines().enumerate() { // we need to find each character that we are going to write @@ -88,7 +93,21 @@ pub(crate) fn render_ascii_generic( string: String::from(the_str), }; - str_to_png(colored, &font, imgii_options) + // check if this image was already rendered before + let rendered_img = rendered_images.get(&colored); + match rendered_img { + // PERF: what is the performance difference between doing a clone vs Rc + Some(rendered_img) => rendered_img.clone(), + None => { + let image_data = str_to_png(&colored, &font, imgii_options); + let copied_data = image_data.clone(); + let result = rendered_images.insert(colored, image_data); + if result.is_some() { + panic!("this key should not exist already in the hash map"); + } + copied_data + } + } } }; diff --git a/src/conversion/render_char_to_png.rs b/src/conversion/render_char_to_png.rs index ff493df..5b13dbf 100644 --- a/src/conversion/render_char_to_png.rs +++ b/src/conversion/render_char_to_png.rs @@ -5,7 +5,9 @@ use imageproc::drawing::draw_text_mut; /// Represents a colored string to write. /// All characters are contiguous and share the same color. -#[derive(Debug, Clone)] +/// Is hashable to act as a key for already rendered +/// images, to prevent rendering them more than once. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) struct ColoredStr { pub(crate) red: u8, pub(crate) blue: u8, @@ -18,7 +20,7 @@ const BACKGROUND_PIXEL: Rgba = Rgba([0, 0, 0, u8::MAX]); /// Converts string data into a png. /// Uses `imageproc` to render text. pub(crate) fn str_to_png( - data: ColoredStr, + data: &ColoredStr, font: &FontRef<'_>, imgii_options: &ImgiiOptions, ) -> ImageData { From 8549ef9d062eb7d6aa91d9eb407a4714322c7daf Mon Sep 17 00:00:00 2001 From: Stattek Date: Thu, 27 Nov 2025 22:55:09 -0600 Subject: [PATCH 2/3] Larger speedup by only rendering unique character images once. --- .../converters/generic_converter.rs | 69 +++++++++++-------- src/error.rs | 40 ++++++++++- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/src/conversion/converters/generic_converter.rs b/src/conversion/converters/generic_converter.rs index 1501c9b..68d20b3 100644 --- a/src/conversion/converters/generic_converter.rs +++ b/src/conversion/converters/generic_converter.rs @@ -1,9 +1,9 @@ -use std::collections::{HashMap, HashSet}; +use std::{collections::HashMap, sync::Arc}; use crate::{ ImgiiOptions, conversion::{image_data::ImageData, render_char_to_png::str_to_png}, - error::{FontError, ImgiiError, ParseIntError}, + error::{FontError, ImgiiError, ParseIntError, RenderError}, }; use super::super::render_char_to_png::{ColoredStr, str_to_transparent_png}; @@ -19,7 +19,7 @@ const FONT_BYTES: &[u8] = include_bytes!("../../../fonts/UbuntuMono.ttf"); /// Simple struct for holding a 2d image with its width and height. #[derive(Clone, Debug)] pub(crate) struct Imgii2dImage { - pub(crate) image_2d: Vec, + pub(crate) image_2d: Vec>, pub(crate) width: usize, pub(crate) height: usize, } @@ -43,16 +43,16 @@ pub(crate) fn render_ascii_generic( // 2d Vec of images for each character let mut image_2d_vec = Vec::new(); - // create this once since it will always be the same - let transparent_png = str_to_transparent_png(imgii_options); - // width and height, in characters // NOTE: we can know height beforehand but we have to wait until we have parsed a whole line of // text to know the width let (mut width, height) = (0, ascii_text.lines().count()); - // hold already rendered images so we don't have to render them more than once - let mut rendered_images: HashMap = HashMap::new(); + // hold already rendered images so we don't have to render them more than once! Rendering is + // slow + let mut rendered_images: HashMap> = HashMap::new(); + // create transparent image once since it will always be the same + let transparent_png = Arc::from(str_to_transparent_png(imgii_options)); // read every line in the file for (i, line) in ascii_text.lines().enumerate() { @@ -96,16 +96,19 @@ pub(crate) fn render_ascii_generic( // check if this image was already rendered before let rendered_img = rendered_images.get(&colored); match rendered_img { - // PERF: what is the performance difference between doing a clone vs Rc + // we have rendered this image before, so clone it Some(rendered_img) => rendered_img.clone(), None => { - let image_data = str_to_png(&colored, &font, imgii_options); - let copied_data = image_data.clone(); - let result = rendered_images.insert(colored, image_data); + // we haven't rendered this image before, so render it + let image_data = Arc::from(str_to_png(&colored, &font, imgii_options)); + let result = rendered_images.insert(colored, image_data.clone()); if result.is_some() { - panic!("this key should not exist already in the hash map"); + return Err(RenderError::new(String::from( + "this image should not exist already in the hash map", + )) + .into()); } - copied_data + image_data } } } @@ -115,26 +118,34 @@ pub(crate) fn render_ascii_generic( image_2d_vec.push(generated_png); } - // check that this width is always the same now that we have the width - if i != 0 { - assert_eq!( - width, line_width, - "width {} is not equal to the current line width {}", - width, line_width - ); - } else { + if i == 0 { + // get the width of the entire image. This should always be the same width = line_width; - // now we can reserve the rest of the space for our vec + // now we can reserve the rest of the capacity we need for our vec + // NOTE: this can panic if the vec is too large image_2d_vec.reserve(width * height); + } else { + // check that this width is always the same now that we have the width + if width != line_width { + return Err(RenderError::new(format!( + "width {} is not equal to the current line width {}", + width, line_width + )) + .into()); + } } } - assert!( - width * height == image_2d_vec.len(), - "expected length of the 2d vector was {} but got {}", - width * height, - image_2d_vec.len() - ); + // Check that the length of the final vector is what we expect. If not, something has gone + // terribly wrong, and we should not continue. + if width * height != image_2d_vec.len() { + return Err(RenderError::new(format!( + "expected length of the 2d vector was {} but got {}", + width * height, + image_2d_vec.len() + )) + .into()); + } Ok(Imgii2dImage { image_2d: image_2d_vec, diff --git a/src/error.rs b/src/error.rs index c8cc214..f1f8624 100644 --- a/src/error.rs +++ b/src/error.rs @@ -85,6 +85,7 @@ pub struct InternalError; pub enum ImageError { InvalidParameter(InvalidParameterError), ParseImage(ParseImageError), + Render(RenderError), } /// Contains other errors. These are errors that can be emitted from other crates for various @@ -105,7 +106,7 @@ pub struct InvalidParameterError { parameter_name: String, } -/// Represents an error that occurred while parsing an image (in a 2D fashion). +/// Represents an error that occurred while parsing an image. /// /// Suberror of [`ImageError`]. #[derive(Debug, Clone)] @@ -114,6 +115,16 @@ pub struct ParseImageError { image_row_number: usize, } +/// Represents an error that occurred while rendering an image. +/// +/// Suberror of [`ImageError`]. +#[derive(Debug, Clone)] +pub struct RenderError { + /// The reason for the render error. Since this error is intended to handle various internals + /// that aren't well represented by errors, this will explain why the error ocurred. + reason: String, +} + /* * NOTE: Implement `Display` below for errors that are intended to also implement Error. */ @@ -190,6 +201,13 @@ impl Display for ImageError { parse_image_error.image_row_number ) } + ImageError::Render(render_error) => { + write!( + f, + "error occurred while rendering image ({})", + render_error.reason + ) + } } } } @@ -223,8 +241,8 @@ impl Error for OtherError {} // do another From impl for the errors that can be converted into a suberror type too. // // The suberrors can implement From for anything that can be converted into them specifically, then -// for each of those, a simple From can be implemented for ImgiiError with a call to `.into()`, -// which will convert into the suberror type, then it should convert into the ImgiiError type. +// for each of those, a simple From can be implemented for ImgiiError so we can call `.into()` +// to convert a suberror type into the main ImgiiError type. // // This makes it easier to maintain, as more errors are added. This pattern should be replicated for // suberrors which have their own suberrors. @@ -270,6 +288,12 @@ impl From for ImgiiError { } } +impl From for ImgiiError { + fn from(value: RenderError) -> Self { + Self::Image(ImageError::Render(value)) + } +} + // for converting from errors boxed at runtime impl From for ImgiiError { fn from(value: BoxedDynErr) -> Self { @@ -361,3 +385,13 @@ impl ParseImageError { Self { image_row_number } } } + +impl RenderError { + /// Creates a new [`RenderError`]. + /// + /// * `reason`: The reason for the render error. + #[must_use] + pub fn new(reason: String) -> Self { + Self { reason } + } +} From 36c22f5aafcc1d22ded3e9662c37fb8aac936e32 Mon Sep 17 00:00:00 2001 From: Stattek Date: Thu, 27 Nov 2025 23:28:28 -0600 Subject: [PATCH 3/3] Update todo --- TODO.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/TODO.md b/TODO.md index 1cdbbbb..df9660c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,7 @@ # TODO - Add brightness setting (0-100) to allow brightening dark images -- Instead of using 2d vectors throughout the program, flatten them into single vectors - Speed up GIF encoding (seems to take up the most time when converting GIFs) -- Probably should remove the "Internal" error since it's vague. ## Bugs