artem 1.1.5

Convert images from multiple formats (jpg, png, webp, etc…) to ASCII art
Documentation
use std::num::NonZeroU32;

use crate::util::{self, ResizingDimension};

/// Target for the Ascii conversion.
///
/// This changes of exactly the image is converted and if it supports color.
/// The first boolean determines if the output should be colored, the second if the color is the background color.
///
/// An target might support none, one or both colors.
///
/// # Examples
///```
/// use artem::options::TargetType;
///
/// assert_eq!(TargetType::Shell(true, false), TargetType::default());
///
///```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetType {
    /// Shell target, Supports color and background colors.
    Shell(bool, bool),
    /// Special Ansi/ans file that will always have colors enabled. Can also have background colors.
    AnsiFile(bool),
    /// Shell target, Supports color and background colors.
    HtmlFile(bool, bool),
    /// Every other file, does not support either colored outputs.
    File,
}

impl Default for TargetType {
    /// Default [`TargetType`]
    ///
    /// The default [`TargetType`] is the shell with colors enabled.
    /// Background colors are disabled by default.
    ///
    /// # Examples
    /// ```
    /// use artem::options::TargetType;
    ///
    /// assert_eq!(TargetType::Shell(true, false), TargetType::default());
    /// ```
    fn default() -> TargetType {
        TargetType::Shell(true, false)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default() {
        assert_eq!(TargetType::Shell(true, false), TargetType::default());
    }
}

///Configuration for the conversion of the image to the ascii image.
#[derive(Debug, PartialEq)]
pub struct Option {
    pub characters: String,
    pub scale: f32,
    pub target_size: u32,
    pub invert: bool,
    pub border: bool,
    pub dimension: ResizingDimension,
    pub transform_x: bool,
    pub transform_y: bool,
    pub center_x: bool,
    pub center_y: bool,
    pub outline: bool,
    pub hysteresis: bool,
    pub target: TargetType,
}

impl Option {
    /// Create [`OptionBuilder`] with default properties.
    ///
    /// # Examples
    /// ```
    /// use artem::options::Option;
    ///
    /// let default = Option::builder();
    /// ```
    pub fn builder() -> OptionBuilder {
        OptionBuilder::default()
    }
}

#[cfg(test)]
mod test_option {
    use super::*;
    #[test]
    fn builder_default() {
        assert_eq!(
            OptionBuilder {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            Option::builder()
        );
    }
}

///A builder to create a [`Option`] struct.
#[derive(PartialEq, Debug)]
pub struct OptionBuilder {
    characters: String,
    scale: f32,
    target_size: u32,
    invert: bool,
    border: bool,
    dimension: ResizingDimension,
    transform_x: bool,
    transform_y: bool,
    center_x: bool,
    center_y: bool,
    outline: bool,
    hysteresis: bool,
    target: TargetType,
}

impl Default for OptionBuilder {
    fn default() -> Self {
        Self {
            //these have to be set to custom defaults for the program to work
            characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
            scale: 0.42f32,
            target_size: 80,
            invert: Default::default(),
            border: Default::default(),
            dimension: Default::default(),
            transform_x: Default::default(),
            transform_y: Default::default(),
            center_x: Default::default(),
            center_y: Default::default(),
            outline: Default::default(),
            hysteresis: Default::default(),
            target: Default::default(),
        }
    }
}

/// Generate a builder property.
///
/// This macro generates a function, which sets the specified property of the Builder.
/// It does NOT check the value passed in, so for example it would be possible to pass in an empty string, which could lead to
/// errors later this. This should be done before setting the value.
///
/// # Examples
///
/// ``` compile_fail, just an internal example code
/// property!{
/// ///Example doc
/// ///This generates a name setter function.
/// => name, String
/// }
/// ```
/// ## Generated function
/// ``` compile_fail, just an internal example code
/// ///Example doc
/// ///This generates a name setter function.
/// pub fn name<'a>(&'a mut self, name: String) -> &'a Self {
///     self.name = name;
///     self
/// }
/// ```
macro_rules! property {
    ($(#[$attr:meta])* => $field:ident, $field_type:ty) => {
        $(#[$attr])*
        pub fn $field<'a>(&'a mut self, $field: $field_type) -> &'a mut Self {
            self.$field = $field;
            self
        }
    };
    ($(#[$attr:meta])* => $field:ident, $field_type:ty, $func:ident) => {
        $(#[$attr])*
        pub fn $field<'a>(&'a mut self, $field: $field_type) -> &'a mut Self {
            self.$field = $field.$func();
            self
        }
    };
}

// impl Default  {

// }

impl OptionBuilder {
    ///Create a new OptionBuilder.
    ///
    /// If an option is not specified, the rust default value will be used, unless specified otherwise.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// ```
    pub fn new() -> OptionBuilder {
        Option::builder()
    }

    ///Set the characters.
    ///
    /// The characters will determine how 'visible'/light/dark a character will be perceived.
    ///
    /// # Errors
    /// When the given characters are empty, the characters will not be changed.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.characters("Mkl. ".to_string());
    /// ```
    #[allow(clippy::needless_lifetimes)] //disable this, as the life is needed for the builder
    pub fn characters<'a>(&'a mut self, characters: String) -> &'a Self {
        if !characters.is_empty() {
            self.characters = characters;
        }
        self
    }

    property! {
    /// Set the scale.
    ///
    /// Used to change the ratio between width and height of an character.
    /// Since a char is a bit higher than wide, the scale should compensate for that.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.scale(0.42f32);
    /// ```
       => scale, f32
    }
    // pub fn scale(&'a mut self, scale: f32) -> &'a mut Self {
    //     self.scale = scale;
    //     self
    // }

    property! {
    /// Set the target_size.
    ///
    /// This is the number of characters that the resulting ascii image will be heigh/wide.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    /// use core::num::NonZeroU32;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.target_size(NonZeroU32::new(80).unwrap());
    /// ```
    => target_size, NonZeroU32, get
    }

    property! {
    ///Invert to density map/characters.
    ///
    /// This inverts the mapping from light to dark characters. It can be useful when
    /// the image has a dark background. It defaults to false.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.invert(true);
    /// ```
    => invert, bool
    // pub fn invert(mut self, invert: bool) -> Self {
    //     self.invert = invert;
    //     self
    }

    property! {
    ///Enable a border surrounding the image.
    ///
    /// The border will take reduced the space of the ascii image, since it will still
    /// use the same target size.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.border(true);
    /// ```
    => border, bool
    }

    property! {
    /// Set which dimension should be scaled first.
    ///
    /// See [`ResizingDimension`] for more information.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    /// use artem::util::ResizingDimension;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.dimension(ResizingDimension::Height);
    /// ```
    => dimension, util::ResizingDimension
    }

    property! {
    ///Flip the image along the X-axis
    ///
    /// This will flip the image horizontally by reversing reading of the horizontal part of the image.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.transform_x(true);
    /// ```
    => transform_x, bool
    }

    property! {
    ///Flip the image along the Y-axis
    ///
    /// This will flip the image vertically by reversing reading of the vertical part of the image.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.transform_y(true);
    /// ```
    => transform_y, bool
    }

    property! {
    /// Center the image horizontally in the terminal
    ///
    /// It will center the image by adding spaces in front of the text.
    /// Since the terminal might have an uneven width, and only monospace chars are allowed,
    /// the spacing might not always be accurate.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.center_x(true);
    /// ```
    => center_x, bool
    }

    property! {
    /// Center the image vertically in the terminal
    ///
    /// It will center the image by adding new lines  above and below the text.
    /// Since the terminal might have an uneven height, and only monospace chars are allowed,
    /// the spacing might not always be accurate.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.center_y(true);
    /// ```
    => center_y, bool
    }

    property! {
    ///Convert the image to it's outline
    ///
    /// This will use gaussian blur and sobel operators to only it's outline,
    /// it might not produce the best result on all images.
    /// Caution, this will take some additional time.
    /// Defaults to false.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.outline(true);
    /// ```
    => outline, bool
    }

    property! {
    /// When converting the image to an outline, also use double threshold and hysteresis
    ///
    /// It will remove small imperfection in the outline for the cost of smaller/thinner lines, which can make
    /// the resulting ascii looking less visible, which might not be desired.
    ///
    /// It will only be used when outlining is set to true.
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// builder.hysteresis(true);
    /// ```
    => hysteresis, bool
    }

    property! {
    ///Set the target type
    ///
    /// This will effect the output, for example if the [`TargetType`] is set to html,
    /// the output will be the content of a Html-file.
    ///
    /// See [`TargetType`] for more information. It defaults to the shell as the target.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    /// use artem::options::TargetType;
    ///
    /// let mut builder = OptionBuilder::new();
    /// //use color, but don't color the background
    /// builder.target(TargetType::HtmlFile(true, false));
    /// ```
        => target,  TargetType
    }

    ///Build the [`Option`] struct.
    ///
    /// This returns a [`Option`], which can than be used for the image conversion using [`convert()`].
    /// If values are not explicitly specified, the default values will be used.
    ///
    /// # Examples
    /// ```
    /// use artem::options::OptionBuilder;
    ///
    /// let mut builder = OptionBuilder::new();
    /// let options = builder.build();
    /// ```
    pub fn build(&self) -> Option {
        Option {
            characters: self.characters.to_owned(),
            scale: self.scale,
            target_size: self.target_size,
            invert: self.invert,
            border: self.border,
            dimension: self.dimension,
            transform_x: self.transform_x,
            transform_y: self.transform_y,
            center_x: self.center_x,
            center_y: self.center_y,
            outline: self.outline,
            hysteresis: self.hysteresis,
            target: self.target,
        }
    }
}

#[cfg(test)]
mod test_conversion_option_builder {
    use super::*;
    #[test]
    fn build_default() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().build()
        );
    }

    #[test]
    fn change_characters() {
        assert_eq!(
            Option {
                characters: r#"characters"#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new()
                .characters("characters".to_string())
                .build()
        );
    }

    #[test]
    fn change_scale() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 3.14f32, //change attribute
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().scale(3.14f32).build()
        );
    }

    #[test]
    fn change_target_size() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 314, //change attribute
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new()
                .target_size(NonZeroU32::new(314).unwrap())
                .build()
        );
    }

    #[test]
    fn change_invert() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: true, //change attribute
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().invert(true).build()
        );
    }

    #[test]
    fn change_border() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: true, //change attribute
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().border(true).build()
        );
    }

    #[test]
    fn change_dimension() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Height, //change attribute
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new()
                .dimension(util::ResizingDimension::Height)
                .build()
        );
    }

    #[test]
    fn change_transform_x() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: true, //change attribute
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().transform_x(true).build()
        );
    }

    #[test]
    fn change_transform_y() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: true, //change attribute
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().transform_y(true).build()
        );
    }

    #[test]
    fn change_center_x() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: true, //change attribute
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().center_x(true).build()
        );
    }

    #[test]
    fn change_center_y() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: true, //change attribute
                outline: false,
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().center_y(true).build()
        );
    }

    #[test]
    fn change_outline() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: true, //change attribute
                hysteresis: false,
                target: TargetType::default(),
            },
            OptionBuilder::new().outline(true).build()
        );
    }

    #[test]
    fn change_hysteresis() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: true, //change attribute
                target: TargetType::default(),
            },
            OptionBuilder::new().hysteresis(true).build()
        );
    }

    #[test]
    fn change_file_type() {
        assert_eq!(
            Option {
                characters: r#"MWNXK0Okxdolc:;,'...   "#.to_string(),
                scale: 0.42f32,
                target_size: 80,
                invert: false,
                border: false,
                dimension: util::ResizingDimension::Width,
                transform_x: false,
                transform_y: false,
                center_x: false,
                center_y: false,
                outline: false,
                hysteresis: false,
                target: TargetType::AnsiFile(false), //change attribute
            },
            OptionBuilder::new()
                .target(TargetType::AnsiFile(false))
                .build()
        );
    }
}