mod assembly;
mod chart_legend;
mod charts;
mod charts_radial;
mod emf;
pub(crate) mod color;
mod fonts;
mod footnotes;
mod header_footer;
mod helpers;
mod images;
mod list_label;
mod layout;
mod positioning;
mod smartart;
mod table;
mod table_layout;
mod textbox_render;
mod wordart;
use std::collections::{HashMap, HashSet};
use pdf_writer::{Content, Name, Pdf, Ref};
use crate::error::Error;
use crate::fonts::FontEntry;
use crate::model::{
Alignment, Block, DocGridType, Document, FieldCode,
HorizontalPosition, LineSpacing, Paragraph, ParagraphBorder,
Run, SectionBreakType, SectionProperties, ShapeFill, ShapeGeometry,
VRelativeFrom, VerticalPosition, WrapText, WrapType,
};
use assembly::{HeadingEntry, assemble_pdf_pages};
use fonts::collect_and_register_fonts;
use footnotes::{compute_footnote_height, render_page_footnotes};
use header_footer::{
HfPageContext, compute_effective_margin_bottom, effective_slot_top, render_header_footer,
resolve_footer_for_page, resolve_header_for_page,
};
pub(super) use helpers::resolve_line_h;
use helpers::borders_match;
use positioning::{render_connector, render_floating_images, resolve_fi_x};
pub(super) use positioning::{resolve_h_position, resolve_fi_y_top};
use images::{EffectXObjs, EmbeddedImages, embed_all_images};
use layout::{
DualRegion, LinkAnnotation, build_paragraph_lines, build_tabbed_line, is_text_empty,
render_paragraph_lines, tallest_run_metrics,
};
use crate::fonts::font_key;
use color::{fill_rgb, stroke_rgb};
use list_label::{collect_paras, label_font_key, para_runs_with_textboxes, render_list_label};
use smartart::draw_shape_path;
use table::render_table;
use textbox_render::render_single_textbox;
pub(super) struct RenderContext<'a> {
pub(super) fonts: &'a HashMap<String, FontEntry>,
pub(super) doc_line_spacing: LineSpacing,
pub(super) default_tab_stop: f32,
pub(super) table_cell_image_names: &'a HashMap<usize, String>,
pub(super) effect_table_names: &'a HashMap<usize, EffectXObjs>,
pub(super) textbox_image_names: &'a HashMap<usize, String>,
pub(super) effect_textbox_names: &'a HashMap<usize, EffectXObjs>,
pub(super) chart_font_name: &'a str,
}
pub(super) struct GradientSpec {
pattern_name: String,
stops: Vec<([u8; 3], f32)>,
angle_deg: f32,
x: f32,
y: f32,
w: f32,
h: f32,
}
pub(super) fn render_shape_fill(
content: &mut Content,
fill: &ShapeFill,
x: f32,
y: f32,
w: f32,
h: f32,
shape: &ShapeGeometry,
gradient_specs: &mut Vec<GradientSpec>,
) {
match fill {
ShapeFill::Solid(c) => {
content.save_state();
fill_rgb(content, *c);
draw_shape_path(content, x, y, w, h, shape);
content.fill_nonzero();
content.restore_state();
}
ShapeFill::LinearGradient { stops, angle_deg } => {
let pat_name = format!("Grd{}", gradient_specs.len());
content.save_state();
draw_shape_path(content, x, y, w, h, shape);
content.clip_nonzero();
content.end_path();
content.set_fill_color_space(pdf_writer::types::ColorSpaceOperand::Pattern);
content.set_fill_pattern([], Name(pat_name.as_bytes()));
draw_shape_path(content, x, y, w, h, shape);
content.fill_nonzero();
content.restore_state();
gradient_specs.push(GradientSpec {
pattern_name: pat_name,
stops: stops.clone(),
angle_deg: *angle_deg,
x,
y,
w,
h,
});
}
}
}
fn label_boosted_line_h(
para: &Paragraph,
fonts: &HashMap<String, FontEntry>,
text_line_h: f32,
effective_ls: LineSpacing,
text_font_size: f32,
) -> f32 {
if para.list_label.is_empty() {
return text_line_h;
}
let label_fs = para.list_label_font_size.unwrap_or(text_font_size);
if (label_fs - text_font_size).abs() > 1.0 {
return text_line_h;
}
let Some(key) = label_font_key(para) else {
return text_line_h;
};
let Some(entry) = fonts.get(&key) else {
return text_line_h;
};
let label_lh = resolve_line_h(effective_ls, label_fs, entry.line_h_ratio);
text_line_h.max(label_lh)
}
fn break_run_lhr(
runs: &[Run],
break_fs: f32,
fonts: &HashMap<String, FontEntry>,
) -> Option<f32> {
let br_run = runs.iter()
.filter(|r| r.is_line_break && (r.font_size - break_fs).abs() < 0.01)
.last();
if let Some(run) = br_run {
let key = font_key(run);
fonts.get(&key).and_then(|e| e.line_h_ratio)
} else {
None
}
}
fn styleref_insert(
map: &mut HashMap<String, String>,
id: &str,
text: &str,
style_id_to_name: &HashMap<String, String>,
) {
map.insert(id.to_string(), text.to_string());
if let Some(name) = style_id_to_name.get(id) {
map.insert(name.clone(), text.to_string());
}
}
fn styleref_insert_first(
map: &mut HashMap<String, String>,
id: &str,
text: &str,
style_id_to_name: &HashMap<String, String>,
) {
map.entry(id.to_string())
.or_insert_with(|| text.to_string());
if let Some(name) = style_id_to_name.get(id) {
map.entry(name.clone()).or_insert_with(|| text.to_string());
}
}
fn update_styleref_from_para(
running: &mut HashMap<String, String>,
page_first: &mut HashMap<String, String>,
para: &Paragraph,
style_id_to_name: &HashMap<String, String>,
) {
if let Some(ref sid) = para.style_id {
let text: String = para.runs.iter().map(|r| r.text.as_str()).collect();
if !text.is_empty() {
styleref_insert(running, sid, &text, style_id_to_name);
styleref_insert_first(page_first, sid, &text, style_id_to_name);
}
}
for run in ¶.runs {
if let Some(ref csid) = run.char_style_id {
if !run.text.is_empty() {
styleref_insert(running, csid, &run.text, style_id_to_name);
styleref_insert_first(page_first, csid, &run.text, style_id_to_name);
}
}
}
}
pub(super) struct FloatZone {
pub top_y: f32,
pub bottom_y: f32,
pub obj_left: f32,
pub obj_right: f32,
pub left_from_text: f32,
pub right_from_text: f32,
pub polygon_pts: Option<Vec<(f32, f32)>>,
pub wrap_text: WrapText,
pub para_relative: bool,
}
impl FloatZone {
fn exclusion_at_y(&self, y: f32) -> (f32, f32) {
if let Some(ref pts) = self.polygon_pts {
if let Some((left, right)) = poly_scanline(pts, y) {
return (left, right);
}
}
(self.obj_left, self.obj_right)
}
#[allow(dead_code)]
fn narrow_paragraph_geometry(
&self,
slot_top: f32,
col_x: f32,
col_w: f32,
indent_left: f32,
indent_right: f32,
indent_hanging: f32,
) -> Option<(f32, f32, f32)> {
if !(slot_top <= self.top_y && slot_top > self.bottom_y) {
return None;
}
let col_right = col_x + col_w;
let (ex_left, ex_right) = self.exclusion_at_y(slot_top);
let space_right = col_right - (ex_right + self.right_from_text);
let space_left = (ex_left - self.left_from_text) - col_x;
let mut para_text_x = col_x + indent_left;
let mut para_text_width = (col_w - indent_left - indent_right).max(1.0);
let mut label_x = col_x + indent_left - indent_hanging;
if self.wrap_text == WrapText::BothSides {
let lw = (space_left - indent_left).max(0.0);
let rw = (space_right - indent_right).max(0.0);
if rw > lw {
let new_left = ex_right + self.right_from_text;
para_text_width = rw.max(1.0);
para_text_x = new_left + indent_left;
label_x = new_left + indent_left - indent_hanging;
} else if lw > 0.0 {
para_text_width = lw.max(1.0);
}
} else {
let use_right = match self.wrap_text {
WrapText::Right => space_right >= 1.0,
WrapText::Left => !(space_left >= 1.0),
_ => space_right >= space_left && space_right >= 72.0,
};
let use_left = match self.wrap_text {
WrapText::Left => space_left >= 1.0,
WrapText::Right => false,
_ => space_left >= 72.0,
};
if use_right {
let new_left = ex_right + self.right_from_text;
para_text_width = (col_right - new_left - indent_right).max(1.0);
para_text_x = new_left + indent_left;
label_x = new_left + indent_left - indent_hanging;
} else if use_left {
let avail_right = ex_left - self.left_from_text;
para_text_width =
(avail_right - col_x - indent_left - indent_right).max(1.0);
}
}
Some((para_text_x, para_text_width, label_x))
}
}
fn poly_scanline(pts: &[(f32, f32)], y: f32) -> Option<(f32, f32)> {
let n = pts.len();
if n < 3 {
return None;
}
let mut min_x = f32::MAX;
let mut max_x = f32::MIN;
for i in 0..n {
let (x0, y0) = pts[i];
let (x1, y1) = pts[(i + 1) % n];
if (y0 <= y && y1 >= y) || (y1 <= y && y0 >= y) {
if (y1 - y0).abs() < 0.001 {
min_x = min_x.min(x0).min(x1);
max_x = max_x.max(x0).max(x1);
} else {
let t = (y - y0) / (y1 - y0);
let x = x0 + t * (x1 - x0);
min_x = min_x.min(x);
max_x = max_x.max(x);
}
}
}
if min_x <= max_x {
Some((min_x, max_x))
} else {
None
}
}
fn convert_polygon_to_page_coords(
vertices: &[(i32, i32)],
img_x: f32,
img_y_top: f32,
display_w: f32,
display_h: f32,
) -> Vec<(f32, f32)> {
vertices
.iter()
.map(|&(px, py)| {
let x_pt = img_x + (px as f32 / 21600.0) * display_w;
let y_pt = img_y_top - (py as f32 / 21600.0) * display_h;
(x_pt, y_pt)
})
.collect()
}
fn draw_debug_wrap_overlay(content: &mut Content, fz: &FloatZone) {
content.save_state();
if let Some(ref pts) = fz.polygon_pts {
if pts.len() >= 3 {
content.set_stroke_rgb(0.0, 0.7, 0.0);
content.set_line_width(0.5);
content.move_to(pts[0].0, pts[0].1);
for &(x, y) in &pts[1..] {
content.line_to(x, y);
}
content.close_path();
content.stroke();
}
}
if let Some(ref pts) = fz.polygon_pts {
content.set_stroke_rgb(0.0, 0.0, 0.8);
content.set_line_width(0.3);
let steps = ((fz.top_y - fz.bottom_y) / 1.0).ceil() as usize;
if steps > 0 {
let mut left_pts = Vec::with_capacity(steps + 1);
let mut right_pts = Vec::with_capacity(steps + 1);
for i in 0..=steps {
let y = fz.top_y - i as f32 * (fz.top_y - fz.bottom_y) / steps as f32;
if let Some((l, r)) = poly_scanline(pts, y) {
left_pts.push((l - fz.left_from_text, y));
right_pts.push((r + fz.right_from_text, y));
}
}
if left_pts.len() >= 2 {
content.move_to(left_pts[0].0, left_pts[0].1);
for &(x, y) in &left_pts[1..] {
content.line_to(x, y);
}
content.stroke();
}
if right_pts.len() >= 2 {
content.move_to(right_pts[0].0, right_pts[0].1);
for &(x, y) in &right_pts[1..] {
content.line_to(x, y);
}
content.stroke();
}
}
}
content.set_stroke_rgb(0.8, 0.0, 0.0);
content.set_line_width(0.3);
let page_left = fz.obj_left - 20.0;
let page_right = fz.obj_right + 20.0;
content.move_to(page_left, fz.top_y);
content.line_to(page_right, fz.top_y);
content.stroke();
content.move_to(page_left, fz.bottom_y);
content.line_to(page_right, fz.bottom_y);
content.stroke();
content.restore_state();
}
pub(super) struct FloatingTablePos {
pub x: f32,
pub y: f32,
pub top_from_text: f32,
pub bottom_from_text: f32,
pub left_from_text: f32,
pub right_from_text: f32,
}
pub(super) struct PageBuilder {
pub(super) content: Content,
pub(super) links: Vec<LinkAnnotation>,
pub(super) footnote_ids: Vec<u32>,
footnote_ids_set: HashSet<u32>,
pub(super) endnote_ids: Vec<u32>,
endnote_ids_set: HashSet<u32>,
pub(super) alpha_states: HashSet<u8>,
pub(super) gradient_specs: Vec<GradientSpec>,
styleref_running: HashMap<String, String>,
styleref_page_first: HashMap<String, String>,
pub(super) slot_top: f32,
pub(super) column_top_y: f32,
pub(super) is_first_page_of_section: bool,
page_hf_section: usize,
pub(super) float_zone: Option<FloatZone>,
all_contents: Vec<Content>,
all_links: Vec<Vec<LinkAnnotation>>,
all_footnote_ids: Vec<Vec<u32>>,
all_alpha_states: Vec<HashSet<u8>>,
all_gradient_specs: Vec<Vec<GradientSpec>>,
page_section_indices: Vec<(usize, bool, usize)>,
all_styleref: Vec<HashMap<String, String>>,
all_first_styleref: Vec<HashMap<String, String>>,
}
impl PageBuilder {
fn new(slot_top: f32) -> Self {
PageBuilder {
content: Content::new(),
links: Vec::new(),
footnote_ids: Vec::new(),
footnote_ids_set: HashSet::new(),
endnote_ids: Vec::new(),
endnote_ids_set: HashSet::new(),
alpha_states: HashSet::new(),
gradient_specs: Vec::new(),
styleref_running: HashMap::new(),
styleref_page_first: HashMap::new(),
slot_top,
column_top_y: slot_top,
is_first_page_of_section: true,
page_hf_section: 0,
float_zone: None,
all_contents: Vec::new(),
all_links: Vec::new(),
all_footnote_ids: Vec::new(),
all_alpha_states: Vec::new(),
all_gradient_specs: Vec::new(),
page_section_indices: Vec::new(),
all_styleref: Vec::new(),
all_first_styleref: Vec::new(),
}
}
pub(super) fn flush_page(&mut self, sect_idx: usize) {
self.all_contents
.push(std::mem::replace(&mut self.content, Content::new()));
self.all_links.push(std::mem::take(&mut self.links));
self.all_footnote_ids
.push(std::mem::take(&mut self.footnote_ids));
self.footnote_ids_set.clear();
self.all_alpha_states
.push(std::mem::take(&mut self.alpha_states));
self.all_gradient_specs
.push(std::mem::take(&mut self.gradient_specs));
self.page_section_indices.push((
self.page_hf_section,
self.is_first_page_of_section,
sect_idx,
));
self.all_styleref.push(self.styleref_running.clone());
self.all_first_styleref
.push(std::mem::take(&mut self.styleref_page_first));
self.float_zone = None;
self.page_hf_section = sect_idx;
}
fn push_blank_page(&mut self, sect_idx: usize) {
self.all_contents.push(Content::new());
self.all_links.push(Vec::new());
self.all_footnote_ids.push(Vec::new());
self.all_alpha_states.push(HashSet::new());
self.all_gradient_specs.push(Vec::new());
self.page_section_indices
.push((self.page_hf_section, false, sect_idx));
self.all_styleref.push(self.styleref_running.clone());
self.all_first_styleref
.push(std::mem::take(&mut self.styleref_page_first));
self.page_hf_section = sect_idx;
}
fn page_count(&self) -> usize {
self.all_contents.len()
}
fn is_at_page_top(&self, sp: &SectionProperties) -> bool {
(self.slot_top - (sp.page_height - sp.margin_top)).abs() < 1.0
}
fn advance_column_or_page(
&mut self,
current_col: &mut usize,
col_count: usize,
sect_idx: usize,
sp: &SectionProperties,
effective_margin_bottom: &mut f32,
ctx: &RenderContext,
) {
if *current_col + 1 < col_count {
*current_col += 1;
self.slot_top = self.column_top_y;
} else {
*current_col = 0;
self.flush_page(sect_idx);
self.slot_top = effective_slot_top(sp, false, ctx);
self.column_top_y = self.slot_top;
*effective_margin_bottom = compute_effective_margin_bottom(sp, false, ctx);
self.is_first_page_of_section = false;
}
}
}
pub(super) struct LayoutState {
pub(super) pb: PageBuilder,
pub(super) prev_space_after: f32,
pub(super) effective_margin_bottom: f32,
pub(super) current_col: usize,
pub(super) global_block_idx: usize,
pub(super) heading_entries: Vec<HeadingEntry>,
pub(super) bookmark_positions: HashMap<String, (usize, f32)>,
}
fn compute_text_hanging(para: &Paragraph, default_tab_stop: f32) -> f32 {
if !para.list_label.is_empty() {
if let Some(nts) = para.num_level_tab_stop {
if nts < para.indent_left
&& (para.indent_left - para.indent_hanging).abs() < 0.5
{
(para.indent_left - nts).max(0.0)
} else if nts > para.indent_left
&& para.indent_hanging == 0.0
&& para.indent_first_line == 0.0
{
-(nts - para.indent_left)
} else if para.indent_first_line > 0.0 && para.indent_hanging == 0.0 {
-para.indent_first_line
} else {
0.0
}
} else if para.indent_hanging == 0.0
&& para.indent_first_line == 0.0
&& default_tab_stop > 0.0
{
let next_tab =
((para.indent_left / default_tab_stop).floor() + 1.0) * default_tab_stop;
-(next_tab - para.indent_left)
} else if para.indent_first_line > 0.0 && para.indent_hanging == 0.0 {
-para.indent_first_line
} else {
0.0
}
} else if para.indent_hanging > 0.0 {
para.indent_hanging
} else {
-para.indent_first_line
}
}
fn compute_bookmark_positions(
doc: &Document,
ctx: &RenderContext,
) -> HashMap<String, (usize, f32)> {
let has_pagerefs = doc.sections.iter().any(|s| {
s.blocks.iter().any(|b| match b {
Block::Paragraph(p) => p
.runs
.iter()
.any(|r| matches!(&r.field_code, Some(FieldCode::PageRef(_)))),
_ => false,
})
});
if !has_pagerefs {
return HashMap::new();
}
let mut bookmark_positions: HashMap<String, (usize, f32)> = HashMap::new();
let mut page_idx = 0usize;
let first_sp = &doc.sections[0].properties;
let mut sp = first_sp;
let mut slot_top = effective_slot_top(sp, true, ctx);
let mut margin_bottom = compute_effective_margin_bottom(sp, true, ctx);
let mut prev_space_after: f32 = 0.0;
let mut prev_contextual: bool = false;
let empty_imgs: HashMap<usize, String> = HashMap::new();
let empty_fx: HashMap<usize, images::EffectXObjs> = HashMap::new();
for (si, section) in doc.sections.iter().enumerate() {
sp = §ion.properties;
if si > 0 {
match sp.break_type {
SectionBreakType::NextPage
| SectionBreakType::OddPage
| SectionBreakType::EvenPage => {
page_idx += 1;
slot_top = effective_slot_top(sp, true, ctx);
margin_bottom = compute_effective_margin_bottom(sp, true, ctx);
prev_space_after = 0.0;
}
SectionBreakType::Continuous => {}
}
}
let text_width = sp.page_width - sp.margin_left - sp.margin_right;
let blocks = §ion.blocks;
for (bi, block) in blocks.iter().enumerate() {
match block {
Block::Paragraph(para) => {
if para.page_break_before
&& slot_top < effective_slot_top(sp, false, ctx)
{
page_idx += 1;
slot_top = effective_slot_top(sp, false, ctx);
margin_bottom = compute_effective_margin_bottom(sp, false, ctx);
prev_space_after = 0.0;
}
for bm in ¶.bookmarks {
bookmark_positions.insert(bm.clone(), (page_idx, slot_top));
}
if para.is_section_break && is_text_empty(¶.runs) {
continue;
}
let (mut font_size, mut tallest_lhr, _) =
tallest_run_metrics(¶.runs, ctx.fonts);
if tallest_lhr.is_none() {
if let Some(mark_fs) = para.paragraph_mark_font_size {
font_size = mark_fs;
}
if let Some(ref mark_fn) = para.paragraph_mark_font_name {
if let Some(entry) = ctx.fonts.get(mark_fn.as_str()) {
tallest_lhr = entry.line_h_ratio;
}
}
}
let effective_ls = para.line_spacing.unwrap_or(ctx.doc_line_spacing);
let line_h = resolve_line_h(effective_ls, font_size, tallest_lhr);
let line_h = if para.snap_to_grid
&& matches!(
sp.grid_type,
DocGridType::Lines
| DocGridType::LinesAndChars
| DocGridType::SnapToChars
)
&& !matches!(effective_ls, LineSpacing::Exact(_))
&& sp.line_pitch > 0.0
{
(line_h / sp.line_pitch).ceil() * sp.line_pitch
} else {
line_h
};
let para_w =
(text_width - para.indent_left - para.indent_right).max(1.0);
let hanging = compute_text_hanging(para, ctx.default_tab_stop);
let has_tabs = para.runs.iter().any(|r| r.is_tab);
let lines = if is_text_empty(¶.runs) {
vec![]
} else if has_tabs {
build_tabbed_line(
¶.runs,
ctx.fonts,
¶.tab_stops,
para.indent_left,
para_w,
para.indent_right,
hanging,
&empty_imgs,
&empty_fx,
doc.default_tab_stop,
)
} else {
build_paragraph_lines(
¶.runs, ctx.fonts, para_w, hanging, &empty_imgs,
&empty_fx, None, None, None,
para.auto_space_de || para.auto_space_dn,
)
};
let num_lines = lines.len().max(1);
let para_has_inline_img =
para.runs.iter().any(|r| r.inline_image.is_some());
let content_h = if para.image.is_some() || para.inline_chart.is_some()
{
para.content_height
} else if para_has_inline_img && para.content_height > 0.0 {
para.content_height.max(num_lines as f32 * line_h)
} else {
num_lines as f32 * line_h
};
let effective_sb = if para.contextual_spacing && prev_contextual {
0.0
} else {
para.space_before
};
let next_contextual = blocks.get(bi + 1).is_some_and(|b| {
matches!(b, Block::Paragraph(p) if p.contextual_spacing)
});
let effective_sa = if para.contextual_spacing && next_contextual {
0.0
} else {
para.space_after
};
let inter_gap = f32::max(prev_space_after, effective_sb);
let needed = inter_gap + content_h;
if slot_top - needed < margin_bottom
&& slot_top < effective_slot_top(sp, false, ctx)
{
page_idx += 1;
slot_top = effective_slot_top(sp, false, ctx);
margin_bottom =
compute_effective_margin_bottom(sp, false, ctx);
slot_top -= content_h;
} else {
slot_top -= inter_gap + content_h;
}
prev_space_after = effective_sa;
prev_contextual = para.contextual_spacing;
}
Block::Table(table) => {
let para_count: usize = table
.rows
.iter()
.flat_map(|r| r.cells.iter())
.map(|c| c.all_paragraphs().len().max(1))
.max()
.unwrap_or(1)
* table.rows.len();
let est_h = para_count as f32 * 14.0;
if slot_top - est_h < margin_bottom {
page_idx += 1;
slot_top = effective_slot_top(sp, false, ctx);
margin_bottom =
compute_effective_margin_bottom(sp, false, ctx);
}
slot_top -= est_h;
prev_space_after = 0.0;
prev_contextual = false;
}
}
}
}
bookmark_positions
}
#[allow(clippy::too_many_arguments)]
fn render_paragraph_block(
para: &Paragraph,
state: &mut LayoutState,
ctx: &RenderContext,
sp: &SectionProperties,
col_geometry: &[(f32, f32)],
col_count: usize,
text_width: f32,
sect_idx: usize,
block_idx: usize,
section_blocks: &[Block],
floating_image_pdf_names: &HashMap<(usize, usize), String>,
inline_image_pdf_names: &HashMap<(usize, usize), String>,
image_pdf_names: &HashMap<usize, String>,
effect_names: &HashMap<usize, EffectXObjs>,
effect_floating_names: &HashMap<(usize, usize), EffectXObjs>,
effect_inline_names: &HashMap<(usize, usize), EffectXObjs>,
footnote_display_order: &HashMap<u32, u32>,
endnote_display_order: &HashMap<u32, u32>,
doc: &Document,
smartart_font_key: &str,
smartart_image_names: &HashMap<usize, String>,
debug_wrap: bool,
) -> bool {
let adjacent_para = |idx: usize| -> Option<&Paragraph> {
match section_blocks.get(idx)? {
Block::Paragraph(p) => Some(p),
Block::Table(_) => None,
}
};
if para.is_section_break
&& is_text_empty(¶.runs)
&& para.image.is_none()
&& para.inline_chart.is_none()
&& para.smartart.is_empty()
&& para.floating_images.is_empty()
&& para.textboxes.is_empty()
{
state.global_block_idx += 1;
return true;
}
if para.page_break_before {
let at_top = state.pb.is_at_page_top(sp);
if !at_top || para.page_break_before_explicit {
state.pb.flush_page(sect_idx);
state.pb.slot_top = effective_slot_top(sp, false, &ctx);
state.pb.column_top_y = state.pb.slot_top;
state.effective_margin_bottom =
compute_effective_margin_bottom(sp, false, &ctx);
state.pb.is_first_page_of_section = false;
state.current_col = 0;
}
state.prev_space_after = 0.0;
if is_text_empty(¶.runs) {
state.global_block_idx += 1;
return true;
}
}
if para.column_break_before && col_count > 1 {
state.pb.advance_column_or_page(
&mut state.current_col,
col_count,
sect_idx,
sp,
&mut state.effective_margin_bottom,
&ctx,
);
state.prev_space_after = 0.0;
}
let next_para = adjacent_para(block_idx + 1);
let prev_para = if block_idx > 0 {
adjacent_para(block_idx - 1)
} else {
None
};
let effective_space_before = if para.contextual_spacing
&& prev_para.is_some_and(|p| p.contextual_spacing)
{
0.0
} else {
para.space_before
};
let effective_space_after = if para.contextual_spacing
&& next_para.is_some_and(|p| p.contextual_spacing)
{
0.0
} else {
para.space_after
};
let mut inter_gap = f32::max(state.prev_space_after, effective_space_before);
let (mut font_size, mut tallest_lhr, tallest_ar) =
tallest_run_metrics(¶.runs, ctx.fonts);
if tallest_lhr.is_none() {
if let Some(mark_fs) = para.paragraph_mark_font_size {
font_size = mark_fs;
}
let mark_font_name = para
.paragraph_mark_font_name
.as_deref()
.unwrap_or(¶.runs.first().map(|r| r.font_name.as_str()).unwrap_or(""));
if !mark_font_name.is_empty() {
if let Some(entry) = ctx.fonts.get(mark_font_name) {
tallest_lhr = entry.line_h_ratio;
}
}
}
let effective_ls = para.line_spacing.unwrap_or(ctx.doc_line_spacing);
let line_h = resolve_line_h(effective_ls, font_size, tallest_lhr);
let grid_snapped = para.snap_to_grid
&& matches!(
sp.grid_type,
DocGridType::Lines
| DocGridType::LinesAndChars
| DocGridType::SnapToChars
)
&& !matches!(effective_ls, LineSpacing::Exact(_))
&& sp.line_pitch > 0.0;
let line_h = if grid_snapped {
(line_h / sp.line_pitch).ceil() * sp.line_pitch
} else {
line_h
};
let (col_x, col_w) = col_geometry[state.current_col];
let mut para_text_x = col_x + para.indent_left;
let mut para_text_width =
(col_w - para.indent_left - para.indent_right).max(1.0);
let mut label_x = col_x + para.indent_left - para.indent_hanging;
if let Some(ref fz) = state.pb.float_zone {
if state.pb.slot_top <= fz.top_y && state.pb.slot_top > fz.bottom_y {
let col_right = col_x + col_w;
let (ex_left, ex_right) = fz.exclusion_at_y(state.pb.slot_top);
let space_right =
col_right - (ex_right + fz.right_from_text);
let space_left = (ex_left - fz.left_from_text) - col_x;
if fz.wrap_text == WrapText::BothSides {
let lw = (space_left - para.indent_left).max(0.0);
let rw = (space_right - para.indent_right).max(0.0);
if rw > lw {
let new_left = ex_right + fz.right_from_text;
para_text_width = rw.max(1.0);
para_text_x = new_left + para.indent_left;
label_x =
new_left + para.indent_left - para.indent_hanging;
} else if lw > 0.0 {
para_text_width = lw.max(1.0);
}
} else {
let use_right = match fz.wrap_text {
WrapText::Right => space_right >= 1.0,
WrapText::Left => !(space_left >= 1.0),
_ => space_right >= space_left && space_right >= 72.0,
};
let use_left = match fz.wrap_text {
WrapText::Left => space_left >= 1.0,
WrapText::Right => false,
_ => space_left >= 72.0,
};
if use_right {
let new_left = ex_right + fz.right_from_text;
para_text_width =
(col_right - new_left - para.indent_right).max(1.0);
para_text_x = new_left + para.indent_left;
label_x =
new_left + para.indent_left - para.indent_hanging;
} else if use_left {
let avail_right = ex_left - fz.left_from_text;
para_text_width = (avail_right - col_x
- para.indent_left
- para.indent_right)
.max(1.0);
}
}
}
}
let text_hanging = compute_text_hanging(para, ctx.default_tab_stop);
let has_footnote_refs = para.runs.iter().any(|r| r.footnote_id.is_some());
let has_endnote_refs = para.runs.iter().any(|r| r.endnote_id.is_some());
let has_pageref = para.runs.iter().any(|r| matches!(&r.field_code, Some(FieldCode::PageRef(_))));
let effective_runs: std::borrow::Cow<'_, Vec<Run>> = if has_footnote_refs || has_endnote_refs || has_pageref {
let substituted: Vec<Run> = para
.runs
.iter()
.map(|run| {
if let Some(id) = run.footnote_id {
let num = footnote_display_order.get(&id).copied().unwrap_or(0);
let mut r = run.clone();
r.text = num.to_string();
r
} else if let Some(id) = run.endnote_id {
let num = endnote_display_order.get(&id).copied().unwrap_or(0);
let mut r = run.clone();
r.text = num.to_string();
r
} else if let Some(FieldCode::PageRef(ref bookmark)) = run.field_code {
let mut r = run.clone();
if let Some(&(page_idx, _)) = state.bookmark_positions.get(bookmark) {
r.text = (page_idx + 1).to_string();
}
r
} else {
run.clone()
}
})
.collect();
std::borrow::Cow::Owned(substituted)
} else {
std::borrow::Cow::Borrowed(¶.runs)
};
let text_empty = is_text_empty(&effective_runs);
let has_tabs = effective_runs.iter().any(|r| r.is_tab);
let block_inline_images: HashMap<usize, String> = inline_image_pdf_names
.iter()
.filter(|((bi, _), _)| *bi == state.global_block_idx)
.map(|((_, ri), name)| (*ri, name.clone()))
.collect();
let block_effect_inlines: HashMap<usize, images::EffectXObjs> = effect_inline_names
.iter()
.filter(|((bi, _), _)| *bi == state.global_block_idx)
.map(|((_, ri), fx)| (*ri, fx.clone()))
.collect();
if !para.floating_images.is_empty()
&& !text_empty
{
if let Some(fi) = para.floating_images.iter().find(|fi| {
matches!(
fi.wrap_type,
WrapType::Square | WrapType::Tight | WrapType::Through
)
}) {
let fi_x =
resolve_fi_x(fi, sp, col_x, col_w, text_width);
let fi_y_top =
resolve_fi_y_top(fi, sp, state.pb.slot_top);
let fi_y_bottom =
fi_y_top - fi.image.display_height;
let polygon_pts =
fi.wrap_polygon.as_ref().map(|verts| {
convert_polygon_to_page_coords(
verts,
fi_x,
fi_y_top,
fi.image.display_width,
fi.image.display_height,
)
});
state.pb.float_zone = Some(FloatZone {
top_y: fi_y_top + fi.dist_top,
bottom_y: fi_y_bottom - fi.dist_bottom,
obj_left: fi_x,
obj_right: fi_x + fi.image.display_width,
left_from_text: fi.dist_left,
right_from_text: fi.dist_right,
polygon_pts,
wrap_text: fi.wrap_text,
para_relative: fi.v_relative_from == VRelativeFrom::Paragraph,
});
let fz = state.pb.float_zone.as_ref().unwrap();
if state.pb.slot_top <= fz.top_y && state.pb.slot_top > fz.bottom_y
{
let col_right = col_x + col_w;
let (ex_left, ex_right) =
fz.exclusion_at_y(state.pb.slot_top);
let space_right = col_right
- (ex_right + fz.right_from_text);
let space_left =
(ex_left - fz.left_from_text) - col_x;
if fz.wrap_text == WrapText::BothSides {
let lw = (space_left - para.indent_left).max(0.0);
let rw = (space_right - para.indent_right).max(0.0);
if rw > lw {
let new_left = ex_right + fz.right_from_text;
para_text_width = rw.max(1.0);
para_text_x = new_left + para.indent_left;
label_x = new_left + para.indent_left
- para.indent_hanging;
} else if lw > 0.0 {
para_text_width = lw.max(1.0);
}
} else {
let use_right = match fz.wrap_text {
WrapText::Right => space_right >= 1.0,
WrapText::Left => !(space_left >= 1.0),
_ => space_right >= space_left && space_right >= 72.0,
};
let use_left = match fz.wrap_text {
WrapText::Left => space_left >= 1.0,
WrapText::Right => false,
_ => space_left >= 72.0,
};
if use_right {
let new_left =
ex_right + fz.right_from_text;
para_text_width = (col_right
- new_left
- para.indent_right)
.max(1.0);
para_text_x =
new_left + para.indent_left;
label_x = new_left + para.indent_left
- para.indent_hanging;
} else if use_left {
let avail_right =
ex_left - fz.left_from_text;
para_text_width = (avail_right - col_x
- para.indent_left
- para.indent_right)
.max(1.0);
}
}
}
}
}
let (poly_line_geom, poly_dual_geom): (Option<Vec<(f32, f32)>>, Option<Vec<DualRegion>>) =
if let Some(fz) = state.pb.float_zone.as_ref() {
let eff_top = state.pb.slot_top - inter_gap;
if eff_top <= fz.bottom_y {
(None, None)
} else {
let is_both_sides =
fz.wrap_text == WrapText::BothSides;
let full_w = (col_w
- para.indent_left
- para.indent_right)
.max(1.0);
let col_right = col_x + col_w;
let max_lines = ((eff_top - fz.bottom_y)
/ line_h)
.ceil() as usize
+ 5;
let max_lines = max_lines.max(50);
let mut geom = Vec::with_capacity(max_lines);
let mut dual = if is_both_sides {
Some(Vec::with_capacity(max_lines))
} else {
None
};
let bottom_threshold = fz.bottom_y + line_h * 0.2;
let partial_overlap =
fz.para_relative || fz.polygon_pts.is_some();
for i in 0..max_lines {
let line_top = eff_top - i as f32 * line_h;
let line_bottom = line_top - line_h;
let in_zone = if partial_overlap {
line_bottom < fz.top_y
} else {
line_top <= fz.top_y
};
if in_zone && line_top > bottom_threshold
{
let query_y = line_top.min(fz.top_y);
let (ex_left, ex_right) =
fz.exclusion_at_y(query_y);
let sr = col_right
- (ex_right + fz.right_from_text);
let sl =
(ex_left - fz.left_from_text) - col_x;
if is_both_sides {
let lx = col_x + para.indent_left;
let lw = (sl - para.indent_left).max(0.0);
let rx = ex_right + fz.right_from_text;
let rw = (sr - para.indent_right).max(0.0);
if let Some(ref mut d) = dual {
d.push((lx, lw, rx, rw));
}
geom.push((lx, lw));
} else {
let use_right = match fz.wrap_text {
WrapText::Right => sr >= 1.0,
WrapText::Left => !(sl >= 1.0),
_ => sr >= sl && sr >= 72.0,
};
let use_left = match fz.wrap_text {
WrapText::Left => sl >= 1.0,
WrapText::Right => false,
_ => sl >= 72.0,
};
if use_right {
let nl =
ex_right + fz.right_from_text;
let w = (col_right
- nl
- para.indent_right)
.max(1.0);
geom.push((
nl + para.indent_left,
w,
));
} else if use_left {
let ar =
ex_left - fz.left_from_text;
let w = (ar - col_x
- para.indent_left
- para.indent_right)
.max(1.0);
geom.push((
col_x + para.indent_left,
w,
));
} else {
geom.push((
col_x + para.indent_left,
full_w,
));
}
}
} else {
geom.push((
col_x + para.indent_left,
full_w,
));
if let Some(ref mut d) = dual {
d.push((col_x + para.indent_left, full_w, 0.0, 0.0));
}
}
}
(Some(geom), dual)
}
} else {
(None, None)
};
let poly_line_widths: Option<Vec<f32>> =
poly_line_geom.as_ref().map(|g| {
g.iter().map(|&(_, w)| w).collect()
});
let mut float_width_change: Option<(usize, f32)> = None;
let mut lookahead_narrow: Option<(f32, f32)> = None;
let has_inline_image_runs = effective_runs.iter().any(|r| r.inline_image.is_some());
let lines = if para.image.is_some() || (text_empty && !has_inline_image_runs) {
vec![]
} else if has_tabs {
build_tabbed_line(
&effective_runs,
ctx.fonts,
¶.tab_stops,
para.indent_left,
para_text_width,
para.indent_right,
text_hanging,
&block_inline_images,
&block_effect_inlines,
doc.default_tab_stop,
)
} else {
let lookahead_fi = if state.pb.float_zone.is_none() {
section_blocks.get(block_idx + 1).and_then(|b| {
if let Block::Paragraph(np) = b {
if !np.floating_images.is_empty()
&& is_text_empty(&np.runs)
&& np.image.is_none()
&& np.inline_chart.is_none()
{
np.floating_images.iter().find(|fi| matches!(
fi.wrap_type,
WrapType::Square | WrapType::Tight | WrapType::Through
) && fi.image.display_width < text_width * 0.5)
} else { None }
} else { None }
})
} else { None };
let auto_space = para.auto_space_de || para.auto_space_dn;
let (lines, final_width_change) = if let Some(fi) = lookahead_fi {
let full_lines = build_paragraph_lines(
&effective_runs, ctx.fonts, para_text_width,
text_hanging, &block_inline_images, &block_effect_inlines, None, None, None,
auto_space,
);
let num_lines = full_lines.len();
let _content_h_est = num_lines as f32 * line_h;
let fi_x = resolve_fi_x(fi, sp, col_x, col_w, col_w);
let space_right = (col_x + col_w)
- (fi_x + fi.image.display_width + fi.dist_right);
let space_left = (fi_x - fi.dist_left) - col_x;
let best = space_right.max(space_left);
let overlap = fi.dist_top;
let lines_to_narrow = if best >= 72.0 && overlap > 0.0 {
((overlap / line_h).ceil() as usize).min(num_lines)
} else { 0 };
if lines_to_narrow > 0 {
let lines_above = num_lines.saturating_sub(lines_to_narrow);
let narrow_w = if space_right >= space_left {
((col_x + col_w) - (fi_x + fi.image.display_width + fi.dist_right)
- para.indent_right).max(1.0)
} else {
(fi_x - fi.dist_left - col_x
- para.indent_left - para.indent_right).max(1.0)
};
let narrow_x = if space_right >= space_left {
fi_x + fi.image.display_width + fi.dist_right
+ para.indent_left
} else {
col_x + para.indent_left
};
lookahead_narrow = Some((narrow_x, narrow_w));
let rebuilt = build_paragraph_lines(
&effective_runs, ctx.fonts, para_text_width,
text_hanging, &block_inline_images, &block_effect_inlines,
Some((lines_above, narrow_w)), None, None,
auto_space,
);
(rebuilt, Some((lines_above, narrow_w)))
} else {
(full_lines, None)
}
} else {
let plw: Option<&[f32]> = if poly_dual_geom.is_some() { None } else { poly_line_widths.as_deref() };
let built = build_paragraph_lines(
&effective_runs, ctx.fonts, para_text_width,
text_hanging, &block_inline_images, &block_effect_inlines, None,
plw, poly_dual_geom.as_deref(),
auto_space,
);
(built, None)
};
float_width_change = final_width_change;
lines
};
let max_inline_img_h = lines
.iter()
.flat_map(|l| l.chunks.iter())
.map(|c| c.inline_image_height)
.fold(0.0f32, f32::max);
let mut content_h = if para.inline_chart.is_some() {
para.content_height
} else if para.image.is_some() {
para.content_height
} else if max_inline_img_h > 0.0 {
let mut h = 0.0f32;
for line in &lines {
let img_h = line
.chunks
.iter()
.map(|c| c.inline_image_height)
.fold(0.0f32, f32::max);
h += if img_h > line_h { img_h } else { line_h };
}
h
} else if text_empty {
if para.paragraph_mark_vanish {
0.0
} else if para.content_height > 0.0 {
para.content_height
} else {
line_h
}
} else {
let num_lines = lines.len();
let first_line_h = label_boosted_line_h(para, ctx.fonts, line_h, effective_ls, font_size);
if num_lines <= 1 {
if let Some(bfs) = lines.first().and_then(|l| l.break_font_size) {
let blhr = break_run_lhr(&effective_runs, bfs, ctx.fonts);
resolve_line_h(effective_ls, bfs, blhr)
} else {
first_line_h
}
} else {
let mut h = first_line_h;
for line in lines.iter().skip(1) {
if let Some(bfs) = line.break_font_size {
let blhr = break_run_lhr(&effective_runs, bfs, ctx.fonts);
h += resolve_line_h(effective_ls, bfs, blhr);
} else {
h += line_h;
}
}
h
}
};
let mut float_overflow_h = 0.0f32;
for fi in ¶.floating_images {
let reserve = match fi.wrap_type {
WrapType::TopAndBottom => true,
WrapType::Square | WrapType::Tight | WrapType::Through => false,
WrapType::None => false,
};
let fi_h = match fi.v_position {
VerticalPosition::Offset(o) => {
o + fi.dist_top + fi.image.display_height + fi.dist_bottom
}
_ => fi.dist_top + fi.image.display_height + fi.dist_bottom,
};
if reserve {
content_h = content_h.max(fi_h);
} else if fi.v_relative_from == VRelativeFrom::Paragraph
&& matches!(
fi.wrap_type,
WrapType::Square | WrapType::Tight | WrapType::Through
)
{
float_overflow_h = float_overflow_h.max(fi_h);
}
}
for tb in ¶.textboxes {
let reserve = match tb.wrap_type {
WrapType::TopAndBottom => true,
WrapType::Square => tb.width_pt >= text_width * 0.5,
_ => false,
};
if reserve {
let tb_bottom = tb.v_offset_pt + tb.height_pt + tb.dist_bottom;
match tb.v_relative_from {
VRelativeFrom::Paragraph => {
content_h = content_h.max(tb_bottom);
}
_ => {
content_h += tb_bottom;
}
}
}
}
if text_empty && para.paragraph_mark_vanish {
content_h = 0.0;
inter_gap = 0.0;
}
let prev_borders_match = prev_para
.is_some_and(|pp| borders_match(&pp.borders, ¶.borders));
let next_borders_match = next_para
.is_some_and(|np| borders_match(¶.borders, &np.borders));
let bdr_top_pad = if prev_borders_match {
0.0
} else {
para.borders
.top
.as_ref()
.map(|b| b.space_pt + b.width_pt / 2.0)
.unwrap_or(0.0)
};
let bdr_bottom_pad = if next_borders_match {
0.0
} else {
para.borders
.bottom
.as_ref()
.map(|b| b.space_pt + b.width_pt / 2.0)
.unwrap_or(0.0)
};
let bdr_bottom_extent = if next_borders_match {
0.0
} else {
para.borders
.bottom
.as_ref()
.map(|b| b.space_pt + b.width_pt)
.unwrap_or(0.0)
};
let needed = inter_gap + bdr_top_pad + content_h + bdr_bottom_extent;
let needed_with_floats = needed.max(inter_gap + float_overflow_h);
let at_page_top = state.pb.is_at_page_top(sp);
let last_line_lead = if !lines.is_empty()
&& para.image.is_none()
&& para.inline_chart.is_none()
&& para.smartart.is_empty()
&& !matches!(effective_ls, LineSpacing::Exact(_))
{
let single_h = tallest_lhr
.map(|r| font_size * r)
.unwrap_or(font_size * 1.2);
(line_h - single_h).max(0.0)
} else {
0.0
};
let keep_next_extra = if para.keep_next {
let mut extra = 0.0;
let mut prev_sa = effective_space_after;
let mut i = block_idx + 1;
while let Some(next) = adjacent_para(i) {
if next.page_break_before {
extra = f32::MAX;
break;
}
let (nfs, nlhr, _) = tallest_run_metrics(&next.runs, ctx.fonts);
let next_inter = f32::max(prev_sa, next.space_before);
let next_first_line_h =
nlhr.map(|ratio| nfs * ratio).unwrap_or(nfs * 1.2);
if !next.keep_next {
let next_ls = next.line_spacing.unwrap_or(ctx.doc_line_spacing);
let next_line_h = resolve_line_h(next_ls, nfs, nlhr);
extra += next_inter + next_first_line_h + next_line_h;
break;
}
if next.page_break_after {
extra = f32::MAX;
break;
}
extra += next_inter + next_first_line_h;
prev_sa = next.space_after;
i += 1;
}
extra
} else {
0.0
};
let para_fn_extra = {
let mut extra = 0.0f32;
let mut seen_ids = HashSet::new();
for run in para.runs.iter() {
if let Some(id) = run.footnote_id {
if !state.pb.footnote_ids_set.contains(&id)
&& seen_ids.insert(id)
{
if let Some(footnote) = doc.footnotes.get(&id) {
extra += compute_footnote_height(
footnote, &ctx, text_width,
);
}
}
}
}
if extra > 0.0 && state.pb.footnote_ids.is_empty() {
extra += 12.0;
}
extra
};
if !at_page_top
&& state.pb.slot_top - needed_with_floats - keep_next_extra + last_line_lead
< state.effective_margin_bottom + para_fn_extra
{
let available =
state.pb.slot_top - inter_gap - state.effective_margin_bottom - para_fn_extra;
let first_line_h = tallest_lhr
.map(|ratio| font_size * ratio)
.unwrap_or(font_size);
let mut lines_that_fit = if line_h > 0.0 && available >= first_line_h {
1 + ((available - first_line_h) / line_h).floor() as usize
} else {
0
};
if para.widow_control {
if lines_that_fit > 0
&& lines.len().saturating_sub(lines_that_fit) < 2
{
lines_that_fit = lines.len().saturating_sub(2);
}
}
if para.keep_lines {
lines_that_fit = 0;
}
let min_split_lines = if para.widow_control { 2 } else { 1 };
if lines_that_fit >= min_split_lines && lines_that_fit < lines.len() {
let first_part = &lines[..lines_that_fit];
state.pb.slot_top -= inter_gap;
let ascender_ratio = tallest_ar.unwrap_or(0.75);
let baseline_offset = if grid_snapped {
sp.line_pitch
} else {
font_size * ascender_ratio
};
let baseline_y = state.pb.slot_top - baseline_offset;
render_list_label(
&mut state.pb.content,
para,
ctx.fonts,
label_x,
baseline_y,
font_size,
);
render_paragraph_lines(
&mut state.pb.content,
first_part,
¶.alignment,
para_text_x,
para_text_width,
baseline_y,
line_h,
lines.len(),
0,
&mut state.pb.links,
text_hanging,
ctx.fonts,
poly_line_geom.as_deref(),
);
state.pb.advance_column_or_page(
&mut state.current_col,
col_count,
sect_idx,
sp,
&mut state.effective_margin_bottom,
&ctx,
);
let rest = &lines[lines_that_fit..];
let rest_content_h = rest.len() as f32 * line_h;
let baseline_offset2 = if grid_snapped {
sp.line_pitch
} else {
font_size * ascender_ratio
};
let baseline_y2 = state.pb.slot_top - baseline_offset2;
let (rest_col_x, rest_col_w) = col_geometry[state.current_col];
let rest_text_x = rest_col_x + para.indent_left;
let rest_text_width =
(rest_col_w - para.indent_left - para.indent_right).max(1.0);
render_paragraph_lines(
&mut state.pb.content,
rest,
¶.alignment,
rest_text_x,
rest_text_width,
baseline_y2,
line_h,
lines.len(),
lines_that_fit,
&mut state.pb.links,
text_hanging,
ctx.fonts,
None,
);
state.pb.slot_top -= rest_content_h;
state.prev_space_after = effective_space_after;
for run in para.runs.iter() {
if let Some(id) = run.footnote_id {
if state.pb.footnote_ids_set.insert(id) {
state.pb.footnote_ids.push(id);
if let Some(footnote) = doc.footnotes.get(&id) {
let fn_height =
compute_footnote_height(footnote, &ctx, text_width);
let separator_h = if state.pb.footnote_ids.len() == 1 {
12.0
} else {
0.0
};
state.effective_margin_bottom += separator_h + fn_height;
}
}
}
if let Some(id) = run.endnote_id {
if state.pb.endnote_ids_set.insert(id) {
state.pb.endnote_ids.push(id);
}
}
}
state.global_block_idx += 1;
return true;
}
state.pb.advance_column_or_page(
&mut state.current_col,
col_count,
sect_idx,
sp,
&mut state.effective_margin_bottom,
&ctx,
);
inter_gap = 0.0;
}
let at_new_page_top = !state.pb.all_contents.is_empty() && state.pb.is_at_page_top(sp);
if at_new_page_top {
if state.pb.is_first_page_of_section {
inter_gap = (effective_space_before - state.prev_space_after).max(0.0);
} else {
inter_gap = 0.0;
}
}
let applied_inter_gap = inter_gap;
state.pb.slot_top -= inter_gap;
for bookmark in ¶.bookmarks {
state.bookmark_positions
.insert(bookmark.clone(), (state.pb.all_contents.len(), state.pb.slot_top));
}
if let Some(level) = para.outline_level {
let title: String = para.runs.iter().map(|r| r.text.as_str()).collect();
if !title.trim().is_empty() {
state.heading_entries.push(HeadingEntry {
title: title.trim().to_string(),
level,
page_idx: state.pb.all_contents.len(),
y_position: state.pb.slot_top,
});
}
}
let (col_x, col_w) = col_geometry[state.current_col];
para_text_x = col_x + para.indent_left;
para_text_width = (col_w - para.indent_left - para.indent_right).max(1.0);
label_x = col_x + para.indent_left - para.indent_hanging;
if let Some(ref fz) = state.pb.float_zone {
if state.pb.slot_top <= fz.top_y && state.pb.slot_top > fz.bottom_y {
let col_right = col_x + col_w;
let (ex_left, ex_right) = fz.exclusion_at_y(state.pb.slot_top);
let space_right =
col_right - (ex_right + fz.right_from_text);
let space_left = (ex_left - fz.left_from_text) - col_x;
if fz.wrap_text == WrapText::BothSides {
let lw = (space_left - para.indent_left).max(0.0);
let rw = (space_right - para.indent_right).max(0.0);
if rw > lw {
let new_left = ex_right + fz.right_from_text;
para_text_width = rw.max(1.0);
para_text_x = new_left + para.indent_left;
label_x =
new_left + para.indent_left - para.indent_hanging;
} else if lw > 0.0 {
para_text_width = lw.max(1.0);
}
} else {
let use_right = match fz.wrap_text {
WrapText::Right => space_right >= 1.0,
WrapText::Left => !(space_left >= 1.0),
_ => space_right >= space_left && space_right >= 72.0,
};
let use_left = match fz.wrap_text {
WrapText::Left => space_left >= 1.0,
WrapText::Right => false,
_ => space_left >= 72.0,
};
if use_right {
let new_left = ex_right + fz.right_from_text;
para_text_width =
(col_right - new_left - para.indent_right).max(1.0);
para_text_x = new_left + para.indent_left;
label_x =
new_left + para.indent_left - para.indent_hanging;
} else if use_left {
let avail_right = ex_left - fz.left_from_text;
para_text_width = (avail_right - col_x
- para.indent_left
- para.indent_right)
.max(1.0);
}
}
}
}
render_floating_images(
¶.floating_images,
true,
state.global_block_idx,
&floating_image_pdf_names,
&effect_floating_names,
sp,
col_x,
col_w,
text_width,
state.pb.slot_top,
&mut state.pb.content,
);
for tb in para.textboxes.iter().filter(|t| t.behind_doc) {
render_single_textbox(
tb,
sp,
col_x,
col_w,
text_width,
state.pb.slot_top,
&mut state.pb.content,
&mut state.pb.gradient_specs,
&ctx,
&mut state.pb.links,
);
}
if let Some(shd_color) = para.shading {
let shd_left_outset = para
.borders
.left
.as_ref()
.map(|b| b.space_pt)
.unwrap_or(0.0);
let shd_right_outset = para
.borders
.right
.as_ref()
.map(|b| b.space_pt)
.unwrap_or(0.0);
let shd_left = col_x - shd_left_outset;
let shd_right = col_x + col_w + shd_right_outset;
let shd_top = state.pb.slot_top
+ if prev_borders_match { applied_inter_gap } else { 0.0 };
let shd_bottom =
state.pb.slot_top - bdr_top_pad - content_h - bdr_bottom_pad;
state.pb.content.save_state();
fill_rgb(&mut state.pb.content, shd_color);
state.pb.content.rect(
shd_left,
shd_bottom,
shd_right - shd_left,
shd_top - shd_bottom,
);
state.pb.content.fill_nonzero();
state.pb.content.restore_state();
}
render_floating_images(
¶.floating_images,
false,
state.global_block_idx,
&floating_image_pdf_names,
&effect_floating_names,
sp,
col_x,
col_w,
text_width,
state.pb.slot_top,
&mut state.pb.content,
);
for fi in ¶.floating_images {
match fi.wrap_type {
WrapType::Square | WrapType::Tight | WrapType::Through => {
let fi_x =
resolve_fi_x(fi, sp, col_x, col_w, text_width);
let fi_y_top =
resolve_fi_y_top(fi, sp, state.pb.slot_top);
let fi_y_bottom =
fi_y_top - fi.image.display_height;
let polygon_pts =
fi.wrap_polygon.as_ref().map(|verts| {
convert_polygon_to_page_coords(
verts,
fi_x,
fi_y_top,
fi.image.display_width,
fi.image.display_height,
)
});
state.pb.float_zone = Some(FloatZone {
top_y: fi_y_top + fi.dist_top,
bottom_y: fi_y_bottom - fi.dist_bottom,
obj_left: fi_x,
obj_right: fi_x + fi.image.display_width,
left_from_text: fi.dist_left,
right_from_text: fi.dist_right,
polygon_pts,
wrap_text: fi.wrap_text,
para_relative: fi.v_relative_from == VRelativeFrom::Paragraph,
});
}
_ => {}
}
}
if debug_wrap {
if let Some(ref fz) = state.pb.float_zone {
draw_debug_wrap_overlay(&mut state.pb.content, fz);
}
}
for tb in para.textboxes.iter().filter(|t| !t.behind_doc) {
render_single_textbox(
tb,
sp,
col_x,
col_w,
text_width,
state.pb.slot_top,
&mut state.pb.content,
&mut state.pb.gradient_specs,
&ctx,
&mut state.pb.links,
);
}
for conn in ¶.connectors {
render_connector(conn, &mut state.pb.content, col_x, state.pb.slot_top);
}
if let Some(ref ic) = para.inline_chart {
let chart_x = col_x
+ match para.alignment {
Alignment::Center => (col_w - ic.display_width).max(0.0) / 2.0,
Alignment::Right => (col_w - ic.display_width).max(0.0),
_ => 0.0,
};
charts::render_chart(
ic,
&mut state.pb.content,
chart_x,
state.pb.slot_top,
ctx.fonts,
ctx.chart_font_name,
&mut state.pb.alpha_states,
);
} else if !para.smartart.is_empty() {
for (i, diagram) in para.smartart.iter().enumerate() {
if i > 0 {
state.pb.slot_top -= diagram.display_height;
}
smartart::render_smartart(
&mut state.pb.content,
diagram,
col_x,
state.pb.slot_top,
ctx.fonts,
smartart_font_key,
&smartart_image_names,
);
}
} else if let Some(ref hr) = para.horizontal_rule {
let rule_w = col_w * hr.width_pct / 100.0;
let rule_x = col_x
+ match para.alignment {
Alignment::Center => (col_w - rule_w) / 2.0,
Alignment::Right => col_w - rule_w,
_ => 0.0,
};
let draw_h = if hr.is_standard { 0.5 } else { hr.height_pt };
let rule_y =
state.pb.slot_top - (content_h - draw_h) / 2.0 - draw_h;
state.pb.content.save_state();
fill_rgb(&mut state.pb.content, hr.fill_color);
state.pb.content
.rect(rule_x, rule_y, rule_w, draw_h);
state.pb.content.fill_nonzero();
state.pb.content.restore_state();
} else if para.image.is_some() && para.content_height > 0.0 {
if let Some(pdf_name) = image_pdf_names.get(&state.global_block_idx) {
let img = para.image.as_ref().unwrap();
let y_bottom = state.pb.slot_top - img.layout_extra_top - img.display_height;
let x = col_x
+ match para.alignment {
Alignment::Center => (col_w - img.display_width).max(0.0) / 2.0,
Alignment::Right => (col_w - img.display_width).max(0.0),
_ => 0.0,
};
let img_fx = effect_names.get(&state.global_block_idx);
if let Some(ref shadow) = img.shadow {
color::draw_image_shadow(
&mut state.pb.content, shadow, x, y_bottom,
img.display_width, img.display_height,
img_fx.and_then(|fx| fx.shadow.as_deref()),
);
}
if let Some(ref glow) = img.glow {
color::draw_image_glow(
&mut state.pb.content, glow, x, y_bottom,
img.display_width, img.display_height,
img_fx.and_then(|fx| fx.glow.as_deref()),
);
}
smartart::render_image_with_clip(
&mut state.pb.content, pdf_name, x, y_bottom,
img.display_width, img.display_height,
img.clip_geometry.as_ref(),
);
if let Some(sc) = img.stroke_color {
smartart::stroke_image_border(
&mut state.pb.content, x, y_bottom,
img.display_width, img.display_height,
sc, img.stroke_width,
img.clip_geometry.as_ref(),
);
}
if let Some(ref inner) = img.inner_shadow {
color::draw_inner_shadow(
&mut state.pb.content, inner, x, y_bottom,
img.display_width, img.display_height,
img_fx.and_then(|fx| fx.inner_shadow.as_deref()),
);
}
if let Some(ref refl) = img.reflection {
color::draw_reflection(
&mut state.pb.content, refl, x, y_bottom,
img.display_width, img.display_height,
img_fx.and_then(|fx| fx.reflection.as_deref()),
);
}
} else if para.image.is_some() {
state.pb.content
.set_fill_gray(0.5)
.rect(col_x, state.pb.slot_top - content_h, col_w, content_h)
.fill_nonzero()
.set_fill_gray(0.0);
}
} else if !lines.is_empty() {
let ascender_ratio = tallest_ar.unwrap_or(0.75);
let baseline_offset = if grid_snapped {
sp.line_pitch
} else {
font_size * ascender_ratio
};
let baseline_y = state.pb.slot_top - bdr_top_pad - baseline_offset;
render_list_label(
&mut state.pb.content,
para,
ctx.fonts,
label_x,
baseline_y,
font_size,
);
if let Some((split_at, _after_w)) = float_width_change {
if split_at < lines.len() {
render_paragraph_lines(
&mut state.pb.content,
&lines[..split_at],
¶.alignment,
para_text_x,
para_text_width,
baseline_y,
line_h,
lines.len(),
0,
&mut state.pb.links,
text_hanging,
ctx.fonts,
poly_line_geom.as_deref(),
);
let (after_x, after_w) = lookahead_narrow
.expect("lookahead_narrow set when float_width_change is Some");
let below_baseline = baseline_y - split_at as f32 * line_h;
render_paragraph_lines(
&mut state.pb.content,
&lines[split_at..],
¶.alignment,
after_x,
after_w,
below_baseline,
line_h,
lines.len(),
split_at,
&mut state.pb.links,
text_hanging,
ctx.fonts,
poly_line_geom.as_deref(),
);
} else {
render_paragraph_lines(
&mut state.pb.content,
&lines,
¶.alignment,
para_text_x,
para_text_width,
baseline_y,
line_h,
lines.len(),
0,
&mut state.pb.links,
text_hanging,
ctx.fonts,
poly_line_geom.as_deref(),
);
}
} else {
render_paragraph_lines(
&mut state.pb.content,
&lines,
¶.alignment,
para_text_x,
para_text_width,
baseline_y,
line_h,
lines.len(),
0,
&mut state.pb.links,
text_hanging,
ctx.fonts,
poly_line_geom.as_deref(),
);
}
}
{
let bdr = ¶.borders;
let box_top = state.pb.slot_top;
let box_bottom =
state.pb.slot_top - bdr_top_pad - content_h - bdr_bottom_pad;
let bdr_left_outset = bdr
.left
.as_ref()
.map(|b| b.space_pt + b.width_pt / 2.0)
.unwrap_or(0.0);
let bdr_right_outset = bdr
.right
.as_ref()
.map(|b| b.space_pt + b.width_pt / 2.0)
.unwrap_or(0.0);
let box_left = col_x - bdr_left_outset;
let box_right = col_x + col_w + bdr_right_outset;
let h_left_ext = bdr.left.as_ref().map(|b| b.width_pt / 2.0).unwrap_or(0.0);
let h_right_ext = bdr.right.as_ref().map(|b| b.width_pt / 2.0).unwrap_or(0.0);
let draw_h_border = |content: &mut Content, b: &ParagraphBorder, y: f32| {
content.save_state();
content.set_line_width(b.width_pt);
stroke_rgb(content, b.color);
content.move_to(box_left - h_left_ext, y);
content.line_to(box_right + h_right_ext, y);
content.stroke();
content.restore_state();
};
let v_border_top = box_top
+ if prev_borders_match { applied_inter_gap } else { 0.0 };
let draw_v_border = |content: &mut Content, b: &ParagraphBorder, x: f32| {
content.save_state();
content.set_line_width(b.width_pt);
stroke_rgb(content, b.color);
content.move_to(x, v_border_top);
content.line_to(x, box_bottom);
content.stroke();
content.restore_state();
};
if !prev_borders_match {
if let Some(b) = &bdr.top {
draw_h_border(&mut state.pb.content, b, box_top);
}
}
if next_borders_match {
if let Some(b) = &bdr.between {
draw_h_border(&mut state.pb.content, b, box_bottom);
}
} else if let Some(b) = &bdr.bottom {
draw_h_border(&mut state.pb.content, b, box_bottom);
}
if let Some(b) = &bdr.left {
draw_v_border(&mut state.pb.content, b, box_left);
}
if let Some(b) = &bdr.right {
draw_v_border(&mut state.pb.content, b, box_right);
}
}
state.pb.slot_top -= content_h + bdr_top_pad + bdr_bottom_extent;
if state.pb.slot_top < state.effective_margin_bottom - 1.0 {
log::warn!(
"Body overflow: slot_top={:.2} < eff_margin_bottom={:.2} after paragraph on page {}",
state.pb.slot_top, state.effective_margin_bottom, state.pb.all_contents.len(),
);
}
if !(text_empty && para.paragraph_mark_vanish) {
state.prev_space_after = effective_space_after;
}
for run in para.runs.iter() {
if let Some(id) = run.footnote_id {
if state.pb.footnote_ids_set.insert(id) {
state.pb.footnote_ids.push(id);
if let Some(footnote) = doc.footnotes.get(&id) {
let fn_height =
compute_footnote_height(footnote, &ctx, text_width);
let separator_h = if state.pb.footnote_ids.len() == 1 {
12.0
} else {
0.0
};
state.effective_margin_bottom += separator_h + fn_height;
}
}
}
if let Some(id) = run.endnote_id {
if state.pb.endnote_ids_set.insert(id) {
state.pb.endnote_ids.push(id);
}
}
}
update_styleref_from_para(
&mut state.pb.styleref_running,
&mut state.pb.styleref_page_first,
para,
&doc.style_id_to_name,
);
if para.page_break_after {
state.pb.flush_page(sect_idx);
state.pb.slot_top = effective_slot_top(sp, false, &ctx);
state.pb.column_top_y = state.pb.slot_top;
state.effective_margin_bottom =
compute_effective_margin_bottom(sp, false, &ctx);
state.pb.is_first_page_of_section = false;
state.prev_space_after = 0.0;
state.current_col = 0;
}
false
}
pub fn render(doc: &Document) -> Result<Vec<u8>, Error> {
let debug_wrap = std::env::var("DOCXSIDE_DEBUG_WRAP").is_ok();
let t0 = std::time::Instant::now();
let mut pdf = Pdf::new();
let mut next_id = 1i32;
let mut alloc = || {
let r = Ref::new(next_id);
next_id += 1;
r
};
let catalog_id = alloc();
let pages_id = alloc();
let (seen_fonts, font_order) = collect_and_register_fonts(doc, &mut pdf, &mut alloc);
let smartart_font_key = font_order.first().map(|s| s.as_str()).unwrap_or("");
let t_fonts = t0.elapsed();
let EmbeddedImages {
image_pdf_names,
inline_image_pdf_names,
floating_image_pdf_names,
image_xobjects,
hf_image_names,
hf_inline_image_names,
hf_floating_image_names,
table_cell_image_names,
textbox_image_names,
smartart_image_names,
effect_names,
effect_floating_names,
effect_inline_names,
effect_hf_names,
effect_hf_inline_names: _,
effect_hf_floating_names,
effect_table_names,
effect_textbox_names,
} = embed_all_images(doc, &mut pdf, &mut alloc);
let ctx = RenderContext {
fonts: &seen_fonts,
doc_line_spacing: doc.line_spacing,
default_tab_stop: doc.default_tab_stop,
table_cell_image_names: &table_cell_image_names,
effect_table_names: &effect_table_names,
textbox_image_names: &textbox_image_names,
effect_textbox_names: &effect_textbox_names,
chart_font_name: &doc.chart_font_name,
};
let t_images = t0.elapsed();
let mut footnote_display_order: HashMap<u32, u32> = HashMap::new();
let mut endnote_display_order: HashMap<u32, u32> = HashMap::new();
{
let mut next_fn_num = 1u32;
let mut next_en_num = 1u32;
for section in &doc.sections {
for block in §ion.blocks {
let runs: Box<dyn Iterator<Item = &Run>> = match block {
Block::Paragraph(p) => Box::new(p.runs.iter()),
Block::Table(t) => Box::new(
t.rows
.iter()
.flat_map(|row| row.cells.iter())
.flat_map(|cell| cell.all_paragraphs())
.flat_map(|p| p.runs.iter()),
),
};
for run in runs {
if let Some(id) = run.footnote_id {
if !footnote_display_order.contains_key(&id) {
footnote_display_order.insert(id, next_fn_num);
next_fn_num += 1;
}
}
if let Some(id) = run.endnote_id {
if !endnote_display_order.contains_key(&id) {
endnote_display_order.insert(id, next_en_num);
next_en_num += 1;
}
}
}
}
}
}
let bookmark_positions = compute_bookmark_positions(doc, &ctx);
let first_sp = &doc.sections[0].properties;
let mut cur_sp = first_sp;
let initial_slot_top = effective_slot_top(cur_sp, true, &ctx);
let mut state = LayoutState {
pb: PageBuilder::new(initial_slot_top),
prev_space_after: 0.0,
effective_margin_bottom: compute_effective_margin_bottom(cur_sp, true, &ctx),
current_col: 0,
global_block_idx: 0,
heading_entries: Vec::new(),
bookmark_positions,
};
for (sect_idx, section) in doc.sections.iter().enumerate() {
let sp = §ion.properties;
if sect_idx > 0 {
match sp.break_type {
SectionBreakType::NextPage
| SectionBreakType::OddPage
| SectionBreakType::EvenPage => {
state.pb.flush_page(sect_idx - 1);
let need_odd = match sp.break_type {
SectionBreakType::OddPage => true,
_ if doc.even_and_odd_headers && sp.page_num_start.is_some() => {
sp.page_num_start.unwrap() % 2 == 1
}
_ => false,
};
let need_even = match sp.break_type {
SectionBreakType::EvenPage => true,
_ if doc.even_and_odd_headers && sp.page_num_start.is_some() => {
sp.page_num_start.unwrap() % 2 == 0
}
_ => false,
};
if need_odd || need_even {
let next_phys = state.pb.page_count() + 1;
let next_is_odd = next_phys % 2 == 1;
if (need_odd && !next_is_odd) || (need_even && next_is_odd) {
state.pb.push_blank_page(sect_idx - 1);
}
}
state.pb.slot_top = effective_slot_top(sp, true, &ctx);
state.pb.column_top_y = state.pb.slot_top;
state.effective_margin_bottom = compute_effective_margin_bottom(sp, true, &ctx);
state.pb.page_hf_section = sect_idx;
state.pb.is_first_page_of_section = true;
}
SectionBreakType::Continuous => {
}
}
}
cur_sp = sp;
let text_width = sp.page_width - sp.margin_left - sp.margin_right;
let col_config = sp.columns.as_ref();
let col_count = col_config.map(|c| c.columns.len()).unwrap_or(1);
let col_geometry: Vec<(f32, f32)> = if let Some(cfg) = col_config {
let mut x = sp.margin_left;
cfg.columns
.iter()
.map(|col| {
let result = (x, col.width);
x += col.width + col.space;
result
})
.collect()
} else {
vec![(sp.margin_left, text_width)]
};
state.current_col = 0;
state.pb.column_top_y = state.pb.slot_top;
for (block_idx, block) in section.blocks.iter().enumerate() {
if let Some(ref fz) = state.pb.float_zone {
if state.pb.slot_top <= fz.bottom_y {
state.pb.float_zone = None;
} else if state.pb.slot_top <= fz.top_y
|| (fz.para_relative && state.pb.slot_top <= fz.top_y + 30.0)
{
let (col_x, col_w) = col_geometry[state.current_col];
let (ex_left, ex_right) = fz.exclusion_at_y(state.pb.slot_top);
let space_right = (col_x + col_w) - (ex_right + fz.right_from_text);
let space_left = (ex_left - fz.left_from_text) - col_x;
let min_wrap_w: f32 = 72.0;
let enough_space = if fz.wrap_text == WrapText::BothSides {
(space_left + space_right) >= min_wrap_w
} else {
let best_side = match fz.wrap_text {
WrapText::Left => space_left,
WrapText::Right => space_right,
_ => space_right.max(space_left),
};
best_side >= min_wrap_w
};
if !enough_space {
let is_empty_para = matches!(block,
Block::Paragraph(p) if p.runs.iter().all(|r|
r.vanish || r.is_line_break
|| (r.text.is_empty() && !r.is_tab && r.inline_image.is_none())
)
&& p.image.is_none()
&& p.inline_chart.is_none()
&& p.smartart.is_empty()
);
if !is_empty_para {
state.pb.slot_top = fz.bottom_y;
state.pb.float_zone = None;
}
}
}
}
match block {
Block::Paragraph(para) => {
if render_paragraph_block(
para,
&mut state,
&ctx,
cur_sp,
&col_geometry,
col_count,
text_width,
sect_idx,
block_idx,
§ion.blocks,
&floating_image_pdf_names,
&inline_image_pdf_names,
&image_pdf_names,
&effect_names,
&effect_floating_names,
&effect_inline_names,
&footnote_display_order,
&endnote_display_order,
doc,
smartart_font_key,
&smartart_image_names,
debug_wrap,
) {
continue;
}
}
Block::Table(table) => {
let override_pos = table.position.as_ref().map(|pos| {
let table_total_w: f32 = table.col_widths.iter().sum();
let x = match pos.h_anchor {
"page" => match pos.h_position {
HorizontalPosition::AlignCenter => {
(sp.page_width - table_total_w) / 2.0
}
HorizontalPosition::AlignRight => sp.page_width - table_total_w,
HorizontalPosition::AlignLeft => 0.0,
HorizontalPosition::Offset(o) => o,
},
"margin" => match pos.h_position {
HorizontalPosition::AlignCenter => {
sp.margin_left + (text_width - table_total_w) / 2.0
}
HorizontalPosition::AlignRight => {
sp.margin_left + text_width - table_total_w
}
HorizontalPosition::AlignLeft => sp.margin_left,
HorizontalPosition::Offset(o) => sp.margin_left + o,
},
_ => {
let (col_x, col_w) = col_geometry[state.current_col];
match pos.h_position {
HorizontalPosition::AlignCenter => {
col_x + (col_w - table_total_w) / 2.0
}
HorizontalPosition::AlignRight => col_x + col_w - table_total_w,
HorizontalPosition::AlignLeft => col_x,
HorizontalPosition::Offset(o) => col_x + o,
}
}
};
let y = match pos.v_anchor {
"page" => sp.page_height - pos.v_offset_pt,
"margin" => sp.page_height - sp.margin_top - pos.v_offset_pt,
_ => state.pb.slot_top - pos.v_offset_pt,
};
FloatingTablePos {
x,
y,
top_from_text: pos.top_from_text,
bottom_from_text: pos.bottom_from_text,
left_from_text: pos.left_from_text,
right_from_text: pos.right_from_text,
}
});
let col_bounds = if col_count > 1 {
Some(col_geometry[state.current_col])
} else {
None
};
render_table(
table,
sp,
&ctx,
&mut state.pb,
sect_idx,
state.prev_space_after,
override_pos,
&doc.footnotes,
&mut state.effective_margin_bottom,
col_bounds,
);
state.prev_space_after = 0.0;
for row in &table.rows {
for cell in &row.cells {
for p in cell.all_paragraphs() {
update_styleref_from_para(
&mut state.pb.styleref_running,
&mut state.pb.styleref_page_first,
p,
&doc.style_id_to_name,
);
}
}
}
}
}
if let Some(ref fz) = state.pb.float_zone {
if state.pb.slot_top <= fz.bottom_y {
state.pb.float_zone = None;
}
}
state.global_block_idx += 1;
}
}
state.pb.flush_page(doc.sections.len() - 1);
let t_layout = t0.elapsed();
for (page_idx, content) in state.pb.all_contents.iter_mut().enumerate() {
let (.., si) = state.pb.page_section_indices[page_idx];
let sp = &doc.sections[si].properties;
if let Some(cfg) = &sp.columns {
if cfg.sep {
let mut x = sp.margin_left;
for (i, col) in cfg.columns.iter().enumerate() {
x += col.width;
if i < cfg.columns.len() - 1 {
let mid_x = x + col.space / 2.0;
content.save_state();
content.set_line_width(0.5);
content.move_to(mid_x, sp.margin_bottom);
content.line_to(mid_x, sp.page_height - sp.margin_top);
content.stroke();
content.restore_state();
x += col.space;
}
}
}
}
}
let last_page_idx = state.pb.all_contents.len().saturating_sub(1);
for (page_idx, content) in state.pb.all_contents.iter_mut().enumerate() {
let (hf_si, is_first, si) = state.pb.page_section_indices[page_idx];
let sp = &doc.sections[hf_si].properties;
let eff_bottom = compute_effective_margin_bottom(sp, is_first, &ctx);
let content_sp = &doc.sections[si].properties;
let text_width = content_sp.page_width - content_sp.margin_left - content_sp.margin_right;
let mut bottom = eff_bottom;
render_page_footnotes(
content,
&state.pb.all_footnote_ids[page_idx],
&doc.footnotes,
&footnote_display_order,
&ctx,
content_sp.margin_left,
bottom,
text_width,
);
if page_idx == last_page_idx && !state.pb.endnote_ids.is_empty() {
let fn_height: f32 = state.pb.all_footnote_ids[page_idx]
.iter()
.filter_map(|id| doc.footnotes.get(id))
.map(|fn_note| compute_footnote_height(fn_note, &ctx, text_width))
.sum();
if fn_height > 0.0 {
bottom += fn_height + 12.0;
}
render_page_footnotes(
content,
&state.pb.endnote_ids,
&doc.endnotes,
&endnote_display_order,
&ctx,
content_sp.margin_left,
bottom,
text_width,
);
}
}
let t_headers = t0.elapsed();
let total_pages = state.pb.all_contents.len();
type HfMaps = (
HashMap<usize, String>,
HashMap<(usize, usize), String>,
HashMap<(usize, usize), String>,
HashMap<usize, EffectXObjs>,
HashMap<(usize, usize), EffectXObjs>,
);
let mut hf_maps_index: HashMap<(usize, u8), HfMaps> = HashMap::new();
for ((s, t, pi), name) in &hf_image_names {
hf_maps_index.entry((*s, *t)).or_default().0.insert(*pi, name.clone());
}
for ((s, t, pi, ri), name) in &hf_inline_image_names {
hf_maps_index.entry((*s, *t)).or_default().1.insert((*pi, *ri), name.clone());
}
for ((s, t, pi, fi), name) in &hf_floating_image_names {
hf_maps_index.entry((*s, *t)).or_default().2.insert((*pi, *fi), name.clone());
}
for ((s, t, pi), fx) in &effect_hf_names {
hf_maps_index.entry((*s, *t)).or_default().3.insert(*pi, fx.clone());
}
for ((s, t, pi, fi), fx) in &effect_hf_floating_names {
hf_maps_index.entry((*s, *t)).or_default().4.insert((*pi, *fi), fx.clone());
}
let empty_hf_maps: HfMaps = Default::default();
let mut page_numbers: Vec<usize> = Vec::with_capacity(total_pages);
let mut page_format_sources: Vec<Option<usize>> = Vec::with_capacity(total_pages);
{
let mut running_num: usize = 0;
let mut running_format_si: Option<usize> = None;
let mut prev_content_si: Option<usize> = None;
for page_idx in 0..total_pages {
let (_, _, content_si) = state.pb.page_section_indices[page_idx];
let csp = &doc.sections[content_si].properties;
if prev_content_si != Some(content_si) {
if let Some(start) = csp.page_num_start {
running_num = start as usize;
running_format_si = if csp.page_num_format.is_some() {
Some(content_si)
} else {
None
};
} else {
running_num += 1;
}
} else {
running_num += 1;
}
page_numbers.push(running_num);
page_format_sources.push(running_format_si);
prev_content_si = Some(content_si);
}
}
let empty_styleref: HashMap<String, String> = HashMap::new();
let mut page_styleref_merged: HashMap<String, String> = HashMap::new();
let mut all_hf_contents: Vec<Option<Content>> = (0..total_pages).map(|_| None).collect();
for (page_idx, hf_content) in all_hf_contents.iter_mut().enumerate() {
let (si, is_first, _content_si) = state.pb.page_section_indices[page_idx];
let sp = &doc.sections[si].properties;
let page_num = page_numbers[page_idx];
let effective_page_num_format = page_format_sources[page_idx]
.and_then(|si| doc.sections[si].properties.page_num_format.as_deref());
let page_first = state.pb
.all_first_styleref
.get(page_idx)
.unwrap_or(&empty_styleref);
let prev_running = if page_idx > 0 {
state.pb.all_styleref.get(page_idx - 1).unwrap_or(&empty_styleref)
} else {
&empty_styleref
};
page_styleref_merged.clone_from(prev_running);
for (k, v) in page_first {
page_styleref_merged.insert(k.clone(), v.clone());
}
let page_styleref = &page_styleref_merged;
let mut hf = Content::new();
let mut has_hf = false;
let (header, hdr_type, hdr_si) =
resolve_header_for_page(doc, si, is_first, page_num);
if let Some(header_data) = header {
let (pi_map, ii_map, fi_map, sh_para, sh_float) = hf_maps_index.get(&(hdr_si, hdr_type)).unwrap_or(&empty_hf_maps);
let pc = HfPageContext {
page_num, total_pages,
para_image_names: pi_map, inline_image_names: ii_map,
floating_image_names: fi_map,
effect_para_names: sh_para, effect_floating_names: sh_float,
styleref_values: page_styleref,
page_num_format: effective_page_num_format,
};
render_header_footer(
&mut hf, header_data, &ctx, sp, true, &pc,
&mut state.pb.all_gradient_specs[page_idx],
);
has_hf = true;
}
let (footer, ftr_type, ftr_si) =
resolve_footer_for_page(doc, si, is_first, page_num);
if let Some(footer_data) = footer {
let (pi_map, ii_map, fi_map, sh_para, sh_float) = hf_maps_index.get(&(ftr_si, ftr_type)).unwrap_or(&empty_hf_maps);
let pc = HfPageContext {
page_num, total_pages,
para_image_names: pi_map, inline_image_names: ii_map,
floating_image_names: fi_map,
effect_para_names: sh_para, effect_floating_names: sh_float,
styleref_values: page_styleref,
page_num_format: effective_page_num_format,
};
render_header_footer(
&mut hf, footer_data, &ctx, sp, false, &pc,
&mut state.pb.all_gradient_specs[page_idx],
);
has_hf = true;
}
if has_hf {
*hf_content = Some(hf);
}
}
assemble_pdf_pages(
&mut pdf,
&mut alloc,
catalog_id,
pages_id,
state.pb.all_contents,
&mut all_hf_contents,
&state.pb.all_links,
&state.pb.all_alpha_states,
&state.pb.all_gradient_specs,
&state.pb.page_section_indices,
ctx.fonts,
&font_order,
&image_xobjects,
doc,
&state.bookmark_positions,
&state.heading_entries,
);
let t_assembly = t0.elapsed();
log::info!(
"Render phases: fonts={:.1}ms, images={:.1}ms, layout={:.1}ms, headers={:.1}ms, assembly={:.1}ms",
t_fonts.as_secs_f64() * 1000.0,
(t_images - t_fonts).as_secs_f64() * 1000.0,
(t_layout - t_images).as_secs_f64() * 1000.0,
(t_headers - t_layout).as_secs_f64() * 1000.0,
(t_assembly - t_headers).as_secs_f64() * 1000.0,
);
Ok(pdf.finish())
}