pdfox 0.1.0

A pure-Rust PDF library — create, parse, and render PDF documents with zero C dependencies
Documentation
/// PDF Table builder — renders a grid of cells with borders, backgrounds,
/// and text content into a ContentStream.

use crate::color::Color;
use crate::content::{ContentStream, TextAlign};
use crate::font::BuiltinFont;

#[derive(Debug, Clone)]
pub struct TableCell {
    pub text: String,
    pub font: BuiltinFont,
    pub font_size: f64,
    pub text_color: Color,
    pub background: Option<Color>,
    pub align: TextAlign,
    pub padding: f64,
    pub bold: bool,
    /// How many columns this cell spans
    pub col_span: usize,
}

impl Default for TableCell {
    fn default() -> Self {
        Self {
            text: String::new(),
            font: BuiltinFont::Helvetica,
            font_size: 10.0,
            text_color: Color::BLACK,
            background: None,
            align: TextAlign::Left,
            padding: 4.0,
            bold: false,
            col_span: 1,
        }
    }
}

impl TableCell {
    pub fn new(text: impl Into<String>) -> Self {
        Self { text: text.into(), ..Default::default() }
    }

    pub fn bold(mut self) -> Self {
        self.bold = true;
        self.font = BuiltinFont::HelveticaBold;
        self
    }

    pub fn font_size(mut self, size: f64) -> Self {
        self.font_size = size;
        self
    }

    pub fn color(mut self, color: Color) -> Self {
        self.text_color = color;
        self
    }

    pub fn background(mut self, color: Color) -> Self {
        self.background = Some(color);
        self
    }

    pub fn align(mut self, align: TextAlign) -> Self {
        self.align = align;
        self
    }

    pub fn col_span(mut self, span: usize) -> Self {
        self.col_span = span;
        self
    }
}

/// A row in the table
#[derive(Debug, Clone)]
pub struct TableRow {
    pub cells: Vec<TableCell>,
    pub height: Option<f64>,
    pub background: Option<Color>,
    pub is_header: bool,
}

impl TableRow {
    pub fn new(cells: Vec<TableCell>) -> Self {
        Self { cells, height: None, background: None, is_header: false }
    }

    pub fn header(cells: Vec<TableCell>) -> Self {
        let cells: Vec<TableCell> = cells.into_iter().map(|c| c.bold()).collect();
        Self { cells, height: None, background: Some(Color::rgb_u8(60, 90, 160)), is_header: true }
    }

    pub fn height(mut self, h: f64) -> Self {
        self.height = Some(h);
        self
    }

    pub fn background(mut self, color: Color) -> Self {
        self.background = Some(color);
        self
    }
}

/// Table configuration
pub struct TableStyle {
    pub border_color: Color,
    pub border_width: f64,
    pub header_text_color: Color,
    pub row_alt_color: Option<Color>,
    pub default_row_height: f64,
}

impl Default for TableStyle {
    fn default() -> Self {
        Self {
            border_color: Color::rgb_u8(180, 180, 180),
            border_width: 0.5,
            header_text_color: Color::WHITE,
            row_alt_color: Some(Color::rgb_u8(245, 247, 250)),
            default_row_height: 20.0,
        }
    }
}

/// The table builder
pub struct Table {
    pub rows: Vec<TableRow>,
    /// Column widths in points
    pub col_widths: Vec<f64>,
    pub style: TableStyle,
    /// Resource name prefix for fonts (e.g. "F")
    pub font_prefix: String,
}

impl Table {
    pub fn new(col_widths: Vec<f64>) -> Self {
        Self {
            rows: Vec::new(),
            col_widths,
            style: TableStyle::default(),
            font_prefix: "F".into(),
        }
    }

    pub fn style(mut self, style: TableStyle) -> Self {
        self.style = style;
        self
    }

    pub fn add_row(&mut self, row: TableRow) -> &mut Self {
        self.rows.push(row);
        self
    }

    /// Total table width
    pub fn total_width(&self) -> f64 {
        self.col_widths.iter().sum()
    }

    /// Calculate total table height based on row heights
    pub fn total_height(&self) -> f64 {
        self.rows.iter().map(|r| r.height.unwrap_or(self.style.default_row_height)).sum()
    }

    /// Render the table into a ContentStream.
    /// `x`, `y` is the top-left corner of the table (PDF y increases upward).
    /// Returns the content stream operations and the y coordinate below the table.
    pub fn render(&self, cs: &mut ContentStream, x: f64, y_top: f64, font_prefix: &str) -> f64 {
        let total_w = self.total_width();
        let mut cur_y = y_top;

        for (row_idx, row) in self.rows.iter().enumerate() {
            let row_h = row.height.unwrap_or(self.style.default_row_height);
            let row_y = cur_y - row_h; // bottom of this row

            // Row background
            let bg = if row.is_header {
                row.background.or(Some(Color::rgb_u8(60, 90, 160)))
            } else if let Some(bg) = row.background {
                Some(bg)
            } else if row_idx % 2 == 1 {
                self.style.row_alt_color
            } else {
                None
            };

            if let Some(bg_color) = bg {
                cs.filled_rect(x, row_y, total_w, row_h, bg_color);
            }

            // Cells
            let mut cell_x = x;
            let mut col_idx = 0;

            for cell in &row.cells {
                let span = cell.col_span.max(1);
                let cell_w: f64 = self.col_widths[col_idx..col_idx + span.min(self.col_widths.len() - col_idx)]
                    .iter()
                    .sum();

                let text_color = if row.is_header {
                    self.style.header_text_color
                } else {
                    cell.text_color
                };

                let font_key = if cell.bold || row.is_header {
                    format!("{}Bold", font_prefix)
                } else {
                    format!("{}Reg", font_prefix)
                };

                let font = if cell.bold || row.is_header {
                    BuiltinFont::HelveticaBold
                } else {
                    cell.font
                };

                // Text: vertically centered in cell
                let text_y = row_y + (row_h - cell.font_size) / 2.0 + 1.0;

                let (text_x, align) = match cell.align {
                    TextAlign::Left => (cell_x + cell.padding, TextAlign::Left),
                    TextAlign::Center => (cell_x + cell_w / 2.0, TextAlign::Center),
                    TextAlign::Right => (cell_x + cell_w - cell.padding, TextAlign::Right),
                };

                cs.draw_text(&cell.text, text_x, text_y, &font_key, font, cell.font_size, text_color, align);

                // Cell border (right edge only, except last column)
                if col_idx + span < self.col_widths.len() {
                    cs.line(
                        cell_x + cell_w, row_y,
                        cell_x + cell_w, cur_y,
                        self.style.border_color,
                        self.style.border_width,
                    );
                }

                cell_x += cell_w;
                col_idx += span;
            }

            // Horizontal border (bottom of row)
            cs.line(x, row_y, x + total_w, row_y, self.style.border_color, self.style.border_width);

            cur_y = row_y;
        }

        // Outer border
        cs.stroked_rect(x, cur_y, total_w, y_top - cur_y, self.style.border_color, self.style.border_width);

        cur_y
    }
}