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}