use std::collections::HashMap;
use pdf_writer::{Content, Name, Str};
use crate::fonts::{FontEntry, encode_as_gids, font_key_buf, to_winansi_bytes};
use crate::model::{
Alignment, Block, BorderStyle, CellBorder, CellMargins, CellVAlign, SectionProperties, Table,
TableAlignment, TableRow, TextDirection, VMerge,
};
use super::RenderContext;
use super::header_footer::substitute_hf_runs;
use super::layout::{
TextLine, build_paragraph_lines, encode_text_for_pdf, font_metric, is_text_empty,
render_paragraph_lines,
};
use super::resolve_line_h;
fn cell_span_width(col_widths: &[f32], grid_col: usize, span: usize) -> f32 {
col_widths[grid_col..col_widths.len().min(grid_col + span)]
.iter()
.sum()
}
fn cell_x_offset(col_widths: &[f32], table_left: f32, grid_col: usize) -> f32 {
table_left
+ col_widths[..grid_col.min(col_widths.len())]
.iter()
.sum::<f32>()
}
fn draw_border(content: &mut Content, border: &CellBorder, x1: f32, y1: f32, x2: f32, y2: f32) {
if !border.present {
return;
}
let w = border.width;
content.save_state();
content.set_line_width(w);
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);
}
match border.style {
BorderStyle::Dotted => {
content.set_line_cap(pdf_writer::types::LineCapStyle::RoundCap);
content.set_dash_pattern([0.0, w * 3.0], 0.0);
}
BorderStyle::Dashed | BorderStyle::DashSmallGap => {
let dash = if border.style == BorderStyle::DashSmallGap {
w * 3.0
} else {
w * 4.0
};
content.set_dash_pattern([dash, w * 2.0], 0.0);
}
BorderStyle::DashDot => {
content.set_dash_pattern([w * 4.0, w * 2.0, 0.0, w * 2.0], 0.0);
}
BorderStyle::DashDotDot => {
content.set_dash_pattern(
[w * 4.0, w * 2.0, 0.0, w * 2.0, 0.0, w * 2.0],
0.0,
);
}
BorderStyle::Double => {
let gap = w.max(0.75);
let thin = (w / 3.0).max(0.25);
content.set_line_width(thin);
content.move_to(x1, y1);
content.line_to(x2, y2);
content.stroke();
let dx = if (x1 - x2).abs() < 0.01 { gap } else { 0.0 };
let dy = if (y1 - y2).abs() < 0.01 { gap } else { 0.0 };
content.move_to(x1 - dx, y1 - dy);
content.line_to(x2 - dx, y2 - dy);
content.stroke();
content.restore_state();
return;
}
BorderStyle::Single => {}
}
content.move_to(x1, y1);
content.line_to(x2, y2);
content.stroke();
content.restore_state();
}
fn draw_cell_borders(
content: &mut Content,
borders: &crate::model::CellBorders,
bx: f32,
top: f32,
bottom: f32,
col_w: f32,
draw_top: bool,
draw_bottom: bool,
) {
if draw_top {
draw_border(content, &borders.top, bx, top, bx + col_w, top);
}
if draw_bottom {
draw_border(content, &borders.bottom, bx, bottom, bx + col_w, bottom);
}
draw_border(content, &borders.left, bx, top, bx, bottom);
draw_border(content, &borders.right, bx + col_w, top, bx + col_w, bottom);
}
fn draw_cell_shading(
content: &mut Content,
shading: [u8; 3],
borders: &crate::model::CellBorders,
x: f32,
y: f32,
w: f32,
h: f32,
) {
let inset =
(borders.top.width + borders.bottom.width + borders.left.width + borders.right.width) / 8.0;
content.save_state();
content.set_fill_rgb(
shading[0] as f32 / 255.0,
shading[1] as f32 / 255.0,
shading[2] as f32 / 255.0,
);
content.rect(x + inset, y + inset, w - 2.0 * inset, h - 2.0 * inset);
content.fill_nonzero();
content.restore_state();
}
fn valign_offset(v_align: CellVAlign, available: f32, content_h: f32) -> f32 {
match v_align {
CellVAlign::Top => 0.0,
CellVAlign::Center => ((available - content_h) / 2.0).max(0.0),
CellVAlign::Bottom => (available - content_h).max(0.0),
}
}
fn para_has_visible_content(para: &CellParagraphLayout) -> bool {
!para.list_label.is_empty()
|| (!para.lines.is_empty() && para.lines.iter().any(|l| !l.chunks.is_empty()))
}
fn para_block_height(p: &CellParagraphLayout) -> f32 {
if p.lines.is_empty() {
if p.paragraph_mark_vanish {
0.0
} else if p.content_height > 0.0 {
p.content_height
} else {
p.line_h
}
} else {
p.lines.len() as f32 * p.line_h
}
}
fn cell_has_visible_content(items: &[CellContentItem]) -> bool {
items.iter().any(|item| match item {
CellContentItem::Paragraph(p) => para_has_visible_content(p),
CellContentItem::NestedTable { height } => *height > 0.0,
})
}
fn render_cell_content(
content: &mut Content,
items: &[CellContentItem],
blocks: &[Block],
cell_x: f32,
col_w: f32,
cursor_y_start: f32,
cm: &CellMargins,
ctx: &RenderContext,
) {
let mut cursor_y = cursor_y_start;
let mut block_idx = 0;
for item in items {
match item {
CellContentItem::Paragraph(para) => {
while block_idx < blocks.len() {
if matches!(&blocks[block_idx], Block::Paragraph(_)) {
block_idx += 1;
break;
}
block_idx += 1;
}
if !para_has_visible_content(para) && para.image_name.is_none() {
cursor_y -= para.space_before + para_block_height(para);
continue;
}
cursor_y -= para.space_before;
if let Some(ref img_name) = para.image_name {
let img_x = cell_x + cm.left;
let img_y = cursor_y - para.image_height;
content.save_state();
content.transform([
para.image_width,
0.0,
0.0,
para.image_height,
img_x,
img_y,
]);
content.x_object(Name(img_name.as_bytes()));
content.restore_state();
cursor_y -= para.content_height;
continue;
}
let text_x = cell_x + cm.left + para.indent_left;
let text_w = (col_w - cm.left - cm.right - para.indent_left).max(0.0);
let baseline_y = cursor_y - para.font_size * para.ascender_ratio;
if !para.list_label.is_empty() {
let label_x = cell_x + cm.left + para.indent_left - para.indent_hanging;
draw_cell_label(content, para, label_x, baseline_y, ctx.fonts);
}
render_paragraph_lines(
content,
¶.lines,
¶.alignment,
text_x,
text_w,
baseline_y,
para.line_h,
para.lines.len(),
0,
&mut Vec::new(),
0.0,
ctx.fonts,
None,
);
cursor_y -= para.lines.len() as f32 * para.line_h;
}
CellContentItem::NestedTable { height } => {
let table = loop {
if block_idx >= blocks.len() {
break None;
}
if let Block::Table(t) = &blocks[block_idx] {
block_idx += 1;
break Some(t);
}
block_idx += 1;
};
if let Some(table) = table {
render_nested_table(table, content, cell_x + cm.left, col_w - cm.left - cm.right, &mut cursor_y, ctx);
} else {
cursor_y -= height;
}
}
}
}
}
fn render_nested_table(
table: &Table,
content: &mut Content,
available_x: f32,
available_w: f32,
cursor_y: &mut f32,
ctx: &RenderContext,
) {
let col_widths = auto_fit_columns(table, ctx.fonts);
let row_layouts = compute_row_layouts(table, &col_widths, ctx, None);
let cm = &table.cell_margins;
let table_total_w: f32 = col_widths.iter().sum();
let table_left = match table.alignment {
TableAlignment::Center => available_x + (available_w - table_total_w) / 2.0,
TableAlignment::Right => available_x + available_w - table_total_w,
TableAlignment::Left => available_x + table.table_indent - cm.left,
};
let merge_spans = compute_merge_spans(table, &row_layouts);
for (ri, (row, layout)) in table.rows.iter().zip(row_layouts.iter()).enumerate() {
let row_h = layout.height;
let row_top = *cursor_y;
let row_bottom = row_top - row_h;
let mut grid_col = 0usize;
for (cell, cell_layout) in row.cells.iter().zip(layout.cells.iter()) {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(&col_widths, grid_col, span);
let cx = cell_x_offset(&col_widths, table_left, grid_col);
let cell_grid_col = grid_col;
grid_col += span;
if cell.v_merge == VMerge::Continue {
continue;
}
let merge_extra = merge_spans
.get(&(ri, cell_grid_col))
.copied()
.unwrap_or(0.0);
let effective_h = row_h + merge_extra;
if let Some(shading) = cell.shading {
content.save_state();
content.set_fill_rgb(
shading[0] as f32 / 255.0,
shading[1] as f32 / 255.0,
shading[2] as f32 / 255.0,
);
content.rect(cx, row_bottom, col_w, row_h);
content.fill_nonzero();
content.restore_state();
}
if cell_has_visible_content(&cell_layout.items) {
let ecm = cell.cell_margins.as_ref().unwrap_or(cm);
let content_h: f32 = cell_layout
.items
.iter()
.map(|item| match item {
CellContentItem::Paragraph(p) => {
p.space_before + para_block_height(p)
}
CellContentItem::NestedTable { height } => *height,
})
.sum();
let avail = effective_h - ecm.top - ecm.bottom;
let v_offset = valign_offset(cell.v_align, avail, content_h);
let cell_cursor_y = row_top - ecm.top - v_offset;
render_cell_content(
content,
&cell_layout.items,
&cell.content,
cx,
col_w,
cell_cursor_y,
ecm,
ctx,
);
}
}
let mut grid_col = 0usize;
for cell in &row.cells {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(&col_widths, grid_col, span);
let bx = cell_x_offset(&col_widths, table_left, grid_col);
grid_col += span;
let draw_top = cell.v_merge != VMerge::Continue && ri == 0;
draw_cell_borders(
content,
&cell.borders,
bx,
row_top,
row_bottom,
col_w,
draw_top,
true,
);
}
*cursor_y = row_bottom;
}
}
fn render_partial_cell_content(
content: &mut Content,
items: &[CellContentItem],
blocks: &[Block],
start: usize,
end: usize,
cell_x: f32,
col_w: f32,
cursor_y_start: f32,
cm: &CellMargins,
ctx: &RenderContext,
) {
let mut cursor_y = cursor_y_start;
let mut block_idx = 0usize;
let mut item_to_block: Vec<usize> = Vec::new();
for item in items {
item_to_block.push(block_idx);
match item {
CellContentItem::Paragraph(_) => {
while block_idx < blocks.len() {
if matches!(&blocks[block_idx], Block::Paragraph(_)) {
block_idx += 1;
break;
}
block_idx += 1;
}
}
CellContentItem::NestedTable { .. } => {
while block_idx < blocks.len() {
if matches!(&blocks[block_idx], Block::Table(_)) {
block_idx += 1;
break;
}
block_idx += 1;
}
}
}
}
for pi in start..end {
match &items[pi] {
CellContentItem::Paragraph(para) => {
let sb = if pi == start { 0.0 } else { para.space_before };
if !para_has_visible_content(para) {
cursor_y -= sb + para_block_height(para);
continue;
}
cursor_y -= sb;
let text_x = cell_x + cm.left + para.indent_left;
let text_w = (col_w - cm.left - cm.right - para.indent_left).max(0.0);
let baseline_y = cursor_y - para.font_size * para.ascender_ratio;
if !para.list_label.is_empty() {
let label_x = cell_x + cm.left + para.indent_left - para.indent_hanging;
draw_cell_label(content, para, label_x, baseline_y, ctx.fonts);
}
render_paragraph_lines(
content,
¶.lines,
¶.alignment,
text_x,
text_w,
baseline_y,
para.line_h,
para.lines.len(),
0,
&mut Vec::new(),
0.0,
ctx.fonts,
None,
);
cursor_y -= para.lines.len() as f32 * para.line_h;
}
CellContentItem::NestedTable { height } => {
let bi = item_to_block.get(pi).copied().unwrap_or(0);
if let Some(Block::Table(table)) = blocks.get(bi) {
render_nested_table(
table, content, cell_x + cm.left, col_w - cm.left - cm.right,
&mut cursor_y, ctx,
);
} else {
cursor_y -= height;
}
}
}
}
}
fn auto_fit_columns(table: &Table, 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;
}
let mut key_buf = String::new();
for para in cell.all_paragraphs() {
for run in ¶.runs {
let key = font_key_buf(run, &mut key_buf);
let Some(entry) = 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 kern = run.kern_threshold.is_some_and(|t| fs >= t);
let ww = entry.word_width(word, fs, kern);
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 CellParagraphLayout {
lines: Vec<TextLine>,
line_h: f32,
font_size: f32,
ascender_ratio: f32,
alignment: Alignment,
space_before: f32,
indent_left: f32,
indent_right: f32,
indent_hanging: f32,
list_label: String,
list_label_font: Option<String>,
label_color: Option<[u8; 3]>,
first_run_font_key: String,
image_name: Option<String>,
image_width: f32,
image_height: f32,
content_height: f32,
paragraph_mark_vanish: bool,
}
enum CellContentItem {
Paragraph(CellParagraphLayout),
NestedTable { height: f32 },
}
struct CellLayout {
items: Vec<CellContentItem>,
total_height: f32,
text_direction: TextDirection,
}
struct RowLayout {
height: f32,
cells: Vec<CellLayout>,
}
fn encode_label(label: &str, entry: &FontEntry) -> Vec<u8> {
match &entry.char_to_gid {
Some(map) => encode_as_gids(label, map),
None => to_winansi_bytes(label),
}
}
fn draw_cell_label(
content: &mut Content,
para: &CellParagraphLayout,
label_x: f32,
baseline_y: f32,
fonts: &HashMap<String, FontEntry>,
) {
let font_key = para
.list_label_font
.as_deref()
.unwrap_or(para.first_run_font_key.as_str());
let Some(entry) = fonts.get(font_key) else {
return;
};
let bytes = encode_label(¶.list_label, entry);
if let Some([r, g, b]) = para.label_color {
content.set_fill_rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
}
content
.begin_text()
.set_font(Name(entry.pdf_name.as_bytes()), para.font_size)
.next_line(label_x, baseline_y)
.show(Str(&bytes))
.end_text();
if para.label_color.is_some() {
content.set_fill_gray(0.0);
}
}
struct HfSubstitution<'a> {
page_num: usize,
total_pages: usize,
styleref_values: &'a HashMap<String, String>,
page_num_format: Option<&'a str>,
}
fn compute_row_layouts(
table: &Table,
col_widths: &[f32],
ctx: &RenderContext,
hf_sub: Option<&HfSubstitution>,
) -> 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 cells: Vec<CellLayout> = row
.cells
.iter()
.map(|cell| {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(col_widths, grid_col, span).max(cell.width);
grid_col += span;
if cell.v_merge == VMerge::Continue {
return CellLayout {
items: vec![],
total_height: 14.4,
text_direction: TextDirection::LrTb,
};
}
let ecm = cell.cell_margins.as_ref().unwrap_or(cm);
let is_rotated = cell.text_direction != TextDirection::LrTb;
let cell_text_w = if is_rotated {
10000.0
} else {
(col_w - ecm.left - ecm.right).max(0.0)
};
let mut total_h: f32 = ecm.top + ecm.bottom;
let mut max_rotated_line_w: f32 = 0.0;
let mut items: Vec<CellContentItem> = Vec::new();
let mut prev_space_after = 0.0f32;
let mut para_idx = 0usize;
for block in &cell.content {
match block {
Block::Paragraph(para) => {
let substituted;
let runs = if let Some(sub) = hf_sub {
substituted = substitute_hf_runs(
¶.runs,
sub.page_num,
sub.total_pages,
sub.styleref_values,
sub.page_num_format,
);
&substituted
} else {
¶.runs
};
let font_size = runs.first().map_or(12.0, |r| r.font_size);
let effective_ls =
para.line_spacing.unwrap_or(ctx.doc_line_spacing);
let tallest_lhr =
font_metric(runs, ctx.fonts, |e| e.line_h_ratio);
let line_h =
resolve_line_h(effective_ls, font_size, tallest_lhr);
let space_before = if para_idx > 0 {
f32::max(prev_space_after, para.space_before)
} else {
para.space_before
};
total_h += space_before;
let mut kb = String::new();
let ascender_ratio = runs
.first()
.map(|r| font_key_buf(r, &mut kb))
.and_then(|k| ctx.fonts.get(k))
.and_then(|e| e.ascender_ratio)
.unwrap_or(0.75);
let lines = if !is_text_empty(runs) {
let para_text_w = (cell_text_w
- para.indent_left
- para.indent_right)
.max(0.0);
let lines = build_paragraph_lines(
runs,
ctx.fonts,
para_text_w,
para.indent_hanging,
&std::collections::HashMap::new(),
None,
None,
);
if is_rotated {
for line in &lines {
max_rotated_line_w =
max_rotated_line_w.max(line.total_width);
}
}
total_h += lines.len() as f32 * line_h;
lines
} else {
if para.paragraph_mark_vanish {
} else if para.content_height > 0.0 {
total_h += para.content_height;
} else {
total_h += line_h;
}
vec![]
};
let first_run_font_key = runs
.first()
.map(|r| {
let mut kb2 = String::new();
font_key_buf(r, &mut kb2).to_owned()
})
.unwrap_or_default();
let image_name = para.image.as_ref().and_then(|img| {
let key = std::sync::Arc::as_ptr(&img.data) as usize;
ctx.table_cell_image_names.get(&key).cloned()
});
let (image_width, image_height) = para
.image
.as_ref()
.map(|img| (img.display_width, img.display_height))
.unwrap_or((0.0, 0.0));
items.push(CellContentItem::Paragraph(CellParagraphLayout {
lines,
line_h,
font_size,
ascender_ratio,
alignment: para.alignment,
space_before,
indent_left: para.indent_left,
indent_right: para.indent_right,
indent_hanging: para.indent_hanging,
list_label: para.list_label.clone(),
list_label_font: para.list_label_font.clone(),
label_color: para.runs.first().and_then(|r| r.color),
first_run_font_key,
image_name,
image_width,
image_height,
content_height: para.content_height,
paragraph_mark_vanish: para.paragraph_mark_vanish,
}));
prev_space_after = para.space_after;
para_idx += 1;
}
Block::Table(nested_table) => {
let nested_cw = auto_fit_columns(nested_table, ctx.fonts);
let nested_layouts =
compute_row_layouts(nested_table, &nested_cw, ctx, hf_sub);
let nested_h: f32 =
nested_layouts.iter().map(|rl| rl.height).sum();
total_h += nested_h;
items.push(CellContentItem::NestedTable { height: nested_h });
prev_space_after = 0.0;
para_idx += 1;
}
}
}
total_h += prev_space_after;
if is_rotated {
total_h = ecm.top + ecm.bottom + max_rotated_line_w;
}
if cell.v_merge != VMerge::Restart {
max_h = max_h.max(total_h);
}
CellLayout {
items,
total_height: total_h,
text_direction: cell.text_direction,
}
})
.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, cells }
})
.collect()
}
fn vmerge_at_col(row: &TableRow, target_col: usize) -> VMerge {
let mut col = 0usize;
for cell in &row.cells {
if col == target_col {
return cell.v_merge;
}
col += cell.grid_span.max(1) as usize;
if col > target_col {
break;
}
}
VMerge::None
}
fn compute_merge_spans(table: &Table, row_layouts: &[RowLayout]) -> HashMap<(usize, usize), f32> {
let mut spans = HashMap::new();
for (ri, row) in table.rows.iter().enumerate() {
let mut grid_col = 0usize;
for cell in &row.cells {
let span = cell.grid_span.max(1) as usize;
if cell.v_merge == VMerge::Restart {
let mut extra = 0.0f32;
for next_ri in (ri + 1)..table.rows.len() {
if vmerge_at_col(&table.rows[next_ri], grid_col) != VMerge::Continue {
break;
}
extra += row_layouts[next_ri].height;
}
if extra > 0.0 {
spans.insert((ri, grid_col), extra);
}
}
grid_col += span;
}
}
spans
}
fn render_table_row(
row: &TableRow,
layout: &RowLayout,
col_widths: &[f32],
cm: &CellMargins,
table_left: f32,
pb: &mut super::PageBuilder,
ctx: &RenderContext,
row_idx: usize,
merge_spans: &HashMap<(usize, usize), f32>,
) {
let row_h = layout.height;
let row_top = pb.slot_top;
let row_bottom = row_top - row_h;
let mut grid_col = 0usize;
for (cell, cell_layout) in row.cells.iter().zip(layout.cells.iter()) {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(col_widths, grid_col, span);
let cell_x = cell_x_offset(col_widths, table_left, grid_col);
let cell_grid_col = grid_col;
grid_col += span;
if cell.v_merge == VMerge::Continue {
continue;
}
let merge_extra = merge_spans
.get(&(row_idx, cell_grid_col))
.copied()
.unwrap_or(0.0);
let effective_h = row_h + merge_extra;
if let Some(shading) = cell.shading {
draw_cell_shading(
&mut pb.content,
shading,
&cell.borders,
cell_x,
row_bottom,
col_w,
row_h,
);
}
let has_content = cell_has_visible_content(&cell_layout.items);
let ecm = cell.cell_margins.as_ref().unwrap_or(cm);
if has_content && cell_layout.text_direction == TextDirection::TbRl {
render_vertical_cjk_cell(
&mut pb.content,
cell_layout,
cell,
cell_x,
row_top,
row_h,
col_w,
ecm,
ctx,
);
} else if has_content {
let content_h: f32 = cell_layout
.items
.iter()
.map(|item| match item {
CellContentItem::Paragraph(p) => {
p.space_before + para_block_height(p)
}
CellContentItem::NestedTable { height } => *height,
})
.sum();
let avail = effective_h - ecm.top - ecm.bottom;
let v_offset = valign_offset(cell.v_align, avail, content_h);
let cursor_y = row_top - ecm.top - v_offset;
render_cell_content(
&mut pb.content,
&cell_layout.items,
&cell.content,
cell_x,
col_w,
cursor_y,
ecm,
ctx,
);
}
}
let mut grid_col = 0usize;
for cell in &row.cells {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(col_widths, grid_col, span);
let bx = cell_x_offset(col_widths, table_left, grid_col);
if cell.v_merge == VMerge::Continue {
grid_col += span;
continue;
}
let merge_extra = merge_spans
.get(&(row_idx, grid_col))
.copied()
.unwrap_or(0.0);
let effective_bottom = row_bottom - merge_extra;
draw_cell_borders(
&mut pb.content,
&cell.borders,
bx,
row_top,
effective_bottom,
col_w,
true,
true,
);
grid_col += span;
}
pb.slot_top = row_bottom;
}
fn render_vertical_cjk_cell(
content: &mut Content,
cell_layout: &CellLayout,
cell: &crate::model::TableCell,
cell_x: f32,
row_top: f32,
row_h: f32,
col_w: f32,
cm: &CellMargins,
ctx: &RenderContext,
) {
let pdf_name_to_entry: HashMap<&str, &FontEntry> = ctx
.fonts
.values()
.map(|e| (e.pdf_name.as_str(), e))
.collect();
let paras: Vec<&CellParagraphLayout> = cell_layout
.items
.iter()
.filter_map(|item| match item {
CellContentItem::Paragraph(p) => Some(p),
_ => None,
})
.collect();
let total_char_h: f32 = paras
.iter()
.flat_map(|p| &p.lines)
.flat_map(|l| &l.chunks)
.filter(|c| !c.text.is_empty())
.map(|c| c.text.chars().count() as f32 * c.font_size)
.sum();
let avail_h = row_h - cm.top - cm.bottom;
let v_offset = valign_offset(cell.v_align, avail_h, total_char_h);
let avail_w = col_w - cm.left - cm.right;
let mut char_y = row_top - cm.top - v_offset;
let mut char_buf = [0u8; 4];
for para in ¶s {
for line in ¶.lines {
for chunk in &line.chunks {
if chunk.text.is_empty() {
continue;
}
let fs = chunk.font_size;
let entry = pdf_name_to_entry.get(chunk.pdf_font.as_str());
let ascender_ratio = entry.and_then(|e| e.ascender_ratio).unwrap_or(0.75);
let widths = entry.and_then(|e| e.char_widths_1000.as_ref());
if let Some([r, g, b]) = chunk.color {
content.set_fill_rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
}
content.begin_text();
content.set_font(Name(chunk.pdf_font.as_bytes()), fs);
let mut td_x = 0.0f32;
let mut td_y = 0.0f32;
for ch in chunk.text.chars() {
let baseline_y = char_y - fs * ascender_ratio;
let char_w = widths
.and_then(|m| m.get(&ch))
.map(|w| w * fs / 1000.0)
.unwrap_or(fs);
let cx = cell_x + cm.left + (avail_w - char_w) / 2.0;
let ch_str = ch.encode_utf8(&mut char_buf);
let bytes = encode_text_for_pdf(ch_str, &chunk.pdf_font, &pdf_name_to_entry);
content.next_line(cx - td_x, baseline_y - td_y);
td_x = cx;
td_y = baseline_y;
content.show(Str(&bytes));
char_y -= fs;
}
content.end_text();
if chunk.color.is_some() {
content.set_fill_gray(0.0);
}
}
}
}
}
fn find_cell_split(cell: &CellLayout, start: usize, available_h: f32, cm: &CellMargins) -> usize {
if start >= cell.items.len() {
return cell.items.len();
}
let mut h = cm.top + cm.bottom;
for pi in start..cell.items.len() {
let item_h = match &cell.items[pi] {
CellContentItem::Paragraph(para) => {
let sb = if pi == start { 0.0 } else { para.space_before };
sb + para_block_height(para)
}
CellContentItem::NestedTable { height } => *height,
};
if h + item_h > available_h && pi > start {
return pi;
}
h += item_h;
}
cell.items.len()
}
fn render_partial_row(
row: &TableRow,
layout: &RowLayout,
col_widths: &[f32],
cm: &CellMargins,
table_left: f32,
pb: &mut super::PageBuilder,
ctx: &RenderContext,
starts: &[usize],
ends: &[usize],
is_first: bool,
is_last: bool,
) {
let mut max_h: f32 = cm.top + cm.bottom;
for (ci, cell_layout) in layout.cells.iter().enumerate() {
let start = starts[ci];
let end = ends[ci];
let mut h = cm.top + cm.bottom;
for pi in start..end {
let item_h = match &cell_layout.items[pi] {
CellContentItem::Paragraph(para) => {
let sb = if pi == start { 0.0 } else { para.space_before };
sb + para_block_height(para)
}
CellContentItem::NestedTable { height } => *height,
};
h += item_h;
}
max_h = max_h.max(h);
}
let row_h = max_h;
let row_top = pb.slot_top;
let row_bottom = row_top - row_h;
let mut grid_col = 0usize;
for (ci, (cell, cell_layout)) in row.cells.iter().zip(layout.cells.iter()).enumerate() {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(col_widths, grid_col, span);
let cell_x = cell_x_offset(col_widths, table_left, grid_col);
grid_col += span;
if cell.v_merge == VMerge::Continue {
continue;
}
let start = starts[ci];
let end = ends[ci];
if let Some(shading) = cell.shading {
draw_cell_shading(
&mut pb.content,
shading,
&cell.borders,
cell_x,
row_bottom,
col_w,
row_h,
);
}
let has_content = (start..end).any(|pi| match &cell_layout.items[pi] {
CellContentItem::Paragraph(p) => para_has_visible_content(p),
CellContentItem::NestedTable { height } => *height > 0.0,
});
if has_content {
render_partial_cell_content(
&mut pb.content,
&cell_layout.items,
&cell.content,
start,
end,
cell_x,
col_w,
row_top - cm.top,
cm,
ctx,
);
}
}
let mut grid_col = 0usize;
for cell in &row.cells {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(col_widths, grid_col, span);
let bx = cell_x_offset(col_widths, table_left, grid_col);
grid_col += span;
let draw_top = is_first && cell.v_merge != VMerge::Continue;
draw_cell_borders(
&mut pb.content,
&cell.borders,
bx,
row_top,
row_bottom,
col_w,
draw_top,
is_last,
);
}
pb.slot_top = row_bottom;
}
fn render_header_rows(
table: &Table,
row_layouts: &[RowLayout],
col_widths: &[f32],
cm: &CellMargins,
table_left: f32,
pb: &mut super::PageBuilder,
ctx: &RenderContext,
merge_spans: &HashMap<(usize, usize), f32>,
header_count: usize,
) {
for hi in 0..header_count {
render_table_row(
&table.rows[hi],
&row_layouts[hi],
col_widths,
cm,
table_left,
pb,
ctx,
hi,
merge_spans,
);
}
}
pub(super) fn render_table(
table: &Table,
sp: &SectionProperties,
ctx: &RenderContext,
pb: &mut super::PageBuilder,
sect_idx: usize,
prev_space_after: f32,
override_pos: Option<super::FloatingTablePos>,
) {
let col_widths = auto_fit_columns(table, ctx.fonts);
let row_layouts = compute_row_layouts(table, &col_widths, ctx, None);
let merge_spans = compute_merge_spans(table, &row_layouts);
let cm = &table.cell_margins;
let is_floating = override_pos.is_some();
let (table_left, saved_slot_top, text_margins) =
if let Some(ref fp) = override_pos {
let saved = Some((pb.slot_top - prev_space_after, fp.y));
pb.slot_top = fp.y;
(fp.x, saved, (fp.top_from_text, fp.bottom_from_text))
} else {
use crate::model::TableAlignment;
let text_width = sp.page_width - sp.margin_left - sp.margin_right;
let table_total_w: f32 = col_widths.iter().sum();
let left = match table.alignment {
TableAlignment::Center => sp.margin_left + (text_width - table_total_w) / 2.0,
TableAlignment::Right => sp.margin_left + text_width - table_total_w,
TableAlignment::Left => sp.margin_left + table.table_indent - cm.left,
};
(left, None, (0.0, 0.0))
};
pb.slot_top -= prev_space_after;
let header_count = table.rows.iter().take_while(|r| r.is_header).count();
let flush_and_render_headers = |pb: &mut super::PageBuilder, ri: usize| {
pb.flush_page(sect_idx);
pb.is_first_page_of_section = false;
pb.slot_top = sp.page_height - sp.margin_top;
if header_count > 0 && ri >= header_count {
render_header_rows(
table,
&row_layouts,
&col_widths,
cm,
table_left,
pb,
ctx,
&merge_spans,
header_count,
);
}
};
let mut did_flush_while_floating = false;
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.cells.len(),
pb.slot_top
);
let at_page_top = (pb.slot_top - (sp.page_height - sp.margin_top)).abs() < 1.0;
let available_h = pb.slot_top - sp.margin_bottom;
let page_content_h = sp.page_height - sp.margin_top - sp.margin_bottom;
if row_h > available_h && (row_h > page_content_h || is_floating) {
let ncells = layout.cells.len();
let mut starts = vec![0usize; ncells];
let mut is_first_chunk = true;
loop {
let avail = pb.slot_top - sp.margin_bottom;
let mut ends = Vec::with_capacity(ncells);
let mut all_done = true;
for ci in 0..ncells {
let end = find_cell_split(&layout.cells[ci], starts[ci], avail, cm);
if end < layout.cells[ci].items.len() {
all_done = false;
}
ends.push(end);
}
render_partial_row(
row,
layout,
&col_widths,
cm,
table_left,
pb,
ctx,
&starts,
&ends,
is_first_chunk,
all_done,
);
if all_done {
break;
}
starts = ends;
is_first_chunk = false;
if is_floating {
did_flush_while_floating = true;
}
flush_and_render_headers(pb, ri);
}
} else if !at_page_top && row_h > available_h {
if is_floating {
did_flush_while_floating = true;
}
flush_and_render_headers(pb, ri);
render_table_row(
row,
layout,
&col_widths,
cm,
table_left,
pb,
ctx,
ri,
&merge_spans,
);
} else {
render_table_row(
row,
layout,
&col_widths,
cm,
table_left,
pb,
ctx,
ri,
&merge_spans,
);
}
}
if let Some((saved, table_top_y)) = saved_slot_top {
if did_flush_while_floating {
} else {
let table_total_w: f32 = col_widths.iter().sum();
let (top_margin, bottom_margin) = text_margins;
let table_bottom = pb.slot_top;
pb.slot_top = saved;
if table_bottom < saved {
let fp = override_pos.as_ref().unwrap();
pb.float_zone = Some(super::FloatZone {
top_y: table_top_y + top_margin,
bottom_y: table_bottom - bottom_margin,
obj_left: table_left,
obj_right: table_left + table_total_w,
left_from_text: fp.left_from_text,
right_from_text: fp.right_from_text,
polygon_pts: None,
wrap_text: crate::model::WrapText::Largest,
});
}
}
}
}
pub(super) fn compute_hf_table_height(table: &Table, ctx: &RenderContext) -> f32 {
let col_widths = auto_fit_columns(table, ctx.fonts);
let row_layouts = compute_row_layouts(table, &col_widths, ctx, None);
row_layouts.iter().map(|r| r.height).sum()
}
pub(super) fn render_header_footer_table(
table: &Table,
sp: &SectionProperties,
ctx: &RenderContext,
content: &mut Content,
cursor_y: &mut f32,
page_num: usize,
total_pages: usize,
styleref_values: &HashMap<String, String>,
) {
let col_widths = auto_fit_columns(table, ctx.fonts);
let hf_sub = HfSubstitution {
page_num,
total_pages,
styleref_values,
page_num_format: sp.page_num_format.as_deref(),
};
let row_layouts = compute_row_layouts(table, &col_widths, ctx, Some(&hf_sub));
let cm = &table.cell_margins;
let table_left = {
use crate::model::TableAlignment;
let text_width = sp.page_width - sp.margin_left - sp.margin_right;
let table_total_w: f32 = col_widths.iter().sum();
match table.alignment {
TableAlignment::Center => sp.margin_left + (text_width - table_total_w) / 2.0,
TableAlignment::Right => sp.margin_left + text_width - table_total_w,
TableAlignment::Left => sp.margin_left + table.table_indent - cm.left,
}
};
let merge_spans = compute_merge_spans(table, &row_layouts);
for (ri, (row, layout)) in table.rows.iter().zip(row_layouts.iter()).enumerate() {
let row_h = layout.height;
let row_top = *cursor_y;
let row_bottom = row_top - row_h;
let mut grid_col = 0usize;
for (cell, cell_layout) in row.cells.iter().zip(layout.cells.iter()) {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(&col_widths, grid_col, span);
let cell_x = cell_x_offset(&col_widths, table_left, grid_col);
let cell_grid_col = grid_col;
grid_col += span;
if cell.v_merge == VMerge::Continue {
continue;
}
let merge_extra = merge_spans
.get(&(ri, cell_grid_col))
.copied()
.unwrap_or(0.0);
let effective_h = row_h + merge_extra;
if let Some(shading) = cell.shading {
content.save_state();
content.set_fill_rgb(
shading[0] as f32 / 255.0,
shading[1] as f32 / 255.0,
shading[2] as f32 / 255.0,
);
content.rect(cell_x, row_bottom, col_w, row_h);
content.fill_nonzero();
content.restore_state();
}
if cell_has_visible_content(&cell_layout.items) {
let ecm = cell.cell_margins.as_ref().unwrap_or(cm);
let content_h: f32 = cell_layout
.items
.iter()
.map(|item| match item {
CellContentItem::Paragraph(p) => {
p.space_before + para_block_height(p)
}
CellContentItem::NestedTable { height } => *height,
})
.sum();
let avail = effective_h - ecm.top - ecm.bottom;
let v_offset = valign_offset(cell.v_align, avail, content_h);
let cell_cursor_y = row_top - ecm.top - v_offset;
render_cell_content(
content,
&cell_layout.items,
&cell.content,
cell_x,
col_w,
cell_cursor_y,
ecm,
ctx,
);
}
}
let mut grid_col = 0usize;
for cell in &row.cells {
let span = cell.grid_span.max(1) as usize;
let col_w = cell_span_width(&col_widths, grid_col, span);
let bx = cell_x_offset(&col_widths, table_left, grid_col);
grid_col += span;
let draw_top = cell.v_merge != VMerge::Continue && ri == 0;
draw_cell_borders(
content,
&cell.borders,
bx,
row_top,
row_bottom,
col_w,
draw_top,
true,
);
}
*cursor_y = row_bottom;
}
}