1use crate::document::format_coord;
2use crate::fonts::{BuiltinFont, FontRef};
3use crate::graphics::Color;
4use crate::textflow::{
5 break_word, line_height_for, measure_word, FitResult, Rect, TextStyle, UsedFonts, WordBreak,
6};
7use crate::truetype::TrueTypeFont;
8use crate::writer::escape_pdf_string;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum TextAlign {
17 #[default]
19 Left,
20 Center,
22 Right,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum CellOverflow {
29 Wrap,
31 Clip,
33 Shrink,
35}
36
37#[derive(Debug, Clone)]
39pub struct CellStyle {
40 pub background_color: Option<Color>,
42 pub text_color: Option<Color>,
44 pub font: FontRef,
46 pub font_size: f64,
48 pub padding: f64,
50 pub overflow: CellOverflow,
52 pub word_break: WordBreak,
54 pub text_align: TextAlign,
56}
57
58impl Default for CellStyle {
59 fn default() -> Self {
60 CellStyle {
61 background_color: None,
62 text_color: None,
63 font: FontRef::Builtin(BuiltinFont::Helvetica),
64 font_size: 10.0,
65 padding: 4.0,
66 overflow: CellOverflow::Wrap,
67 word_break: WordBreak::BreakAll,
68 text_align: TextAlign::Left,
69 }
70 }
71}
72
73#[derive(Clone)]
75pub struct Cell {
76 pub text: String,
78 pub style: CellStyle,
80 pub col_span: usize,
82}
83
84impl Cell {
85 pub fn new(text: impl Into<String>) -> Self {
87 Cell {
88 text: text.into(),
89 style: CellStyle::default(),
90 col_span: 1,
91 }
92 }
93
94 pub fn styled(text: impl Into<String>, style: CellStyle) -> Self {
96 Cell {
97 text: text.into(),
98 style,
99 col_span: 1,
100 }
101 }
102}
103
104#[derive(Clone)]
106pub struct Row {
107 pub cells: Vec<Cell>,
110 pub background_color: Option<Color>,
113 pub height: Option<f64>,
116}
117
118impl Row {
119 pub fn new(cells: Vec<Cell>) -> Self {
121 Row {
122 cells,
123 background_color: None,
124 height: None,
125 }
126 }
127}
128
129pub struct Table {
133 pub columns: Vec<f64>,
135 pub default_style: CellStyle,
138 pub border_color: Color,
140 pub border_width: f64,
142}
143
144impl Table {
145 pub fn new(columns: Vec<f64>) -> Self {
147 Table {
148 columns,
149 default_style: CellStyle::default(),
150 border_color: Color::rgb(0.0, 0.0, 0.0),
151 border_width: 0.5,
152 }
153 }
154
155 pub(crate) fn generate_row_ops(
160 &self,
161 row: &Row,
162 cursor: &mut TableCursor,
163 tt_fonts: &mut [TrueTypeFont],
164 ) -> (Vec<u8>, FitResult, UsedFonts) {
165 let row_height = measure_row_height(row, &self.columns, &self.default_style, tt_fonts);
166 let bottom = cursor.rect.y - cursor.rect.height;
167
168 if cursor.current_y - row_height < bottom {
169 let result = if cursor.first_row {
172 FitResult::BoxEmpty
173 } else {
174 FitResult::BoxFull
175 };
176 return (Vec::new(), result, UsedFonts::default());
177 }
178
179 let mut output: Vec<u8> = Vec::new();
180 let mut used = UsedFonts::default();
181
182 let segments = cell_segments(&row.cells, &self.columns, cursor.rect.x);
183
184 draw_row_backgrounds_with_segments(
185 row,
186 &segments,
187 cursor.rect.x,
188 cursor.current_y,
189 row_height,
190 &self.columns,
191 &mut output,
192 );
193
194 for (cell, (cell_x, cell_width)) in row.cells.iter().zip(segments.iter()) {
195 render_cell(
196 cell,
197 *cell_x,
198 cursor.current_y,
199 *cell_width,
200 row_height,
201 tt_fonts,
202 &mut output,
203 &mut used,
204 );
205 }
206
207 if self.border_width > 0.0 {
208 draw_row_borders(
209 &self.columns,
210 &row.cells,
211 cursor.rect.x,
212 cursor.current_y,
213 row_height,
214 self.border_color,
215 self.border_width,
216 &mut output,
217 );
218 }
219
220 cursor.current_y -= row_height;
221 cursor.first_row = false;
222
223 (output, FitResult::Stop, used)
224 }
225}
226
227pub struct TableCursor {
260 pub(crate) rect: Rect,
262 pub(crate) current_y: f64,
264 pub(crate) first_row: bool,
266}
267
268impl TableCursor {
269 pub fn new(rect: &Rect) -> Self {
271 TableCursor {
272 rect: *rect,
273 current_y: rect.y,
274 first_row: true,
275 }
276 }
277
278 pub fn reset(&mut self, rect: &Rect) {
280 self.rect = *rect;
281 self.current_y = rect.y;
282 self.first_row = true;
283 }
284
285 pub fn is_first_row(&self) -> bool {
290 self.first_row
291 }
292
293 pub fn current_y(&self) -> f64 {
299 self.current_y
300 }
301}
302
303fn cell_segments(cells: &[Cell], columns: &[f64], row_x: f64) -> Vec<(f64, f64)> {
309 let mut segments = Vec::with_capacity(cells.len());
310 let mut col_idx = 0;
311 let mut x = row_x;
312 for cell in cells {
313 let span = cell.col_span.max(1);
314 let end = (col_idx + span).min(columns.len());
315 let width: f64 = columns[col_idx..end].iter().sum();
316 segments.push((x, width));
317 x += width;
318 col_idx = end;
319 }
320 segments
321}
322
323fn visible_dividers(cells: &[Cell], columns: &[f64]) -> Vec<bool> {
328 let n = columns.len();
329 if n <= 1 {
330 return vec![];
331 }
332 let mut visible = vec![true; n - 1];
333 let mut col_idx = 0;
334 for cell in cells {
335 let span = cell.col_span.max(1);
336 for k in col_idx..(col_idx + span).saturating_sub(1) {
338 if k < visible.len() {
339 visible[k] = false;
340 }
341 }
342 col_idx += span;
343 }
344 visible
345}
346
347fn measure_row_height(
353 row: &Row,
354 columns: &[f64],
355 default_style: &CellStyle,
356 tt_fonts: &[TrueTypeFont],
357) -> f64 {
358 if let Some(h) = row.height {
359 return h;
360 }
361 let mut max_height = 0.0_f64;
362 let mut col_idx = 0;
363 for cell in &row.cells {
364 let span = cell.col_span.max(1);
365 let end = (col_idx + span).min(columns.len());
366 let cell_width: f64 = columns[col_idx..end].iter().sum();
367 let h = measure_cell_height(&cell.text, &cell.style, cell_width, tt_fonts);
368 max_height = max_height.max(h);
369 col_idx = end;
370 }
371 if max_height == 0.0 {
372 let ts = make_text_style(default_style);
374 line_height_for(&ts, tt_fonts) + 2.0 * default_style.padding
375 } else {
376 max_height
377 }
378}
379
380fn measure_cell_height(
382 text: &str,
383 style: &CellStyle,
384 col_width: f64,
385 tt_fonts: &[TrueTypeFont],
386) -> f64 {
387 let avail_width = col_width - 2.0 * style.padding;
388 let ts = make_text_style(style);
389 let lh = line_height_for(&ts, tt_fonts);
390 let lines = count_lines(text, avail_width, &ts, style.word_break, tt_fonts);
391 lines as f64 * lh + 2.0 * style.padding
392}
393
394fn make_text_style(style: &CellStyle) -> TextStyle {
396 TextStyle {
397 font: style.font,
398 font_size: style.font_size,
399 }
400}
401
402fn count_lines(
404 text: &str,
405 avail_width: f64,
406 style: &TextStyle,
407 word_break: WordBreak,
408 tt_fonts: &[TrueTypeFont],
409) -> usize {
410 if text.is_empty() {
411 return 1;
412 }
413 text.split('\n')
414 .map(|para| count_paragraph_lines(para, avail_width, style, word_break, tt_fonts))
415 .sum::<usize>()
416 .max(1)
417}
418
419fn count_paragraph_lines(
421 text: &str,
422 avail_width: f64,
423 style: &TextStyle,
424 word_break: WordBreak,
425 tt_fonts: &[TrueTypeFont],
426) -> usize {
427 let text = text.trim();
428 if text.is_empty() {
429 return 1;
430 }
431 let mut lines = 1usize;
432 let mut line_width = 0.0_f64;
433
434 for word in text.split_whitespace() {
435 let word_w = measure_word(word, style, tt_fonts);
436 let space_w = if line_width == 0.0 {
437 0.0
438 } else {
439 measure_word(" ", style, tt_fonts)
440 };
441 let needed = line_width + space_w + word_w;
442
443 if needed > avail_width && line_width > 0.0 {
444 lines += 1;
445 line_width = word_w;
446 if word_break != WordBreak::Normal && word_w > avail_width {
448 lines += count_break_lines(word, avail_width, style, word_break, tt_fonts) - 1;
449 line_width = trailing_piece_width(word, avail_width, style, word_break, tt_fonts);
450 }
451 } else if word_break != WordBreak::Normal && word_w > avail_width {
452 lines += count_break_lines(word, avail_width, style, word_break, tt_fonts) - 1;
454 line_width = trailing_piece_width(word, avail_width, style, word_break, tt_fonts);
455 } else {
456 line_width = needed;
457 }
458 }
459 lines
460}
461
462fn count_break_lines(
464 word: &str,
465 avail_width: f64,
466 style: &TextStyle,
467 word_break: WordBreak,
468 tt_fonts: &[TrueTypeFont],
469) -> usize {
470 break_word(word, avail_width, style, word_break, tt_fonts).len()
471}
472
473fn trailing_piece_width(
475 word: &str,
476 avail_width: f64,
477 style: &TextStyle,
478 word_break: WordBreak,
479 tt_fonts: &[TrueTypeFont],
480) -> f64 {
481 break_word(word, avail_width, style, word_break, tt_fonts)
482 .last()
483 .map_or(0.0, |p| measure_word(p, style, tt_fonts))
484}
485
486fn wrap_text(
488 text: &str,
489 avail_width: f64,
490 style: &TextStyle,
491 word_break: WordBreak,
492 tt_fonts: &[TrueTypeFont],
493) -> Vec<String> {
494 let mut lines: Vec<String> = Vec::new();
495 for para in text.split('\n') {
496 wrap_paragraph(
497 para.trim(),
498 avail_width,
499 style,
500 word_break,
501 tt_fonts,
502 &mut lines,
503 );
504 }
505 if lines.is_empty() {
506 lines.push(String::new());
507 }
508 lines
509}
510
511fn wrap_paragraph(
513 text: &str,
514 avail_width: f64,
515 style: &TextStyle,
516 word_break: WordBreak,
517 tt_fonts: &[TrueTypeFont],
518 out: &mut Vec<String>,
519) {
520 if text.is_empty() {
521 out.push(String::new());
522 return;
523 }
524 let mut current_line = String::new();
525 let mut line_width = 0.0_f64;
526
527 for word in text.split_whitespace() {
528 let word_w = measure_word(word, style, tt_fonts);
529 let space_w = if current_line.is_empty() {
530 0.0
531 } else {
532 measure_word(" ", style, tt_fonts)
533 };
534 let needed = line_width + space_w + word_w;
535
536 if needed > avail_width && !current_line.is_empty() {
537 out.push(current_line.clone());
538 current_line = String::new();
539 line_width = 0.0;
540 place_word_on_line(
542 word,
543 avail_width,
544 style,
545 word_break,
546 tt_fonts,
547 &mut current_line,
548 &mut line_width,
549 out,
550 );
551 } else if word_w > avail_width && word_break != WordBreak::Normal && current_line.is_empty()
552 {
553 place_word_on_line(
555 word,
556 avail_width,
557 style,
558 word_break,
559 tt_fonts,
560 &mut current_line,
561 &mut line_width,
562 out,
563 );
564 } else {
565 if !current_line.is_empty() {
566 current_line.push(' ');
567 }
568 current_line.push_str(word);
569 line_width = needed;
570 }
571 }
572 if !current_line.is_empty() {
573 out.push(current_line);
574 }
575}
576
577fn place_word_on_line(
583 word: &str,
584 avail_width: f64,
585 style: &TextStyle,
586 word_break: WordBreak,
587 tt_fonts: &[TrueTypeFont],
588 current_line: &mut String,
589 line_width: &mut f64,
590 out: &mut Vec<String>,
591) {
592 let word_w = measure_word(word, style, tt_fonts);
593
594 if word_w <= avail_width || word_break == WordBreak::Normal {
595 if !current_line.is_empty() {
596 current_line.push(' ');
597 }
598 current_line.push_str(word);
599 *line_width += word_w;
600 return;
601 }
602
603 let pieces = break_word(word, avail_width, style, word_break, tt_fonts);
604 let last_idx = pieces.len() - 1;
605 for (i, piece) in pieces.into_iter().enumerate() {
606 if i < last_idx {
607 out.push(piece);
608 } else {
609 *current_line = piece.clone();
610 *line_width = measure_word(&piece, style, tt_fonts);
611 }
612 }
613}
614
615fn pdf_font_name(font: FontRef, tt_fonts: &[TrueTypeFont]) -> String {
621 match font {
622 FontRef::Builtin(b) => b.pdf_name().to_string(),
623 FontRef::TrueType(id) => tt_fonts[id.0].pdf_name.clone(),
624 }
625}
626
627fn record_font(font: &FontRef, used: &mut UsedFonts) {
629 match font {
630 FontRef::Builtin(b) => {
631 used.builtin.insert(*b);
632 }
633 FontRef::TrueType(id) => {
634 used.truetype.insert(id.0);
635 }
636 }
637}
638
639fn emit_cell_text(text: &str, font: FontRef, tt_fonts: &mut [TrueTypeFont], output: &mut Vec<u8>) {
641 if text.is_empty() {
642 return;
643 }
644 match font {
645 FontRef::Builtin(_) => {
646 let escaped = escape_pdf_string(text);
647 output.extend_from_slice(format!("({}) Tj\n", escaped).as_bytes());
648 }
649 FontRef::TrueType(id) => {
650 let hex = tt_fonts[id.0].encode_text_hex(text);
651 output.extend_from_slice(format!("{} Tj\n", hex).as_bytes());
652 }
653 }
654}
655
656fn draw_row_backgrounds_with_segments(
661 row: &Row,
662 segments: &[(f64, f64)],
663 row_x: f64,
664 row_top: f64,
665 row_height: f64,
666 columns: &[f64],
667 output: &mut Vec<u8>,
668) {
669 let row_bottom = row_top - row_height;
670
671 if let Some(bg) = row.background_color {
672 let total_width: f64 = columns.iter().sum();
673 output.extend_from_slice(
674 format!(
675 "{} {} {} rg\n{} {} {} {} re\nf\n",
676 format_coord(bg.r),
677 format_coord(bg.g),
678 format_coord(bg.b),
679 format_coord(row_x),
680 format_coord(row_bottom),
681 format_coord(total_width),
682 format_coord(row_height),
683 )
684 .as_bytes(),
685 );
686 }
687
688 for (cell, &(cell_x, cell_width)) in row.cells.iter().zip(segments.iter()) {
689 if let Some(bg) = cell.style.background_color {
690 output.extend_from_slice(
691 format!(
692 "{} {} {} rg\n{} {} {} {} re\nf\n",
693 format_coord(bg.r),
694 format_coord(bg.g),
695 format_coord(bg.b),
696 format_coord(cell_x),
697 format_coord(row_bottom),
698 format_coord(cell_width),
699 format_coord(row_height),
700 )
701 .as_bytes(),
702 );
703 }
704 }
705}
706
707fn draw_row_borders(
711 columns: &[f64],
712 cells: &[Cell],
713 row_x: f64,
714 row_top: f64,
715 row_height: f64,
716 border_color: Color,
717 border_width: f64,
718 output: &mut Vec<u8>,
719) {
720 let row_bottom = row_top - row_height;
721 let total_width: f64 = columns.iter().sum();
722
723 output.extend_from_slice(b"q\n");
724 output.extend_from_slice(
725 format!(
726 "{} {} {} RG\n{} w\n",
727 format_coord(border_color.r),
728 format_coord(border_color.g),
729 format_coord(border_color.b),
730 format_coord(border_width),
731 )
732 .as_bytes(),
733 );
734
735 output.extend_from_slice(
737 format!(
738 "{} {} {} {} re\nS\n",
739 format_coord(row_x),
740 format_coord(row_bottom),
741 format_coord(total_width),
742 format_coord(row_height),
743 )
744 .as_bytes(),
745 );
746
747 let visible = visible_dividers(cells, columns);
749 let mut col_x = row_x;
750 for (k, &col_width) in columns[..columns.len().saturating_sub(1)]
751 .iter()
752 .enumerate()
753 {
754 col_x += col_width;
755 if visible.get(k).copied().unwrap_or(true) {
756 output.extend_from_slice(
757 format!(
758 "{} {} m\n{} {} l\nS\n",
759 format_coord(col_x),
760 format_coord(row_top),
761 format_coord(col_x),
762 format_coord(row_bottom),
763 )
764 .as_bytes(),
765 );
766 }
767 }
768
769 output.extend_from_slice(b"Q\n");
770}
771
772fn aligned_x(
774 line: &str,
775 align: TextAlign,
776 cell_x: f64,
777 col_width: f64,
778 padding: f64,
779 ts: &TextStyle,
780 tt_fonts: &[TrueTypeFont],
781) -> f64 {
782 match align {
783 TextAlign::Left => cell_x + padding,
784 TextAlign::Right => {
785 let line_w = measure_word(line, ts, tt_fonts);
786 cell_x + col_width - padding - line_w
787 }
788 TextAlign::Center => {
789 let avail = col_width - 2.0 * padding;
790 let line_w = measure_word(line, ts, tt_fonts);
791 cell_x + padding + (avail - line_w).max(0.0) / 2.0
792 }
793 }
794}
795
796fn render_cell(
801 cell: &Cell,
802 cell_x: f64,
803 row_top: f64,
804 col_width: f64,
805 row_height: f64,
806 tt_fonts: &mut [TrueTypeFont],
807 output: &mut Vec<u8>,
808 used: &mut UsedFonts,
809) {
810 let style = &cell.style;
811 let avail_width = (col_width - 2.0 * style.padding).max(0.0);
812 let avail_height = (row_height - 2.0 * style.padding).max(0.0);
813
814 let effective_font_size = if style.overflow == CellOverflow::Shrink {
816 shrink_font_size(
817 &cell.text,
818 style.font,
819 style.font_size,
820 avail_width,
821 avail_height,
822 style.word_break,
823 tt_fonts,
824 )
825 } else {
826 style.font_size
827 };
828
829 let ts = TextStyle {
830 font: style.font,
831 font_size: effective_font_size,
832 };
833 let lh = line_height_for(&ts, tt_fonts);
834 let lines = wrap_text(&cell.text, avail_width, &ts, style.word_break, tt_fonts);
835
836 output.extend_from_slice(b"q\n");
837
838 if style.overflow == CellOverflow::Clip {
840 let clip_bottom = row_top - row_height;
841 output.extend_from_slice(
842 format!(
843 "{} {} {} {} re\nW\nn\n",
844 format_coord(cell_x),
845 format_coord(clip_bottom),
846 format_coord(col_width),
847 format_coord(row_height),
848 )
849 .as_bytes(),
850 );
851 }
852
853 let first_line_y = row_top - style.padding - effective_font_size;
855
856 output.extend_from_slice(b"BT\n");
857
858 let text_color = style
862 .text_color
863 .unwrap_or_else(|| Color::rgb(0.0, 0.0, 0.0));
864 output.extend_from_slice(
865 format!(
866 "{} {} {} rg\n",
867 format_coord(text_color.r),
868 format_coord(text_color.g),
869 format_coord(text_color.b),
870 )
871 .as_bytes(),
872 );
873
874 let font_name = pdf_font_name(ts.font, tt_fonts);
875 output.extend_from_slice(
876 format!("/{} {} Tf\n", font_name, format_coord(effective_font_size)).as_bytes(),
877 );
878 record_font(&ts.font, used);
879
880 let align = style.text_align;
881 let mut current_x = cell_x + style.padding; for (i, line) in lines.iter().enumerate() {
884 let line_x = aligned_x(line, align, cell_x, col_width, style.padding, &ts, tt_fonts);
885 if i == 0 {
886 output.extend_from_slice(
887 format!(
888 "{} {} Td\n",
889 format_coord(line_x),
890 format_coord(first_line_y)
891 )
892 .as_bytes(),
893 );
894 } else {
895 let dx = line_x - current_x;
896 output.extend_from_slice(
897 format!("{} {} Td\n", format_coord(dx), format_coord(-lh)).as_bytes(),
898 );
899 }
900 current_x = line_x;
901 emit_cell_text(line, ts.font, tt_fonts, output);
902 }
903
904 output.extend_from_slice(b"ET\n");
905 output.extend_from_slice(b"Q\n");
906}
907
908fn shrink_font_size(
915 text: &str,
916 font: FontRef,
917 initial_size: f64,
918 avail_width: f64,
919 avail_height: f64,
920 word_break: WordBreak,
921 tt_fonts: &[TrueTypeFont],
922) -> f64 {
923 const MIN_FONT_SIZE: f64 = 4.0;
924 const STEP: f64 = 0.5;
925
926 let mut font_size = initial_size;
927 loop {
928 let ts = TextStyle { font, font_size };
929 let lh = line_height_for(&ts, tt_fonts);
930 let lines = count_lines(text, avail_width, &ts, word_break, tt_fonts);
931 let fits_height = lines as f64 * lh <= avail_height;
932 let fits_width = word_break != WordBreak::Normal
933 || text
934 .split_whitespace()
935 .all(|w| measure_word(w, &ts, tt_fonts) <= avail_width);
936 if (fits_height && fits_width) || font_size <= MIN_FONT_SIZE {
937 break;
938 }
939 font_size = (font_size - STEP).max(MIN_FONT_SIZE);
940 }
941 font_size
942}