lopdf_table/
table.rs

1//! Core table structures
2
3use crate::Result;
4use crate::style::{CellStyle, RowStyle, TableStyle};
5use tracing::trace;
6
7/// Column width specification
8#[derive(Debug, Clone)]
9pub enum ColumnWidth {
10    /// Fixed width in points
11    Pixels(f32),
12    /// Percentage of available table width
13    Percentage(f32),
14    /// Automatically calculate based on content
15    Auto,
16}
17
18/// Represents a table with rows and styling
19#[derive(Debug, Clone)]
20pub struct Table {
21    pub rows: Vec<Row>,
22    pub style: TableStyle,
23    /// Column width specifications
24    pub column_widths: Option<Vec<ColumnWidth>>,
25    /// Total table width (if None, auto-calculate based on content)
26    pub total_width: Option<f32>,
27    /// Number of header rows to repeat on each page when paginating
28    pub header_rows: usize,
29}
30
31impl Table {
32    /// Create a new empty table
33    pub fn new() -> Self {
34        Self {
35            rows: Vec::new(),
36            style: TableStyle::default(),
37            column_widths: None,
38            total_width: None,
39            header_rows: 0,
40        }
41    }
42
43    /// Add a row to the table
44    pub fn add_row(mut self, row: Row) -> Self {
45        trace!("Adding row with {} cells", row.cells.len());
46        self.rows.push(row);
47        self
48    }
49
50    /// Set the table style
51    pub fn with_style(mut self, style: TableStyle) -> Self {
52        self.style = style;
53        self
54    }
55
56    /// Set column width specifications
57    pub fn with_column_widths(mut self, widths: Vec<ColumnWidth>) -> Self {
58        self.column_widths = Some(widths);
59        self
60    }
61
62    /// Set total table width
63    pub fn with_total_width(mut self, width: f32) -> Self {
64        self.total_width = Some(width);
65        self
66    }
67
68    /// Convenience method to set pixel widths for all columns
69    pub fn with_pixel_widths(mut self, widths: Vec<f32>) -> Self {
70        self.column_widths = Some(widths.into_iter().map(ColumnWidth::Pixels).collect());
71        self
72    }
73
74    /// Set border width for the entire table
75    pub fn with_border(mut self, width: f32) -> Self {
76        self.style.border_width = width;
77        self
78    }
79
80    /// Set the number of header rows to repeat on each page
81    pub fn with_header_rows(mut self, count: usize) -> Self {
82        self.header_rows = count;
83        self
84    }
85
86    /// Get the number of columns (based on the first row, accounting for colspan)
87    pub fn column_count(&self) -> usize {
88        self.rows
89            .first()
90            .map(|r| r.cells.iter().map(|c| c.colspan.max(1)).sum())
91            .unwrap_or(0)
92    }
93
94    /// Validate table structure
95    pub fn validate(&self) -> Result<()> {
96        if self.rows.is_empty() {
97            return Err(crate::error::TableError::InvalidTable(
98                "Table has no rows".to_string(),
99            ));
100        }
101
102        let expected_cols = self.column_count();
103        for (i, row) in self.rows.iter().enumerate() {
104            // Calculate the total column coverage including colspan
105            let mut total_coverage = 0;
106            for cell in &row.cells {
107                total_coverage += cell.colspan.max(1);
108            }
109
110            if total_coverage != expected_cols {
111                return Err(crate::error::TableError::InvalidTable(format!(
112                    "Row {} covers {} columns (with colspan), expected {}",
113                    i, total_coverage, expected_cols
114                )));
115            }
116        }
117
118        if let Some(ref widths) = self.column_widths {
119            if widths.len() != expected_cols {
120                return Err(crate::error::TableError::InvalidTable(format!(
121                    "Column widths array has {} elements, but table has {} columns",
122                    widths.len(),
123                    expected_cols
124                )));
125            }
126
127            // Check that percentage widths don't exceed 100%
128            let total_percentage: f32 = widths
129                .iter()
130                .filter_map(|w| match w {
131                    ColumnWidth::Percentage(p) => Some(*p),
132                    _ => None,
133                })
134                .sum();
135
136            if total_percentage > 100.0 {
137                return Err(crate::error::TableError::InvalidTable(format!(
138                    "Total percentage widths ({:.1}%) exceed 100%",
139                    total_percentage
140                )));
141            }
142        }
143
144        Ok(())
145    }
146}
147
148impl Default for Table {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154/// Represents a row in a table
155#[derive(Debug, Clone)]
156pub struct Row {
157    pub cells: Vec<Cell>,
158    pub style: Option<RowStyle>,
159    /// Explicit height (if None, auto-calculate)
160    pub height: Option<f32>,
161}
162
163impl Row {
164    /// Create a new row with cells
165    pub fn new(cells: Vec<Cell>) -> Self {
166        Self {
167            cells,
168            style: None,
169            height: None,
170        }
171    }
172
173    /// Set row style
174    pub fn with_style(mut self, style: RowStyle) -> Self {
175        self.style = Some(style);
176        self
177    }
178
179    /// Set explicit row height
180    pub fn with_height(mut self, height: f32) -> Self {
181        self.height = Some(height);
182        self
183    }
184}
185
186/// Represents a cell in a table
187#[derive(Debug, Clone)]
188pub struct Cell {
189    pub content: String,
190    pub style: Option<CellStyle>,
191    pub colspan: usize,
192    pub rowspan: usize,
193    /// Enable text wrapping for this cell
194    pub text_wrap: bool,
195}
196
197impl Cell {
198    /// Create a new cell with text content
199    pub fn new<S: Into<String>>(content: S) -> Self {
200        Self {
201            content: content.into(),
202            style: None,
203            colspan: 1,
204            rowspan: 1,
205            text_wrap: false,
206        }
207    }
208
209    /// Create an empty cell
210    pub fn empty() -> Self {
211        Self::new("")
212    }
213
214    /// Enable text wrapping for this cell
215    pub fn with_wrap(mut self, wrap: bool) -> Self {
216        self.text_wrap = wrap;
217        self
218    }
219
220    /// Set cell style
221    pub fn with_style(mut self, style: CellStyle) -> Self {
222        self.style = Some(style);
223        self
224    }
225
226    /// Set colspan
227    pub fn with_colspan(mut self, span: usize) -> Self {
228        self.colspan = span.max(1);
229        self
230    }
231
232    /// Set rowspan
233    pub fn with_rowspan(mut self, span: usize) -> Self {
234        self.rowspan = span.max(1);
235        self
236    }
237
238    /// Make text bold
239    pub fn bold(mut self) -> Self {
240        let mut style = self.style.unwrap_or_default();
241        style.bold = true;
242        self.style = Some(style);
243        self
244    }
245
246    /// Make text italic
247    pub fn italic(mut self) -> Self {
248        let mut style = self.style.unwrap_or_default();
249        style.italic = true;
250        self.style = Some(style);
251        self
252    }
253
254    /// Set font size
255    pub fn with_font_size(mut self, size: f32) -> Self {
256        let mut style = self.style.unwrap_or_default();
257        style.font_size = Some(size);
258        self.style = Some(style);
259        self
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_table_validation() {
269        let mut table = Table::new();
270        assert!(table.validate().is_err());
271
272        table = table.add_row(Row::new(vec![Cell::new("A"), Cell::new("B")]));
273        assert!(table.validate().is_ok());
274
275        table = table.add_row(Row::new(vec![Cell::new("C")]));
276        assert!(table.validate().is_err());
277    }
278
279    #[test]
280    fn test_cell_builder() {
281        let cell = Cell::new("Test")
282            .bold()
283            .italic()
284            .with_font_size(14.0)
285            .with_colspan(2);
286
287        assert_eq!(cell.content, "Test");
288        assert_eq!(cell.colspan, 2);
289        let style = cell.style.unwrap();
290        assert!(style.bold);
291        assert!(style.italic);
292        assert_eq!(style.font_size, Some(14.0));
293    }
294}