deckcraft-render 0.6.1

DeckCraft renderer: slides, layouts and masters to pixels (vello_cpu): fills, outlines, effects, pictures, text, tables, charts
Documentation
//! Preset pattern fills (`a:pattFill`): an 8×8 two-colour tile, each cell one 96-dpi pixel
//! (0.75 pt) of the slide, as PowerPoint draws them.
//!
//! The tiles were read from PowerPoint's own slide export of every preset (black on white, at
//! exactly two output pixels per pattern pixel), so the marks, their density and their phase
//! match it. Bit 7 of each row is the leftmost cell; a set bit is the foreground colour.

use std::sync::Arc;

use deckcraft_color::Rgba;
use kurbo::{Affine, BezPath};
use vello_cpu::{Pixmap, RenderContext, peniko};

/// Side of one pattern cell, in points.
pub const CELL: f64 = 0.75;

const TILES: &[(&str, [u8; 8])] = &[
    ("pct5", [0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00]),
    ("pct10", [0x80, 0x00, 0x08, 0x00, 0x80, 0x00, 0x08, 0x00]),
    ("pct20", [0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00]),
    ("pct25", [0x88, 0x22, 0x88, 0x22, 0x88, 0x22, 0x88, 0x22]),
    ("pct30", [0xAA, 0x44, 0xAA, 0x11, 0xAA, 0x44, 0xAA, 0x11]),
    ("pct40", [0xAA, 0x55, 0xAA, 0x51, 0xAA, 0x55, 0xAA, 0x15]),
    ("pct50", [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55]),
    ("pct60", [0xEE, 0x55, 0xBB, 0x55, 0xEE, 0x55, 0xBB, 0x55]),
    ("pct70", [0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD]),
    ("pct75", [0x77, 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF]),
    ("pct80", [0xEF, 0xFF, 0xFE, 0xFF, 0xEF, 0xFF, 0xFE, 0xFF]),
    ("pct90", [0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0x7F]),
    ("horz", [0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
    ("vert", [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]),
    ("ltHorz", [0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00]),
    ("ltVert", [0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88]),
    ("dkHorz", [0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00]),
    ("dkVert", [0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC]),
    ("narHorz", [0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00]),
    ("narVert", [0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55]),
    ("dashHorz", [0xF0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00]),
    ("dashVert", [0x80, 0x80, 0x80, 0x80, 0x08, 0x08, 0x08, 0x08]),
    ("cross", [0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]),
    ("dnDiag", [0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01]),
    ("upDiag", [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]),
    ("ltDnDiag", [0x88, 0x44, 0x22, 0x11, 0x88, 0x44, 0x22, 0x11]),
    ("ltUpDiag", [0x11, 0x22, 0x44, 0x88, 0x11, 0x22, 0x44, 0x88]),
    ("dkDnDiag", [0xCC, 0x66, 0x33, 0x99, 0xCC, 0x66, 0x33, 0x99]),
    ("dkUpDiag", [0x33, 0x66, 0xCC, 0x99, 0x33, 0x66, 0xCC, 0x99]),
    ("wdDnDiag", [0xC1, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x83]),
    ("wdUpDiag", [0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xC1]),
    (
        "dashDnDiag",
        [0x00, 0x00, 0x88, 0x44, 0x22, 0x11, 0x00, 0x00],
    ),
    (
        "dashUpDiag",
        [0x00, 0x00, 0x11, 0x22, 0x44, 0x88, 0x00, 0x00],
    ),
    (
        "diagCross",
        [0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81],
    ),
    ("smCheck", [0x99, 0x66, 0x66, 0x99, 0x99, 0x66, 0x66, 0x99]),
    ("lgCheck", [0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F]),
    ("smGrid", [0xFF, 0x88, 0x88, 0x88, 0xFF, 0x88, 0x88, 0x88]),
    ("lgGrid", [0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]),
    ("dotGrid", [0xAA, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00]),
    (
        "smConfetti",
        [0x80, 0x08, 0x40, 0x02, 0x10, 0x01, 0x20, 0x04],
    ),
    (
        "lgConfetti",
        [0xB1, 0x30, 0x03, 0x1B, 0xD8, 0xC0, 0x0C, 0x8D],
    ),
    (
        "horzBrick",
        [0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08, 0x08],
    ),
    (
        "diagBrick",
        [0x01, 0x02, 0x04, 0x08, 0x18, 0x24, 0x42, 0x81],
    ),
    (
        "solidDmnd",
        [0x10, 0x38, 0x7C, 0xFE, 0x7C, 0x38, 0x10, 0x00],
    ),
    ("openDmnd", [0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x01]),
    ("dotDmnd", [0x80, 0x00, 0x22, 0x00, 0x08, 0x00, 0x22, 0x00]),
    ("plaid", [0xAA, 0x55, 0xAA, 0x55, 0xF0, 0xF0, 0xF0, 0xF0]),
    ("sphere", [0x77, 0x89, 0x8F, 0x8F, 0x77, 0x98, 0xF8, 0xF8]),
    ("weave", [0x88, 0x54, 0x22, 0x45, 0x88, 0x14, 0x22, 0x51]),
    ("divot", [0x00, 0x10, 0x08, 0x10, 0x00, 0x80, 0x01, 0x80]),
    ("shingle", [0x03, 0x84, 0x48, 0x30, 0x0C, 0x02, 0x01, 0x01]),
    ("wave", [0x00, 0x18, 0x25, 0xC0, 0x00, 0x18, 0x25, 0xC0]),
    ("trellis", [0xFF, 0x66, 0xFF, 0x99, 0xFF, 0x66, 0xFF, 0x99]),
    ("zigZag", [0x81, 0x42, 0x24, 0x18, 0x81, 0x42, 0x24, 0x18]),
];

/// The 8×8 tile of preset `name` (`pct50` for an unknown name).
pub fn tile(name: &str) -> [u8; 8] {
    TILES
        .iter()
        .find(|(n, _)| *n == name)
        .or_else(|| TILES.iter().find(|(n, _)| *n == "pct50"))
        .map(|(_, t)| *t)
        .unwrap_or([0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55])
}

/// Is cell (`x`, `y`) of the tile the foreground?
pub fn is_fg(tile: &[u8; 8], x: usize, y: usize) -> bool {
    tile.get(y % 8)
        .is_some_and(|row| row & (0x80 >> (x % 8)) != 0)
}

fn premul(c: Rgba, alpha: f64) -> vello_cpu::color::PremulRgba8 {
    let a = (c.a as f64 * alpha.clamp(0.0, 1.0))
        .round()
        .clamp(0.0, 255.0);
    let m = |v: u8| (v as f64 * a / 255.0).round().clamp(0.0, 255.0) as u8;
    vello_cpu::color::PremulRgba8 {
        r: m(c.r),
        g: m(c.g),
        b: m(c.b),
        a: a as u8,
    }
}

/// Fill `path` (in the space `m` maps to the output) with preset `name` in `fg` on `bg`, faded by
/// `alpha`.
pub fn fill(
    ctx: &mut RenderContext,
    name: &str,
    fg: Rgba,
    bg: Rgba,
    alpha: f64,
    path: &BezPath,
    m: Affine,
) {
    let det = m.determinant();
    if !det.is_finite() || det.abs() <= 1e-12 {
        return;
    }
    let t = tile(name);
    let (f, b) = (premul(fg, alpha), premul(bg, alpha));
    let data: Vec<vello_cpu::color::PremulRgba8> = (0..64)
        .map(|i| if is_fg(&t, i % 8, i / 8) { f } else { b })
        .collect();
    let pm = Arc::new(Pixmap::from_parts(data, 8, 8));
    ctx.set_transform(m);
    ctx.set_paint(vello_cpu::Image {
        image: vello_cpu::ImageSource::Pixmap(pm),
        sampler: peniko::ImageSampler {
            x_extend: peniko::Extend::Repeat,
            y_extend: peniko::Extend::Repeat,
            quality: peniko::ImageQuality::Low,
            alpha: 1.0,
        },
    });
    // Cells are laid out on the output from its origin (the slide's top-left corner), so
    // neighbouring shapes' patterns line up, as in PowerPoint.
    let k = crate::scale_of(m);
    let k = if k.is_finite() && k > 0.0 { k } else { 1.0 };
    ctx.set_paint_transform(m.inverse() * Affine::scale(CELL * k));
    ctx.fill_path(path);
    ctx.reset_paint_transform();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_drawingml_preset_has_a_tile() {
        // ECMA-376 ST_PresetPatternVal.
        let all = [
            "pct5",
            "pct10",
            "pct20",
            "pct25",
            "pct30",
            "pct40",
            "pct50",
            "pct60",
            "pct70",
            "pct75",
            "pct80",
            "pct90",
            "horz",
            "vert",
            "ltHorz",
            "ltVert",
            "dkHorz",
            "dkVert",
            "narHorz",
            "narVert",
            "dashHorz",
            "dashVert",
            "cross",
            "dnDiag",
            "upDiag",
            "ltDnDiag",
            "ltUpDiag",
            "dkDnDiag",
            "dkUpDiag",
            "wdDnDiag",
            "wdUpDiag",
            "dashDnDiag",
            "dashUpDiag",
            "diagCross",
            "smCheck",
            "lgCheck",
            "smGrid",
            "lgGrid",
            "dotGrid",
            "smConfetti",
            "lgConfetti",
            "horzBrick",
            "diagBrick",
            "solidDmnd",
            "openDmnd",
            "dotDmnd",
            "plaid",
            "sphere",
            "weave",
            "divot",
            "shingle",
            "wave",
            "trellis",
            "zigZag",
        ];
        assert_eq!(all.len(), 54);
        for n in all {
            assert!(TILES.iter().any(|(t, _)| *t == n), "{n}");
        }
        assert_eq!(TILES.len(), 54);
    }

    #[test]
    fn percent_presets_get_denser() {
        // The pctN tiles set more cells as N grows (PowerPoint's own: pct5 sets 2 of 64, pct50
        // half, pct90 all but 2).
        let share = |n: &str| {
            let t = tile(n);
            (0..64).filter(|i| is_fg(&t, i % 8, i / 8)).count()
        };
        let names = [
            "pct5", "pct10", "pct20", "pct25", "pct30", "pct40", "pct50", "pct60", "pct70",
            "pct75", "pct80", "pct90",
        ];
        assert!(
            names.windows(2).all(|w| share(w[0]) <= share(w[1])),
            "{:?}",
            names.map(share)
        );
        assert_eq!((share("pct5"), share("pct50"), share("pct90")), (2, 32, 62));
        assert_eq!(tile("nonsense"), tile("pct50"));
    }
}