bland 0.2.1

Pure-Rust library for paper-ready, monochrome, hatch-patterned technical plots in the visual tradition of 1960s-80s engineering reports.
Documentation
//! SVG → pixmap rasterization for embedding bland figures in external GUI
//! frameworks (egui, slint, tauri, ...).
//!
//! Available only when the `raster` cargo feature is enabled.

use std::sync::OnceLock;

use resvg::usvg;
use tiny_skia::{Color, Pixmap, Transform};

use crate::Figure;

// System fonts are scanned once and reused across calls. fontdb scanning
// is the dominant cost on first render; without caching, a UI updating at
// 5 Hz would re-scan the system font directory 5 times per second.
static FONTDB: OnceLock<usvg::fontdb::Database> = OnceLock::new();

fn shared_fontdb() -> &'static usvg::fontdb::Database {
    FONTDB.get_or_init(|| {
        let mut db = usvg::fontdb::Database::new();
        db.load_system_fonts();
        db
    })
}

pub(crate) fn to_pixmap(fig: &Figure, width: u32, height: u32) -> Vec<u8> {
    assert!(width > 0 && height > 0, "to_pixmap requires non-zero dimensions");

    let svg = fig.to_svg();

    let mut opt = usvg::Options::default();
    // Borrow the cached fontdb. Cloning Arc<…> here is cheap if usvg
    // exposes that path; otherwise the fontdb is shared by reference.
    opt.fontdb = std::sync::Arc::new(shared_fontdb().clone());

    let tree = usvg::Tree::from_str(&svg, &opt)
        .expect("bland generates well-formed SVG; tree parse should be infallible");

    let svg_size = tree.size();
    let scale_x = width as f32 / svg_size.width();
    let scale_y = height as f32 / svg_size.height();
    let scale = scale_x.min(scale_y);

    let mut pixmap = Pixmap::new(width, height).expect("non-zero dimensions checked above");
    pixmap.fill(Color::WHITE);

    resvg::render(&tree, Transform::from_scale(scale, scale), &mut pixmap.as_mut());

    pixmap.data().to_vec()
}

#[cfg(test)]
mod tests {
    use crate::Figure;

    #[test]
    fn to_pixmap_returns_correct_size() {
        let fig = Figure::new()
            .title("test")
            .line(&[0.0, 1.0, 2.0], &[0.0, 1.0, 4.0], |s| s);
        let buf = fig.to_pixmap(200, 150);
        assert_eq!(buf.len(), 200 * 150 * 4);
    }

    #[test]
    fn to_pixmap_fills_some_non_white_pixels() {
        let fig = Figure::new().line(&[0.0, 1.0], &[0.0, 1.0], |s| s);
        let buf = fig.to_pixmap(200, 150);
        let any_drawn = buf
            .chunks_exact(4)
            .any(|px| !(px[0] == 255 && px[1] == 255 && px[2] == 255));
        assert!(any_drawn, "rasterized figure should contain non-white pixels");
    }
}