maimai 0.1.1

Markup-based meme generator
Documentation
mod finalize;
pub mod partial;

use std::str;

use camino::Utf8PathBuf;

pub use self::partial::PartialMeme;

/// A finalized meme definition.
///
/// This can be created by merging multiple
/// [`PartialMeme`](partial::PartialMeme)s.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Meme {
    /// The base layer of the meme.
    pub base: MemeBase,
    /// The text that will be placed on top of the base.
    pub text: Vec<TextBox>,
}

/// The base layer of a meme.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum MemeBase {
    /// Draw the meme onto a blank canvas.
    #[non_exhaustive]
    Canvas {
        /// Background color of the canvas.
        color: Color,
        /// Dimensions of the canvas as `(width, height)`.
        size: (u32, u32),
    },
    /// Draw the meme onto an image.
    Image(Utf8PathBuf),
}

/// A text box that is placed on top of the base layer.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct TextBox {
    /// Text content.
    pub text: String,

    /// Top-left position of the text box as `(x, y)`
    /// (before rotation is applied).
    pub position: (f32, f32),
    /// Dimensions of the text box as `(width, height)`.
    pub size: (f32, f32),
    /// Clockwise rotation around the center of the text box in degrees.
    pub rotate: Option<f32>,

    /// Font family name. Defaults to `sans-serif`.
    pub font: String,
    /// Write the entire text content in capital letters.
    pub caps: bool,
    /// Text color.
    pub color: Color,
    /// Draw a colored outline around the text glyphs.
    pub outline: Option<TextOutline>,
    /// Maximum font size.
    ///
    /// If the text doesn't fit inside the text box, the font size is
    /// appropriately reduced.
    pub font_size: f32,
    /// Line height relative to the font size.
    pub line_height: f32,

    /// How to align the text horizontally inside the text box.
    pub halign: HAlign,
    /// How to align the text vertically inside the text box.
    pub valign: VAlign,
}

/// A colored outline that is drawn around the text glyphs.
#[non_exhaustive]
#[derive(Copy, Clone, Debug)]
pub struct TextOutline {
    /// Color of the outline as a CSS color string.
    pub color: Color,
    /// Width of the outline.
    ///
    /// Values >=1.0 are interpreted an absolute size in Pixels, values <1.0 are
    /// interpreted relative to the font size.
    pub width: f32,
}

impl TextOutline {
    pub(crate) const DEFAULT_WIDTH: f32 = 0.075;
}

impl TextOutline {
    pub(crate) fn width_for_font_size(&self, font_size: f32) -> f32 {
        if self.width < 1.0 {
            self.width * font_size
        } else {
            self.width
        }
    }
}

/// A RGBA color where all values must be in the `0.0..=1.0` range
#[derive(Clone, Copy, Debug)]
pub struct Color([f32; 4]);

#[allow(dead_code)]
impl Color {
    const WHITE: Self = Color([1.0, 1.0, 1.0, 1.0]);
    const BLACK: Self = Color([0.0, 0.0, 0.0, 1.0]);
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Color {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        csscolorparser::Color::deserialize(deserializer).map(|color| Self(color.to_array()))
    }
}

#[cfg(feature = "render")]
impl From<Color> for tiny_skia::Color {
    fn from(Color([r, g, b, a]): Color) -> Self {
        tiny_skia::Color::from_rgba(r, g, b, a)
            .expect("`Color` should only be able to be instanciated with valid values")
    }
}

/// Vertical alignment.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum VAlign {
    Top,
    #[default]
    Center,
    Bottom,
}

/// Horizontal alignment.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum HAlign {
    Left,
    #[default]
    Center,
    Right,
}

#[cfg(feature = "render")]
impl From<HAlign> for parley::Alignment {
    fn from(align: HAlign) -> Self {
        match align {
            HAlign::Left => parley::Alignment::Left,
            HAlign::Center => parley::Alignment::Middle,
            HAlign::Right => parley::Alignment::Right,
        }
    }
}