symbolize 0.2.3

This crate allows you to convert raster images into their symbolic versions
Documentation
//! This crate provides [`symbolize`] function that allows you to convert bitmap images into fine text art.
//! It supports scaling the [`symbolize`]d images as well as coloring them for terminals with RGB-support.
//!
//! [`SymbolizeResult`] is a wrapper that allows you to easy convert a result to [`Vec<String>`], [`Vec<u8>`] or [`String`]
//!
//! The "original_image" parameter provides an original image as a [`DynamicImage`]
//!
//! The "palette" parameter determines which characters will be used when converting the image.
//! The symbols are arranged in descending order of the frequency of their appearance on the image.
//!
//! The "scale" parameter determines the size of the output image relative to the size of the original.
//!
//! The "filter_type" parameter defines what type of filtering will be used when scaling the image. For more info read [`FilterType`] docs.
//!
//! The "colorize" parameter determines whether the output should be colorized for RGB-terminals or not.
//!
//! # Example usage:
//!
//! ```ignore
//! use image::{imageops::FilterType, open};
//! use std::{process, error::Error};
//! use symbolize::symbolize;
//!
//! fn main() -> Result<(), Box<dyn Error>> {
//!     let result = symbolize(
//!         open("./path/to/image.png")?
//!         &vec!['*', '#', '@', ' '],
//!         0.1,
//!         FilterType::Nearest,
//!         false,
//!     );
//!
//!     match result {
//!         Err(e) => {
//!             eprintln!("{}", e);
//!             process::exit(1);
//!         }
//!         Ok(result) => {
//!             for line in Into::<Vec<String>>::into(result) {
//!                 println!("{}", line)
//!             }
//!         }
//!     }
//!
//!     Ok(())
//! }
//! ```
//!
//! # Example output:
//!
//! ```ignore
//!                               @@  @@@@  @@                              
//!                           @@  @@@@@@@@@@@@@@@@@@                        
//!                     @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                    
//!                     @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@            @@      
//!   @@  @@          @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@      
//!   @@  @@@@        @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@  @@@@
//! @@@@  @@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  @@@@@@@@  
//!   @@@@@@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@@@@@  
//!     @@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    
//!       @@@@  @@@@@@@@@@@@@@@@@@&&    @@@@&&&&  @@@@@@@@@@@@@@  @@        
//!         @@@@@@@@@@@@@@@@@@@@@@&&    @@@@      @@@@@@@@@@@@@@@@@@        
//!           @@@@@@@@@@@@@@@@@@@@      @@@@      @@@@@@@@@@@@@@@@@@        
//!         @@@@@@##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@####@@@@@@      
//!           @@@@##  ####@@@@@@@@@@@@@@    @@@@@@@@@@####    ##@@@@        
//!             @@  ##        ######################        ##  @@          
//!               @@                                            @@          
//!                                                           @@
//! ```

use std::{
    collections::{hash_map::Entry, HashMap},
    error::Error,
    io,
};

use crossterm::style::{style, Color, Stylize};
use image::{
    imageops::{resize, FilterType},
    DynamicImage, Rgb, RgbImage,
};

/// Helper wrapper struct that provides some [`Into`] implementations for easier convertation
pub struct SymbolizeResult(pub Vec<Vec<String>>);

impl Into<String> for SymbolizeResult {
    fn into(self) -> String {
        self.0
            .iter()
            .map(|row| row.join(""))
            .collect::<Vec<String>>()
            .join("\n")
    }
}

impl Into<Vec<u8>> for SymbolizeResult {
    fn into(self) -> Vec<u8> {
        self.0
            .iter()
            .map(|row| row.join(""))
            .collect::<Vec<String>>()
            .join("\n")
            .into_bytes()
    }
}

impl Into<Vec<String>> for SymbolizeResult {
    fn into(self) -> Vec<String> {
        self.0
            .iter()
            .map(|row| row.join(""))
            .collect::<Vec<String>>()
    }
}

/// Main function of this crate. Turns your bitmap image into text art.
pub fn symbolize(
    original_image: DynamicImage,
    scale: f32,
    palette: &[char],
    filter_type: FilterType,
    colorize: bool,
) -> Result<SymbolizeResult, Box<dyn Error>> {
    if palette.is_empty() {
        return Err(Box::new(io::Error::new(
            io::ErrorKind::InvalidInput,
            "pallete should contain at leasst one symbol, aborting",
        )));
    }

    if scale < 0.0 {
        return Err(Box::new(io::Error::new(
            io::ErrorKind::InvalidInput,
            "scale should be > 0, aborting",
        )));
    }

    let original_image_rgb = original_image.into_rgb8();
    let scaled_image = resize(
        &original_image_rgb,
        (original_image_rgb.width() as f32 * scale) as u32,
        (original_image_rgb.height() as f32 * scale) as u32,
        filter_type,
    );
    let colors_to_use = get_most_used_colours_with_symbols(&scaled_image, palette);

    let mut result = vec![];
    for row in scaled_image.rows() {
        let mut result_row = vec![];
        for pixel in row {
            let (symbol, average_pixel) = get_symbol_by_pixel(&colors_to_use, pixel)?;

            let str_symbol = if colorize {
                format!(
                    "{}",
                    style(symbol.to_string()).with(Color::from((
                        average_pixel.0[0],
                        average_pixel.0[1],
                        average_pixel.0[2]
                    )))
                )
            } else {
                symbol.to_string()
            };
            result_row.push(str_symbol.clone());
            result_row.push(str_symbol);
        }

        result.push(result_row)
    }

    Ok(SymbolizeResult(result))
}

#[derive(Debug)]
struct PixelWithSymbol {
    pixel: Rgb<u8>,
    symbol: char,
}

impl PixelWithSymbol {
    fn new(pixel: Rgb<u8>, symbol: char) -> Self {
        Self { pixel, symbol }
    }
}

fn get_most_used_colours_with_symbols(image: &RgbImage, symbols: &[char]) -> Vec<PixelWithSymbol> {
    let mut colors_uses: HashMap<&image::Rgb<u8>, usize> = HashMap::new();
    for pixel in image.pixels() {
        match colors_uses.entry(pixel) {
            Entry::Vacant(entry) => {
                entry.insert(1);
            }
            Entry::Occupied(mut entry) => {
                *entry.get_mut() += 1;
            }
        }
    }
    let mut colours_uses_vec: Vec<(&Rgb<u8>, usize)> = colors_uses.into_iter().collect();
    colours_uses_vec.sort_by_key(|(_, count)| *count);

    let (start, end) = (
        colours_uses_vec
            .len()
            .checked_sub(symbols.len())
            .unwrap_or(0),
        colours_uses_vec.len(),
    );

    let mut symbol_idx = symbols.len() - 1;
    colours_uses_vec
        .drain(start..end)
        .map(|(pixel, _)| {
            let pixel_with_symbol = PixelWithSymbol::new(*pixel, symbols[symbol_idx]);
            symbol_idx = symbol_idx.checked_sub(1).unwrap_or(0);

            pixel_with_symbol
        })
        .collect()
}

fn get_symbol_by_pixel(
    pixels_with_symbols: &[PixelWithSymbol],
    pixel_to_compare: &Rgb<u8>,
) -> Result<(char, Rgb<u8>), io::Error> {
    let mut char = None;
    let mut rgb_pixel = None;
    let mut comparison = None;

    for PixelWithSymbol { pixel, symbol } in pixels_with_symbols {
        let pretendent_comparison = get_pixel_comparison(pixel_to_compare, pixel);
        if comparison.is_none() || pretendent_comparison < comparison.unwrap() {
            char = Some(*symbol);
            comparison = Some(pretendent_comparison);
            rgb_pixel = Some(*pixel);
        }
    }

    if let (Some(char), Some(rgb_pixel)) = (char, rgb_pixel) {
        return Ok((char, rgb_pixel));
    }

    Err(io::Error::new(
        io::ErrorKind::Other,
        "unexpected error, can't find matching char for a pixel. aborting",
    ))
}

fn get_pixel_comparison(first: &Rgb<u8>, second: &Rgb<u8>) -> usize {
    ((first.0[0] as i16 - second.0[0] as i16).abs()
        + (first.0[1] as i16 - second.0[1] as i16).abs()
        + (first.0[2] as i16 - second.0[2] as i16).abs()) as usize
}

#[cfg(test)]
mod tests {
    use image::{imageops::FilterType, open};

    use crate::symbolize;

    fn get_ferris() -> Vec<&'static str> {
        vec![
            "                                                                        ",
            "                                                                        ",
            "                                                                        ",
            "                              @@  @@@@  @@                              ",
            "                          @@  @@@@@@@@@@@@@@@@@@                        ",
            "                    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                    ",
            "                    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@            @@      ",
            "  @@  @@          @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@      ",
            "  @@  @@@@        @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@  @@@@",
            "@@@@  @@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  @@@@@@@@  ",
            "  @@@@@@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@@@@@  ",
            "    @@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    ",
            "      @@@@  @@@@@@@@@@@@@@@@@@&&    @@@@&&&&  @@@@@@@@@@@@@@  @@        ",
            "        @@@@@@@@@@@@@@@@@@@@@@&&    @@@@      @@@@@@@@@@@@@@@@@@        ",
            "          @@@@@@@@@@@@@@@@@@@@      @@@@      @@@@@@@@@@@@@@@@@@        ",
            "        @@@@@@$$@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@$$$$@@@@@@      ",
            "          @@@@$$  $$$$@@@@@@@@@@@@@@    @@@@@@@@@@$$$$    $$@@@@        ",
            "            @@  $$        $$$$$$$$$$$$$$$$$$$$$$        $$  @@          ",
            "              @@                                            @@          ",
            "                                                          @@            ",
            "                                                                        ",
            "                                                                        ",
            "                                                                        ",
            "                                                                        ",
        ]
    }

    fn get_colorized_ferris() -> &'static str {
        "\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"
    }

    #[test]
    fn renders_ferris_as_vec_of_strings() {
        let image = open("./test-data/ferris.png").unwrap();
        let result: Vec<String> = symbolize(
            image,
            0.03,
            &vec![' ', '@', '$', '&'],
            FilterType::Nearest,
            false,
        )
        .unwrap()
        .into();

        assert_eq!(result, get_ferris());
    }

    #[test]
    fn renders_colorized_ferris_as_string() {
        let image = open("./test-data/ferris.png").unwrap();
        let result: String = symbolize(
            image,
            0.01,
            &vec![' ', '@', '$', '&'],
            FilterType::Nearest,
            true,
        )
        .unwrap()
        .into();

        assert_eq!(result, get_colorized_ferris());
    }

    #[test]
    fn renders_ferris_as_string() {
        let image = open("./test-data/ferris.png").unwrap();
        let result: String = symbolize(
            image,
            0.03,
            &vec![' ', '@', '$', '&'],
            FilterType::Nearest,
            false,
        )
        .unwrap()
        .into();

        assert_eq!(result, get_ferris().join("\n"));
    }

    #[test]
    fn renders_ferris_as_byteslice() {
        let image = open("./test-data/ferris.png").unwrap();
        let result: Vec<u8> = symbolize(
            image,
            0.03,
            &vec![' ', '@', '$', '&'],
            FilterType::Nearest,
            false,
        )
        .unwrap()
        .into();

        assert_eq!(result, get_ferris().join("\n").into_bytes());
    }

    #[test]
    fn returns_error_if_no_palette_passed() {
        let image = open("./test-data/ferris.png").unwrap();
        let result = symbolize(image, 0.03, &vec![], FilterType::Nearest, false);

        assert!(result.is_err());
        assert_eq!(
            result.err().unwrap().to_string(),
            "pallete should contain at leasst one symbol, aborting"
        );
    }

    #[test]
    fn returns_error_if_scale_less_than_zero() {
        let image = open("./test-data/ferris.png").unwrap();
        let result = symbolize(image, -0.03, &vec![' '], FilterType::Nearest, false);

        assert!(result.is_err());
        assert_eq!(
            result.err().unwrap().to_string(),
            "scale should be > 0, aborting"
        );
    }
}