rascii_art/
image_renderer.rs

1use std::io;
2
3use ansi_term::Color;
4use image::{
5    DynamicImage,
6    Rgba,
7};
8
9use super::renderer::{
10    RenderOptions,
11    Renderer,
12};
13
14pub struct ImageRenderer<'a> {
15    resource: &'a DynamicImage,
16    options: &'a RenderOptions<'a>,
17}
18
19impl ImageRenderer<'_> {
20    fn get_char_for_pixel(&self, pixel: &Rgba<u8>, maximum: f64) -> &str {
21        let as_grayscale = self.get_grayscale(pixel) / maximum;
22
23        // TODO: Use alpha channel to determine if pixel is transparent?
24        let char_index = (as_grayscale * (self.options.charset.len() as f64 - 1.0)) as usize;
25
26        self.options.charset[if self.options.invert {
27            self.options.charset.len() - 1 - char_index
28        } else {
29            char_index
30        }]
31    }
32
33    fn get_grayscale(&self, pixel: &Rgba<u8>) -> f64 {
34        ((pixel[0] as f64 * 0.299) + (pixel[1] as f64 * 0.587) + (pixel[2] as f64 * 0.114)) / 255.0
35    }
36}
37
38impl<'a> Renderer<'a, DynamicImage> for ImageRenderer<'a> {
39    fn new(resource: &'a DynamicImage, options: &'a RenderOptions<'a>) -> Self {
40        Self { resource, options }
41    }
42
43    fn render(&self, writer: &mut impl io::Write) -> io::Result<()> {
44        let (width, height) = (
45            self.options.width.unwrap_or_else(|| {
46                (self
47                    .options
48                    .height
49                    .expect("Either width or height must be set") as f64
50                    * self.resource.width() as f64
51                    / self.resource.height() as f64
52                    // This is because the font is rarely square.
53                    * 2.0)
54                    .ceil() as u32
55            }),
56            self.options.height.unwrap_or_else(|| {
57                (self
58                    .options
59                    .width
60                    .expect("Either width or height must be set") as f64
61                    * self.resource.height() as f64
62                    / self.resource.width() as f64
63                    // This is because the font is rarely square.
64                    / 2.0)
65                    .ceil() as u32
66            }),
67        );
68
69        let image = self.resource.thumbnail_exact(width, height).to_rgba8();
70
71        let mut last_color: Option<Color> = None;
72        let mut current_line = 0;
73        let maximum = image
74            .pixels()
75            .fold(0.0, |acc, pixel| self.get_grayscale(pixel).max(acc));
76        for (_, line, pixel) in image.enumerate_pixels() {
77            if current_line < line {
78                current_line = line;
79
80                if let Some(last_color_value) = last_color {
81                    write!(writer, "{}", last_color_value.suffix())?;
82                    last_color = None;
83                }
84
85                writeln!(writer)?;
86            }
87
88            if self.options.colored {
89                let color = Color::RGB(pixel[0], pixel[1], pixel[2]);
90
91                if last_color != Some(color) {
92                    write!(writer, "{}", color.prefix())?;
93                }
94
95                last_color = Some(color);
96            }
97
98            let char_for_pixel = self.get_char_for_pixel(pixel, maximum);
99            write!(writer, "{char_for_pixel}")?;
100        }
101
102        if let Some(last_color) = last_color {
103            write!(writer, "{}", last_color.suffix())?;
104        }
105
106        writer.flush()?;
107
108        Ok(())
109    }
110
111    fn render_to(&self, buffer: &mut String) -> io::Result<()> {
112        let (width, height) = (
113            self.options.width.unwrap_or_else(|| {
114
115                (self
116                    .options
117                    .height
118                    .expect("Either width or height must be set") as f64
119                    * self.resource.width() as f64
120                    / self.resource.height() as f64
121                    // This is because the font is rarely square.
122                    * 2.0)
123                    .ceil() as u32
124            }),
125
126            self.options.height.unwrap_or_else(|| {
127                (self
128                    .options
129                    .width
130                    .expect("Either width or height must be set") as f64
131                    * self.resource.height() as f64
132                    / self.resource.width() as f64
133                    // This is because the font is rarely square.
134                    / 2.0)
135                    .ceil() as u32
136            }),
137
138        );
139
140        let image = self.resource.thumbnail_exact(width, height).to_rgba8();
141
142        let mut last_color: Option<Color> = None;
143        let mut current_line = 0;
144        let maximum = image
145            .pixels()
146            .fold(0.0, |acc, pixel| self.get_grayscale(pixel).max(acc));
147        for (_, line, pixel) in image.enumerate_pixels() {
148            if current_line < line {
149                current_line = line;
150
151                if let Some(last_color_value) = last_color {
152                    buffer.push_str(&last_color_value.suffix().to_string()); // TODO look up for a
153                    // better solution after benchmarking.
154                    last_color = None;
155                }
156
157                buffer.push('\n');
158            }
159
160            if self.options.colored {
161                let color = Color::RGB(pixel[0], pixel[1], pixel[2]);
162
163                if last_color != Some(color) {
164                    buffer.push_str(&color.prefix().to_string());
165                }
166
167                last_color = Some(color);
168            }
169
170            // Normally this char_for_pixel has to be a char but because of the compatibility
171            // reasons with unicode-segmentation It's implemented as a &str (WORKAROUND)
172            let char_for_pixel = self.get_char_for_pixel(pixel, maximum);
173            buffer.push_str(char_for_pixel);
174        }
175
176
177        if let Some(last_color) = last_color {
178            buffer.push_str(&last_color.suffix().to_string()); // TODO look up for a
179            // better solution after benchmarking.
180        }
181
182        Ok(())
183    }
184
185}