maimai 0.1.1

Markup-based meme generator
Documentation
use std::borrow::Cow;

use tracing::debug;

use crate::{
    IncompleteMemeDefinition,
    meme::{
        Color, Meme, MemeBase, TextBox, TextOutline,
        partial::{
            PartialCanvas, PartialMeme, PartialMemeBase, PartialText, PartialTextBox,
            PartialTextOutline, PartialTextOutlineParameters,
        },
    },
};

impl Meme {
    pub fn from_partial(meme: PartialMeme) -> crate::Result<Self> {
        debug!(?meme);

        macro_rules! field_name {
            ( $static_:literal ) => {
                Cow::Borrowed($static_)
            };
            ( $dynamic:ident ) => {
                Cow::Owned($dynamic.clone())
            };
        }
        macro_rules! field_or_error {
            ( $field:ident, $errors:expr, [ $( $path_item:tt ),* $(,)? ]) => {
                field_or_error!(
                    $field,
                    fallback: Default::default(),
                    $errors,
                    [ $( $path_item, )* ]
                )
            };
            ( $field:ident, fallback: $fallback:expr, $errors:expr, [ $( $path_item:tt ),* $(,)? ]) => {
                match $field {
                    Some(field) => field,
                    None => {
                        $errors.push(IncompleteMemeDefinition::MissingField(vec![
                            $( field_name!( $path_item ) ),* ,
                            Cow::Borrowed(stringify!( $field ))
                        ]));


                        $fallback
                    }
                }
            };
        }

        let mut errors = Vec::new();

        let base = match meme.base {
            PartialMemeBase::Image(path) => MemeBase::Image(path.into()),
            PartialMemeBase::Canvas(PartialCanvas { color, size }) => MemeBase::Canvas {
                color: field_or_error!(
                    color,
                    fallback: Color::WHITE,
                    &mut errors,
                    ["canvas"]
                ),
                size: field_or_error!(size, &mut errors, ["canvas"]),
            },
            PartialMemeBase::Extends(_) => {
                unreachable!("dependencies should all be resolved")
            }
        };

        let text = meme
            .text
            .into_iter()
            .flat_map(|(name, text)| {
                let PartialText::TextBox(PartialTextBox {
                    text,
                    position,
                    size,
                    rotate,
                    font,
                    caps,
                    color,
                    outline,
                    font_size,
                    line_height,
                    halign,
                    valign,
                }) = text
                else {
                    errors.push(IncompleteMemeDefinition::MissingParameters(vec![
                        Cow::Borrowed("text"),
                        Cow::Owned(name),
                    ]));
                    return None;
                };

                let text = field_or_error!(text, &mut errors, ["text", name]);
                let position = field_or_error!(position, &mut errors, ["text", name]);
                let size = field_or_error!(size, &mut errors, ["text", name]);
                let rotate = rotate.and_then(|angle| (angle != 0.0).then_some(angle));
                let font = font.unwrap_or_else(|| "sans-serif".to_owned());
                let caps = caps.unwrap_or(false);
                let color = field_or_error!(
                    color,
                    fallback: Color::WHITE,
                    &mut errors,
                    ["text", name]
                );
                let outline = match outline {
                    Some(outline) => match outline {
                        PartialTextOutline::Enabled(_) => {
                            errors.push(IncompleteMemeDefinition::MissingParameters(vec![
                                Cow::Borrowed("text"),
                                Cow::Owned(name.clone()),
                                Cow::Borrowed("outline"),
                            ]));
                            None
                        }
                        PartialTextOutline::Parameters(PartialTextOutlineParameters {
                            enabled,
                            color,
                            width,
                        }) => enabled.then(|| TextOutline {
                            color: field_or_error!(
                                color,
                                fallback: Color::WHITE,
                                &mut errors,
                                ["text", name, "outline"]
                            ),
                            width: width.unwrap_or(TextOutline::DEFAULT_WIDTH),
                        }),
                    },
                    None => None,
                };
                let font_size = font_size.unwrap_or(
                    outline
                        .map(|outline| {
                            if outline.width < 1.0 {
                                size.1 - (2.0 * outline.width * size.1)
                            } else {
                                size.1 - outline.width
                            }
                        })
                        .unwrap_or(size.1),
                );
                let line_height = line_height.unwrap_or(1.0);
                let halign = halign.unwrap_or_default();
                let valign = valign.unwrap_or_default();

                Some(TextBox {
                    text,
                    position,
                    size,
                    rotate,
                    font,
                    caps,
                    color,
                    outline,
                    font_size,
                    line_height,
                    halign,
                    valign,
                })
            })
            .collect();

        if errors.is_empty() {
            Ok(Self { base, text })
        } else {
            Err(crate::Error::new(crate::ErrorKind::Incomplete(errors)))
        }
    }
}