use std::collections::HashMap;
use pdf_writer::{Content, Name, Str};
use crate::fonts::{FontEntry, encode_as_gids, to_winansi_bytes};
use crate::model::{
Alignment, Block, BorderStyle, CellBorder, CellMargins, CellVAlign,
Paragraph, SectionProperties, Table, TableAlignment, TableRow, TextDirection, VMerge,
};
use super::color::{fill_rgb, stroke_rgb};
use super::header_footer::{compute_effective_margin_bottom, effective_slot_top};
use super::RenderContext;
use super::layout::{
encode_text_for_pdf, render_paragraph_lines,
};
use super::table_layout::{
CellContentItem, CellLayout, CellParagraphLayout, HfSubstitution,
RowLayout, apply_pct_width, auto_fit_columns, cell_span_width, cell_x_offset, compute_merge_spans,
compute_row_layouts, find_cell_split, para_block_height,
};
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(c) = border.color {
stroke_rgb(content, c);
}
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 thin = w.max(0.25);
let gap = thin * 2.0;
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 paint_cell_background(
content: &mut Content,
shading: Option<[u8; 3]>,
hatch: Option<crate::model::HatchPattern>,
x: f32,
y: f32,
w: f32,
h: f32,
) {
if let Some(hp) = hatch {
draw_hatch(content, &hp, x, y, w, h);
} else if let Some(s) = shading {
content.save_state();
fill_rgb(content, s);
content.rect(x, y, w, h);
content.fill_nonzero();
content.restore_state();
}
}
fn draw_hatch(content: &mut Content, hp: &crate::model::HatchPattern, x: f32, y: f32, w: f32, h: f32) {
use crate::model::HatchKind::*;
if w <= 0.0 || h <= 0.0 {
return;
}
content.save_state();
fill_rgb(content, hp.bg);
content.rect(x, y, w, h);
content.fill_nonzero();
content.rect(x, y, w, h);
content.clip_nonzero();
content.end_path();
stroke_rgb(content, hp.fg);
content.set_line_width(0.5);
let s = 4.0_f32;
if matches!(hp.kind, Horz | CrossHorzVert) {
let mut yy = y + s;
while yy < y + h {
content.move_to(x, yy);
content.line_to(x + w, yy);
yy += s;
}
}
if matches!(hp.kind, Vert | CrossHorzVert) {
let mut xx = x + s;
while xx < x + w {
content.move_to(xx, y);
content.line_to(xx, y + h);
xx += s;
}
}
if matches!(hp.kind, DiagFwd | CrossDiag) {
let mut off = 0.0;
while off <= w + h {
content.move_to(x + off - h, y);
content.line_to(x + off, y + h);
off += s;
}
}
if matches!(hp.kind, DiagBack | CrossDiag) {
let mut off = 0.0;
while off <= w + h {
content.move_to(x + off - h, y + h);
content.line_to(x + off, y);
off += s;
}
}
content.stroke();
content.restore_state();
}
fn draw_cell_shading(
content: &mut Content,
shading: Option<[u8; 3]>,
hatch: Option<crate::model::HatchPattern>,
borders: &crate::model::CellBorders,
x: f32,
y: f32,
w: f32,
h: f32,
) {
let bw = |b: &crate::model::CellBorder| if b.present { b.width } else { 0.0 };
let inset = (bw(&borders.top) + bw(&borders.bottom) + bw(&borders.left) + bw(&borders.right)) / 8.0;
paint_cell_background(
content,
shading,
hatch,
x + inset,
y + inset,
w - 2.0 * inset,
h - 2.0 * inset,
);
}
fn render_cell_inline_image(
content: &mut Content,
para: &CellParagraphLayout,
img_name: &str,
cell_x: f32,
col_w: f32,
cursor_y: f32,
cm: &CellMargins,
) -> f32 {
let text_w = (col_w - cm.left - cm.right).max(0.0);
let img_x = match para.alignment {
Alignment::Center => cell_x + cm.left + (text_w - para.image_width) / 2.0,
Alignment::Right => cell_x + cm.left + text_w - para.image_width,
_ => cell_x + cm.left,
};
let img_y = cursor_y - para.image_height;
if let Some(ref shadow) = para.image_shadow {
super::color::draw_image_shadow(
content, shadow, img_x, img_y,
para.image_width, para.image_height,
para.image_shadow_xobj.as_deref(),
);
}
if let Some(ref glow) = para.image_glow {
super::color::draw_image_glow(
content, glow, img_x, img_y,
para.image_width, para.image_height,
para.image_glow_xobj.as_deref(),
);
}
super::smartart::render_image_with_clip(
content, img_name, img_x, img_y,
para.image_width, para.image_height,
para.image_clip.as_ref(),
);
if let Some(sc) = para.image_stroke_color {
super::smartart::stroke_image_border(
content, img_x, img_y,
para.image_width, para.image_height,
sc, para.image_stroke_width,
para.image_clip.as_ref(),
);
}
para.image_height
}
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()))
|| !para.floating_images.is_empty()
|| para.image_name.is_some()
|| para.has_textboxes
|| para.has_connectors
}
fn cell_content_h_for_valign(items: &[CellContentItem]) -> f32 {
let mut h: f32 = items
.iter()
.map(|item| match item {
CellContentItem::Paragraph(p) => p.space_before + para_block_height(p),
CellContentItem::NestedTable { height } => *height,
})
.sum();
if let Some(CellContentItem::Paragraph(last_para)) = items.last() {
h += last_para.space_after;
}
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,
gradient_specs: &mut Vec<super::GradientSpec>,
) {
let mut cursor_y = cursor_y_start;
let mut block_idx = 0;
for item in items {
match item {
CellContentItem::Paragraph(para) => {
let mut source_para: Option<&Paragraph> = None;
while block_idx < blocks.len() {
if let Block::Paragraph(p) = &blocks[block_idx] {
source_para = Some(p);
block_idx += 1;
break;
}
block_idx += 1;
}
let para_top = cursor_y;
if !para_has_visible_content(para)
&& !para.has_textboxes
&& !para.has_connectors
{
cursor_y -= para.space_before + para_block_height(para);
continue;
}
cursor_y -= para.space_before;
for fi in ¶.floating_images {
let fi_x = cell_x + fi.h_offset;
let fi_y_top = cursor_y - fi.v_offset;
let fi_y_bottom = fi_y_top - fi.display_height;
content.save_state();
content.transform([
fi.display_width,
0.0,
0.0,
fi.display_height,
fi_x,
fi_y_bottom,
]);
content.x_object(Name(fi.pdf_name.as_bytes()));
content.restore_state();
}
if let Some(ref img_name) = para.image_name {
cursor_y -= render_cell_inline_image(
content, para, img_name, cell_x, col_w, cursor_y, cm,
);
continue;
}
let text_x = cell_x + cm.left + para.indent_left + para.float_indent_left;
let text_w =
(col_w - cm.left - cm.right - para.indent_left - para.float_indent_left)
.max(0.0);
let baseline_y = cursor_y - para.font_size;
let first_line_hanging = if para.list_label.is_empty() {
para.indent_hanging
} else {
let label_x = cell_x + cm.left + para.indent_left - para.indent_hanging;
draw_cell_label(content, para, label_x, baseline_y, ctx.fonts);
if para.indent_first_line > 0.0 && para.indent_hanging == 0.0 {
-para.indent_first_line
} else {
0.0
}
};
render_paragraph_lines(
content,
¶.lines,
¶.alignment,
text_x,
text_w,
baseline_y,
para.line_h,
para.lines.len(),
0,
&mut Vec::new(),
first_line_hanging,
ctx.fonts,
None,
gradient_specs,
None,
);
cursor_y -= para.lines.len() as f32 * para.line_h;
if let Some(src) = source_para {
render_cell_floating_shapes(
content, src, cell_x, col_w, para_top, ctx, gradient_specs,
);
}
}
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,
gradient_specs,
);
} else {
cursor_y -= height;
}
}
}
}
}
fn render_cell_floating_shapes(
content: &mut Content,
para: &Paragraph,
cell_x: f32,
col_w: f32,
para_top: f32,
ctx: &RenderContext,
gradient_specs: &mut Vec<super::GradientSpec>,
) {
use crate::model::{HorizontalPosition, VerticalPosition};
use super::positioning::render_connector;
for conn in ¶.connectors {
let conn_x = match conn.connector_type {
_ => conn.x,
};
let mut conn_clone = conn.clone();
conn_clone.x = conn_x;
render_connector(&conn_clone, content, cell_x, para_top);
}
for tb in ¶.textboxes {
let h_off = match tb.h_position {
HorizontalPosition::Offset(o) => o,
HorizontalPosition::AlignCenter => (col_w - tb.width_pt) / 2.0,
HorizontalPosition::AlignRight => col_w - tb.width_pt,
HorizontalPosition::AlignLeft => 0.0,
};
let tb_x = cell_x + h_off;
let tb_y_top = para_top - tb.v_offset_pt;
render_simple_textbox(content, tb, tb_x, tb_y_top, ctx, gradient_specs);
}
}
fn render_simple_textbox(
content: &mut Content,
tb: &crate::model::Textbox,
tb_x: f32,
tb_y_top: f32,
ctx: &RenderContext,
gradient_specs: &mut Vec<super::GradientSpec>,
) {
use crate::model::ShapeFill;
use super::layout::{build_paragraph_lines, render_paragraph_lines, tallest_run_metrics};
use super::resolve_line_h;
use super::smartart::draw_shape_path;
let tb_width = tb.width_pt;
let tb_height = tb.height_pt;
if let Some(ShapeFill::Solid(color)) = &tb.fill {
content.save_state();
fill_rgb(content, *color);
draw_shape_path(content, tb_x, tb_y_top - tb_height, tb_width, tb_height, &tb.shape_type);
content.fill_nonzero();
content.restore_state();
}
if let Some(stroke) = tb.stroke_color {
if tb.stroke_width > 0.0 {
content.save_state();
content.set_line_width(tb.stroke_width);
stroke_rgb(content, stroke);
draw_shape_path(content, tb_x, tb_y_top - tb_height, tb_width, tb_height, &tb.shape_type);
content.stroke();
content.restore_state();
}
}
let content_x = tb_x + tb.margin_left;
let content_w = (tb_width - tb.margin_left - tb.margin_right).max(0.0);
let mut text_y = tb_y_top - tb.margin_top;
let empty: HashMap<usize, String> = HashMap::new();
let empty_fx: HashMap<usize, super::images::EffectXObjs> = HashMap::new();
for tp in &tb.paragraphs {
let lines = build_paragraph_lines(
&tp.runs, ctx.fonts, content_w, 0.0, &empty, &empty_fx, None, None, None, true,
);
let (fs, lhr, _) = tallest_run_metrics(&tp.runs, ctx.fonts);
let lh = resolve_line_h(tp.line_spacing.unwrap_or(ctx.doc_line_spacing), fs, lhr);
text_y -= tp.space_before;
let baseline_y = text_y - fs;
render_paragraph_lines(
content,
&lines,
&tp.alignment,
content_x,
content_w,
baseline_y,
lh,
lines.len(),
0,
&mut Vec::new(),
0.0,
ctx.fonts,
None,
gradient_specs,
None,
);
text_y -= lines.len().max(1) as f32 * lh + tp.space_after;
}
}
fn render_nested_table(
table: &Table,
content: &mut Content,
available_x: f32,
available_w: f32,
cursor_y: &mut f32,
ctx: &RenderContext,
gradient_specs: &mut Vec<super::GradientSpec>,
) {
let mut col_widths = auto_fit_columns(table, ctx.fonts, Some(available_w));
apply_pct_width(table, &mut col_widths, available_w);
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,
};
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;
paint_cell_background(content, cell.shading, cell.hatch, cx, row_bottom, col_w, row_h);
if cell_has_visible_content(&cell_layout.items) {
let ecm = cell.cell_margins.as_ref().unwrap_or(cm);
let content_h = cell_content_h_for_valign(&cell_layout.items);
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,
gradient_specs,
);
}
}
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);
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_bottom = row_bottom - merge_extra;
draw_cell_borders(
content,
&cell.borders,
bx,
row_top,
effective_bottom,
col_w,
ri == 0,
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,
gradient_specs: &mut Vec<super::GradientSpec>,
) {
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;
for fi in ¶.floating_images {
let fi_x = cell_x + fi.h_offset;
let fi_y_top = cursor_y - fi.v_offset;
let fi_y_bottom = fi_y_top - fi.display_height;
content.save_state();
content.transform([
fi.display_width,
0.0,
0.0,
fi.display_height,
fi_x,
fi_y_bottom,
]);
content.x_object(Name(fi.pdf_name.as_bytes()));
content.restore_state();
}
if let Some(ref img_name) = para.image_name {
cursor_y -= render_cell_inline_image(
content, para, img_name, cell_x, col_w, cursor_y, cm,
);
continue;
}
let text_x = cell_x + cm.left + para.indent_left + para.float_indent_left;
let text_w =
(col_w - cm.left - cm.right - para.indent_left - para.float_indent_left)
.max(0.0);
let baseline_y = cursor_y - para.font_size;
let first_line_hanging = if para.list_label.is_empty() {
para.indent_hanging
} else {
let label_x = cell_x + cm.left + para.indent_left - para.indent_hanging;
draw_cell_label(content, para, label_x, baseline_y, ctx.fonts);
if para.indent_first_line > 0.0 && para.indent_hanging == 0.0 {
-para.indent_first_line
} else {
0.0
}
};
render_paragraph_lines(
content,
¶.lines,
¶.alignment,
text_x,
text_w,
baseline_y,
para.line_h,
para.lines.len(),
0,
&mut Vec::new(),
first_line_hanging,
ctx.fonts,
None,
gradient_specs,
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, gradient_specs,
);
} else {
cursor_y -= height;
}
}
}
}
}
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(c) = para.label_color {
fill_rgb(content, c);
}
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);
}
}
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;
draw_cell_shading(
&mut pb.content,
cell.shading,
cell.hatch,
&cell.borders,
cell_x,
row_top - effective_h,
col_w,
effective_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,
effective_h,
col_w,
ecm,
ctx,
);
} else if has_content {
let content_h = cell_content_h_for_valign(&cell_layout.items);
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,
&mut pb.gradient_specs,
);
}
}
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 effective_v_align = if cell.v_align == CellVAlign::Top {
match paras.first().map(|p| p.alignment) {
Some(Alignment::Center) => CellVAlign::Center,
Some(Alignment::Right) => CellVAlign::Bottom,
_ => CellVAlign::Center,
}
} else {
cell.v_align
};
let v_offset = valign_offset(effective_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(c) = chunk.color {
fill_rgb(content, c);
}
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 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,
fill_to_bottom_y: Option<f32>,
) {
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_top = pb.slot_top;
let fill_h = fill_to_bottom_y
.map(|y_bot| (row_top - y_bot).max(max_h))
.unwrap_or(max_h);
let row_h = fill_h;
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];
draw_cell_shading(
&mut pb.content,
cell.shading,
cell.hatch,
&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,
&mut pb.gradient_specs,
);
}
}
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;
if cell.v_merge == VMerge::Continue {
continue;
}
let draw_top = is_first;
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>,
footnotes: &std::collections::HashMap<u32, crate::model::Footnote>,
effective_margin_bottom: &mut f32,
column_bounds: Option<(f32, f32)>,
) {
let available_w = column_bounds.map(|(_, w)| w);
let mut col_widths = if table.fixed_layout {
table.col_widths.clone()
} else {
auto_fit_columns(table, ctx.fonts, available_w)
};
apply_pct_width(
table,
&mut col_widths,
available_w.unwrap_or(sp.page_width - sp.margin_left - sp.margin_right),
);
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 (area_left, area_width) = column_bounds
.unwrap_or((sp.margin_left, 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 => area_left + (area_width - table_total_w) / 2.0,
TableAlignment::Right => area_left + area_width - table_total_w,
TableAlignment::Left => {
let ind = table.table_indent;
let explicit_real_indent = table.table_indent_explicit
&& (ind - cm.left).abs() > 1.0;
if explicit_real_indent {
area_left + ind
} else {
area_left + ind - 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 row_footnote_ids: Vec<Vec<u32>> = table.rows.iter().map(|row| {
let mut ids = Vec::new();
for cell in &row.cells {
for p in cell.all_paragraphs() {
for run in &p.runs {
if let Some(id) = run.footnote_id {
ids.push(id);
}
}
}
}
ids
}).collect();
let row_endnote_ids: Vec<Vec<u32>> = table.rows.iter().map(|row| {
let mut ids = Vec::new();
for cell in &row.cells {
for p in cell.all_paragraphs() {
for run in &p.runs {
if let Some(id) = run.endnote_id {
ids.push(id);
}
}
}
}
ids
}).collect();
let fn_text_width = sp.page_width - sp.margin_left - sp.margin_right;
let flush_and_render_headers = |pb: &mut super::PageBuilder, ri: usize, emb: &mut f32| {
pb.flush_page(sect_idx);
pb.is_first_page_of_section = false;
pb.slot_top = effective_slot_top(sp, false, ctx);
pb.column_top_y = pb.slot_top;
*emb = compute_effective_margin_bottom(sp, false, ctx);
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;
let mut flushed_page_top: Option<f32> = None;
let text_anchor_offset = override_pos
.as_ref()
.filter(|fp| fp.v_anchor_text)
.map_or(0.0, |fp| fp.v_offset_pt);
let split_row_across_pages = |row: &TableRow,
layout: &RowLayout,
pb: &mut super::PageBuilder,
ri: usize,
did_flush: &mut bool,
emb: &mut f32| {
let ncells = layout.cells.len();
let mut starts = vec![0usize; ncells];
let mut is_first_chunk = true;
loop {
let avail = pb.slot_top - *emb;
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);
}
let fill_to_bottom_y = if all_done { None } else { Some(*emb) };
render_partial_row(
row, layout, &col_widths, cm, table_left,
pb, ctx, &starts, &ends, is_first_chunk, all_done,
fill_to_bottom_y,
);
if all_done {
break;
}
starts = ends;
is_first_chunk = false;
if is_floating {
*did_flush = true;
}
flush_and_render_headers(pb, ri, emb);
}
};
if is_floating && !row_layouts.is_empty() {
let eff_top = effective_slot_top(sp, pb.is_first_page_of_section, ctx);
let at_page_top = (pb.slot_top - eff_top).abs() < 1.0;
let available = pb.slot_top - *effective_margin_bottom;
let total_h: f32 = row_layouts.iter().map(|r| r.height).sum();
let page_content_h = eff_top - *effective_margin_bottom;
if !at_page_top && total_h > available && total_h <= page_content_h {
pb.flush_page(sect_idx);
pb.is_first_page_of_section = false;
pb.slot_top = effective_slot_top(sp, false, ctx);
pb.column_top_y = pb.slot_top;
*effective_margin_bottom = compute_effective_margin_bottom(sp, false, ctx);
did_flush_while_floating = true;
flushed_page_top = Some(pb.slot_top);
pb.slot_top -= text_anchor_offset;
}
}
for (ri, (row, layout)) in table.rows.iter().zip(row_layouts.iter()).enumerate() {
let mut row_fn_extra = 0.0f32;
for &fn_id in &row_footnote_ids[ri] {
if !pb.footnote_ids_set.contains(&fn_id) {
if let Some(footnote) = footnotes.get(&fn_id) {
row_fn_extra += super::footnotes::compute_footnote_height(
footnote, ctx, fn_text_width,
);
}
}
}
if row_fn_extra > 0.0 && pb.footnote_ids.is_empty() {
row_fn_extra += 12.0; }
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 eff_top = effective_slot_top(sp, pb.is_first_page_of_section, ctx);
let eff_bottom = *effective_margin_bottom + row_fn_extra;
let at_page_top = (pb.slot_top - eff_top).abs() < 1.0;
let available_h = pb.slot_top - eff_bottom;
let page_content_h = eff_top - eff_bottom;
let any_cell_multi_item = layout.cells.iter().any(|c| c.items.len() > 1);
let can_meaningfully_split = !row.cant_split
&& any_cell_multi_item
&& !at_page_top
&& row_h > page_content_h * 0.5
&& available_h > page_content_h * 0.25;
if row_h > available_h && (row_h > page_content_h || is_floating) && !row.cant_split {
split_row_across_pages(row, layout, pb, ri, &mut did_flush_while_floating, effective_margin_bottom);
} else if row_h > available_h && can_meaningfully_split {
split_row_across_pages(row, layout, pb, ri, &mut did_flush_while_floating, effective_margin_bottom);
} else if !at_page_top && row_h > available_h {
if is_floating {
did_flush_while_floating = true;
}
flush_and_render_headers(pb, ri, effective_margin_bottom);
let new_eff_top = effective_slot_top(sp, pb.is_first_page_of_section, ctx);
let new_eff_bot = *effective_margin_bottom;
let new_available = pb.slot_top - new_eff_bot;
let new_page_h = new_eff_top - new_eff_bot;
if row_h > new_available && (row_h > new_page_h || is_floating) && !row.cant_split {
split_row_across_pages(row, layout, pb, ri, &mut did_flush_while_floating, effective_margin_bottom);
} else {
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,
);
}
for &fn_id in &row_footnote_ids[ri] {
if pb.footnote_ids_set.insert(fn_id) {
pb.footnote_ids.push(fn_id);
if let Some(footnote) = footnotes.get(&fn_id) {
let fn_h = super::footnotes::compute_footnote_height(
footnote, ctx, fn_text_width,
);
let sep = if pb.footnote_ids.len() == 1 { 12.0 } else { 0.0 };
*effective_margin_bottom += sep + fn_h;
}
}
}
for &en_id in &row_endnote_ids[ri] {
if pb.endnote_ids_set.insert(en_id) {
pb.endnote_ids.push(en_id);
}
}
if pb.slot_top < *effective_margin_bottom - 1.0 {
log::warn!(
"Table overflow: row={} slot_top={:.2} < eff_margin_bottom={:.2} row_h={:.2} page={}",
ri, pb.slot_top, *effective_margin_bottom, row_h, pb.all_contents.len(),
);
}
}
if let Some((saved, table_top_y)) = saved_slot_top {
if did_flush_while_floating {
if let Some(top) = flushed_page_top {
pb.pending_float_anchor = Some(top);
pb.slot_top += text_anchor_offset;
}
} 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();
let text_area_left = sp.margin_left;
let text_area_right = sp.page_width - sp.margin_right;
let space_left = table_left - text_area_left;
let space_right = text_area_right - (table_left + table_total_w);
let wrap_text = if space_left >= 72.0 && space_right >= 72.0 {
crate::model::WrapText::BothSides
} else {
crate::model::WrapText::Largest
};
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,
para_relative: false,
});
}
}
}
}
pub(super) fn compute_hf_table_height(table: &Table, ctx: &RenderContext, content_w: f32) -> f32 {
let mut col_widths = auto_fit_columns(table, ctx.fonts, None);
apply_pct_width(table, &mut col_widths, content_w);
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>,
page_num_format: Option<&str>,
gradient_specs: &mut Vec<super::GradientSpec>,
) {
let mut col_widths = auto_fit_columns(table, ctx.fonts, None);
apply_pct_width(
table,
&mut col_widths,
sp.page_width - sp.margin_left - sp.margin_right,
);
let hf_sub = HfSubstitution {
page_num,
total_pages,
styleref_values,
page_num_format,
};
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;
paint_cell_background(content, cell.shading, cell.hatch, cell_x, row_bottom, col_w, row_h);
if cell_has_visible_content(&cell_layout.items) {
let ecm = cell.cell_margins.as_ref().unwrap_or(cm);
let content_h = cell_content_h_for_valign(&cell_layout.items);
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,
gradient_specs,
);
}
}
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);
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_bottom = row_bottom - merge_extra;
draw_cell_borders(
content,
&cell.borders,
bx,
row_top,
effective_bottom,
col_w,
ri == 0,
true,
);
}
*cursor_y = row_bottom;
}
}