ascii_rs/
image_proc.rs

1use std::{error::Error, io};
2
3use ansi_term::Color;
4use image::imageops::FilterType;
5use image::DynamicImage;
6use image::Rgba;
7
8use crate::ascii::DEFAULT;
9
10/// Engine for rendering rgba images to ascii text
11///
12/// * `source`: DynamicImage
13/// * `edge_map`: TODO: implement Edge detection methods
14pub struct ImageEngine {
15    source: DynamicImage,
16    #[allow(unused)]
17    edge_map: Option<Vec<(u8, u8)>>,
18}
19
20impl ImageEngine {
21    /// Construct a new engine from an owned dynamic image
22    /// # Usage
23    /// ```rust
24    ///     
25    /// use std::error::Error;
26    /// use rustascii::{image, image_proc::ImageEngine};
27    ///
28    /// fn main() -> Result<(), Box<dyn Error>> {
29    ///     let source = image::open("/")?;
30    ///     let engine = ImageEngine::new(source);
31    ///     // Do your stuff with the engine
32    ///     Ok(())
33    ///}
34    /// ```
35    ///
36    /// * `source`: constructed `DynamicImage`
37    pub fn new(source: DynamicImage) -> Self {
38        Self {
39            source,
40            edge_map: None, // TODO: Implement edge detection
41        }
42    }
43
44    /// Construct a new engine from a slice of bytes
45    /// # Usage
46    /// ```rust
47    ///     use rustascii::{image_proc::ImageEngine};
48    ///     use std::error::Error;
49    ///
50    ///     fn main() -> Result<(), Box<dyn Error>> {
51    ///         let source = include_bytes!("your image path");
52    ///         let engine = ImageEngine::from_slice()?;
53    ///         // Do stuff with the engine
54    ///         Ok(())
55    ///     }
56    /// ```
57    /// * `source`: a slice of bytes
58    pub fn from_slice(source: &[u8]) -> Result<Self, Box<dyn Error>> {
59        let image = image::load_from_memory(source)?;
60
61        Ok(Self {
62            source: image,
63            edge_map: None,
64        })
65    }
66
67    /// Process the image, with scaling, and write the output to a writer.
68    ///
69    /// Note that either `width` or `height` must be Some(value)
70    ///
71    /// # Usage
72    /// Here is a simple example writing to stdout.
73    /// ```rust
74    ///     use rustascii::{image_proc::ImageEngine};
75    ///     use std::{error::Error, io::stdout};
76    ///
77    ///     fn main() -> Result<(), Box<dyn Error>> {
78    ///         let source = include_bytes!("your-path");
79    ///         let engine = ImageEngine::from_slice(source)?;
80    ///
81    ///         let mut writer = stdout(); // stdout implements io::Write!
82    ///
83    ///         // If only one of the axis is set,
84    ///         // the image aspect ratio will be preserved
85    ///         engine.render_to_text(&mut writer, 0, Some(128), None)?;
86    ///         Ok(())
87    ///     }
88    /// ```
89    ///
90    /// You can also do some more advance with writer, like TcpStream, or File
91    /// ```rust
92    ///     use rustascii::{image_proc::ImageEngine};
93    ///     use std::{error::Error, io::stdout};
94    ///
95    ///     fn main() -> Result<(), Box<dyn Error>> {
96    ///         let source = include_bytes!("your-path");
97    ///         let engine = ImageEngine::from_slice(source)?;
98    ///
99    ///         let mut file_writer = fs::File::create_new("your-new-file")?;
100    ///
101    ///         // If only one of the axis is set,
102    ///         // the image aspect ratio will be preserved
103    ///         engine.render_to_text(&mut file_writer, 0, Some(128), None)?;
104    ///         Ok(())
105    ///     }
106    /// ```
107    ///
108    /// * `writer`: Some thing that implements `io::Write`
109    /// * `alpha_threshold`: Lowest possible alpha value for ascii text to be rendered
110    /// * `width`: New width of the ascii text
111    /// * `height`: New height of the ascii text
112    pub fn render_to_text(
113        &self,
114        writer: &mut dyn io::Write,
115        alpha_threshold: u8,
116        width: Option<u32>,
117        height: Option<u32>,
118    ) -> io::Result<()> {
119        let (width, height) = self.calculate_dimensions(width, height);
120        let image = self
121            .source
122            .resize_exact(width, height, FilterType::Nearest)
123            .to_rgba8();
124
125        let mut prev_color: Option<Color> = None;
126        let mut current_line = 0;
127
128        let maximum = image
129            .pixels()
130            .fold(0.0, |acc, pixel| self.get_grayscale_pixel(pixel).max(acc));
131
132        for (_, line, pixel) in image.enumerate_pixels() {
133            if current_line < line {
134                current_line = line;
135                if let Some(color) = prev_color {
136                    write!(writer, "{}", color.suffix())?;
137                    prev_color = None;
138                };
139                writeln!(writer)?;
140            }
141
142            let color = Color::RGB(pixel[0], pixel[1], pixel[2]);
143            if prev_color != Some(color) {
144                write!(writer, "{}", color.prefix())?;
145            }
146            prev_color = Some(color);
147
148            let char_for_pixel = self.get_char_for_pixel(pixel, alpha_threshold, maximum);
149            write!(writer, "{char_for_pixel}")?;
150        }
151
152        if let Some(color) = prev_color {
153            write!(writer, "{}", color.prefix())?;
154        }
155
156        writer.flush()?;
157
158        Ok(())
159    }
160
161    /// Get all of the content as a string, using this is not recommended, using `render_to_text`
162    /// should covered almost all cases.
163    ///
164    /// * `alpha_threshold`: Lowest possible alpha value for ascii text to be rendered
165    /// * `width`: New width of the ascii text
166    /// * `height`: New height of the ascii text
167    pub fn get_ascii_as_string(
168        &self,
169        alpha_threshold: u8,
170        width: Option<u32>,
171        height: Option<u32>,
172    ) -> String {
173        let (width, height) = self.calculate_dimensions(width, height);
174        let image = self
175            .source
176            .resize_exact(width, height, FilterType::Nearest)
177            .to_rgba8();
178
179        let mut output = String::new();
180        let mut prev_color: Option<Color> = None;
181        let mut current_line = 0;
182
183        let maximum = image
184            .pixels()
185            .fold(0.0, |acc, pixel| self.get_grayscale_pixel(pixel).max(acc));
186
187        for (_, line, pixel) in image.enumerate_pixels() {
188            if current_line < line {
189                current_line = line;
190                if let Some(color) = prev_color {
191                    output.push_str(&format!("{}", color.suffix()));
192                    prev_color = None;
193                };
194                output.push('\n');
195            }
196
197            let color = Color::RGB(pixel[0], pixel[1], pixel[2]);
198            if prev_color != Some(color) {
199                output.push_str(&format!("{}", color.prefix()));
200            }
201            prev_color = Some(color);
202
203            let char_for_pixel = self.get_char_for_pixel(pixel, alpha_threshold, maximum);
204            output.push_str(&format!("{char_for_pixel}"));
205        }
206
207        if let Some(color) = prev_color {
208            output.push_str(&format!("{}", color.prefix()));
209        }
210
211        output
212    }
213
214    fn get_char_for_pixel(&self, pixel: &Rgba<u8>, alpha_threshold: u8, maximum: f64) -> char {
215        let gray_scale = self.get_grayscale_pixel(pixel) / maximum;
216        if pixel.0[3] <= alpha_threshold {
217            return ' ';
218        }
219
220        DEFAULT[(gray_scale * (DEFAULT.len() - 1) as f64) as usize]
221    }
222
223    fn get_grayscale_pixel(&self, pixel: &Rgba<u8>) -> f64 {
224        ((pixel.0[0] as f64) * 0.2989)
225            + (pixel.0[1] as f64 * 0.5870)
226            + ((pixel.0[2] as f64) * 0.1140) / 255.0
227    }
228
229    fn calculate_dimensions(&self, width: Option<u32>, height: Option<u32>) -> (u32, u32) {
230        (
231            width.unwrap_or_else(|| {
232                (height.expect("Either width or weight must be specified") as f64
233                    * self.source.width() as f64
234                    / self.source.height() as f64
235                    / 2.0)
236                    .ceil() as u32
237            }),
238            height.unwrap_or_else(|| {
239                (width.expect("Either height or width must be specified") as f64
240                    * self.source.height() as f64
241                    / self.source.width() as f64
242                    / 2.0)
243                    .ceil() as u32
244            }),
245        )
246    }
247}