use std::collections::HashMap;
use pdf_writer::Content;
use crate::fonts::{FontEntry, font_key};
use crate::model::{Alignment, CellVAlign, LineSpacing, SectionProperties, Table, VMerge};
use super::resolve_line_h;
use super::layout::{
LinkAnnotation, TextLine, build_paragraph_lines, font_metric, is_text_empty,
render_paragraph_lines,
};
fn auto_fit_columns(table: &Table, seen_fonts: &HashMap<String, FontEntry>) -> Vec<f32> {
let ncols = table.col_widths.len();
if ncols == 0 {
return table.col_widths.clone();
}
let mut min_widths = vec![0.0f32; ncols];
for row in &table.rows {
let mut grid_col = 0usize;
for cell in &row.cells {
let span = cell.grid_span.max(1) as usize;
if grid_col >= ncols || span > 1 {
grid_col += span;
continue;
}
for para in &cell.paragraphs {
for run in ¶.runs {
let key = font_key(run);
let Some(entry) = seen_fonts.get(&key) else {
continue;
};
let text = if run.caps || run.small_caps {
std::borrow::Cow::Owned(run.text.to_uppercase())
} else {
std::borrow::Cow::Borrowed(&run.text)
};
let fs = if run.small_caps {
(run.font_size - 2.0).max(1.0)
} else {
run.font_size
};
for word in text.split_whitespace() {
let ww = entry.word_width(word, fs);
min_widths[grid_col] = min_widths[grid_col].max(ww);
}
}
}
grid_col += span;
}
}
let total: f32 = table.col_widths.iter().sum();
let mut widths = table.col_widths.clone();
let mut extra_needed: f32 = 0.0;
let mut shrinkable: f32 = 0.0;
for i in 0..ncols {
if min_widths[i] > widths[i] {
extra_needed += min_widths[i] - widths[i];
widths[i] = min_widths[i];
} else {
shrinkable += widths[i] - min_widths[i];
}
}
if extra_needed > 0.0 && shrinkable > 0.0 {
let factor = extra_needed.min(shrinkable) / shrinkable;
for i in 0..ncols {
if widths[i] > min_widths[i] {
let available = widths[i] - min_widths[i];
widths[i] -= available * factor;
}
}
let new_total: f32 = widths.iter().sum();
if (new_total - total).abs() > 0.01 {
let scale = total / new_total;
for w in &mut widths {
*w *= scale;
}
}
}
widths
}
struct RowLayout {
height: f32,
cell_lines: Vec<(Vec<TextLine>, f32, f32)>, }
fn compute_row_layouts(
table: &Table,
col_widths: &[f32],
doc_line_spacing: LineSpacing,
seen_fonts: &HashMap<String, FontEntry>,
) -> Vec<RowLayout> {
let cm = &table.cell_margins;
table
.rows
.iter()
.map(|row| {
let mut max_h: f32 = 0.0;
let mut grid_col = 0usize;
let cell_lines: Vec<(Vec<TextLine>, f32, f32)> = row
.cells
.iter()
.map(|cell| {
let span = cell.grid_span.max(1) as usize;
let col_w: f32 = col_widths[grid_col..col_widths.len().min(grid_col + span)]
.iter()
.sum::<f32>()
.max(cell.width);
grid_col += span;
if cell.v_merge == VMerge::Continue {
return (vec![], 14.4, 12.0);
}
let cell_text_w = (col_w - cm.left - cm.right).max(0.0);
let mut total_h: f32 = cm.top + cm.bottom;
let mut all_lines = Vec::new();
let mut first_font_size = 12.0f32;
let mut first_line_h = 14.4f32;
for para in &cell.paragraphs {
let font_size = para.runs.first().map_or(12.0, |r| r.font_size);
let effective_ls = para.line_spacing.unwrap_or(doc_line_spacing);
let tallest_lhr = font_metric(¶.runs, seen_fonts, |e| e.line_h_ratio);
let line_h = resolve_line_h(effective_ls, font_size, tallest_lhr);
if all_lines.is_empty() {
first_font_size = font_size;
first_line_h = line_h;
}
if !is_text_empty(¶.runs) {
let lines = build_paragraph_lines(¶.runs, seen_fonts, cell_text_w, 0.0, &std::collections::HashMap::new());
total_h += lines.len() as f32 * line_h;
all_lines.extend(lines);
}
}
max_h = max_h.max(total_h);
(all_lines, first_line_h, first_font_size)
})
.collect();
let content_h = max_h + 0.5;
let height = match (row.height, row.height_exact) {
(Some(h), true) => h,
(Some(h), false) => content_h.max(h),
_ => content_h,
};
RowLayout {
height,
cell_lines,
}
})
.collect()
}
pub(super) fn render_table(
table: &Table,
sp: &SectionProperties,
doc_line_spacing: LineSpacing,
seen_fonts: &HashMap<String, FontEntry>,
content: &mut Content,
all_contents: &mut Vec<Content>,
all_page_links: &mut Vec<Vec<LinkAnnotation>>,
current_page_links: &mut Vec<LinkAnnotation>,
page_section_indices: &mut Vec<(usize, bool)>,
sect_idx: usize,
is_first_page_of_section: &mut bool,
slot_top: &mut f32,
prev_space_after: f32,
) {
let col_widths = auto_fit_columns(table, seen_fonts);
let row_layouts = compute_row_layouts(table, &col_widths, doc_line_spacing, seen_fonts);
let cm = &table.cell_margins;
let table_left = sp.margin_left + table.table_indent - cm.left;
*slot_top -= prev_space_after;
for (ri, (row, layout)) in table.rows.iter().zip(row_layouts.iter()).enumerate() {
let row_h = layout.height;
log::debug!(
"TABLE row={} row_h={:.2} cells={} slot_top={:.2}",
ri,
row_h,
layout.cell_lines.len(),
*slot_top
);
let at_page_top = (*slot_top - (sp.page_height - sp.margin_top)).abs() < 1.0;
if !at_page_top && *slot_top - row_h < sp.margin_bottom {
all_contents.push(std::mem::replace(content, Content::new()));
all_page_links.push(std::mem::take(current_page_links));
page_section_indices.push((sect_idx, *is_first_page_of_section));
*is_first_page_of_section = false;
*slot_top = sp.page_height - sp.margin_top;
}
let row_top = *slot_top;
let row_bottom = row_top - row_h;
let mut grid_col = 0usize;
for (cell, (lines, line_h, font_size)) in
row.cells.iter().zip(layout.cell_lines.iter())
{
let span = cell.grid_span.max(1) as usize;
let col_w: f32 = col_widths[grid_col..col_widths.len().min(grid_col + span)]
.iter()
.sum();
let cell_x = table_left
+ col_widths[..grid_col.min(col_widths.len())]
.iter()
.sum::<f32>();
grid_col += span;
if cell.v_merge == VMerge::Continue {
continue;
}
if let Some([r, g, b]) = cell.shading {
let b_borders = &cell.borders;
let inset = (b_borders.top.width + b_borders.bottom.width
+ b_borders.left.width + b_borders.right.width)
/ 8.0;
content.save_state();
content.set_fill_rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
content.rect(
cell_x + inset,
row_bottom + inset,
col_w - 2.0 * inset,
row_h - 2.0 * inset,
);
content.fill_nonzero();
content.restore_state();
}
if !lines.is_empty() && !lines.iter().all(|l| l.chunks.is_empty()) {
let text_x = cell_x + cm.left;
let text_w = (col_w - cm.left - cm.right).max(0.0);
let first_run = cell.paragraphs.first().and_then(|p| p.runs.first());
let ascender_ratio = first_run
.map(font_key)
.and_then(|k| seen_fonts.get(&k))
.and_then(|e| e.ascender_ratio)
.unwrap_or(0.75);
let content_h = lines.len() as f32 * line_h;
let baseline_y = match cell.v_align {
CellVAlign::Top => row_top - cm.top - font_size * ascender_ratio,
CellVAlign::Center => {
let avail = row_h - cm.top - cm.bottom;
let offset = (avail - content_h) / 2.0;
row_top - cm.top - offset.max(0.0) - font_size * ascender_ratio
}
CellVAlign::Bottom => {
let avail = row_h - cm.top - cm.bottom;
let offset = avail - content_h;
row_top - cm.top - offset.max(0.0) - font_size * ascender_ratio
}
};
let alignment = cell
.paragraphs
.first()
.map(|p| p.alignment)
.unwrap_or(Alignment::Left);
render_paragraph_lines(
content,
lines,
&alignment,
text_x,
text_w,
baseline_y,
*line_h,
lines.len(),
0,
&mut Vec::new(),
0.0,
seen_fonts,
);
}
}
let mut grid_col = 0usize;
for cell in &row.cells {
let span = cell.grid_span.max(1) as usize;
let col_w: f32 = col_widths[grid_col..col_widths.len().min(grid_col + span)]
.iter()
.sum();
let bx = table_left
+ col_widths[..grid_col.min(col_widths.len())]
.iter()
.sum::<f32>();
grid_col += span;
let b = &cell.borders;
let draw_border =
|content: &mut Content, border: &crate::model::CellBorder, x1, y1, x2, y2| {
if !border.present {
return;
}
content.save_state();
content.set_line_width(border.width);
if let Some([r, g, b]) = border.color {
content
.set_stroke_rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
}
content.move_to(x1, y1);
content.line_to(x2, y2);
content.stroke();
content.restore_state();
};
if cell.v_merge != VMerge::Continue {
draw_border(content, &b.top, bx, row_top, bx + col_w, row_top);
}
draw_border(content, &b.bottom, bx, row_bottom, bx + col_w, row_bottom);
draw_border(content, &b.left, bx, row_top, bx, row_bottom);
draw_border(content, &b.right, bx + col_w, row_top, bx + col_w, row_bottom);
}
*slot_top = row_bottom;
}
}