diff --git a/src/conversion.rs b/src/conversion.rs index c17b537..76a0de9 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -1,7 +1,7 @@ //! Holds types and code necessary for converting images to ASCII and rendering as images. Can //! handle different image types. -pub mod converters; -pub mod image_data; -pub mod image_writer; -pub mod render_char_to_png; +pub(crate) mod converters; +pub(crate) mod image_data; +pub(crate) mod image_writer; +pub(crate) mod render_char_to_png; diff --git a/src/conversion/converters.rs b/src/conversion/converters.rs index 75af8a3..25d3ff1 100644 --- a/src/conversion/converters.rs +++ b/src/conversion/converters.rs @@ -1,5 +1,5 @@ //! Holds converters for different image types. -pub mod generic_converter; -pub mod gif_converter; -pub mod png_converter; +pub(crate) mod generic_converter; +pub(crate) mod gif_converter; +pub(crate) mod png_converter; diff --git a/src/conversion/converters/generic_converter.rs b/src/conversion/converters/generic_converter.rs index 826a923..f1a73ab 100644 --- a/src/conversion/converters/generic_converter.rs +++ b/src/conversion/converters/generic_converter.rs @@ -14,38 +14,54 @@ use regex::Regex; const FONT_FILE: &str = "../../../fonts/UbuntuMono.ttf"; 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) width: usize, + pub(crate) height: usize, +} + /// Generic function for parsing and rendering ASCII into an image. /// /// * `imgii_options`: The imgii options for rendering ASCII. /// * `ascii_text`: The ASCII text to render. -pub fn render_ascii_generic( +/// +/// # Returns +/// `Ok` containing a 2d `Vec` if `ImageData`, holding each character image, otherwise an `Err`. +pub(crate) fn render_ascii_generic( imgii_options: &ImgiiOptions, ascii_text: String, -) -> Result>, ImgiiError> { +) -> Result { // set up font for rendering let font = FontRef::try_from_slice(FONT_BYTES) // there's nothing useful in this error, convert it! .map_err(|_| FontError::new(String::from(FONT_FILE)))?; - // contains lines of images - // starting at 0 is the top, first line of the vector - // inside an inner vec, 0 starts at the leftmost character of the line - let mut image_2d_vec = vec![]; + // 2d Vec of images for each character + let mut image_2d_vec = Vec::new(); - // read every line in the file - for line in ascii_text.lines() { - let mut char_images = vec![]; + // 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()); + // 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 // we assume that there's only one character for each color - let control_char = '\u{1b}'; // represents the ansi escape character `\033` - let mut pattern_string = String::from(control_char); - let pattern = r"\[38;2;([0-9]+);([0-9]+);([0-9]+)m(.)"; - pattern_string += pattern; + // NOTE: \u{1b} represents the \033 character + let pattern_str = concat!('\u{1b}', r"\[38;2;([0-9]+);([0-9]+);([0-9]+)m(.)"); // TODO: if multiple threads are using this same regex object, maybe we could make it a // static global or compile it early so we can reuse it? Maybe as a "parser" object? - let re = Regex::new(&pattern_string)?; + let re = Regex::new(pattern_str)?; + + // current line's width + let mut line_width = 0; // create the image for this character for (_full_str, [r, g, b, the_str]) in re.captures_iter(line).map(|c| c.extract()) { @@ -62,7 +78,7 @@ pub fn render_ascii_generic( let generated_png = { if the_str.trim().is_empty() { // create a transparent png for a space - str_to_transparent_png(imgii_options) + transparent_png.clone() } else { // render the actual text if it's not empty let colored = ColoredStr { @@ -76,11 +92,34 @@ pub fn render_ascii_generic( } }; - char_images.push(generated_png); + line_width += 1; + image_2d_vec.push(generated_png); } - image_2d_vec.push(char_images); + // 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 { + width = line_width; + // now we can reserve the rest of the space for our vec + image_2d_vec.reserve(width * height); + } } - Ok(image_2d_vec) + assert!( + width * height == image_2d_vec.len(), + "expected length of the 2d vector was {} but got {}", + width * height, + image_2d_vec.len() + ); + + Ok(Imgii2dImage { + image_2d: image_2d_vec, + width, + height, + }) } diff --git a/src/conversion/converters/gif_converter.rs b/src/conversion/converters/gif_converter.rs index d4f2ee6..ff0356a 100644 --- a/src/conversion/converters/gif_converter.rs +++ b/src/conversion/converters/gif_converter.rs @@ -1,7 +1,7 @@ use std::{fs::File, io::BufReader}; use crate::{ - conversion::{converters::generic_converter::render_ascii_generic, image_data::ImageData}, + conversion::converters::generic_converter::{Imgii2dImage, render_ascii_generic}, error::{BoxedDynErr, ImgiiError}, options::{ImgiiOptions, RasciiOptions}, }; @@ -12,7 +12,7 @@ use rayon::iter::{IntoParallelIterator, ParallelIterator}; /// Holds the metadata for a frame that has been deconstructed. #[derive(Debug, Clone)] -pub struct FrameMetadata { +pub(crate) struct FrameMetadata { /// The left value for this frame. left: u32, /// The top value for this frame. @@ -24,7 +24,7 @@ pub struct FrameMetadata { /// Holds the deconstructed frame data for a single frame, before it is converted to image data. /// Holds the raw ASCII string and frame metadata. #[derive(Debug, Clone)] -pub struct NonRenderedFramePart { +pub(crate) struct NonRenderedFramePart { /// The ASCII representation of the frame. image_ascii: String, /// The frame metadata for this frame. @@ -33,9 +33,9 @@ pub struct NonRenderedFramePart { /// Holds the deconstructed frame data for a single frame that has been rendered to a 2D vector. #[derive(Debug, Clone)] -pub struct RenderedFramePart { +pub(crate) struct RenderedFramePart { /// The image data with the rendered image data for this frame as a 2D vector. - image_data: Vec>, + image_data: Imgii2dImage, /// The frame metadata for this frame. frame_metadata: FrameMetadata, } @@ -47,25 +47,25 @@ pub struct RenderedFramePart { impl FrameMetadata { /// Creates a new [`FrameMetadata`]. #[must_use] - pub fn new(left: u32, top: u32, delay: Delay) -> Self { + pub(crate) fn new(left: u32, top: u32, delay: Delay) -> Self { Self { left, top, delay } } /// Gets the x offset for this frame. #[must_use] - pub fn left(&self) -> u32 { + pub(crate) fn left(&self) -> u32 { self.left } /// Gets the y offset for this frame. #[must_use] - pub fn top(&self) -> u32 { + pub(crate) fn top(&self) -> u32 { self.top } /// Gets the delay of this frame. #[must_use] - pub fn delay(&self) -> Delay { + pub(crate) fn delay(&self) -> Delay { self.delay } } @@ -77,7 +77,8 @@ impl RenderedFramePart { /// * `image_data`: The image data. /// * `frame_metadata`: The frame metadata. #[must_use] - pub fn new(image_data: Vec>, frame_metadata: FrameMetadata) -> Self { + #[allow(dead_code)] + pub(crate) fn new(image_data: Imgii2dImage, frame_metadata: FrameMetadata) -> Self { Self { image_data, frame_metadata, @@ -86,20 +87,23 @@ impl RenderedFramePart { /// Gets the image data for this frame. #[must_use] - pub fn image_data(&self) -> &Vec> { + #[allow(dead_code)] + pub(crate) fn image_data(&self) -> &Imgii2dImage { &self.image_data } /// Gets the metadata for this frame. #[must_use] - pub fn frame_metadata(&self) -> &FrameMetadata { + #[allow(dead_code)] + pub(crate) fn frame_metadata(&self) -> &FrameMetadata { &self.frame_metadata } /// Moves out of this RenderedFramePart, returning a tuple containing the image data followed /// by metadata. #[must_use] - pub fn into_frame_data(self) -> (Vec>, FrameMetadata) { + #[allow(dead_code)] + pub(crate) fn into_frame_data(self) -> (Imgii2dImage, FrameMetadata) { (self.image_data, self.frame_metadata) } } @@ -111,7 +115,7 @@ impl NonRenderedFramePart { /// * `image_ascii`: The ASCII string representation of an image. /// * `frame_metadata`: The frame metadata. #[must_use] - pub fn new(image_ascii: String, frame_metadata: FrameMetadata) -> Self { + pub(crate) fn new(image_ascii: String, frame_metadata: FrameMetadata) -> Self { Self { image_ascii, frame_metadata, @@ -129,7 +133,7 @@ impl NonRenderedFramePart { /// /// * `input_file_name`: The input file name. /// * `rascii_options`: The RASCII options for converting to ASCII. -pub fn read_gif_as_deconstructed_ascii( +pub(crate) fn read_gif_as_deconstructed_ascii( input_file_name: &str, rascii_options: &RasciiOptions, ) -> Result>, ImgiiError> { @@ -159,7 +163,7 @@ pub fn read_gif_as_deconstructed_ascii( /// /// * `input_file_name`: the input file name. /// * `imgii_options`: the imgii options for rendering ascii. -pub fn read_as_deconstructed_rendered_gif_vec( +pub(crate) fn read_as_deconstructed_rendered_gif_vec( input_file_name: &str, imgii_options: &ImgiiOptions, ) -> Result>, ImgiiError> { @@ -196,7 +200,7 @@ pub fn read_as_deconstructed_rendered_gif_vec( /// /// # Returns /// `Err()` upon error reading the GIF, `Ok()` otherwise. -pub fn read_deconstructed_gif( +pub(crate) fn read_deconstructed_gif( input_file_name: &str, ) -> Result, ImgiiError> { let file_in = BufReader::new(File::open(input_file_name)?); diff --git a/src/conversion/converters/png_converter.rs b/src/conversion/converters/png_converter.rs index a97869a..7144589 100644 --- a/src/conversion/converters/png_converter.rs +++ b/src/conversion/converters/png_converter.rs @@ -1,6 +1,6 @@ use super::generic_converter::render_ascii_generic; use crate::{ - conversion::image_data::ImageData, + conversion::converters::generic_converter::Imgii2dImage, error::{BoxedDynErr, ImgiiError}, options::{ImgiiOptions, RasciiOptions}, }; @@ -17,10 +17,10 @@ use rascii_art_img::render_image_to; /// # Returns /// * `Vec>`: A 2d `Vec` of images, containing each rendered character from the /// image. -pub fn parse_ascii_to_2d_png_vec( +pub(crate) fn parse_ascii_to_2d_png_vec( input_file_name: &str, imgii_options: &ImgiiOptions, -) -> Result>, ImgiiError> { +) -> Result { let ascii_text = read_png_as_ascii(input_file_name, imgii_options.rascii_options())?; render_ascii_generic(imgii_options, ascii_text) } @@ -33,7 +33,7 @@ pub fn parse_ascii_to_2d_png_vec( /// /// # Returns /// * `String` containing the colored image data as ASCII, colored using terminal escape sequences. -pub fn read_png_as_ascii( +pub(crate) fn read_png_as_ascii( input_file_name: &str, rascii_options: &RasciiOptions, ) -> Result { diff --git a/src/conversion/image_data.rs b/src/conversion/image_data.rs index 6c77408..50ef605 100644 --- a/src/conversion/image_data.rs +++ b/src/conversion/image_data.rs @@ -1,21 +1,21 @@ use image::ImageBuffer; // easier to read -pub type InternalImage = ImageBuffer, Vec>; +pub(crate) type InternalImage = ImageBuffer, Vec>; /// Represents the image data to work with. /// Holds an `ImageBuffer` with the image data. #[derive(Debug, Clone)] -pub struct ImageData(InternalImage); +pub(crate) struct ImageData(InternalImage); impl ImageData { /// Create a new ImageData struct as this image buffer. - pub fn new(image_buffer: InternalImage) -> Self { + pub(crate) fn new(image_buffer: InternalImage) -> Self { Self(image_buffer) } /// Gets a reference to the internal buffer for this image data. - pub fn as_buffer(&self) -> &InternalImage { + pub(crate) fn as_buffer(&self) -> &InternalImage { &self.0 } } diff --git a/src/conversion/image_writer.rs b/src/conversion/image_writer.rs index f179b2c..13c5ef5 100644 --- a/src/conversion/image_writer.rs +++ b/src/conversion/image_writer.rs @@ -1,17 +1,16 @@ use crate::{ conversion::{ + converters::generic_converter::Imgii2dImage, image_data::{ImageData, InternalImage}, - render_char_to_png::calculate_char_dimensions, }, - error::{ImgiiError, InvalidParameterError, ParseImageError}, - options::ImgiiOptions, + error::{ImgiiError, InvalidParameterError}, }; use rayon::prelude::*; /// An image writer which holds a rendered ASCII image. #[derive(Debug, Clone)] -pub struct AsciiImageWriter { - pub imagebuf: ImageData, +pub(crate) struct AsciiImageWriter { + pub(crate) imagebuf: ImageData, } impl From for AsciiImageWriter { @@ -35,38 +34,27 @@ impl AsciiImageWriter { /// # Returns /// - An `Option` containing `Some` `AsciiImageWriter` upon success, or a /// `None` upon failure. - pub fn from_2d_vec( - parts: Vec>, - pngii_options: &ImgiiOptions, - ) -> Result { - if parts.is_empty() || parts[0].is_empty() { + pub(crate) fn from_2d_vec(the_image: Imgii2dImage) -> Result { + if the_image.image_2d.is_empty() { // no image to build return Err(InvalidParameterError::new(String::from("parts")).into()); } - let font_size = pngii_options.font_size(); - - let (mut height, mut width) = (0, 0); // find out the new canvas size - for (cur_line, line) in parts.iter().enumerate() { - // assume that every image has the same height and width - if !line.is_empty() { - height += line[0].as_buffer().height(); - // calculate the width - width = line[0].as_buffer().width() * line.len() as u32; - } else { - return Err(ParseImageError::new(cur_line).into()); - } - } + // this should always exist + let char_width = the_image.image_2d[0].as_buffer().width(); + let char_height = the_image.image_2d[0].as_buffer().height(); + + // calculate image resolution in pixels based on this reference image + let height = char_height * the_image.height as u32; + let width = char_width * the_image.width as u32; // create the new canvas to write to let mut canvas: InternalImage = image::ImageBuffer::new(width, height); - // calculate character width and height - let (char_width, char_height) = calculate_char_dimensions(font_size); - + // copy over pixels to canvas canvas.par_enumerate_pixels_mut().for_each(|(x, y, pixel)| { - // the index into the row and column from the parts vec + // the index into the row and column from the image_2d vec let row = y / char_height; let column = x / char_width; @@ -74,7 +62,7 @@ impl AsciiImageWriter { let inner_x = x % char_width; let inner_y = y % char_height; - let new_pixel = parts[row as usize][column as usize] + let new_pixel = the_image.image_2d[column as usize + row as usize * the_image.width] .as_buffer() .get_pixel(inner_x, inner_y); // write the pixel we have chosen diff --git a/src/conversion/render_char_to_png.rs b/src/conversion/render_char_to_png.rs index 93db035..ff493df 100644 --- a/src/conversion/render_char_to_png.rs +++ b/src/conversion/render_char_to_png.rs @@ -1,38 +1,41 @@ use crate::{conversion::image_data::ImageData, options::ImgiiOptions}; use ab_glyph::{FontRef, PxScale}; -use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage}; +use image::{ImageBuffer, Rgba}; use imageproc::drawing::draw_text_mut; -use rayon::prelude::*; /// Represents a colored string to write. /// All characters are contiguous and share the same color. #[derive(Debug, Clone)] -pub struct ColoredStr { - pub red: u8, - pub blue: u8, - pub green: u8, - pub string: String, +pub(crate) struct ColoredStr { + pub(crate) red: u8, + pub(crate) blue: u8, + pub(crate) green: u8, + pub(crate) string: String, } const BACKGROUND_PIXEL: Rgba = Rgba([0, 0, 0, u8::MAX]); -/// Converts string data into a png +/// Converts string data into a png. /// Uses `imageproc` to render text. -pub fn str_to_png(data: ColoredStr, font: &FontRef<'_>, imgii_options: &ImgiiOptions) -> ImageData { +pub(crate) fn str_to_png( + data: ColoredStr, + font: &FontRef<'_>, + imgii_options: &ImgiiOptions, +) -> ImageData { let font_size = imgii_options.font_size(); let (char_width, char_height) = calculate_char_dimensions(font_size); // create our image to work with - let mut image = RgbaImage::new(char_width, char_height); + let mut image = if imgii_options.background() { + // create with background + ImageBuffer::from_pixel(char_width, char_height, BACKGROUND_PIXEL) + } else { + ImageBuffer::new(char_width, char_height) + }; let scale = PxScale { x: font_size as f32, y: font_size as f32, }; - // set background if user wants it - if imgii_options.background() { - set_background(&mut image); - } - draw_text_mut( &mut image, Rgba([data.red, data.green, data.blue, u8::MAX]), @@ -46,27 +49,16 @@ pub fn str_to_png(data: ColoredStr, font: &FontRef<'_>, imgii_options: &ImgiiOpt ImageData::new(image) } -// PERF: this is a costly operation and should probably be removed -fn set_background(image: &mut ImageBuffer, Vec>) { - image.par_enumerate_pixels_mut().for_each(|(_, _, pixel)| { - // set background - *pixel = BACKGROUND_PIXEL; - }); -} - /// Creates a transparent png in place of a character -pub fn str_to_transparent_png(imgii_options: &ImgiiOptions) -> ImageData { +pub(crate) fn str_to_transparent_png(imgii_options: &ImgiiOptions) -> ImageData { let (char_width, char_height) = calculate_char_dimensions(imgii_options.font_size()); - let mut output = DynamicImage::new_rgba8(char_width, char_height).into(); - - // TODO: instead of doing a background like this, why don't we create a single image that is a - // solid color (or we could do more interesting backgrounds) and overlay the output image over - // top of that? - - // set background if user wants it - if imgii_options.background() { - set_background(&mut output); - } + let output = if imgii_options.background() { + // create image with background + ImageBuffer::from_pixel(char_width, char_height, BACKGROUND_PIXEL) + } else { + // empty image + ImageBuffer::new(char_width, char_height) + }; ImageData::new(output) } @@ -75,6 +67,7 @@ pub fn str_to_transparent_png(imgii_options: &ImgiiOptions) -> ImageData { /// /// # Returns /// (width, height) in a tuple -pub fn calculate_char_dimensions(font_size: u32) -> (u32, u32) { +#[inline] +pub(crate) fn calculate_char_dimensions(font_size: u32) -> (u32, u32) { (font_size / 2, font_size) } diff --git a/src/lib.rs b/src/lib.rs index f41a72e..c577759 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! Imgii is a library for converting images to ASCII and rendering as different image types. For //! example, it can take a PNG input and convert it into ASCII, render it, and save it. -pub mod conversion; +pub(crate) mod conversion; pub mod error; pub mod image_types; pub mod options; @@ -78,7 +78,7 @@ pub fn convert_to_ascii_png( imgii_options: &ImgiiOptions, ) -> Result<(), ImgiiError> { let lines = parse_ascii_to_2d_png_vec(input_file_name, imgii_options)?; - let final_image_writer = AsciiImageWriter::from_2d_vec(lines, imgii_options)?; + let final_image_writer = AsciiImageWriter::from_2d_vec(lines)?; // write the image final_image_writer @@ -152,10 +152,7 @@ pub fn convert_to_ascii_gif( .filter_map(|frame_part| frame_part) .map(|frame_part| { let (image_data, frame_metadata) = frame_part.into_frame_data(); - ( - AsciiImageWriter::from_2d_vec(image_data, imgii_options), - frame_metadata, - ) + (AsciiImageWriter::from_2d_vec(image_data), frame_metadata) }) .collect::>();