Skip to main content

xberg_native_pdf/elements/
table.rs

1//! Table content element for PDF read/write operations.
2//!
3//! This module provides structures for representing table content that can be
4//! extracted from existing PDFs or generated into new PDFs.
5
6use crate::geometry::Rect;
7
8/// Source of table data - how the table was identified/created.
9///
10/// This tracks the origin of table content to help downstream consumers
11/// understand the reliability and nature of the table data.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum TableSource {
14    /// Table extracted from PDF structure tree (Tagged PDF).
15    ///
16    /// High confidence - explicitly marked in PDF per ISO 32000-1:2008 Section 14.8.4.3.4.
17    /// Includes THead, TBody, TFoot, TR, TH, TD elements.
18    StructureTree,
19    /// Table detected via spatial/heuristic analysis.
20    ///
21    /// Medium confidence - detected through layout analysis, grid detection,
22    /// or other spatial heuristics. May have false positives.
23    #[default]
24    SpatialDetection,
25    /// Table created programmatically by the user/API.
26    ///
27    /// High confidence - explicitly constructed for PDF generation.
28    UserGenerated,
29}
30
31/// Detection metadata for tables.
32///
33/// Provides additional context about how a table was detected,
34/// including confidence scores for heuristic detections.
35#[derive(Debug, Clone, Default)]
36pub struct TableDetectionInfo {
37    /// Source of this table data.
38    pub source: TableSource,
39    /// Confidence score for heuristic detections (0.0 - 1.0).
40    ///
41    /// - StructureTree: always 1.0 (explicit PDF markup)
42    /// - SpatialDetection: varies based on detection quality
43    /// - UserGenerated: always 1.0 (explicit creation)
44    pub confidence: f32,
45    /// Optional detection method name for debugging.
46    pub detection_method: Option<String>,
47}
48
49impl TableDetectionInfo {
50    /// Create info for structure tree source (high confidence).
51    pub fn from_structure_tree() -> Self {
52        Self {
53            source: TableSource::StructureTree,
54            confidence: 1.0,
55            detection_method: Some("structure_tree".to_string()),
56        }
57    }
58
59    /// Create info for spatial detection with confidence.
60    pub fn from_spatial_detection(confidence: f32, method: impl Into<String>) -> Self {
61        Self {
62            source: TableSource::SpatialDetection,
63            confidence: confidence.clamp(0.0, 1.0),
64            detection_method: Some(method.into()),
65        }
66    }
67
68    /// Create info for user-generated table.
69    pub fn user_generated() -> Self {
70        Self {
71            source: TableSource::UserGenerated,
72            confidence: 1.0,
73            detection_method: None,
74        }
75    }
76}
77
78/// Table content element.
79///
80/// Represents a table with rows, columns, and cells that can be
81/// extracted from or written to a PDF.
82#[derive(Debug, Clone)]
83pub struct TableContent {
84    /// Bounding box of the entire table
85    pub bbox: Rect,
86    /// Table rows
87    pub rows: Vec<TableRowContent>,
88    /// Column widths (if known)
89    pub column_widths: Vec<f32>,
90    /// Reading order index
91    pub reading_order: Option<usize>,
92    /// Optional table caption
93    pub caption: Option<String>,
94    /// Table style information
95    pub style: TableContentStyle,
96    /// Detection/source information
97    pub detection_info: TableDetectionInfo,
98}
99
100impl Default for TableContent {
101    fn default() -> Self {
102        Self {
103            bbox: Rect::new(0.0, 0.0, 0.0, 0.0),
104            rows: Vec::new(),
105            column_widths: Vec::new(),
106            reading_order: None,
107            caption: None,
108            style: TableContentStyle::default(),
109            detection_info: TableDetectionInfo::default(),
110        }
111    }
112}
113
114impl TableContent {
115    /// Create a new empty table with a bounding box.
116    pub fn new(bbox: Rect) -> Self {
117        Self {
118            bbox,
119            ..Default::default()
120        }
121    }
122
123    /// Add a row to the table.
124    pub fn add_row(&mut self, row: TableRowContent) {
125        self.rows.push(row);
126    }
127
128    /// Get the number of rows.
129    pub fn row_count(&self) -> usize {
130        self.rows.len()
131    }
132
133    /// Get the number of columns (based on first row).
134    pub fn column_count(&self) -> usize {
135        self.rows.first().map(|r| r.cells.len()).unwrap_or(0)
136    }
137
138    /// Get a cell at the specified row and column.
139    pub fn get_cell(&self, row: usize, col: usize) -> Option<&TableCellContent> {
140        self.rows.get(row).and_then(|r| r.cells.get(col))
141    }
142
143    /// Check if the table has a header row.
144    pub fn has_header(&self) -> bool {
145        self.rows.first().is_some_and(|r| r.is_header)
146    }
147
148    /// Set the detection info for this table.
149    pub fn with_detection_info(mut self, info: TableDetectionInfo) -> Self {
150        self.detection_info = info;
151        self
152    }
153
154    /// Create a user-generated table.
155    pub fn user_generated(bbox: Rect) -> Self {
156        Self {
157            bbox,
158            detection_info: TableDetectionInfo::user_generated(),
159            ..Default::default()
160        }
161    }
162
163    /// Check if table came from structure tree.
164    pub fn is_from_structure_tree(&self) -> bool {
165        self.detection_info.source == TableSource::StructureTree
166    }
167
168    /// Get detection confidence.
169    pub fn detection_confidence(&self) -> f32 {
170        self.detection_info.confidence
171    }
172}
173
174/// A row in a table.
175#[derive(Debug, Clone, Default)]
176pub struct TableRowContent {
177    /// Cells in this row
178    pub cells: Vec<TableCellContent>,
179    /// Whether this is a header row
180    pub is_header: bool,
181    /// Row height (if known)
182    pub height: Option<f32>,
183    /// Background color (R, G, B, each 0.0-1.0)
184    pub background: Option<(f32, f32, f32)>,
185}
186
187impl TableRowContent {
188    /// Create a new row with cells.
189    pub fn new(cells: Vec<TableCellContent>) -> Self {
190        Self {
191            cells,
192            ..Default::default()
193        }
194    }
195
196    /// Create a header row with cells.
197    pub fn header(cells: Vec<TableCellContent>) -> Self {
198        Self {
199            cells,
200            is_header: true,
201            ..Default::default()
202        }
203    }
204
205    /// Add a cell to the row.
206    pub fn add_cell(&mut self, cell: TableCellContent) {
207        self.cells.push(cell);
208    }
209}
210
211/// A cell in a table.
212#[derive(Debug, Clone)]
213pub struct TableCellContent {
214    /// Cell text content
215    pub text: String,
216    /// Bounding box of the cell
217    pub bbox: Rect,
218    /// Number of columns this cell spans
219    pub colspan: usize,
220    /// Number of rows this cell spans
221    pub rowspan: usize,
222    /// Horizontal alignment
223    pub align: TableCellAlign,
224    /// Vertical alignment
225    pub valign: TableCellVAlign,
226    /// Whether this is a header cell
227    pub is_header: bool,
228    /// Background color (R, G, B, each 0.0-1.0)
229    pub background: Option<(f32, f32, f32)>,
230    /// Font size
231    pub font_size: Option<f32>,
232    /// Font name
233    pub font_name: Option<String>,
234    /// Whether text is bold
235    pub bold: bool,
236    /// Whether text is italic
237    pub italic: bool,
238}
239
240impl Default for TableCellContent {
241    fn default() -> Self {
242        Self {
243            text: String::new(),
244            bbox: Rect::new(0.0, 0.0, 0.0, 0.0),
245            colspan: 1,
246            rowspan: 1,
247            align: TableCellAlign::Left,
248            valign: TableCellVAlign::Top,
249            is_header: false,
250            background: None,
251            font_size: None,
252            font_name: None,
253            bold: false,
254            italic: false,
255        }
256    }
257}
258
259impl TableCellContent {
260    /// Create a new cell with text.
261    pub fn new(text: impl Into<String>) -> Self {
262        Self {
263            text: text.into(),
264            ..Default::default()
265        }
266    }
267
268    /// Create a header cell with text.
269    pub fn header(text: impl Into<String>) -> Self {
270        Self {
271            text: text.into(),
272            is_header: true,
273            bold: true,
274            ..Default::default()
275        }
276    }
277
278    /// Set the cell's bounding box.
279    pub fn with_bbox(mut self, bbox: Rect) -> Self {
280        self.bbox = bbox;
281        self
282    }
283
284    /// Set column span.
285    pub fn with_colspan(mut self, colspan: usize) -> Self {
286        self.colspan = colspan;
287        self
288    }
289
290    /// Set row span.
291    pub fn with_rowspan(mut self, rowspan: usize) -> Self {
292        self.rowspan = rowspan;
293        self
294    }
295
296    /// Set horizontal alignment.
297    pub fn with_align(mut self, align: TableCellAlign) -> Self {
298        self.align = align;
299        self
300    }
301
302    /// Set vertical alignment.
303    pub fn with_valign(mut self, valign: TableCellVAlign) -> Self {
304        self.valign = valign;
305        self
306    }
307
308    /// Set background color.
309    pub fn with_background(mut self, r: f32, g: f32, b: f32) -> Self {
310        self.background = Some((r, g, b));
311        self
312    }
313
314    /// Set font size.
315    pub fn with_font_size(mut self, size: f32) -> Self {
316        self.font_size = Some(size);
317        self
318    }
319
320    /// Set bold.
321    pub fn with_bold(mut self, bold: bool) -> Self {
322        self.bold = bold;
323        self
324    }
325
326    /// Set italic.
327    pub fn with_italic(mut self, italic: bool) -> Self {
328        self.italic = italic;
329        self
330    }
331}
332
333/// Horizontal alignment for table cells.
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
335pub enum TableCellAlign {
336    /// Left-aligned (default)
337    #[default]
338    Left,
339    /// Center-aligned
340    Center,
341    /// Right-aligned
342    Right,
343}
344
345/// Vertical alignment for table cells.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
347pub enum TableCellVAlign {
348    /// Top-aligned (default)
349    #[default]
350    Top,
351    /// Middle-aligned
352    Middle,
353    /// Bottom-aligned
354    Bottom,
355}
356
357/// Style information for a table.
358#[derive(Debug, Clone)]
359pub struct TableContentStyle {
360    /// Border width (0.0 for no border)
361    pub border_width: f32,
362    /// Border color (R, G, B, each 0.0-1.0)
363    pub border_color: (f32, f32, f32),
364    /// Cell padding
365    pub cell_padding: f32,
366    /// Whether to show horizontal borders
367    pub horizontal_borders: bool,
368    /// Whether to show vertical borders
369    pub vertical_borders: bool,
370    /// Whether to show outer border
371    pub outer_border: bool,
372    /// Header background color
373    pub header_background: Option<(f32, f32, f32)>,
374    /// Alternating row background color (for striped tables)
375    pub stripe_background: Option<(f32, f32, f32)>,
376}
377
378impl Default for TableContentStyle {
379    fn default() -> Self {
380        Self {
381            border_width: 0.5,
382            border_color: (0.0, 0.0, 0.0),
383            cell_padding: 4.0,
384            horizontal_borders: true,
385            vertical_borders: true,
386            outer_border: true,
387            header_background: None,
388            stripe_background: None,
389        }
390    }
391}
392
393impl TableContentStyle {
394    /// Create a minimal style (horizontal lines only).
395    pub fn minimal() -> Self {
396        Self {
397            vertical_borders: false,
398            outer_border: false,
399            ..Default::default()
400        }
401    }
402
403    /// Create a bordered style (all borders).
404    pub fn bordered() -> Self {
405        Self::default()
406    }
407
408    /// Create a striped style with alternating row colors.
409    pub fn striped() -> Self {
410        Self {
411            stripe_background: Some((0.95, 0.95, 0.95)),
412            ..Default::default()
413        }
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_table_content_creation() {
423        let mut table = TableContent::new(Rect::new(0.0, 0.0, 400.0, 200.0));
424
425        let header = TableRowContent::header(vec![
426            TableCellContent::header("Name"),
427            TableCellContent::header("Value"),
428        ]);
429        table.add_row(header);
430
431        let row = TableRowContent::new(vec![TableCellContent::new("Item 1"), TableCellContent::new("100")]);
432        table.add_row(row);
433
434        assert_eq!(table.row_count(), 2);
435        assert_eq!(table.column_count(), 2);
436        assert!(table.has_header());
437    }
438
439    #[test]
440    fn test_cell_content_builder() {
441        let cell = TableCellContent::new("Test")
442            .with_colspan(2)
443            .with_rowspan(1)
444            .with_align(TableCellAlign::Center)
445            .with_valign(TableCellVAlign::Middle)
446            .with_background(0.9, 0.9, 0.9)
447            .with_font_size(12.0)
448            .with_bold(true);
449
450        assert_eq!(cell.text, "Test");
451        assert_eq!(cell.colspan, 2);
452        assert_eq!(cell.align, TableCellAlign::Center);
453        assert_eq!(cell.valign, TableCellVAlign::Middle);
454        assert!(cell.bold);
455        assert_eq!(cell.font_size, Some(12.0));
456    }
457
458    #[test]
459    fn test_table_get_cell() {
460        let mut table = TableContent::default();
461        table.add_row(TableRowContent::new(vec![
462            TableCellContent::new("A1"),
463            TableCellContent::new("B1"),
464        ]));
465        table.add_row(TableRowContent::new(vec![
466            TableCellContent::new("A2"),
467            TableCellContent::new("B2"),
468        ]));
469
470        assert_eq!(table.get_cell(0, 0).map(|c| c.text.as_str()), Some("A1"));
471        assert_eq!(table.get_cell(1, 1).map(|c| c.text.as_str()), Some("B2"));
472        assert!(table.get_cell(2, 0).is_none());
473    }
474
475    #[test]
476    fn test_table_style_presets() {
477        let minimal = TableContentStyle::minimal();
478        assert!(!minimal.vertical_borders);
479        assert!(!minimal.outer_border);
480
481        let bordered = TableContentStyle::bordered();
482        assert!(bordered.vertical_borders);
483        assert!(bordered.outer_border);
484
485        let striped = TableContentStyle::striped();
486        assert!(striped.stripe_background.is_some());
487    }
488}