graphitepdf 0.2.0

A Rust PDF rendering engine for layout, composition, and rendering pipelines.
Documentation
use graphitepdf::{
    Document, Node, NodeKind, PdfMetadata, Style, StyleValue, Stylesheet, StylesheetContainer,
    TextNode,
};
use graphitepdf::{document, style};
use graphitepdf::{layout, render, renderer};

#[test]
fn root_layout_facade_accepts_compat_documents() {
    let container = StylesheetContainer::new(240.0, 180.0);
    let stylesheet = Stylesheet::new(StyleValue::Object(
        [
            ("fontFamily".to_string(), "Inter".into()),
            ("fontStyle".to_string(), "italic".into()),
            ("fontWeight".to_string(), 700.into()),
        ]
        .into_iter()
        .collect(),
    ));
    let document = Document::new()
        .set_metadata(PdfMetadata {
            title: Some(String::from("Compat facade")),
            ..PdfMetadata::default()
        })
        .add_page(Node::new(
            NodeKind::View {
                children: vec![Node::from_stylesheet(
                    NodeKind::Text(TextNode::new("Hello facade")),
                    &container,
                    &stylesheet,
                )],
            },
            Style::default(),
        ));

    let layout = layout::LayoutEngine::new()
        .layout_document(&document)
        .expect("compat document should layout through the root facade");

    assert_eq!(layout.metadata.title.as_deref(), Some("Compat facade"));
    assert_eq!(layout.pages().len(), 1);
    assert_eq!(layout.pages()[0].nodes().len(), 1);
    assert_eq!(
        layout.pages()[0].nodes()[0]
            .font_descriptor()
            .map(|descriptor| descriptor.family()),
        Some("Inter")
    );
}

#[test]
fn root_render_and_renderer_facades_work_with_compat_documents() {
    let document = Document::new()
        .set_metadata(PdfMetadata {
            title: Some(String::from("Render facade")),
            ..PdfMetadata::default()
        })
        .add_page(Node::new(
            NodeKind::View {
                children: vec![Node::new(
                    NodeKind::Text(TextNode::new("Render me")),
                    Style {
                        background_color: Some(graphitepdf::Color::rgb(0xee, 0xf2, 0xff)),
                        ..Style::default()
                    },
                )],
            },
            Style::default(),
        ));

    let layout = layout::LayoutEngine::new()
        .layout_document(&document)
        .expect("compat document should layout through the root facade");
    let rendered = render::RenderEngine::new()
        .build(&layout)
        .expect("split render engine should accept the root layout facade output");
    let bytes =
        renderer::render_to_bytes(&document).expect("renderer facade should accept root documents");

    assert_eq!(rendered.pages.len(), 1);
    assert!(
        rendered.pages[0]
            .commands
            .iter()
            .any(|command| matches!(command, render::RenderCommand::DrawText(_)))
    );
    assert!(bytes.starts_with(b"%PDF-1.7"));
}

#[test]
fn root_compat_document_round_trips_into_split_layout_types() {
    let document = Document::new().add_page(Node::new(
        NodeKind::View {
            children: vec![Node::new(
                NodeKind::Text(TextNode::new("Round trip")),
                Style::default(),
            )],
        },
        Style::default(),
    ));

    let core_document = layout::Document::from(&document);

    assert_eq!(core_document.pages().len(), 1);
    assert_eq!(core_document.pages()[0].nodes().len(), 1);
}

#[test]
fn root_document_module_keeps_legacy_image_height_adapter() {
    let node = document::Node::new(
        document::NodeKind::Image(document::ImageNode::new(
            graphitepdf::RemoteImageSource::new("https://example.com/image.png"),
        )),
        style::Style::default(),
    );

    let layout_node = layout::Node::from(&node);

    assert_eq!(
        layout_node.style().height,
        Some(graphitepdf::Pt::new(120.0))
    );
}

#[test]
fn root_style_module_keeps_compat_style_conversions() {
    let stylesheet = style::Stylesheet::new(style::StyleValue::Object(
        [
            ("fontFamily".to_string(), "Inter".into()),
            ("fontWeight".to_string(), 700.into()),
            ("justifyContent".to_string(), "center".into()),
        ]
        .into_iter()
        .collect(),
    ));
    let compat_style =
        style::Style::from_stylesheet(&style::StylesheetContainer::new(120.0, 80.0), &stylesheet);

    let descriptor = compat_style
        .font_descriptor()
        .expect("compat style should still expose a font descriptor");
    let layout_style = graphitepdf_layout::LayoutStyle::from(&compat_style);
    let round_tripped = style::Style::from(layout_style.clone());

    assert_eq!(descriptor.family(), "Inter");
    assert_eq!(
        descriptor.font_weight(),
        graphitepdf::FontVariantWeight::BOLD
    );
    assert_eq!(compat_style.justify_content, style::JustifyContent::Center);
    assert_eq!(layout_style.font_family.as_deref(), Some("Inter"));
    assert_eq!(round_tripped.font_family.as_deref(), Some("Inter"));
}