use super::cell_style::{BorderConfiguration, BorderStyle, CellAlignment, CellStyle};
use super::header_builder::HeaderBuilder;
use super::table_builder::{AdvancedTable, CellData, RowData};
use crate::error::PdfError;
use crate::graphics::Color;
use crate::page::Page;
use crate::text::{measure_text, Font};
pub struct TableRenderer {
pub default_row_height: f64,
pub default_header_height: f64,
pub auto_height: bool,
}
impl TableRenderer {
pub fn new() -> Self {
Self {
default_row_height: 25.0,
default_header_height: 30.0,
auto_height: true,
}
}
pub fn calculate_table_height(&self, table: &AdvancedTable) -> f64 {
let mut total_height = 0.0;
if table.show_header {
if let Some(header) = &table.header {
total_height += header.calculate_height();
} else if !table.columns.is_empty() {
total_height += self.default_header_height;
}
}
let mut rows_to_skip: usize = 0;
for row in &table.rows {
let row_height = row.min_height.unwrap_or(self.default_row_height);
if rows_to_skip > 0 {
rows_to_skip -= 1;
continue;
}
let max_rowspan = row.cells.iter().map(|cell| cell.rowspan).max().unwrap_or(1);
if max_rowspan > 1 {
total_height += row_height * max_rowspan as f64;
rows_to_skip = max_rowspan - 1;
} else {
total_height += row_height;
}
}
if table.table_border {
total_height += 2.0; }
total_height
}
pub fn render_table(
&self,
page: &mut Page,
table: &AdvancedTable,
x: f64,
y: f64,
) -> Result<f64, PdfError> {
table
.validate()
.map_err(|e| PdfError::InvalidOperation(e.to_string()))?;
let mut current_y = y;
if table.show_header {
if let Some(header) = &table.header {
current_y = self.render_header(page, table, header, x, current_y)?;
} else if !table.columns.is_empty() {
current_y = self.render_simple_header(page, table, x, current_y)?;
}
}
current_y = self.render_rows(page, table, x, current_y)?;
if table.table_border {
self.render_table_border(page, table, x, y, current_y)?;
}
Ok(current_y)
}
fn render_header(
&self,
page: &mut Page,
table: &AdvancedTable,
header: &HeaderBuilder,
x: f64,
start_y: f64,
) -> Result<f64, PdfError> {
let mut current_y = start_y;
let column_positions = self.calculate_column_positions(table, x);
for level in header.levels.iter() {
let row_height = self.default_header_height;
for cell in level {
let cell_x = column_positions[cell.start_col];
let cell_width = self.calculate_span_width(table, cell.start_col, cell.colspan);
let cell_height = row_height * cell.rowspan as f64;
let style = cell.style.as_ref().unwrap_or(&table.header_style);
self.render_cell(
page,
&cell.text,
cell_x,
current_y - cell_height,
cell_width,
cell_height,
style,
)?;
}
current_y -= row_height;
}
Ok(current_y)
}
fn render_simple_header(
&self,
page: &mut Page,
table: &AdvancedTable,
x: f64,
start_y: f64,
) -> Result<f64, PdfError> {
let column_positions = self.calculate_column_positions(table, x);
let header_height = self.default_header_height;
for (col_idx, column) in table.columns.iter().enumerate() {
let cell_x = column_positions[col_idx];
let cell_width = column.width;
self.render_cell(
page,
&column.header,
cell_x,
start_y - header_height,
cell_width,
header_height,
&table.header_style,
)?;
}
Ok(start_y - header_height)
}
fn calculate_content_row_height(
&self,
table: &AdvancedTable,
row: &RowData,
row_idx: usize,
) -> f64 {
let mut max_height = self.default_row_height;
let mut actual_col = 0usize;
for cell in row.cells.iter() {
let style = self.resolve_cell_style(table, cell, row_idx, actual_col);
let font = style.font.clone().unwrap_or(Font::Helvetica);
let font_size = style.font_size.unwrap_or(12.0);
let col_width: f64 = (actual_col..actual_col + cell.colspan)
.filter_map(|c| table.columns.get(c).map(|col| col.width))
.sum();
let available_width = col_width - style.padding.left - style.padding.right;
if available_width > 0.0 && style.text_wrap {
let lines =
self.wrap_text_to_lines(&cell.content, available_width, &font, font_size);
let line_height = font_size * 1.2;
let needed =
(lines.len() as f64 * line_height) + style.padding.top + style.padding.bottom;
if needed > max_height {
max_height = needed;
}
}
actual_col += cell.colspan;
}
max_height
}
fn render_rows(
&self,
page: &mut Page,
table: &AdvancedTable,
x: f64,
start_y: f64,
) -> Result<f64, PdfError> {
let mut current_y = start_y;
let column_positions = self.calculate_column_positions(table, x);
let num_cols = table.columns.len();
let mut rowspan_end: Vec<usize> = vec![0; num_cols];
for (row_idx, row) in table.rows.iter().enumerate() {
let base_height = row.min_height.unwrap_or(self.default_row_height);
let row_height = if self.auto_height {
let content_height = self.calculate_content_row_height(table, row, row_idx);
base_height.max(content_height)
} else {
base_height
};
let mut actual_col = 0usize;
for cell in row.cells.iter() {
while actual_col < num_cols && rowspan_end[actual_col] > row_idx {
actual_col += 1;
}
if actual_col >= column_positions.len() {
break;
}
let cell_x = column_positions[actual_col];
let cell_width = self.calculate_span_width(table, actual_col, cell.colspan);
let cell_height = row_height * cell.rowspan as f64;
let style = self.resolve_cell_style(table, cell, row_idx, actual_col);
self.render_cell(
page,
&cell.content,
cell_x,
current_y - cell_height,
cell_width,
cell_height,
&style,
)?;
if cell.rowspan > 1 {
for c in actual_col..(actual_col + cell.colspan).min(num_cols) {
rowspan_end[c] = row_idx + cell.rowspan;
}
}
actual_col += cell.colspan;
}
current_y -= row_height;
}
Ok(current_y)
}
#[allow(clippy::too_many_arguments)]
fn render_cell(
&self,
page: &mut Page,
content: &str,
x: f64,
y: f64,
width: f64,
height: f64,
style: &CellStyle,
) -> Result<(), PdfError> {
if let Some(bg_color) = style.background_color {
page.graphics()
.save_state()
.set_fill_color(bg_color)
.rectangle(x, y, width, height)
.fill()
.restore_state();
}
self.render_cell_borders(page, x, y, width, height, &style.border)?;
if !content.is_empty() {
self.render_cell_text(page, content, x, y, width, height, style)?;
}
Ok(())
}
fn render_cell_borders(
&self,
page: &mut Page,
x: f64,
y: f64,
width: f64,
height: f64,
border_config: &BorderConfiguration,
) -> Result<(), PdfError> {
let graphics = page.graphics();
if border_config.top.style != BorderStyle::None {
graphics
.save_state()
.set_stroke_color(border_config.top.color)
.set_line_width(border_config.top.width);
self.apply_line_style(graphics, border_config.top.style);
graphics
.move_to(x, y + height)
.line_to(x + width, y + height)
.stroke()
.restore_state();
}
if border_config.bottom.style != BorderStyle::None {
graphics
.save_state()
.set_stroke_color(border_config.bottom.color)
.set_line_width(border_config.bottom.width);
self.apply_line_style(graphics, border_config.bottom.style);
graphics
.move_to(x, y)
.line_to(x + width, y)
.stroke()
.restore_state();
}
if border_config.left.style != BorderStyle::None {
graphics
.save_state()
.set_stroke_color(border_config.left.color)
.set_line_width(border_config.left.width);
self.apply_line_style(graphics, border_config.left.style);
graphics
.move_to(x, y)
.line_to(x, y + height)
.stroke()
.restore_state();
}
if border_config.right.style != BorderStyle::None {
graphics
.save_state()
.set_stroke_color(border_config.right.color)
.set_line_width(border_config.right.width);
self.apply_line_style(graphics, border_config.right.style);
graphics
.move_to(x + width, y)
.line_to(x + width, y + height)
.stroke()
.restore_state();
}
Ok(())
}
fn apply_line_style(
&self,
_graphics: &mut crate::graphics::GraphicsContext,
_style: BorderStyle,
) {
}
fn truncate_text_to_width(
&self,
text: &str,
max_width: f64,
font: &Font,
font_size: f64,
) -> String {
let full_width = measure_text(text, font, font_size);
if full_width <= max_width {
return text.to_string();
}
let ellipsis = "...";
let ellipsis_width = measure_text(ellipsis, font, font_size);
if ellipsis_width > max_width {
return String::new();
}
if ellipsis_width == max_width {
return ellipsis.to_string();
}
let available_width = max_width - ellipsis_width;
let mut last_fit_end = 0usize;
let mut width_so_far = 0.0f64;
for (byte_pos, ch) in text.char_indices() {
let ch_len = ch.len_utf8();
let ch_str = &text[byte_pos..byte_pos + ch_len];
let ch_width = measure_text(ch_str, font, font_size);
if width_so_far + ch_width > available_width {
break;
}
width_so_far += ch_width;
last_fit_end = byte_pos + ch_len;
}
if last_fit_end == 0 {
ellipsis.to_string()
} else {
format!("{}{}", &text[..last_fit_end], ellipsis)
}
}
fn wrap_text_to_lines(
&self,
text: &str,
max_width: f64,
font: &Font,
font_size: f64,
) -> Vec<String> {
let mut lines = Vec::new();
for paragraph in text.split('\n') {
if paragraph.is_empty() {
lines.push(String::new());
continue;
}
let paragraph_width = measure_text(paragraph, font, font_size);
if paragraph_width <= max_width {
lines.push(paragraph.to_string());
continue;
}
let words: Vec<&str> = paragraph.split_whitespace().collect();
if words.is_empty() {
continue;
}
let mut current_line = String::new();
let mut current_line_width = 0.0f64;
let space_width = measure_text(" ", font, font_size);
for word in words {
let word_width = measure_text(word, font, font_size);
if current_line.is_empty() {
if word_width <= max_width {
current_line = word.to_string();
current_line_width = word_width;
} else {
let chars: Vec<char> = word.chars().collect();
let mut char_line = String::new();
let mut char_line_width = 0.0f64;
for c in chars {
let char_width = measure_text(&c.to_string(), font, font_size);
if char_line_width + char_width <= max_width {
char_line.push(c);
char_line_width += char_width;
} else {
if !char_line.is_empty() {
lines.push(char_line);
}
char_line = c.to_string();
char_line_width = char_width;
}
}
current_line = char_line;
current_line_width = char_line_width;
}
} else {
let test_width = current_line_width + space_width + word_width;
if test_width <= max_width {
current_line.push(' ');
current_line.push_str(word);
current_line_width = test_width;
} else {
lines.push(current_line);
if word_width <= max_width {
current_line = word.to_string();
current_line_width = word_width;
} else {
let chars: Vec<char> = word.chars().collect();
let mut char_line = String::new();
let mut char_line_width = 0.0f64;
for c in chars {
let char_width = measure_text(&c.to_string(), font, font_size);
if char_line_width + char_width <= max_width {
char_line.push(c);
char_line_width += char_width;
} else {
if !char_line.is_empty() {
lines.push(char_line);
}
char_line = c.to_string();
char_line_width = char_width;
}
}
current_line = char_line;
current_line_width = char_line_width;
}
}
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
#[allow(clippy::too_many_arguments)]
fn render_cell_text(
&self,
page: &mut Page,
content: &str,
x: f64,
y: f64,
width: f64,
height: f64,
style: &CellStyle,
) -> Result<(), PdfError> {
let font = style.font.clone().unwrap_or(Font::Helvetica);
let font_size = style.font_size.unwrap_or(12.0);
let text_color = style.text_color.unwrap_or(Color::black());
let available_width = width - style.padding.left - style.padding.right;
if available_width <= 0.0 {
return Ok(());
}
if style.text_wrap {
let lines = self.wrap_text_to_lines(content, available_width, &font, font_size);
if lines.is_empty() || (lines.len() == 1 && lines[0].is_empty()) {
return Ok(());
}
let line_height = font_size * 1.2;
let total_text_height = lines.len() as f64 * line_height;
let available_height = height - style.padding.top - style.padding.bottom;
let text_block_top =
y + height - style.padding.top - (available_height - total_text_height) / 2.0;
for (line_idx, line) in lines.iter().enumerate() {
if line.is_empty() {
continue;
}
let text_x = match style.alignment {
CellAlignment::Left => x + style.padding.left,
CellAlignment::Center => {
let line_width = measure_text(line, &font, font_size);
x + style.padding.left + (available_width - line_width) / 2.0
}
CellAlignment::Right => {
let line_width = measure_text(line, &font, font_size);
x + width - style.padding.right - line_width
}
CellAlignment::Justify => x + style.padding.left,
};
let text_y = text_block_top - (line_idx as f64 + 0.8) * line_height;
if text_y >= y + style.padding.bottom {
page.text()
.set_font(font.clone(), font_size)
.set_fill_color(text_color)
.at(text_x, text_y)
.write(line)?;
}
}
} else {
let display_text =
self.truncate_text_to_width(content, available_width, &font, font_size);
let text_x = match style.alignment {
CellAlignment::Left => x + style.padding.left,
CellAlignment::Center => {
let text_width = measure_text(&display_text, &font, font_size);
x + style.padding.left + (available_width - text_width) / 2.0
}
CellAlignment::Right => {
let text_width = measure_text(&display_text, &font, font_size);
x + width - style.padding.right - text_width
}
CellAlignment::Justify => x + style.padding.left,
};
let text_y = style
.padding
.pad_vertically(&page.coordinate_system(), y + height / 2.0);
if !display_text.is_empty() {
let text_obj = page
.text()
.set_font(font, font_size)
.set_fill_color(text_color);
text_obj.at(text_x, text_y).write(&display_text)?;
}
}
Ok(())
}
fn calculate_column_positions(&self, table: &AdvancedTable, start_x: f64) -> Vec<f64> {
let mut positions = Vec::new();
let mut current_x = start_x;
for column in &table.columns {
positions.push(current_x);
current_x += column.width + table.cell_spacing;
}
positions
}
fn calculate_span_width(&self, table: &AdvancedTable, start_col: usize, colspan: usize) -> f64 {
let mut total_width = 0.0;
for i in 0..colspan {
if let Some(column) = table.columns.get(start_col + i) {
total_width += column.width;
if i > 0 {
total_width += table.cell_spacing;
}
}
}
total_width
}
fn resolve_cell_style(
&self,
table: &AdvancedTable,
cell: &CellData,
row_idx: usize,
col_idx: usize,
) -> CellStyle {
if let Some(cell_style) = &cell.style {
return cell_style.clone();
}
table.get_cell_style(row_idx, col_idx)
}
fn render_table_border(
&self,
page: &mut Page,
table: &AdvancedTable,
x: f64,
start_y: f64,
end_y: f64,
) -> Result<(), PdfError> {
let total_width = table.calculate_width();
let height = start_y - end_y;
page.graphics()
.save_state()
.set_stroke_color(Color::black())
.set_line_width(1.0)
.rectangle(x, end_y, total_width, height)
.stroke()
.restore_state();
Ok(())
}
}
impl Default for TableRenderer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::Font;
#[test]
fn test_truncate_text_to_width_no_truncation_needed() {
let renderer = TableRenderer::new();
let text = "Short";
let max_width = 100.0;
let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
assert_eq!(result, "Short");
}
#[test]
fn test_truncate_text_to_width_with_truncation() {
let renderer = TableRenderer::new();
let text = "This is a very long text that should be truncated";
let max_width = 50.0; let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
assert!(result.ends_with("..."));
assert!(result.len() < text.len());
let truncated_width = measure_text(&result, &font, font_size);
assert!(truncated_width <= max_width);
}
#[test]
fn test_truncate_text_to_width_empty_when_too_narrow() {
let renderer = TableRenderer::new();
let text = "Any text";
let max_width = 5.0; let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
assert_eq!(result, "");
}
#[test]
fn test_truncate_text_to_width_exactly_ellipsis_width() {
let renderer = TableRenderer::new();
let text = "Some text";
let font = Font::Helvetica;
let font_size = 12.0;
let ellipsis_width = measure_text("...", &font, font_size);
let result = renderer.truncate_text_to_width(text, ellipsis_width, &font, font_size);
assert_eq!(result, "...");
}
#[test]
fn test_truncate_text_to_width_single_character() {
let renderer = TableRenderer::new();
let text = "A";
let max_width = 50.0;
let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
assert_eq!(result, "A");
}
#[test]
fn test_truncate_text_to_width_different_fonts() {
let renderer = TableRenderer::new();
let text = "This text will be truncated";
let max_width = 60.0;
let font_size = 12.0;
let helvetica_result =
renderer.truncate_text_to_width(text, max_width, &Font::Helvetica, font_size);
let courier_result =
renderer.truncate_text_to_width(text, max_width, &Font::Courier, font_size);
let times_result =
renderer.truncate_text_to_width(text, max_width, &Font::TimesRoman, font_size);
for result in [&helvetica_result, &courier_result, ×_result] {
assert!(result.ends_with("..."));
assert!(result.len() < text.len());
}
assert!(!helvetica_result.is_empty());
assert!(!courier_result.is_empty());
assert!(!times_result.is_empty());
}
#[test]
fn test_truncate_text_to_width_empty_input() {
let renderer = TableRenderer::new();
let text = "";
let max_width = 100.0;
let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
assert_eq!(result, "");
}
#[test]
fn test_truncate_text_to_width_unicode_characters() {
let renderer = TableRenderer::new();
let text = "Héllö Wørld with ümlauts and émojis 🚀🎉";
let max_width = 80.0;
let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
if result != text {
assert!(result.ends_with("..."));
}
let result_width = measure_text(&result, &font, font_size);
assert!(result_width <= max_width);
}
#[test]
fn test_truncate_linear_short_text_fits() {
let renderer = TableRenderer::new();
let text = "Hi";
let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, 500.0, &font, font_size);
assert_eq!(result, "Hi");
}
#[test]
fn test_truncate_linear_overflow_adds_ellipsis() {
let renderer = TableRenderer::new();
let text = "This is a long sentence that will not fit";
let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, 40.0, &font, font_size);
assert!(
result.ends_with("..."),
"Expected ellipsis suffix, got: {result}"
);
assert!(result.len() < text.len());
let result_width = measure_text(&result, &font, font_size);
assert!(result_width <= 40.0, "Truncated text exceeds max_width");
}
#[test]
fn test_truncate_linear_unicode() {
let renderer = TableRenderer::new();
let text = "日本語テスト文字列";
let font = Font::Helvetica;
let font_size = 12.0;
let result = renderer.truncate_text_to_width(text, 50.0, &font, font_size);
assert!(std::str::from_utf8(result.as_bytes()).is_ok());
if result != text {
assert!(
result.ends_with("..."),
"Truncated unicode should end with ellipsis"
);
}
let result_width = measure_text(&result, &font, font_size);
assert!(result_width <= 50.0);
}
#[test]
fn test_wrap_text_to_lines_no_wrap_needed() {
let renderer = TableRenderer::new();
let text = "Short text";
let max_width = 200.0;
let font = Font::Helvetica;
let font_size = 12.0;
let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "Short text");
}
#[test]
fn test_wrap_text_to_lines_simple_wrap() {
let renderer = TableRenderer::new();
let text = "This is a longer text that should wrap to multiple lines";
let max_width = 80.0; let font = Font::Helvetica;
let font_size = 12.0;
let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
assert!(lines.len() > 1, "Text should wrap to multiple lines");
for line in &lines {
let line_width = measure_text(line, &font, font_size);
assert!(
line_width <= max_width + 1.0, "Line '{}' exceeds max_width (width: {}, max: {})",
line,
line_width,
max_width
);
}
}
#[test]
fn test_wrap_text_to_lines_preserves_newlines() {
let renderer = TableRenderer::new();
let text = "Line one\nLine two\nLine three";
let max_width = 200.0; let font = Font::Helvetica;
let font_size = 12.0;
let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "Line one");
assert_eq!(lines[1], "Line two");
assert_eq!(lines[2], "Line three");
}
#[test]
fn test_wrap_text_to_lines_empty_input() {
let renderer = TableRenderer::new();
let text = "";
let max_width = 100.0;
let font = Font::Helvetica;
let font_size = 12.0;
let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "");
}
#[test]
fn test_wrap_text_to_lines_single_word_too_long() {
let renderer = TableRenderer::new();
let text = "Supercalifragilisticexpialidocious";
let max_width = 50.0; let font = Font::Helvetica;
let font_size = 12.0;
let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
assert!(lines.len() >= 1, "Should produce at least one line");
let joined: String = lines.join("");
assert_eq!(joined, text);
}
#[test]
fn test_wrap_text_to_lines_multiple_spaces() {
let renderer = TableRenderer::new();
let text = "Word with spaces";
let max_width = 200.0;
let font = Font::Helvetica;
let font_size = 12.0;
let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
assert_eq!(lines.len(), 1);
assert!(!lines[0].is_empty());
}
#[test]
fn test_wrap_text_to_lines_unicode() {
let renderer = TableRenderer::new();
let text = "日本語テキスト with English words";
let max_width = 100.0;
let font = Font::Helvetica;
let font_size = 12.0;
let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
assert!(!lines.is_empty());
}
}