ironpress 1.4.4

Pure Rust HTML/CSS/Markdown to PDF converter with layout engine, LaTeX math, tables, images, custom fonts, and streaming output. No browser, no system dependencies.
Documentation
use super::*;
use crate::layout::cells::{CellBox, CellContent, TableCell};
use crate::layout::elements::{
    BoxFragmentation, BoxModel, BoxPaint, FlexContent, LayoutSize, OverflowBehavior,
    TableBoxDecoration, TableCells, TextBlock,
};
use crate::layout::engine::{FlexItemBlockSize, LayoutBorderSide};
use crate::types::Color;

fn container(children: Vec<LayoutNode>, height: Option<f32>) -> LayoutNode {
    Container {
        children,
        box_model: BoxModel {
            size: LayoutSize::fixed(40.0, height),
            padding: EdgeSizes::uniform(2.0),
            ..Default::default()
        },
        paint: BoxPaint {
            background: crate::layout::elements::BackgroundPaint {
                color: Some(Color::from_srgb(0.1, 0.2, 0.3, 1.0)),
                ..Default::default()
            },
            ..Default::default()
        },
        fragmentation: BoxFragmentation {
            decoration: BoxDecorationBreak::Slice,
            ..Default::default()
        },
        overflow: OverflowBehavior {
            combined: Overflow::Visible,
            x: Overflow::Visible,
            y: Overflow::Visible,
        },
        ..Default::default()
    }
    .boxed()
}

fn row(nested: Vec<LayoutNode>) -> LayoutNode {
    FlexRow {
        content: FlexContent {
            cells: vec![FlexCell {
                width: 40.0,
                natural_height: 40.0,
                fragmentation: FlexItemFragmentation::definite(),
                nested_elements: nested,
                ..Default::default()
            }],
            row_height: 40.0,
            alignment: AlignItems::FlexStart,
            ..Default::default()
        },
        box_model: BoxModel {
            size: LayoutSize::fixed(40.0, None),
            ..Default::default()
        },
        paint: BoxPaint {
            background: crate::layout::elements::BackgroundPaint {
                color: Some(Color::from_srgb(0.9, 0.9, 0.9, 1.0)),
                ..Default::default()
            },
            ..Default::default()
        },
        ..Default::default()
    }
    .boxed()
}

#[derive(Default)]
struct FragmentSnapshot {
    flex_cell_count: usize,
    flex_row_height: f32,
    flex_has_background: bool,
    flex_cell_has_background: bool,
    flex_cell_fragment_extent: Option<f32>,
    flex_cell_border_top: f32,
    flex_cell_border_bottom: f32,
    container_child_count: usize,
    container_height: Option<f32>,
    container_has_background: bool,
    container_reference_slice: Option<(f32, f32)>,
}

impl LayoutVisitor for FragmentSnapshot {
    fn visit_flex_row(&mut self, element: &FlexRow) {
        self.flex_cell_count = element.content.cells.len();
        self.flex_row_height = element.content.row_height;
        self.flex_has_background = element.paint.background.color.is_some();
        if let Some(cell) = element.content.cells.first() {
            self.flex_cell_has_background = cell.paint.background.color.is_some();
            self.flex_cell_fragment_extent = cell.fragmentation.fragment_block_extent;
            self.flex_cell_border_top = cell.border.top.width;
            self.flex_cell_border_bottom = cell.border.bottom.width;
        }
        if let Some(container) = element
            .content
            .cells
            .first()
            .and_then(|cell| cell.nested_elements.first())
        {
            container.accept(self);
        }
    }

    fn visit_container(&mut self, element: &Container) {
        self.container_child_count = element.children.len();
        self.container_height = element.box_model.size.height.used();
        self.container_has_background = element.paint.background.color.is_some();
        self.container_reference_slice = element
            .fragmentation
            .reference_slice
            .map(|slice| (slice.block_offset(), slice.composite_block_size()));
    }
}

fn fragment_snapshot(element: &dyn LayoutElement) -> FragmentSnapshot {
    let mut snapshot = FragmentSnapshot::default();
    element.accept(&mut snapshot);
    snapshot
}

#[test]
fn block_edge_break_bubbles_without_an_empty_box_fragment() {
    let element = container(
        vec![
            PageBreak {
                side: PageBreakSide::Any,
                page_name: None,
            }
            .boxed(),
            container(Vec::new(), None),
        ],
        Some(40.0),
    );
    let (before, after, _) = split_flow_at_descendant_break(element.as_ref(), 100.0).unwrap();

    assert!(before.is_none());
    let after = fragment_snapshot(
        after
            .as_deref()
            .expect("the principal box follows its propagated break"),
    );
    assert_eq!(after.container_child_count, 1);
    assert_eq!(after.container_height, Some(40.0));
    assert!(after.container_has_background);
}

#[test]
fn internal_block_break_slices_the_ancestor_box() {
    let element = container(
        vec![
            container(Vec::new(), None),
            PageBreak {
                side: PageBreakSide::Any,
                page_name: None,
            }
            .boxed(),
            container(Vec::new(), None),
        ],
        Some(40.0),
    );
    let (before, after, _) = split_flow_at_descendant_break(element.as_ref(), 100.0).unwrap();

    let before = fragment_snapshot(
        before
            .as_deref()
            .expect("content before the break forms a fragment"),
    );
    assert_eq!(before.container_child_count, 1);
    assert_eq!(before.container_height, Some(40.0));

    let after = after
        .as_deref()
        .expect("content after the break forms an overflow fragment");
    assert_eq!(
        after.page_content_role(),
        crate::layout::elements::PageContentRole::OverflowContinuation
    );
    let after = fragment_snapshot(after);
    assert_eq!(after.container_child_count, 1);
    assert_eq!(after.container_height, Some(0.0));
    assert!(!after.container_has_background);
}

#[test]
fn auto_container_fragments_share_one_composite_decoration_box() {
    let element = container(
        vec![
            container(Vec::new(), None),
            PageBreak {
                side: PageBreakSide::Any,
                page_name: None,
            }
            .boxed(),
            container(Vec::new(), None),
        ],
        None,
    );
    let (before, after, _) = split_flow_at_descendant_break(element.as_ref(), 100.0).unwrap();

    let before = fragment_snapshot(before.as_deref().expect("open principal fragment"));
    let after = fragment_snapshot(after.as_deref().expect("continuation fragment"));
    assert_eq!(before.container_height, Some(100.0));
    assert!(before.container_has_background);
    assert!(after.container_has_background);
    let (first_offset, first_composite) = before
        .container_reference_slice
        .expect("first reference slice");
    let (continuation_offset, continuation_composite) = after
        .container_reference_slice
        .expect("continued reference slice");
    assert_eq!(first_offset, 0.0);
    assert_eq!(continuation_offset, 100.0);
    assert_eq!(first_composite, continuation_composite);
    assert!(first_composite > continuation_offset);
}

#[test]
fn table_row_break_slices_its_wrapper_decoration() {
    let border_side = LayoutBorderSide {
        width: 1.0,
        ..Default::default()
    };
    let decoration = TableBoxDecoration::new(TextBlock {
        box_model: BoxModel {
            size: LayoutSize::fixed(40.0, Some(50.0)),
            margins: BlockMargins::new(10.0, -52.0),
            border: crate::layout::engine::LayoutBorder::uniform(border_side),
            ..Default::default()
        },
        paint: BoxPaint {
            background: crate::layout::elements::BackgroundPaint {
                color: Some(Color::from_srgb(0.1, 0.2, 0.3, 1.0)),
                ..Default::default()
            },
            ..Default::default()
        },
        fragmentation: crate::layout::elements::TextFragmentation {
            box_fragmentation: BoxFragmentation {
                decoration: BoxDecorationBreak::Slice,
                ..Default::default()
            },
            ..Default::default()
        },
        ..Default::default()
    })
    .boxed();
    let table = TableRow {
        content: TableCells {
            cells: vec![TableCell {
                layout: CellBox {
                    content: CellContent {
                        children: vec![
                            container(Vec::new(), Some(10.0)),
                            PageBreak {
                                side: PageBreakSide::Any,
                                page_name: None,
                            }
                            .boxed(),
                            container(Vec::new(), Some(20.0)),
                        ],
                        ..Default::default()
                    },
                    ..Default::default()
                },
                ..Default::default()
            }],
            column_widths: vec![40.0],
        },
        ..Default::default()
    }
    .boxed();

    let split = split_sequence(&[decoration, table], FragmentainerSpace::new(100.0))
        .expect("forced break inside the table cell");
    let first = split.before[0]
        .table_box_decoration_owner()
        .expect("open table decoration")
        .decoration();
    let continuation = split.after[0]
        .table_box_decoration_owner()
        .expect("continued table decoration")
        .decoration();

    assert_eq!(first.box_model.border.top.width, 1.0);
    assert_eq!(first.box_model.border.bottom.width, 0.0);
    assert_eq!(first.box_model.size.height.used(), Some(89.0));
    assert_eq!(first.box_model.margins.end, -90.0);
    assert_eq!(continuation.box_model.border.top.width, 0.0);
    assert_eq!(continuation.box_model.border.bottom.width, 1.0);
    assert_eq!(continuation.box_model.size.height.used(), Some(19.0));
    assert_eq!(continuation.box_model.margins.start, 0.0);
    assert_eq!(continuation.box_model.margins.end, -20.0);
}

#[test]
fn fixed_container_descendant_break_becomes_overflow_fragment() {
    let nested = container(
        vec![
            container(Vec::new(), None),
            PageBreak {
                side: PageBreakSide::Any,
                page_name: None,
            }
            .boxed(),
            container(Vec::new(), None),
        ],
        Some(40.0),
    );
    let (before, after, _) = split_flow_at_descendant_break(&row(vec![nested]), 100.0).unwrap();
    let before = before.expect("parallel flow retains a leading fragment");
    let after = after.expect("parallel flow retains a continuation fragment");

    let before = fragment_snapshot(before.as_ref());
    assert_eq!(before.flex_cell_count, 1);
    assert_eq!(before.container_child_count, 1);

    let after = fragment_snapshot(after.as_ref());
    assert_eq!(after.flex_row_height, 0.0);
    assert!(!after.flex_has_background);
    assert_eq!(after.container_child_count, 1);
    assert_eq!(after.container_height, Some(0.0));
    assert!(!after.container_has_background);
}

#[test]
fn minimum_sized_flex_item_keeps_principal_box_fragments() {
    let nested = vec![
        container(Vec::new(), None),
        PageBreak {
            side: PageBreakSide::Any,
            page_name: None,
        }
        .boxed(),
        container(Vec::new(), None),
    ];
    let mut element = row(nested);
    struct MakeFragmentable;
    impl crate::layout::elements::LayoutVisitorMut for MakeFragmentable {
        fn visit_flex_row(&mut self, element: &mut FlexRow) {
            let cell = &mut element.content.cells[0];
            cell.fragmentation.block_size = FlexItemBlockSize::Minimum;
            cell.paint.background.color = Some(Color::from_srgb(0.2, 0.3, 0.4, 1.0));
            cell.border = crate::types::PhysicalEdges::uniform(LayoutBorderSide {
                width: 1.0,
                ..Default::default()
            });
        }
    }
    element.accept_mut(&mut MakeFragmentable);

    let (before, after, _) = split_flow_at_descendant_break(element.as_ref(), 100.0).unwrap();
    let before = before.expect("parallel flow retains a leading fragment");
    let after = after.expect("parallel flow retains a continuation fragment");

    let before = fragment_snapshot(before.as_ref());
    assert!(before.flex_cell_has_background);
    assert_eq!(before.flex_cell_fragment_extent, Some(100.0));
    assert_eq!(before.flex_cell_border_top, 1.0);
    assert_eq!(before.flex_cell_border_bottom, 0.0);

    let after = fragment_snapshot(after.as_ref());
    assert!(after.flex_cell_has_background);
    assert_eq!(after.flex_cell_fragment_extent, None);
    assert_eq!(after.flex_cell_border_top, 0.0);
    assert_eq!(after.flex_cell_border_bottom, 1.0);
}