Skip to main content

symbolize/
lib.rs

1//! This crate provides [`symbolize`] function that allows you to convert bitmap images into fine text art.
2//! It supports scaling the [`symbolize`]d images as well as coloring them for terminals with RGB-support.
3//!
4//! [`SymbolizeResult`] is a wrapper that allows you to easy convert a result to [`Vec<String>`], [`Vec<u8>`] or [`String`]
5//!
6//! The "original_image" parameter provides an original image as a [`DynamicImage`]
7//!
8//! The "palette" parameter determines which characters will be used when converting the image.
9//! The symbols are arranged in descending order of the frequency of their appearance on the image.
10//!
11//! The "scale" parameter determines the size of the output image relative to the size of the original.
12//!
13//! The "filter_type" parameter defines what type of filtering will be used when scaling the image. For more info read [`FilterType`] docs.
14//!
15//! The "colorize" parameter determines whether the output should be colorized for RGB-terminals or not.
16//!
17//! # Example usage:
18//!
19//! ```ignore
20//! use image::{imageops::FilterType, open};
21//! use std::{process, error::Error};
22//! use symbolize::symbolize;
23//!
24//! fn main() -> Result<(), Box<dyn Error>> {
25//!     let result = symbolize(
26//!         open("./path/to/image.png")?
27//!         &vec!['*', '#', '@', ' '],
28//!         0.1,
29//!         FilterType::Nearest,
30//!         false,
31//!     );
32//!
33//!     match result {
34//!         Err(e) => {
35//!             eprintln!("{}", e);
36//!             process::exit(1);
37//!         }
38//!         Ok(result) => {
39//!             for line in Into::<Vec<String>>::into(result) {
40//!                 println!("{}", line)
41//!             }
42//!         }
43//!     }
44//!
45//!     Ok(())
46//! }
47//! ```
48//!
49//! # Example output:
50//!
51//! ```ignore
52//!                               @@  @@@@  @@                              
53//!                           @@  @@@@@@@@@@@@@@@@@@                        
54//!                     @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                    
55//!                     @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@            @@      
56//!   @@  @@          @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@      
57//!   @@  @@@@        @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@  @@@@
58//! @@@@  @@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  @@@@@@@@  
59//!   @@@@@@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@@@@@  
60//!     @@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    
61//!       @@@@  @@@@@@@@@@@@@@@@@@&&    @@@@&&&&  @@@@@@@@@@@@@@  @@        
62//!         @@@@@@@@@@@@@@@@@@@@@@&&    @@@@      @@@@@@@@@@@@@@@@@@        
63//!           @@@@@@@@@@@@@@@@@@@@      @@@@      @@@@@@@@@@@@@@@@@@        
64//!         @@@@@@##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@####@@@@@@      
65//!           @@@@##  ####@@@@@@@@@@@@@@    @@@@@@@@@@####    ##@@@@        
66//!             @@  ##        ######################        ##  @@          
67//!               @@                                            @@          
68//!                                                           @@
69//! ```
70
71use std::{
72    collections::{hash_map::Entry, HashMap},
73    error::Error,
74    io,
75};
76
77use crossterm::style::{style, Color, Stylize};
78use image::{
79    imageops::{resize, FilterType},
80    DynamicImage, Rgb, RgbImage,
81};
82
83/// Helper wrapper struct that provides some [`Into`] implementations for easier convertation
84pub struct SymbolizeResult(pub Vec<Vec<String>>);
85
86impl Into<String> for SymbolizeResult {
87    fn into(self) -> String {
88        self.0
89            .iter()
90            .map(|row| row.join(""))
91            .collect::<Vec<String>>()
92            .join("\n")
93    }
94}
95
96impl Into<Vec<u8>> for SymbolizeResult {
97    fn into(self) -> Vec<u8> {
98        self.0
99            .iter()
100            .map(|row| row.join(""))
101            .collect::<Vec<String>>()
102            .join("\n")
103            .into_bytes()
104    }
105}
106
107impl Into<Vec<String>> for SymbolizeResult {
108    fn into(self) -> Vec<String> {
109        self.0
110            .iter()
111            .map(|row| row.join(""))
112            .collect::<Vec<String>>()
113    }
114}
115
116/// Main function of this crate. Turns your bitmap image into text art.
117pub fn symbolize(
118    original_image: DynamicImage,
119    scale: f32,
120    palette: &[char],
121    filter_type: FilterType,
122    colorize: bool,
123) -> Result<SymbolizeResult, Box<dyn Error>> {
124    if palette.is_empty() {
125        return Err(Box::new(io::Error::new(
126            io::ErrorKind::InvalidInput,
127            "pallete should contain at leasst one symbol, aborting",
128        )));
129    }
130
131    if scale < 0.0 {
132        return Err(Box::new(io::Error::new(
133            io::ErrorKind::InvalidInput,
134            "scale should be > 0, aborting",
135        )));
136    }
137
138    let original_image_rgb = original_image.into_rgb8();
139    let scaled_image = resize(
140        &original_image_rgb,
141        (original_image_rgb.width() as f32 * scale) as u32,
142        (original_image_rgb.height() as f32 * scale) as u32,
143        filter_type,
144    );
145    let colors_to_use = get_most_used_colours_with_symbols(&scaled_image, palette);
146
147    let mut result = vec![];
148    for row in scaled_image.rows() {
149        let mut result_row = vec![];
150        for pixel in row {
151            let (symbol, average_pixel) = get_symbol_by_pixel(&colors_to_use, pixel)?;
152
153            let str_symbol = if colorize {
154                format!(
155                    "{}",
156                    style(symbol.to_string()).with(Color::from((
157                        average_pixel.0[0],
158                        average_pixel.0[1],
159                        average_pixel.0[2]
160                    )))
161                )
162            } else {
163                symbol.to_string()
164            };
165            result_row.push(str_symbol.clone());
166            result_row.push(str_symbol);
167        }
168
169        result.push(result_row)
170    }
171
172    Ok(SymbolizeResult(result))
173}
174
175#[derive(Debug)]
176struct PixelWithSymbol {
177    pixel: Rgb<u8>,
178    symbol: char,
179}
180
181impl PixelWithSymbol {
182    fn new(pixel: Rgb<u8>, symbol: char) -> Self {
183        Self { pixel, symbol }
184    }
185}
186
187fn get_most_used_colours_with_symbols(image: &RgbImage, symbols: &[char]) -> Vec<PixelWithSymbol> {
188    let mut colors_uses: HashMap<&image::Rgb<u8>, usize> = HashMap::new();
189    for pixel in image.pixels() {
190        match colors_uses.entry(pixel) {
191            Entry::Vacant(entry) => {
192                entry.insert(1);
193            }
194            Entry::Occupied(mut entry) => {
195                *entry.get_mut() += 1;
196            }
197        }
198    }
199    let mut colours_uses_vec: Vec<(&Rgb<u8>, usize)> = colors_uses.into_iter().collect();
200    colours_uses_vec.sort_by_key(|(_, count)| *count);
201
202    let (start, end) = (
203        colours_uses_vec
204            .len()
205            .checked_sub(symbols.len())
206            .unwrap_or(0),
207        colours_uses_vec.len(),
208    );
209
210    let mut symbol_idx = symbols.len() - 1;
211    colours_uses_vec
212        .drain(start..end)
213        .map(|(pixel, _)| {
214            let pixel_with_symbol = PixelWithSymbol::new(*pixel, symbols[symbol_idx]);
215            symbol_idx = symbol_idx.checked_sub(1).unwrap_or(0);
216
217            pixel_with_symbol
218        })
219        .collect()
220}
221
222fn get_symbol_by_pixel(
223    pixels_with_symbols: &[PixelWithSymbol],
224    pixel_to_compare: &Rgb<u8>,
225) -> Result<(char, Rgb<u8>), io::Error> {
226    let mut char = None;
227    let mut rgb_pixel = None;
228    let mut comparison = None;
229
230    for PixelWithSymbol { pixel, symbol } in pixels_with_symbols {
231        let pretendent_comparison = get_pixel_comparison(pixel_to_compare, pixel);
232        if comparison.is_none() || pretendent_comparison < comparison.unwrap() {
233            char = Some(*symbol);
234            comparison = Some(pretendent_comparison);
235            rgb_pixel = Some(*pixel);
236        }
237    }
238
239    if let (Some(char), Some(rgb_pixel)) = (char, rgb_pixel) {
240        return Ok((char, rgb_pixel));
241    }
242
243    Err(io::Error::new(
244        io::ErrorKind::Other,
245        "unexpected error, can't find matching char for a pixel. aborting",
246    ))
247}
248
249fn get_pixel_comparison(first: &Rgb<u8>, second: &Rgb<u8>) -> usize {
250    ((first.0[0] as i16 - second.0[0] as i16).abs()
251        + (first.0[1] as i16 - second.0[1] as i16).abs()
252        + (first.0[2] as i16 - second.0[2] as i16).abs()) as usize
253}
254
255#[cfg(test)]
256mod tests {
257    use image::{imageops::FilterType, open};
258
259    use crate::symbolize;
260
261    fn get_ferris() -> Vec<&'static str> {
262        vec![
263            "                                                                        ",
264            "                                                                        ",
265            "                                                                        ",
266            "                              @@  @@@@  @@                              ",
267            "                          @@  @@@@@@@@@@@@@@@@@@                        ",
268            "                    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                    ",
269            "                    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@            @@      ",
270            "  @@  @@          @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@      ",
271            "  @@  @@@@        @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@  @@@@",
272            "@@@@  @@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  @@@@@@@@  ",
273            "  @@@@@@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@@@@@  ",
274            "    @@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    ",
275            "      @@@@  @@@@@@@@@@@@@@@@@@&&    @@@@&&&&  @@@@@@@@@@@@@@  @@        ",
276            "        @@@@@@@@@@@@@@@@@@@@@@&&    @@@@      @@@@@@@@@@@@@@@@@@        ",
277            "          @@@@@@@@@@@@@@@@@@@@      @@@@      @@@@@@@@@@@@@@@@@@        ",
278            "        @@@@@@$$@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@$$$$@@@@@@      ",
279            "          @@@@$$  $$$$@@@@@@@@@@@@@@    @@@@@@@@@@$$$$    $$@@@@        ",
280            "            @@  $$        $$$$$$$$$$$$$$$$$$$$$$        $$  @@          ",
281            "              @@                                            @@          ",
282            "                                                          @@            ",
283            "                                                                        ",
284            "                                                                        ",
285            "                                                                        ",
286            "                                                                        ",
287        ]
288    }
289
290    fn get_colorized_ferris() -> &'static str {
291        "\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\n\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\n\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\n\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\n\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\n\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;165;43;0m&\u{1b}[39m\u{1b}[38;2;165;43;0m&\u{1b}[39m\u{1b}[38;2;165;43;0m&\u{1b}[39m\u{1b}[38;2;165;43;0m&\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;165;43;0m&\u{1b}[39m\u{1b}[38;2;165;43;0m&\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;247;76;0m$\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\n\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\n\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m\u{1b}[38;2;0;0;0m@\u{1b}[39m"
292    }
293
294    #[test]
295    fn renders_ferris_as_vec_of_strings() {
296        let image = open("./test-data/ferris.png").unwrap();
297        let result: Vec<String> = symbolize(
298            image,
299            0.03,
300            &vec![' ', '@', '$', '&'],
301            FilterType::Nearest,
302            false,
303        )
304        .unwrap()
305        .into();
306
307        assert_eq!(result, get_ferris());
308    }
309
310    #[test]
311    fn renders_colorized_ferris_as_string() {
312        let image = open("./test-data/ferris.png").unwrap();
313        let result: String = symbolize(
314            image,
315            0.01,
316            &vec![' ', '@', '$', '&'],
317            FilterType::Nearest,
318            true,
319        )
320        .unwrap()
321        .into();
322
323        assert_eq!(result, get_colorized_ferris());
324    }
325
326    #[test]
327    fn renders_ferris_as_string() {
328        let image = open("./test-data/ferris.png").unwrap();
329        let result: String = symbolize(
330            image,
331            0.03,
332            &vec![' ', '@', '$', '&'],
333            FilterType::Nearest,
334            false,
335        )
336        .unwrap()
337        .into();
338
339        assert_eq!(result, get_ferris().join("\n"));
340    }
341
342    #[test]
343    fn renders_ferris_as_byteslice() {
344        let image = open("./test-data/ferris.png").unwrap();
345        let result: Vec<u8> = symbolize(
346            image,
347            0.03,
348            &vec![' ', '@', '$', '&'],
349            FilterType::Nearest,
350            false,
351        )
352        .unwrap()
353        .into();
354
355        assert_eq!(result, get_ferris().join("\n").into_bytes());
356    }
357
358    #[test]
359    fn returns_error_if_no_palette_passed() {
360        let image = open("./test-data/ferris.png").unwrap();
361        let result = symbolize(image, 0.03, &vec![], FilterType::Nearest, false);
362
363        assert!(result.is_err());
364        assert_eq!(
365            result.err().unwrap().to_string(),
366            "pallete should contain at leasst one symbol, aborting"
367        );
368    }
369
370    #[test]
371    fn returns_error_if_scale_less_than_zero() {
372        let image = open("./test-data/ferris.png").unwrap();
373        let result = symbolize(image, -0.03, &vec![' '], FilterType::Nearest, false);
374
375        assert!(result.is_err());
376        assert_eq!(
377            result.err().unwrap().to_string(),
378            "scale should be > 0, aborting"
379        );
380    }
381}