//! 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, /// Height of the rendered macro (if it is to be different from the template). pub height: Option, /// Text captions to render over the template. pub captions: Vec, } /// 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, } 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 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>(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 { let &Color(r, g, b) = self; Rgb{data: [r, g, b]} } #[inline] pub(crate) fn to_rgba(&self, alpha: u8) -> Rgba { let &Color(r, g, b) = self; Rgba{data: [r, g, b, alpha]} } } impl From for Rgb { #[inline] fn from(color: Color) -> Rgb { 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) } }