1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! Module defining the model types.

#![allow(missing_docs)]  // Because of IterVariants used by HAlign & VAlign.

use std::fmt;

use image::{Rgb, Rgba};

use super::constants::{DEFAULT_COLOR, DEFAULT_OUTLINE_COLOR, DEFAULT_HALIGN, DEFAULT_FONT};


/// Describes an image macro. Used as an input structure.
///
/// *Note*: If `width` or `height` is provided, the result will be resized
/// whilst preserving the original aspect ratio of the template.
/// This means the final size of the image may be smaller than requested.
#[derive(Default)]
pub struct ImageMacro {
    /// Name of the template used by this image macro.
    pub template: String,
    /// Width of the rendered macro (if it is to be different from the template).
    pub width: Option<u32>,
    /// Height of the rendered macro (if it is to be different from the template).
    pub height: Option<u32>,
    /// Text captions to render over the template.
    pub captions: Vec<Caption>,
}

/// Describes a single piece of text rendered on the image macro.
///
/// Use the provided `Caption::text_at` method to create it
/// with most of the fields set to default values.
#[derive(Clone, PartialEq)]
pub struct Caption {
    /// Text to render.
    ///
    /// Newline characters (`"\n"`) cause the text to wrap.
    pub text: String,
    /// Horizontal alignment of the caption within the template rectangle.
    /// Default is `HAlign::Center`.
    pub halign: HAlign,
    /// Vertical alignment of the caption within the template rectangle.
    pub valign: VAlign,
    /// Name of the font to render the caption with. Defaults to `"Impact".
    pub font: String,  // TODO: this could be a Cow, but needs lifetime param
    /// Text color, defaults to white.
    pub color: Color,
    /// Text of the color outline, if any. Defaults to black.
    /// Pass `None` to draw the text without outline.
    pub outline: Option<Color>,
}

macro_attr! {
    /// Horizontal alignment of text within a rectangle.
    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash,
             Deserialize, IterVariants!(HAligns))]
    #[serde(rename_all = "lowercase")]
    pub enum HAlign {
        /// Left alignment.
        Left,
        /// Horizontal centering.
        Center,
        /// Right alignment.
        Right,
    }
}

macro_attr! {
    /// Vertical alignment of text within a rectangle.
    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash,
             Deserialize, IterVariants!(VAligns))]
    #[serde(rename_all = "lowercase")]
    pub enum VAlign {
        /// Top alignment.
        Top,
        /// Vertical centering.
        Middle,
        /// Bottom alignment.
        Bottom,
    }
}

/// RGB color of the text.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Color(pub u8, pub u8, pub u8);


impl ImageMacro {
    /// Whether the image macro includes any text.
    #[inline]
    pub fn has_text(&self) -> bool {
        self.captions.len() > 0 && self.captions.iter().any(|c| !c.text.is_empty())
    }
}
impl PartialEq<ImageMacro> for ImageMacro {
    /// Check equality with another ImageMacro.
    /// This is implemented not to take the order of Captions into account.
    fn eq(&self, other: &Self) -> bool {
        self.template == other.template &&
        self.width == other.width &&
        self.height == other.height &&
        // O(n^2), I know.
        self.captions.iter().all(|c1| other.captions.iter().any(|c2| c1 == c2))
        // TODO: consider implementing captions as HashSet for this reason
    }
}
impl fmt::Debug for ImageMacro {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        let mut ds = fmt.debug_struct("ImageMacro");
        ds.field("template", &self.template);

        macro_rules! fmt_opt_field {
            ($name:ident) => (
                if let Some(ref $name) = self.$name {
                    ds.field(stringify!($name), $name);
                }
            );
        }
        fmt_opt_field!(width);
        fmt_opt_field!(height);

        if self.captions.len() > 0 {
            ds.field("captions", &self.captions);
        }

        ds.finish()
    }
}

impl Caption {
    /// Create an empty Caption at the particular vertical alignment.
    #[inline]
    pub fn at(valign: VAlign) -> Self {
        Self::text_at(valign, "")
    }

    /// Create a Caption with a text at the particular vertical alignment.
    #[inline]
    pub fn text_at<S: Into<String>>(valign: VAlign, s: S) -> Self {
        Caption{
            text: s.into(),
            halign: DEFAULT_HALIGN,
            valign: valign,
            font: DEFAULT_FONT.into(),
            color: DEFAULT_COLOR,
            outline: Some(DEFAULT_OUTLINE_COLOR),
        }
    }
}
impl fmt::Debug for Caption {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        write!(fmt, "{valign:?}{halign:?}{{{font:?} {outline}[{color}]}}({text:?})",
            text = self.text,
            halign = self.halign,
            valign = self.valign,
            font = self.font,
            color = self.color,
            outline = self.outline.map(|o| format!("{}", o)).unwrap_or_else(String::new))
    }
}

impl Color {
    /// Create a white color.
    #[inline]
    pub fn white() -> Self {
        Self::gray(0xff)
    }

    /// Create a black color.
    #[inline]
    pub fn black() -> Self {
        Self::gray(0xff)
    }

    /// Create a gray color of given intensity.
    #[inline]
    pub fn gray(value: u8) -> Self {
        Color(value, value, value)
    }
}
impl Color {
    /// Convert the color to its chromatic inverse.
    #[inline]
    pub fn invert(self) -> Self {
        let Color(r, g, b) = self;
        Color(0xff - r, 0xff - g, 0xff - b)
    }

    #[inline]
    pub(crate) fn to_rgb(&self) -> Rgb<u8> {
        let &Color(r, g, b) = self;
        Rgb{data: [r, g, b]}
    }

    #[inline]
    pub(crate) fn to_rgba(&self, alpha: u8) -> Rgba<u8> {
        let &Color(r, g, b) = self;
        Rgba{data: [r, g, b, alpha]}
    }
}
impl From<Color> for Rgb<u8> {
    #[inline]
    fn from(color: Color) -> Rgb<u8> {
        color.to_rgb()
    }
}
impl fmt::Display for Color {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        let &Color(r, g, b) = self;
        write!(fmt, "#{:0>2x}{:0>2x}{:0>2x}", r, g, b)
    }
}