oxipdf-ir 0.1.0

Intermediate representation types for the oxipdf PDF engine
Documentation
//! Computed layout results — the output of the layout engine.
//!
//! `ComputedLayout` maps each `NodeId` to its absolute position and
//! dimensions in the continuous (unpaginated) canvas. This data flows
//! from `oxipdf-layout` to `oxipdf-emit`.

use std::collections::BTreeMap;

use crate::color::Color;
use crate::node::NodeId;
use crate::style::visual::BorderStyle;
use crate::units::Pt;

/// Resolved table layout data for a table node.
///
/// Computed during layout, consumed by fragmentation and emission.
#[derive(Debug, Clone)]
pub struct TableLayoutData {
    /// Absolute X position of each column edge (length = num_columns + 1).
    pub column_edges: Vec<Pt>,
    /// Absolute Y position of each row edge (length = num_rows + 1).
    pub row_edges: Vec<Pt>,
    /// For border-collapse: resolved border segments to draw.
    /// Empty when border model is Separate.
    pub collapsed_borders: Vec<CollapsedBorder>,
    /// Indices of header rows (into the flattened row list).
    pub header_row_indices: Vec<usize>,
    /// Total height of the header row group.
    pub header_height: Pt,
}

/// A single collapsed border segment to render.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CollapsedBorder {
    /// Start X.
    pub x1: Pt,
    /// Start Y.
    pub y1: Pt,
    /// End X.
    pub x2: Pt,
    /// End Y.
    pub y2: Pt,
    /// Border width.
    pub width: Pt,
    /// Border color.
    pub color: Color,
    /// Border style.
    pub style: BorderStyle,
}

/// Line-level layout data for a text node whose content was broken into lines.
///
/// Produced by the layout engine when text shaping + line breaking is active.
/// The fragmentation crate uses this to split text across pages at line
/// boundaries instead of treating text as an atomic block.
#[derive(Debug, Clone)]
pub struct TextLines {
    /// Height of each line (typically ascent - descent + line_gap, scaled to font size).
    pub line_height: Pt,
    /// Number of lines in this text node after line breaking.
    pub line_count: u32,
}

/// Pre-measured footnote body data for a footnote node.
///
/// Produced by the layout engine, consumed by the fragmenter and emitter.
#[derive(Debug, Clone)]
pub struct FootnoteLayout {
    /// Height of the footnote body when laid out at the page content width.
    pub body_height: Pt,
    /// The marker text (auto-numbered or custom).
    pub marker: String,
}

/// The complete layout result for a `StyledTree`.
#[derive(Debug, Clone)]
pub struct ComputedLayout {
    /// Computed box for each node, indexed by `NodeId::raw()`.
    boxes: Vec<ComputedBox>,
    /// Line-level data for text nodes that have been broken into lines.
    /// Only populated when the shaping layout path is used.
    text_lines: BTreeMap<NodeId, TextLines>,
    /// Table layout data for table nodes.
    table_data: BTreeMap<NodeId, TableLayoutData>,
    /// Pre-measured footnote body data.
    footnote_data: BTreeMap<NodeId, FootnoteLayout>,
}

impl ComputedLayout {
    /// Create from a pre-built vec of boxes (one per node in arena order).
    #[must_use]
    pub fn from_vec(boxes: Vec<ComputedBox>) -> Self {
        Self {
            boxes,
            text_lines: BTreeMap::new(),
            table_data: BTreeMap::new(),
            footnote_data: BTreeMap::new(),
        }
    }

    /// Create from boxes and text line data.
    #[must_use]
    pub fn with_text_lines(
        boxes: Vec<ComputedBox>,
        text_lines: BTreeMap<NodeId, TextLines>,
    ) -> Self {
        Self {
            boxes,
            text_lines,
            table_data: BTreeMap::new(),
            footnote_data: BTreeMap::new(),
        }
    }

    /// Get the text line data for a node, if it was broken into lines.
    #[must_use]
    pub fn text_lines(&self, id: NodeId) -> Option<&TextLines> {
        self.text_lines.get(&id)
    }

    /// Get the table layout data for a table node.
    #[must_use]
    pub fn table_data(&self, id: NodeId) -> Option<&TableLayoutData> {
        self.table_data.get(&id)
    }

    /// Insert table layout data for a node.
    pub fn insert_table_data(&mut self, id: NodeId, data: TableLayoutData) {
        self.table_data.insert(id, data);
    }

    /// Get the footnote layout data for a node.
    #[must_use]
    pub fn footnote_data(&self, id: NodeId) -> Option<&FootnoteLayout> {
        self.footnote_data.get(&id)
    }

    /// Insert footnote layout data for a node.
    pub fn insert_footnote_data(&mut self, id: NodeId, data: FootnoteLayout) {
        self.footnote_data.insert(id, data);
    }

    /// Overwrite the computed box for a node (used by table layout to
    /// position cell content nodes after the initial taffy pass).
    ///
    /// # Panics
    /// Panics if `id` is out of range.
    pub fn set(&mut self, id: NodeId, cbox: ComputedBox) {
        self.boxes[id.raw() as usize] = cbox;
    }

    /// Mutable access to the underlying boxes slice (used by inline layout
    /// to re-position children in-place).
    pub fn boxes_mut(&mut self) -> &mut [ComputedBox] {
        &mut self.boxes
    }

    /// Get the computed box for a node.
    ///
    /// # Panics
    /// Panics if `id` is out of range.
    #[must_use]
    pub fn get(&self, id: NodeId) -> &ComputedBox {
        &self.boxes[id.raw() as usize]
    }

    /// Total number of boxes.
    #[must_use]
    pub fn len(&self) -> usize {
        self.boxes.len()
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.boxes.is_empty()
    }

    /// Iterate all boxes with their NodeIds.
    pub fn iter(&self) -> impl Iterator<Item = (NodeId, &ComputedBox)> {
        self.boxes
            .iter()
            .enumerate()
            .map(|(i, b)| (NodeId::from_raw(i as u32), b))
    }
}

/// Absolute position and dimensions of a single node on the canvas.
///
/// All coordinates are in PDF points, with origin at top-left of the
/// continuous canvas, Y increasing downward (taffy convention). The
/// renderer converts to PDF's bottom-left / Y-up system during emission.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComputedBox {
    /// Absolute X of the border box's top-left corner.
    pub x: Pt,
    /// Absolute Y of the border box's top-left corner.
    pub y: Pt,
    /// Width of the border box.
    pub width: Pt,
    /// Height of the border box.
    pub height: Pt,
    /// Padding-box insets (content area offset from border box).
    pub padding_top: Pt,
    pub padding_right: Pt,
    pub padding_bottom: Pt,
    pub padding_left: Pt,
    /// Border widths.
    pub border_top: Pt,
    pub border_right: Pt,
    pub border_bottom: Pt,
    pub border_left: Pt,
}

impl ComputedBox {
    /// The content area X (absolute).
    #[must_use]
    pub fn content_x(&self) -> Pt {
        self.x + self.border_left + self.padding_left
    }

    /// The content area Y (absolute).
    #[must_use]
    pub fn content_y(&self) -> Pt {
        self.y + self.border_top + self.padding_top
    }

    /// The content area width.
    #[must_use]
    pub fn content_width(&self) -> Pt {
        self.width - self.border_left - self.border_right - self.padding_left - self.padding_right
    }

    /// The content area height.
    #[must_use]
    pub fn content_height(&self) -> Pt {
        self.height - self.border_top - self.border_bottom - self.padding_top - self.padding_bottom
    }
}

impl Default for ComputedBox {
    fn default() -> Self {
        Self {
            x: Pt::ZERO,
            y: Pt::ZERO,
            width: Pt::ZERO,
            height: Pt::ZERO,
            padding_top: Pt::ZERO,
            padding_right: Pt::ZERO,
            padding_bottom: Pt::ZERO,
            padding_left: Pt::ZERO,
            border_top: Pt::ZERO,
            border_right: Pt::ZERO,
            border_bottom: Pt::ZERO,
            border_left: Pt::ZERO,
        }
    }
}

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

    #[test]
    fn content_area_computation() {
        let b = ComputedBox {
            x: Pt::new(10.0),
            y: Pt::new(20.0),
            width: Pt::new(200.0),
            height: Pt::new(100.0),
            padding_top: Pt::new(5.0),
            padding_right: Pt::new(10.0),
            padding_bottom: Pt::new(5.0),
            padding_left: Pt::new(10.0),
            border_top: Pt::new(1.0),
            border_right: Pt::new(1.0),
            border_bottom: Pt::new(1.0),
            border_left: Pt::new(1.0),
        };
        assert_eq!(b.content_x().get(), 21.0); // 10 + 1 + 10
        assert_eq!(b.content_y().get(), 26.0); // 20 + 1 + 5
        assert_eq!(b.content_width().get(), 178.0); // 200 - 2 - 20
        assert_eq!(b.content_height().get(), 88.0); // 100 - 2 - 10
    }
}