Skip to main content

oxidize_pdf/
page_tables.rs

1//! Page extension for table rendering
2//!
3//! This module provides traits and implementations to easily add tables to PDF pages.
4
5use crate::error::PdfError;
6use crate::graphics::Color;
7use crate::page::Page;
8use crate::text::{Font, HeaderStyle, Table, TableOptions};
9
10/// Extension trait for adding tables to pages
11pub trait PageTables {
12    /// Add a simple table to the page
13    fn add_simple_table(&mut self, table: &Table, x: f64, y: f64) -> Result<&mut Self, PdfError>;
14
15    /// Create and add a quick table with equal columns
16    fn add_quick_table(
17        &mut self,
18        data: Vec<Vec<String>>,
19        x: f64,
20        y: f64,
21        width: f64,
22        options: Option<TableOptions>,
23    ) -> Result<&mut Self, PdfError>;
24
25    /// Create and add an advanced table with custom styling
26    fn add_styled_table(
27        &mut self,
28        headers: Vec<String>,
29        data: Vec<Vec<String>>,
30        x: f64,
31        y: f64,
32        width: f64,
33        style: TableStyle,
34    ) -> Result<&mut Self, PdfError>;
35}
36
37/// Predefined table styles
38#[derive(Debug, Clone)]
39pub struct TableStyle {
40    /// Header background color
41    pub header_background: Option<Color>,
42    /// Header text color
43    pub header_text_color: Option<Color>,
44    /// Default font size
45    pub font_size: f64,
46}
47
48impl TableStyle {
49    /// Create a minimal table style (no borders)
50    pub fn minimal() -> Self {
51        Self {
52            header_background: None,
53            header_text_color: None,
54            font_size: 10.0,
55        }
56    }
57
58    /// Create a simple table style with borders
59    pub fn simple() -> Self {
60        Self {
61            header_background: None,
62            header_text_color: None,
63            font_size: 10.0,
64        }
65    }
66
67    /// Create a professional table style
68    pub fn professional() -> Self {
69        Self {
70            header_background: Some(Color::gray(0.1)),
71            header_text_color: Some(Color::white()),
72            font_size: 10.0,
73        }
74    }
75
76    /// Create a colorful table style
77    pub fn colorful() -> Self {
78        Self {
79            header_background: Some(Color::rgb(0.2, 0.4, 0.8)),
80            header_text_color: Some(Color::white()),
81            font_size: 10.0,
82        }
83    }
84}
85
86impl PageTables for Page {
87    fn add_simple_table(&mut self, table: &Table, x: f64, y: f64) -> Result<&mut Self, PdfError> {
88        let mut table_clone = table.clone();
89        table_clone.set_position(x, y);
90        table_clone.render(self.graphics())?;
91        Ok(self)
92    }
93
94    fn add_quick_table(
95        &mut self,
96        data: Vec<Vec<String>>,
97        x: f64,
98        y: f64,
99        width: f64,
100        options: Option<TableOptions>,
101    ) -> Result<&mut Self, PdfError> {
102        if data.is_empty() {
103            return Ok(self);
104        }
105
106        let num_columns = data[0].len();
107        let mut table = Table::with_equal_columns(num_columns, width);
108
109        if let Some(opts) = options {
110            table.set_options(opts);
111        }
112
113        for row in data {
114            table.add_row(row)?;
115        }
116
117        self.add_simple_table(&table, x, y)
118    }
119
120    fn add_styled_table(
121        &mut self,
122        headers: Vec<String>,
123        data: Vec<Vec<String>>,
124        x: f64,
125        y: f64,
126        width: f64,
127        style: TableStyle,
128    ) -> Result<&mut Self, PdfError> {
129        let num_columns = headers.len();
130        if num_columns == 0 {
131            return Ok(self);
132        }
133
134        // Create a simple table with the given style
135        let mut table = Table::with_equal_columns(num_columns, width);
136
137        // Create table options based on style
138        let header_style = if style.header_background.is_some() || style.header_text_color.is_some()
139        {
140            Some(HeaderStyle {
141                background_color: style.header_background.unwrap_or(Color::white()),
142                text_color: style.header_text_color.unwrap_or(Color::black()),
143                font: Font::Helvetica,
144                bold: true,
145            })
146        } else {
147            None
148        };
149
150        let options = TableOptions {
151            font_size: style.font_size,
152            header_style,
153            ..Default::default()
154        };
155
156        table.set_options(options);
157
158        // Add header row
159        table.add_row(headers)?;
160
161        // Add data rows
162        for row_data in data {
163            table.add_row(row_data)?;
164        }
165
166        self.add_simple_table(&table, x, y)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::page::Page;
174
175    // ==================== TableStyle Tests ====================
176
177    #[test]
178    fn test_table_style_minimal() {
179        let style = TableStyle::minimal();
180        assert_eq!(style.header_background, None);
181        assert_eq!(style.header_text_color, None);
182        assert_eq!(style.font_size, 10.0);
183    }
184
185    #[test]
186    fn test_table_style_simple() {
187        let style = TableStyle::simple();
188        assert_eq!(style.header_background, None);
189        assert_eq!(style.header_text_color, None);
190        assert_eq!(style.font_size, 10.0);
191    }
192
193    #[test]
194    fn test_table_style_professional() {
195        let style = TableStyle::professional();
196        assert!(style.header_background.is_some());
197        assert!(style.header_text_color.is_some());
198        assert_eq!(style.font_size, 10.0);
199
200        // Verify dark header background
201        if let Some(bg) = style.header_background {
202            assert!(bg.r() < 0.2, "Professional header should be dark");
203        }
204
205        // Verify white text
206        if let Some(text) = style.header_text_color {
207            assert_eq!(text, Color::white());
208        }
209    }
210
211    #[test]
212    fn test_table_style_colorful() {
213        let style = TableStyle::colorful();
214        assert!(style.header_background.is_some());
215        assert!(style.header_text_color.is_some());
216        assert_eq!(style.font_size, 10.0);
217
218        // Verify blue-ish header background (0.2, 0.4, 0.8)
219        if let Some(bg) = style.header_background {
220            assert!(bg.b() > bg.r(), "Colorful header should be blue-ish");
221            assert!(bg.b() > bg.g(), "Colorful header should be blue-ish");
222        }
223
224        // Verify white text
225        if let Some(text) = style.header_text_color {
226            assert_eq!(text, Color::white());
227        }
228    }
229
230    #[test]
231    fn test_table_style_clone() {
232        let original = TableStyle::professional();
233        let cloned = original.clone();
234
235        assert_eq!(cloned.header_background, original.header_background);
236        assert_eq!(cloned.header_text_color, original.header_text_color);
237        assert_eq!(cloned.font_size, original.font_size);
238    }
239
240    #[test]
241    fn test_table_style_debug() {
242        let style = TableStyle::minimal();
243        let debug_str = format!("{:?}", style);
244        assert!(debug_str.contains("TableStyle"));
245    }
246
247    #[test]
248    fn test_table_style_mutability() {
249        let mut style = TableStyle::minimal();
250
251        style.header_background = Some(Color::red());
252        style.header_text_color = Some(Color::blue());
253        style.font_size = 14.0;
254
255        assert_eq!(style.header_background, Some(Color::red()));
256        assert_eq!(style.header_text_color, Some(Color::blue()));
257        assert_eq!(style.font_size, 14.0);
258    }
259
260    #[test]
261    fn test_table_styles() {
262        let minimal = TableStyle::minimal();
263        assert_eq!(minimal.font_size, 10.0);
264
265        let simple = TableStyle::simple();
266        assert_eq!(simple.font_size, 10.0);
267
268        let professional = TableStyle::professional();
269        assert!(professional.header_background.is_some());
270
271        let colorful = TableStyle::colorful();
272        assert!(colorful.header_background.is_some());
273    }
274
275    // ==================== Page Integration Tests ====================
276
277    #[test]
278    fn test_page_tables_trait() {
279        let mut page = Page::a4();
280
281        // Test quick table
282        let data = vec![
283            vec!["Name".to_string(), "Age".to_string()],
284            vec!["John".to_string(), "30".to_string()],
285        ];
286
287        let result = page.add_quick_table(data, 50.0, 700.0, 400.0, None);
288        assert!(result.is_ok());
289    }
290
291    #[test]
292    fn test_quick_table_with_options() {
293        let mut page = Page::a4();
294
295        let data = vec![
296            vec!["A".to_string(), "B".to_string()],
297            vec!["C".to_string(), "D".to_string()],
298        ];
299
300        let options = TableOptions {
301            font_size: 12.0,
302            ..Default::default()
303        };
304
305        let result = page.add_quick_table(data, 50.0, 700.0, 400.0, Some(options));
306        assert!(result.is_ok());
307    }
308
309    #[test]
310    fn test_styled_table() {
311        let mut page = Page::a4();
312
313        let headers = vec!["Column 1".to_string(), "Column 2".to_string()];
314        let data = vec![
315            vec!["Data 1".to_string(), "Data 2".to_string()],
316            vec!["Data 3".to_string(), "Data 4".to_string()],
317        ];
318
319        let result = page.add_styled_table(
320            headers,
321            data,
322            50.0,
323            700.0,
324            500.0,
325            TableStyle::professional(),
326        );
327
328        assert!(result.is_ok());
329    }
330
331    #[test]
332    fn test_styled_table_minimal() {
333        let mut page = Page::a4();
334
335        let headers = vec!["H1".to_string(), "H2".to_string()];
336        let data = vec![vec!["V1".to_string(), "V2".to_string()]];
337
338        let result =
339            page.add_styled_table(headers, data, 50.0, 700.0, 400.0, TableStyle::minimal());
340        assert!(result.is_ok());
341    }
342
343    #[test]
344    fn test_styled_table_colorful() {
345        let mut page = Page::a4();
346
347        let headers = vec!["Header".to_string()];
348        let data = vec![vec!["Value".to_string()]];
349
350        let result =
351            page.add_styled_table(headers, data, 50.0, 700.0, 300.0, TableStyle::colorful());
352        assert!(result.is_ok());
353    }
354
355    #[test]
356    fn test_styled_table_empty_headers() {
357        let mut page = Page::a4();
358
359        let headers: Vec<String> = vec![];
360        let data = vec![vec!["Data".to_string()]];
361
362        // Empty headers should return Ok (early return)
363        let result = page.add_styled_table(headers, data, 50.0, 700.0, 400.0, TableStyle::simple());
364        assert!(result.is_ok());
365    }
366
367    #[test]
368    fn test_styled_table_empty_data() {
369        let mut page = Page::a4();
370
371        let headers = vec!["H1".to_string(), "H2".to_string()];
372        let data: Vec<Vec<String>> = vec![];
373
374        // Headers only, no data rows
375        let result = page.add_styled_table(
376            headers,
377            data,
378            50.0,
379            700.0,
380            400.0,
381            TableStyle::professional(),
382        );
383        assert!(result.is_ok());
384    }
385
386    #[test]
387    fn test_empty_table() {
388        let mut page = Page::a4();
389
390        let data: Vec<Vec<String>> = vec![];
391        let result = page.add_quick_table(data, 50.0, 700.0, 400.0, None);
392        assert!(result.is_ok());
393    }
394
395    #[test]
396    fn test_single_cell_table() {
397        let mut page = Page::a4();
398
399        let data = vec![vec!["Single".to_string()]];
400        let result = page.add_quick_table(data, 50.0, 700.0, 200.0, None);
401        assert!(result.is_ok());
402    }
403
404    #[test]
405    fn test_single_row_table() {
406        let mut page = Page::a4();
407
408        let data = vec![vec![
409            "A".to_string(),
410            "B".to_string(),
411            "C".to_string(),
412            "D".to_string(),
413        ]];
414        let result = page.add_quick_table(data, 50.0, 700.0, 500.0, None);
415        assert!(result.is_ok());
416    }
417
418    #[test]
419    fn test_single_column_table() {
420        let mut page = Page::a4();
421
422        let data = vec![
423            vec!["Row 1".to_string()],
424            vec!["Row 2".to_string()],
425            vec!["Row 3".to_string()],
426        ];
427        let result = page.add_quick_table(data, 50.0, 700.0, 150.0, None);
428        assert!(result.is_ok());
429    }
430
431    #[test]
432    fn test_many_rows_table() {
433        let mut page = Page::a4();
434
435        let data: Vec<Vec<String>> = (0..50)
436            .map(|i| vec![format!("Row {}", i), format!("Value {}", i)])
437            .collect();
438
439        let result = page.add_quick_table(data, 50.0, 700.0, 400.0, None);
440        assert!(result.is_ok());
441    }
442
443    #[test]
444    fn test_many_columns_table() {
445        let mut page = Page::a4();
446
447        let headers: Vec<String> = (0..10).map(|i| format!("Col {}", i)).collect();
448        let data = vec![(0..10).map(|i| format!("V{}", i)).collect()];
449
450        let result = page.add_styled_table(headers, data, 50.0, 700.0, 550.0, TableStyle::simple());
451        assert!(result.is_ok());
452    }
453
454    #[test]
455    fn test_table_at_different_positions() {
456        let mut page = Page::a4();
457
458        let data = vec![vec!["Test".to_string()]];
459
460        // Top-left
461        let result = page.add_quick_table(data.clone(), 0.0, 800.0, 100.0, None);
462        assert!(result.is_ok());
463
464        // Center-ish
465        let result = page.add_quick_table(data.clone(), 200.0, 400.0, 100.0, None);
466        assert!(result.is_ok());
467
468        // Bottom-right area
469        let result = page.add_quick_table(data, 400.0, 100.0, 100.0, None);
470        assert!(result.is_ok());
471    }
472
473    #[test]
474    fn test_styled_table_with_only_header_background() {
475        let mut page = Page::a4();
476
477        let mut style = TableStyle::minimal();
478        style.header_background = Some(Color::green());
479        // header_text_color remains None
480
481        let headers = vec!["Test".to_string()];
482        let data = vec![vec!["Data".to_string()]];
483
484        let result = page.add_styled_table(headers, data, 50.0, 700.0, 200.0, style);
485        assert!(result.is_ok());
486    }
487
488    #[test]
489    fn test_styled_table_with_only_header_text_color() {
490        let mut page = Page::a4();
491
492        let mut style = TableStyle::minimal();
493        style.header_text_color = Some(Color::red());
494        // header_background remains None
495
496        let headers = vec!["Test".to_string()];
497        let data = vec![vec!["Data".to_string()]];
498
499        let result = page.add_styled_table(headers, data, 50.0, 700.0, 200.0, style);
500        assert!(result.is_ok());
501    }
502
503    #[test]
504    fn test_styled_table_custom_font_size() {
505        let mut page = Page::a4();
506
507        let mut style = TableStyle::professional();
508        style.font_size = 16.0;
509
510        let headers = vec!["Big".to_string(), "Text".to_string()];
511        let data = vec![vec!["Large".to_string(), "Font".to_string()]];
512
513        let result = page.add_styled_table(headers, data, 50.0, 700.0, 300.0, style);
514        assert!(result.is_ok());
515    }
516
517    #[test]
518    fn test_all_styles_integration() {
519        let mut page = Page::a4();
520
521        let headers = vec!["A".to_string(), "B".to_string()];
522        let data = vec![vec!["1".to_string(), "2".to_string()]];
523
524        let styles = vec![
525            TableStyle::minimal(),
526            TableStyle::simple(),
527            TableStyle::professional(),
528            TableStyle::colorful(),
529        ];
530
531        for (i, style) in styles.into_iter().enumerate() {
532            let y = 700.0 - (i as f64 * 100.0);
533            let result =
534                page.add_styled_table(headers.clone(), data.clone(), 50.0, y, 200.0, style);
535            assert!(result.is_ok(), "Failed for style index {}", i);
536        }
537    }
538}