oxidize_pdf/text/
table.rs

1//! Simple table rendering for PDF documents
2//!
3//! This module provides basic table functionality without CSS styling,
4//! suitable for structured data presentation in PDF documents.
5
6use crate::error::PdfError;
7use crate::graphics::{Color, GraphicsContext};
8use crate::text::{measure_text, Font, TextAlign};
9
10/// Represents a simple table in a PDF document
11#[derive(Debug, Clone)]
12pub struct Table {
13    /// Table rows
14    rows: Vec<TableRow>,
15    /// Column widths (in points)
16    column_widths: Vec<f64>,
17    /// Table position (x, y)
18    position: (f64, f64),
19    /// Table options
20    options: TableOptions,
21}
22
23/// Options for table rendering
24#[derive(Debug, Clone)]
25pub struct TableOptions {
26    /// Border width in points
27    pub border_width: f64,
28    /// Border color
29    pub border_color: Color,
30    /// Cell padding in points
31    pub cell_padding: f64,
32    /// Row height in points (0 for auto)
33    pub row_height: f64,
34    /// Font for table text
35    pub font: Font,
36    /// Font size in points
37    pub font_size: f64,
38    /// Text color
39    pub text_color: Color,
40    /// Header row styling
41    pub header_style: Option<HeaderStyle>,
42}
43
44/// Header row styling options
45#[derive(Debug, Clone)]
46pub struct HeaderStyle {
47    /// Background color for header cells
48    pub background_color: Color,
49    /// Text color for header cells
50    pub text_color: Color,
51    /// Font for header text
52    pub font: Font,
53    /// Make header text bold
54    pub bold: bool,
55}
56
57/// Represents a row in the table
58#[derive(Debug, Clone)]
59pub struct TableRow {
60    /// Cells in this row
61    cells: Vec<TableCell>,
62    /// Whether this is a header row
63    is_header: bool,
64}
65
66/// Represents a cell in the table
67#[derive(Debug, Clone)]
68pub struct TableCell {
69    /// Cell content
70    content: String,
71    /// Text alignment
72    align: TextAlign,
73    /// Column span (default 1)
74    colspan: usize,
75}
76
77impl Default for TableOptions {
78    fn default() -> Self {
79        Self {
80            border_width: 1.0,
81            border_color: Color::black(),
82            cell_padding: 5.0,
83            row_height: 0.0, // Auto
84            font: Font::Helvetica,
85            font_size: 10.0,
86            text_color: Color::black(),
87            header_style: None,
88        }
89    }
90}
91
92impl Table {
93    /// Create a new table with specified column widths
94    pub fn new(column_widths: Vec<f64>) -> Self {
95        Self {
96            rows: Vec::new(),
97            column_widths,
98            position: (0.0, 0.0),
99            options: TableOptions::default(),
100        }
101    }
102
103    /// Create a table with equal column widths
104    pub fn with_equal_columns(num_columns: usize, total_width: f64) -> Self {
105        let column_width = total_width / num_columns as f64;
106        let column_widths = vec![column_width; num_columns];
107        Self::new(column_widths)
108    }
109
110    /// Set table position
111    pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
112        self.position = (x, y);
113        self
114    }
115
116    /// Set table options
117    pub fn set_options(&mut self, options: TableOptions) -> &mut Self {
118        self.options = options;
119        self
120    }
121
122    /// Add a header row
123    pub fn add_header_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
124        if cells.len() != self.column_widths.len() {
125            return Err(PdfError::InvalidStructure(
126                "Header cells count doesn't match column count".to_string(),
127            ));
128        }
129
130        let row_cells: Vec<TableCell> = cells
131            .into_iter()
132            .map(|content| TableCell {
133                content,
134                align: TextAlign::Center,
135                colspan: 1,
136            })
137            .collect();
138
139        self.rows.push(TableRow {
140            cells: row_cells,
141            is_header: true,
142        });
143
144        Ok(self)
145    }
146
147    /// Add a data row
148    pub fn add_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
149        self.add_row_with_alignment(cells, TextAlign::Left)
150    }
151
152    /// Add a data row with specific alignment
153    pub fn add_row_with_alignment(
154        &mut self,
155        cells: Vec<String>,
156        align: TextAlign,
157    ) -> Result<&mut Self, PdfError> {
158        if cells.len() != self.column_widths.len() {
159            return Err(PdfError::InvalidStructure(
160                "Row cells count doesn't match column count".to_string(),
161            ));
162        }
163
164        let row_cells: Vec<TableCell> = cells
165            .into_iter()
166            .map(|content| TableCell {
167                content,
168                align,
169                colspan: 1,
170            })
171            .collect();
172
173        self.rows.push(TableRow {
174            cells: row_cells,
175            is_header: false,
176        });
177
178        Ok(self)
179    }
180
181    /// Add a row with custom cells (allows colspan)
182    pub fn add_custom_row(&mut self, cells: Vec<TableCell>) -> Result<&mut Self, PdfError> {
183        // Validate total colspan matches column count
184        let total_colspan: usize = cells.iter().map(|c| c.colspan).sum();
185        if total_colspan != self.column_widths.len() {
186            return Err(PdfError::InvalidStructure(
187                "Total colspan doesn't match column count".to_string(),
188            ));
189        }
190
191        self.rows.push(TableRow {
192            cells,
193            is_header: false,
194        });
195
196        Ok(self)
197    }
198
199    /// Calculate the height of a row
200    fn calculate_row_height(&self, _row: &TableRow) -> f64 {
201        if self.options.row_height > 0.0 {
202            self.options.row_height
203        } else {
204            // Auto height: font size + padding
205            self.options.font_size + (self.options.cell_padding * 2.0)
206        }
207    }
208
209    /// Get total table height
210    pub fn get_height(&self) -> f64 {
211        self.rows
212            .iter()
213            .map(|row| self.calculate_row_height(row))
214            .sum()
215    }
216
217    /// Get total table width
218    pub fn get_width(&self) -> f64 {
219        self.column_widths.iter().sum()
220    }
221
222    /// Render the table to a graphics context
223    pub fn render(&self, graphics: &mut GraphicsContext) -> Result<(), PdfError> {
224        let (start_x, start_y) = self.position;
225        let mut current_y = start_y;
226
227        // Draw each row
228        for row in self.rows.iter() {
229            let row_height = self.calculate_row_height(row);
230            let mut current_x = start_x;
231
232            // Determine if we should use header styling
233            let use_header_style = row.is_header && self.options.header_style.is_some();
234            let header_style = self.options.header_style.as_ref();
235
236            // Draw cells
237            let mut col_index = 0;
238            for cell in &row.cells {
239                // Calculate cell width (considering colspan)
240                let mut cell_width = 0.0;
241                for i in 0..cell.colspan {
242                    if col_index + i < self.column_widths.len() {
243                        cell_width += self.column_widths[col_index + i];
244                    }
245                }
246
247                // Draw cell background if header
248                if use_header_style {
249                    if let Some(style) = header_style {
250                        graphics.save_state();
251                        graphics.set_fill_color(style.background_color);
252                        graphics.rectangle(current_x, current_y, cell_width, row_height);
253                        graphics.fill();
254                        graphics.restore_state();
255                    }
256                }
257
258                // Draw cell border
259                graphics.save_state();
260                graphics.set_stroke_color(self.options.border_color);
261                graphics.set_line_width(self.options.border_width);
262                graphics.rectangle(current_x, current_y, cell_width, row_height);
263                graphics.stroke();
264                graphics.restore_state();
265
266                // Draw cell text
267                let text_x = current_x + self.options.cell_padding;
268                let text_y =
269                    current_y + row_height - self.options.cell_padding - self.options.font_size;
270                let text_width = cell_width - (2.0 * self.options.cell_padding);
271
272                graphics.save_state();
273
274                // Set font and color
275                if use_header_style {
276                    if let Some(style) = header_style {
277                        let font = if style.bold {
278                            match style.font {
279                                Font::Helvetica => Font::HelveticaBold,
280                                Font::TimesRoman => Font::TimesBold,
281                                Font::Courier => Font::CourierBold,
282                                _ => style.font.clone(),
283                            }
284                        } else {
285                            style.font.clone()
286                        };
287                        graphics.set_font(font, self.options.font_size);
288                        graphics.set_fill_color(style.text_color);
289                    }
290                } else {
291                    graphics.set_font(self.options.font.clone(), self.options.font_size);
292                    graphics.set_fill_color(self.options.text_color);
293                }
294
295                // Draw text with alignment
296                match cell.align {
297                    TextAlign::Left => {
298                        graphics.begin_text();
299                        graphics.set_text_position(text_x, text_y);
300                        graphics.show_text(&cell.content)?;
301                        graphics.end_text();
302                    }
303                    TextAlign::Center => {
304                        // Determine which font to use based on header style
305                        let font_to_measure = if use_header_style {
306                            if let Some(style) = header_style {
307                                if style.bold {
308                                    match style.font {
309                                        Font::Helvetica => Font::HelveticaBold,
310                                        Font::TimesRoman => Font::TimesBold,
311                                        Font::Courier => Font::CourierBold,
312                                        _ => style.font.clone(),
313                                    }
314                                } else {
315                                    style.font.clone()
316                                }
317                            } else {
318                                self.options.font.clone()
319                            }
320                        } else {
321                            self.options.font.clone()
322                        };
323
324                        let text_width_measured =
325                            measure_text(&cell.content, font_to_measure, self.options.font_size);
326                        let centered_x = text_x + (text_width - text_width_measured) / 2.0;
327                        graphics.begin_text();
328                        graphics.set_text_position(centered_x, text_y);
329                        graphics.show_text(&cell.content)?;
330                        graphics.end_text();
331                    }
332                    TextAlign::Right => {
333                        // Determine which font to use based on header style
334                        let font_to_measure = if use_header_style {
335                            if let Some(style) = header_style {
336                                if style.bold {
337                                    match style.font {
338                                        Font::Helvetica => Font::HelveticaBold,
339                                        Font::TimesRoman => Font::TimesBold,
340                                        Font::Courier => Font::CourierBold,
341                                        _ => style.font.clone(),
342                                    }
343                                } else {
344                                    style.font.clone()
345                                }
346                            } else {
347                                self.options.font.clone()
348                            }
349                        } else {
350                            self.options.font.clone()
351                        };
352
353                        let text_width_measured =
354                            measure_text(&cell.content, font_to_measure, self.options.font_size);
355                        let right_x = text_x + text_width - text_width_measured;
356                        graphics.begin_text();
357                        graphics.set_text_position(right_x, text_y);
358                        graphics.show_text(&cell.content)?;
359                        graphics.end_text();
360                    }
361                    TextAlign::Justified => {
362                        // For simple tables, treat justified as left-aligned
363                        graphics.begin_text();
364                        graphics.set_text_position(text_x, text_y);
365                        graphics.show_text(&cell.content)?;
366                        graphics.end_text();
367                    }
368                }
369
370                graphics.restore_state();
371
372                current_x += cell_width;
373                col_index += cell.colspan;
374            }
375
376            current_y += row_height;
377        }
378
379        Ok(())
380    }
381}
382
383impl TableRow {
384    /// Create a new row with cells
385    #[allow(dead_code)]
386    pub fn new(cells: Vec<TableCell>) -> Self {
387        Self {
388            cells,
389            is_header: false,
390        }
391    }
392
393    /// Create a header row
394    #[allow(dead_code)]
395    pub fn header(cells: Vec<TableCell>) -> Self {
396        Self {
397            cells,
398            is_header: true,
399        }
400    }
401}
402
403impl TableCell {
404    /// Create a new cell with content
405    pub fn new(content: String) -> Self {
406        Self {
407            content,
408            align: TextAlign::Left,
409            colspan: 1,
410        }
411    }
412
413    /// Create a cell with specific alignment
414    pub fn with_align(content: String, align: TextAlign) -> Self {
415        Self {
416            content,
417            align,
418            colspan: 1,
419        }
420    }
421
422    /// Create a cell with colspan
423    pub fn with_colspan(content: String, colspan: usize) -> Self {
424        Self {
425            content,
426            align: TextAlign::Left,
427            colspan,
428        }
429    }
430
431    /// Set cell alignment
432    pub fn set_align(&mut self, align: TextAlign) -> &mut Self {
433        self.align = align;
434        self
435    }
436
437    /// Set cell colspan
438    pub fn set_colspan(&mut self, colspan: usize) -> &mut Self {
439        self.colspan = colspan;
440        self
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn test_table_creation() {
450        let table = Table::new(vec![100.0, 150.0, 200.0]);
451        assert_eq!(table.column_widths.len(), 3);
452        assert_eq!(table.rows.len(), 0);
453    }
454
455    #[test]
456    fn test_table_equal_columns() {
457        let table = Table::with_equal_columns(4, 400.0);
458        assert_eq!(table.column_widths.len(), 4);
459        assert_eq!(table.column_widths[0], 100.0);
460        assert_eq!(table.get_width(), 400.0);
461    }
462
463    #[test]
464    fn test_add_header_row() {
465        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
466        let result = table.add_header_row(vec![
467            "Name".to_string(),
468            "Age".to_string(),
469            "City".to_string(),
470        ]);
471        assert!(result.is_ok());
472        assert_eq!(table.rows.len(), 1);
473        assert!(table.rows[0].is_header);
474    }
475
476    #[test]
477    fn test_add_row_mismatch() {
478        let mut table = Table::new(vec![100.0, 100.0]);
479        let result = table.add_row(vec![
480            "John".to_string(),
481            "25".to_string(),
482            "NYC".to_string(),
483        ]);
484        assert!(result.is_err());
485    }
486
487    #[test]
488    fn test_table_cell_creation() {
489        let cell = TableCell::new("Test".to_string());
490        assert_eq!(cell.content, "Test");
491        assert_eq!(cell.align, TextAlign::Left);
492        assert_eq!(cell.colspan, 1);
493    }
494
495    #[test]
496    fn test_table_cell_with_colspan() {
497        let cell = TableCell::with_colspan("Merged".to_string(), 3);
498        assert_eq!(cell.content, "Merged");
499        assert_eq!(cell.colspan, 3);
500    }
501
502    #[test]
503    fn test_custom_row_colspan_validation() {
504        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
505        let cells = vec![
506            TableCell::new("Normal".to_string()),
507            TableCell::with_colspan("Merged".to_string(), 2),
508        ];
509        let result = table.add_custom_row(cells);
510        assert!(result.is_ok());
511        assert_eq!(table.rows.len(), 1);
512    }
513
514    #[test]
515    fn test_custom_row_invalid_colspan() {
516        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
517        let cells = vec![
518            TableCell::new("Normal".to_string()),
519            TableCell::with_colspan("Merged".to_string(), 3), // Total would be 4
520        ];
521        let result = table.add_custom_row(cells);
522        assert!(result.is_err());
523    }
524
525    #[test]
526    fn test_table_options_default() {
527        let options = TableOptions::default();
528        assert_eq!(options.border_width, 1.0);
529        assert_eq!(options.border_color, Color::black());
530        assert_eq!(options.cell_padding, 5.0);
531        assert_eq!(options.font_size, 10.0);
532    }
533
534    #[test]
535    fn test_header_style() {
536        let style = HeaderStyle {
537            background_color: Color::gray(0.9),
538            text_color: Color::black(),
539            font: Font::HelveticaBold,
540            bold: true,
541        };
542        assert_eq!(style.background_color, Color::gray(0.9));
543        assert!(style.bold);
544    }
545
546    #[test]
547    fn test_table_dimensions() {
548        let mut table = Table::new(vec![100.0, 150.0, 200.0]);
549        table.options.row_height = 20.0;
550
551        table
552            .add_row(vec!["A".to_string(), "B".to_string(), "C".to_string()])
553            .unwrap();
554        table
555            .add_row(vec!["D".to_string(), "E".to_string(), "F".to_string()])
556            .unwrap();
557
558        assert_eq!(table.get_width(), 450.0);
559        assert_eq!(table.get_height(), 40.0);
560    }
561
562    #[test]
563    fn test_table_position() {
564        let mut table = Table::new(vec![100.0]);
565        table.set_position(50.0, 100.0);
566        assert_eq!(table.position, (50.0, 100.0));
567    }
568
569    #[test]
570    fn test_row_with_alignment() {
571        let mut table = Table::new(vec![100.0, 100.0]);
572        let result = table.add_row_with_alignment(
573            vec!["Left".to_string(), "Right".to_string()],
574            TextAlign::Right,
575        );
576        assert!(result.is_ok());
577        assert_eq!(table.rows[0].cells[0].align, TextAlign::Right);
578    }
579
580    #[test]
581    fn test_table_cell_setters() {
582        let mut cell = TableCell::new("Test".to_string());
583        cell.set_align(TextAlign::Center).set_colspan(2);
584        assert_eq!(cell.align, TextAlign::Center);
585        assert_eq!(cell.colspan, 2);
586    }
587
588    #[test]
589    fn test_auto_row_height() {
590        let table = Table::new(vec![100.0]);
591        let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
592        let height = table.calculate_row_height(&row);
593        assert_eq!(height, 20.0); // font_size (10) + padding*2 (5*2)
594    }
595
596    #[test]
597    fn test_fixed_row_height() {
598        let mut table = Table::new(vec![100.0]);
599        table.options.row_height = 30.0;
600        let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
601        let height = table.calculate_row_height(&row);
602        assert_eq!(height, 30.0);
603    }
604}