1use crate::document::Document;
6use crate::error::{ensure_finite, PdfError};
7use crate::graphics::Color;
8use crate::page::Page;
9use crate::text::{Font, HeaderStyle, Table, TableOptions};
10
11pub trait PageTables {
13 fn add_simple_table(&mut self, table: &Table, x: f64, y: f64) -> Result<&mut Self, PdfError>;
15
16 fn add_quick_table(
18 &mut self,
19 data: Vec<Vec<String>>,
20 x: f64,
21 y: f64,
22 width: f64,
23 options: Option<TableOptions>,
24 ) -> Result<&mut Self, PdfError>;
25
26 fn add_styled_table(
28 &mut self,
29 headers: Vec<String>,
30 data: Vec<Vec<String>>,
31 x: f64,
32 y: f64,
33 width: f64,
34 style: TableStyle,
35 ) -> Result<&mut Self, PdfError>;
36}
37
38#[derive(Debug, Clone)]
40pub struct TableStyle {
41 pub header_background: Option<Color>,
43 pub header_text_color: Option<Color>,
45 pub font_size: f64,
47 pub header_font: Option<Font>,
50 pub header_bold: Option<bool>,
56}
57
58impl TableStyle {
59 pub fn minimal() -> Self {
61 Self {
62 header_background: None,
63 header_text_color: None,
64 font_size: 10.0,
65 header_font: None,
66 header_bold: None,
67 }
68 }
69
70 pub fn simple() -> Self {
72 Self {
73 header_background: None,
74 header_text_color: None,
75 font_size: 10.0,
76 header_font: None,
77 header_bold: None,
78 }
79 }
80
81 pub fn professional() -> Self {
83 Self {
84 header_background: Some(Color::gray(0.1)),
85 header_text_color: Some(Color::white()),
86 font_size: 10.0,
87 header_font: None,
88 header_bold: None,
89 }
90 }
91
92 pub fn colorful() -> Self {
94 Self {
95 header_background: Some(Color::rgb(0.2, 0.4, 0.8)),
96 header_text_color: Some(Color::white()),
97 font_size: 10.0,
98 header_font: None,
99 header_bold: None,
100 }
101 }
102
103 pub fn with_header_font(mut self, font: Font) -> Self {
112 self.header_font = Some(font);
113 self
114 }
115
116 pub fn with_header_bold(mut self, bold: bool) -> Self {
124 self.header_bold = Some(bold);
125 self
126 }
127}
128
129impl PageTables for Page {
130 fn add_simple_table(&mut self, table: &Table, x: f64, y: f64) -> Result<&mut Self, PdfError> {
131 let mut table_clone = table.clone();
132 table_clone.set_position(x, y);
133 table_clone.render(self.graphics())?;
134 Ok(self)
135 }
136
137 fn add_quick_table(
138 &mut self,
139 data: Vec<Vec<String>>,
140 x: f64,
141 y: f64,
142 width: f64,
143 options: Option<TableOptions>,
144 ) -> Result<&mut Self, PdfError> {
145 if data.is_empty() {
146 return Ok(self);
147 }
148
149 let num_columns = data[0].len();
150 let mut table = Table::with_equal_columns(num_columns, width);
151
152 if let Some(opts) = options {
153 table.set_options(opts);
154 }
155
156 for row in data {
157 table.add_row(row)?;
158 }
159
160 self.add_simple_table(&table, x, y)
161 }
162
163 fn add_styled_table(
164 &mut self,
165 headers: Vec<String>,
166 data: Vec<Vec<String>>,
167 x: f64,
168 y: f64,
169 width: f64,
170 style: TableStyle,
171 ) -> Result<&mut Self, PdfError> {
172 let num_columns = headers.len();
173 if num_columns == 0 {
174 return Ok(self);
175 }
176
177 let mut table = Table::with_equal_columns(num_columns, width);
179
180 let header_style = if style.header_background.is_some()
187 || style.header_text_color.is_some()
188 || style.header_font.is_some()
189 || style.header_bold.is_some()
190 {
191 Some(HeaderStyle {
192 background_color: style.header_background.unwrap_or(Color::white()),
193 text_color: style.header_text_color.unwrap_or(Color::black()),
194 font: style.header_font.clone().unwrap_or(Font::Helvetica),
195 bold: style.header_bold.unwrap_or(true),
196 })
197 } else {
198 None
199 };
200
201 let options = TableOptions {
202 font_size: style.font_size,
203 header_style,
204 ..Default::default()
205 };
206
207 table.set_options(options);
208
209 table.add_header_row(headers)?;
215
216 for row_data in data {
218 table.add_row(row_data)?;
219 }
220
221 self.add_simple_table(&table, x, y)
222 }
223}
224
225pub trait DocumentTables {
234 fn add_paginated_table(
259 &mut self,
260 starting_page_index: usize,
261 table: &Table,
262 x: f64,
263 y: f64,
264 bottom_y: f64,
265 next_page_y: f64,
266 ) -> Result<(usize, f64), PdfError>;
267}
268
269impl DocumentTables for Document {
270 fn add_paginated_table(
271 &mut self,
272 starting_page_index: usize,
273 table: &Table,
274 x: f64,
275 y: f64,
276 bottom_y: f64,
277 next_page_y: f64,
278 ) -> Result<(usize, f64), PdfError> {
279 ensure_finite("x", x)?;
283 ensure_finite("y", y)?;
284 ensure_finite("bottom_y", bottom_y)?;
285 ensure_finite("next_page_y", next_page_y)?;
286
287 let repeat_headers = table.options().repeat_header_on_split;
288
289 let mut current_table = table.clone();
290 current_table.set_position(x, y);
291
292 let mut current_page_idx = starting_page_index;
293
294 loop {
295 let (page_width, page_height) = match self.page(current_page_idx) {
298 Some(p) => (p.width(), p.height()),
299 None => {
300 return Err(PdfError::InvalidStructure(format!(
301 "page index {current_page_idx} out of bounds (page_count={})",
302 self.page_count()
303 )))
304 }
305 };
306
307 let current_data_rows = current_table.row_count() - current_table.header_count();
313
314 let tail = {
315 let page = self.page_mut(current_page_idx).expect("checked above");
316 current_table.render_with_split(page.graphics(), bottom_y)?
317 };
318
319 match tail {
320 None => {
321 let final_y = current_table.position().1 - current_table.get_height();
322 return Ok((current_page_idx, final_y));
323 }
324 Some(mut tail) => {
325 let tail_data_rows = tail.row_count() - tail.header_count();
330 let data_rows_drawn = current_data_rows.saturating_sub(tail_data_rows);
331 if data_rows_drawn == 0 {
332 return Err(PdfError::TableOverflow {
333 rendered: current_table.row_count() - tail.row_count(),
334 dropped: tail.row_count(),
335 bottom_y,
336 });
337 }
338
339 self.add_page(Page::new(page_width, page_height));
340 current_page_idx = self.page_count() - 1;
341
342 if repeat_headers {
343 tail.prepend_headers_from(table);
344 }
345 tail.set_position(x, next_page_y);
346 current_table = tail;
347 }
348 }
349 }
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::page::Page;
357
358 #[test]
361 fn test_table_style_minimal() {
362 let style = TableStyle::minimal();
363 assert_eq!(style.header_background, None);
364 assert_eq!(style.header_text_color, None);
365 assert_eq!(style.font_size, 10.0);
366 }
367
368 #[test]
369 fn test_table_style_simple() {
370 let style = TableStyle::simple();
371 assert_eq!(style.header_background, None);
372 assert_eq!(style.header_text_color, None);
373 assert_eq!(style.font_size, 10.0);
374 }
375
376 #[test]
377 fn test_table_style_professional() {
378 let style = TableStyle::professional();
379 assert!(style.header_background.is_some());
380 assert!(style.header_text_color.is_some());
381 assert_eq!(style.font_size, 10.0);
382
383 if let Some(bg) = style.header_background {
385 assert!(bg.r() < 0.2, "Professional header should be dark");
386 }
387
388 if let Some(text) = style.header_text_color {
390 assert_eq!(text, Color::white());
391 }
392 }
393
394 #[test]
395 fn test_table_style_colorful() {
396 let style = TableStyle::colorful();
397 assert!(style.header_background.is_some());
398 assert!(style.header_text_color.is_some());
399 assert_eq!(style.font_size, 10.0);
400
401 if let Some(bg) = style.header_background {
403 assert!(bg.b() > bg.r(), "Colorful header should be blue-ish");
404 assert!(bg.b() > bg.g(), "Colorful header should be blue-ish");
405 }
406
407 if let Some(text) = style.header_text_color {
409 assert_eq!(text, Color::white());
410 }
411 }
412
413 #[test]
414 fn test_table_style_clone() {
415 let original = TableStyle::professional();
416 let cloned = original.clone();
417
418 assert_eq!(cloned.header_background, original.header_background);
419 assert_eq!(cloned.header_text_color, original.header_text_color);
420 assert_eq!(cloned.font_size, original.font_size);
421 }
422
423 #[test]
424 fn test_table_style_debug() {
425 let style = TableStyle::minimal();
426 let debug_str = format!("{:?}", style);
427 assert!(debug_str.contains("TableStyle"));
428 }
429
430 #[test]
431 fn test_table_style_mutability() {
432 let mut style = TableStyle::minimal();
433
434 style.header_background = Some(Color::red());
435 style.header_text_color = Some(Color::blue());
436 style.font_size = 14.0;
437
438 assert_eq!(style.header_background, Some(Color::red()));
439 assert_eq!(style.header_text_color, Some(Color::blue()));
440 assert_eq!(style.font_size, 14.0);
441 }
442
443 #[test]
444 fn test_table_styles() {
445 let minimal = TableStyle::minimal();
446 assert_eq!(minimal.font_size, 10.0);
447
448 let simple = TableStyle::simple();
449 assert_eq!(simple.font_size, 10.0);
450
451 let professional = TableStyle::professional();
452 assert!(professional.header_background.is_some());
453
454 let colorful = TableStyle::colorful();
455 assert!(colorful.header_background.is_some());
456 }
457
458 #[test]
461 fn test_page_tables_trait() {
462 let mut page = Page::a4();
463
464 let data = vec![
466 vec!["Name".to_string(), "Age".to_string()],
467 vec!["John".to_string(), "30".to_string()],
468 ];
469
470 let result = page.add_quick_table(data, 50.0, 700.0, 400.0, None);
471 assert!(result.is_ok());
472 }
473
474 #[test]
475 fn test_quick_table_with_options() {
476 let mut page = Page::a4();
477
478 let data = vec![
479 vec!["A".to_string(), "B".to_string()],
480 vec!["C".to_string(), "D".to_string()],
481 ];
482
483 let options = TableOptions {
484 font_size: 12.0,
485 ..Default::default()
486 };
487
488 let result = page.add_quick_table(data, 50.0, 700.0, 400.0, Some(options));
489 assert!(result.is_ok());
490 }
491
492 #[test]
493 fn test_styled_table() {
494 let mut page = Page::a4();
495
496 let headers = vec!["Column 1".to_string(), "Column 2".to_string()];
497 let data = vec![
498 vec!["Data 1".to_string(), "Data 2".to_string()],
499 vec!["Data 3".to_string(), "Data 4".to_string()],
500 ];
501
502 let result = page.add_styled_table(
503 headers,
504 data,
505 50.0,
506 700.0,
507 500.0,
508 TableStyle::professional(),
509 );
510
511 assert!(result.is_ok());
512 }
513
514 #[test]
515 fn test_styled_table_minimal() {
516 let mut page = Page::a4();
517
518 let headers = vec!["H1".to_string(), "H2".to_string()];
519 let data = vec![vec!["V1".to_string(), "V2".to_string()]];
520
521 let result =
522 page.add_styled_table(headers, data, 50.0, 700.0, 400.0, TableStyle::minimal());
523 assert!(result.is_ok());
524 }
525
526 #[test]
527 fn test_styled_table_colorful() {
528 let mut page = Page::a4();
529
530 let headers = vec!["Header".to_string()];
531 let data = vec![vec!["Value".to_string()]];
532
533 let result =
534 page.add_styled_table(headers, data, 50.0, 700.0, 300.0, TableStyle::colorful());
535 assert!(result.is_ok());
536 }
537
538 #[test]
539 fn test_styled_table_empty_headers() {
540 let mut page = Page::a4();
541
542 let headers: Vec<String> = vec![];
543 let data = vec![vec!["Data".to_string()]];
544
545 let result = page.add_styled_table(headers, data, 50.0, 700.0, 400.0, TableStyle::simple());
547 assert!(result.is_ok());
548 }
549
550 #[test]
551 fn test_styled_table_empty_data() {
552 let mut page = Page::a4();
553
554 let headers = vec!["H1".to_string(), "H2".to_string()];
555 let data: Vec<Vec<String>> = vec![];
556
557 let result = page.add_styled_table(
559 headers,
560 data,
561 50.0,
562 700.0,
563 400.0,
564 TableStyle::professional(),
565 );
566 assert!(result.is_ok());
567 }
568
569 #[test]
570 fn test_empty_table() {
571 let mut page = Page::a4();
572
573 let data: Vec<Vec<String>> = vec![];
574 let result = page.add_quick_table(data, 50.0, 700.0, 400.0, None);
575 assert!(result.is_ok());
576 }
577
578 #[test]
579 fn test_single_cell_table() {
580 let mut page = Page::a4();
581
582 let data = vec![vec!["Single".to_string()]];
583 let result = page.add_quick_table(data, 50.0, 700.0, 200.0, None);
584 assert!(result.is_ok());
585 }
586
587 #[test]
588 fn test_single_row_table() {
589 let mut page = Page::a4();
590
591 let data = vec![vec![
592 "A".to_string(),
593 "B".to_string(),
594 "C".to_string(),
595 "D".to_string(),
596 ]];
597 let result = page.add_quick_table(data, 50.0, 700.0, 500.0, None);
598 assert!(result.is_ok());
599 }
600
601 #[test]
602 fn test_single_column_table() {
603 let mut page = Page::a4();
604
605 let data = vec![
606 vec!["Row 1".to_string()],
607 vec!["Row 2".to_string()],
608 vec!["Row 3".to_string()],
609 ];
610 let result = page.add_quick_table(data, 50.0, 700.0, 150.0, None);
611 assert!(result.is_ok());
612 }
613
614 #[test]
615 fn test_many_rows_table() {
616 let mut page = Page::a4();
617
618 let data: Vec<Vec<String>> = (0..50)
619 .map(|i| vec![format!("Row {}", i), format!("Value {}", i)])
620 .collect();
621
622 let result = page.add_quick_table(data, 50.0, 700.0, 400.0, None);
623 assert!(result.is_ok());
624 }
625
626 #[test]
627 fn test_many_columns_table() {
628 let mut page = Page::a4();
629
630 let headers: Vec<String> = (0..10).map(|i| format!("Col {}", i)).collect();
631 let data = vec![(0..10).map(|i| format!("V{}", i)).collect()];
632
633 let result = page.add_styled_table(headers, data, 50.0, 700.0, 550.0, TableStyle::simple());
634 assert!(result.is_ok());
635 }
636
637 #[test]
638 fn test_table_at_different_positions() {
639 let mut page = Page::a4();
640
641 let data = vec![vec!["Test".to_string()]];
642
643 let result = page.add_quick_table(data.clone(), 0.0, 800.0, 100.0, None);
645 assert!(result.is_ok());
646
647 let result = page.add_quick_table(data.clone(), 200.0, 400.0, 100.0, None);
649 assert!(result.is_ok());
650
651 let result = page.add_quick_table(data, 400.0, 100.0, 100.0, None);
653 assert!(result.is_ok());
654 }
655
656 #[test]
657 fn test_styled_table_with_only_header_background() {
658 let mut page = Page::a4();
659
660 let mut style = TableStyle::minimal();
661 style.header_background = Some(Color::green());
662 let headers = vec!["Test".to_string()];
665 let data = vec![vec!["Data".to_string()]];
666
667 let result = page.add_styled_table(headers, data, 50.0, 700.0, 200.0, style);
668 assert!(result.is_ok());
669 }
670
671 #[test]
672 fn test_styled_table_with_only_header_text_color() {
673 let mut page = Page::a4();
674
675 let mut style = TableStyle::minimal();
676 style.header_text_color = Some(Color::red());
677 let headers = vec!["Test".to_string()];
680 let data = vec![vec!["Data".to_string()]];
681
682 let result = page.add_styled_table(headers, data, 50.0, 700.0, 200.0, style);
683 assert!(result.is_ok());
684 }
685
686 #[test]
687 fn test_styled_table_custom_font_size() {
688 let mut page = Page::a4();
689
690 let mut style = TableStyle::professional();
691 style.font_size = 16.0;
692
693 let headers = vec!["Big".to_string(), "Text".to_string()];
694 let data = vec![vec!["Large".to_string(), "Font".to_string()]];
695
696 let result = page.add_styled_table(headers, data, 50.0, 700.0, 300.0, style);
697 assert!(result.is_ok());
698 }
699
700 #[test]
701 fn test_all_styles_integration() {
702 let mut page = Page::a4();
703
704 let headers = vec!["A".to_string(), "B".to_string()];
705 let data = vec![vec!["1".to_string(), "2".to_string()]];
706
707 let styles = vec![
708 TableStyle::minimal(),
709 TableStyle::simple(),
710 TableStyle::professional(),
711 TableStyle::colorful(),
712 ];
713
714 for (i, style) in styles.into_iter().enumerate() {
715 let y = 700.0 - (i as f64 * 100.0);
716 let result =
717 page.add_styled_table(headers.clone(), data.clone(), 50.0, y, 200.0, style);
718 assert!(result.is_ok(), "Failed for style index {}", i);
719 }
720 }
721}