maimai 0.1.1

Markup-based meme generator
Documentation
use parley::{GlyphRun, Layout, PositionedLayoutItem};
use skrifa::{
    OutlineGlyph,
    outline::{DrawSettings, OutlinePen},
    prelude::*,
};
use tiny_skia::{
    FillRule, LineCap, LineJoin, Paint, PathBuilder, PixmapMut, Point, Rect, Stroke, Transform,
};

use crate::meme::{Color, TextOutline};

pub fn render_layout(
    layout: Layout<()>,
    pixmap: &mut PixmapMut<'_>,
    transform: Transform,
    fg_color: Color,
    outline: Option<TextOutline>,
) {
    fn render_with(color: Color, pen: &mut TinySkiaPen<'_, '_>, layout: &Layout<()>) {
        for line in layout.lines() {
            for item in line.items() {
                match item {
                    PositionedLayoutItem::GlyphRun(glyph_run) => {
                        render_glyph_run(color, &glyph_run, pen);
                    }
                    PositionedLayoutItem::InlineBox(inline_box) => {
                        pen.set_origin(Point::from_xy(inline_box.x, inline_box.y));
                        pen.set_color(color);
                        pen.fill_rect(inline_box.width, inline_box.height);
                    }
                }
            }
        }
    }

    if let Some(TextOutline { color, width }) = outline {
        render_with(
            color,
            &mut TinySkiaPen::new(PenMode::Stroke(2.0 * width), pixmap, transform),
            &layout,
        );
    }
    render_with(
        fg_color,
        &mut TinySkiaPen::new(PenMode::Fill, pixmap, transform),
        &layout,
    );
}
fn render_glyph_run(color: Color, glyph_run: &GlyphRun<'_, ()>, pen: &mut TinySkiaPen<'_, '_>) {
    // Resolve properties of the GlyphRun
    let mut run_x = glyph_run.offset();
    let run_y = glyph_run.baseline();

    // Get the "Run" from the "GlyphRun"
    let run = glyph_run.run();

    // Resolve properties of the Run>
    let font = run.font();
    let font_size = run.font_size();

    let normalized_coords = run
        .normalized_coords()
        .iter()
        .map(|coord| NormalizedCoord::from_bits(*coord))
        .collect::<Vec<_>>();

    // Get glyph outlines using Skrifa. This can be cached in production code.
    let font_collection_ref = font.data.as_ref();
    let font_ref = FontRef::from_index(font_collection_ref, font.index).unwrap();
    let outlines = font_ref.outline_glyphs();

    // Iterates over the glyphs in the GlyphRun
    for glyph in glyph_run.glyphs() {
        let glyph_x = run_x + glyph.x;
        let glyph_y = run_y - glyph.y;
        run_x += glyph.advance;

        let glyph_id = GlyphId::from(glyph.id);
        if let Some(glyph_outline) = outlines.get(glyph_id) {
            pen.set_origin(Point::from_xy(glyph_x, glyph_y));
            pen.set_color(color);
            pen.draw_glyph(&glyph_outline, font_size, &normalized_coords);
        }
    }

    // Draw decorations: underline & strikethrough
    let style = glyph_run.style();
    let run_metrics = run.metrics();
    if let Some(decoration) = &style.underline {
        let offset = decoration.offset.unwrap_or(run_metrics.underline_offset);
        let size = decoration.size.unwrap_or(run_metrics.underline_size);
        render_decoration(color, pen, glyph_run, offset, size);
    }
    if let Some(decoration) = &style.strikethrough {
        let offset = decoration
            .offset
            .unwrap_or(run_metrics.strikethrough_offset);
        let size = decoration.size.unwrap_or(run_metrics.strikethrough_size);
        render_decoration(color, pen, glyph_run, offset, size);
    }
}

fn render_decoration(
    color: Color,
    pen: &mut TinySkiaPen<'_, '_>,
    glyph_run: &GlyphRun<'_, ()>,
    offset: f32,
    width: f32,
) {
    let y = glyph_run.baseline() - offset;
    let x = glyph_run.offset();
    pen.set_color(color);
    pen.set_origin(Point { x, y });
    pen.fill_rect(glyph_run.advance(), width);
}

struct TinySkiaPen<'p, 'pref>
where
    'p: 'pref,
{
    mode: PenMode,
    pixmap: &'pref mut PixmapMut<'p>,
    origin: Point,
    transform: Transform,
    paint: Paint<'static>,
    open_path: PathBuilder,
}

#[derive(Clone, Copy)]
enum PenMode {
    Stroke(f32),
    Fill,
}

impl<'p, 'pref> TinySkiaPen<'p, 'pref>
where
    'p: 'pref,
{
    fn new(
        mode: PenMode,
        pixmap: &'pref mut PixmapMut<'p>,
        transform: Transform,
    ) -> TinySkiaPen<'p, 'pref> {
        TinySkiaPen {
            mode,
            pixmap,
            origin: Point::default(),
            transform,
            paint: Paint::default(),
            open_path: PathBuilder::new(),
        }
    }

    fn set_origin(&mut self, origin: Point) {
        self.origin = origin
    }

    fn set_color(&mut self, color: Color) {
        self.paint.set_color(color.into());
    }

    fn fill_rect(&mut self, width: f32, height: f32) {
        let rect = Rect::from_xywh(self.origin.x, self.origin.y, width, height).unwrap();
        self.pixmap
            .fill_rect(rect, &self.paint, self.transform, None);
    }

    fn draw_glyph(
        &mut self,
        glyph: &OutlineGlyph<'_>,
        size: f32,
        normalized_coords: &[NormalizedCoord],
    ) {
        let location_ref = LocationRef::new(normalized_coords);
        let settings = DrawSettings::unhinted(Size::new(size), location_ref);
        glyph.draw(settings, self).unwrap();

        let builder = core::mem::replace(&mut self.open_path, PathBuilder::new());
        if let Some(path) = builder.finish() {
            match self.mode {
                PenMode::Stroke(width) => self.pixmap.stroke_path(
                    &path,
                    &self.paint,
                    &Stroke {
                        width,
                        line_cap: LineCap::Round,
                        line_join: LineJoin::Round,
                        ..Default::default()
                    },
                    self.transform,
                    None,
                ),
                PenMode::Fill => self.pixmap.fill_path(
                    &path,
                    &self.paint,
                    FillRule::Winding,
                    self.transform,
                    None,
                ),
            }
        }
    }
}

impl OutlinePen for TinySkiaPen<'_, '_> {
    fn move_to(&mut self, x: f32, y: f32) {
        self.open_path.move_to(self.origin.x + x, self.origin.y - y);
    }

    fn line_to(&mut self, x: f32, y: f32) {
        self.open_path.line_to(self.origin.x + x, self.origin.y - y);
    }

    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
        self.open_path.quad_to(
            self.origin.x + cx0,
            self.origin.y - cy0,
            self.origin.x + x,
            self.origin.y - y,
        );
    }

    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
        self.open_path.cubic_to(
            self.origin.x + cx0,
            self.origin.y - cy0,
            self.origin.x + cx1,
            self.origin.y - cy1,
            self.origin.x + x,
            self.origin.y - y,
        );
    }

    fn close(&mut self) {
        self.open_path.close();
    }
}