pub(super) mod block;
pub(super) mod convert;
pub(super) mod floating;
pub(super) mod list_label;
pub(super) mod table;
use std::collections::HashMap;
use crate::model::{self, Block};
use crate::render::dimension::Pt;
use crate::render::layout::fragment::Fragment;
use crate::render::layout::measurer::TextMeasurer;
use crate::render::layout::page::PageConfig;
use crate::render::layout::paragraph::ParagraphStyle;
use crate::render::layout::section::LayoutBlock;
use crate::render::resolve::images::MediaEntry;
use crate::render::resolve::sections::ResolvedSection;
use crate::render::resolve::ResolvedDocument;
use block::{build_block, build_fragments, collect_endnotes};
use convert::{
doc_font_family, doc_font_size, paragraph_style_from_props, resolve_paragraph_defaults,
};
use floating::{extract_floating_images, find_vml_absolute_position, AnchorFrame};
use table::build_table;
pub(super) const SPEC_FALLBACK_FONT: &str = "Times New Roman";
pub(super) const SPEC_DEFAULT_FONT_SIZE: Pt = Pt::new(10.0);
pub struct BuildContext<'a> {
pub measurer: &'a TextMeasurer<'a>,
pub resolved: &'a ResolvedDocument,
}
impl BuildContext<'_> {
pub(super) fn media(&self) -> &HashMap<model::RelId, MediaEntry> {
&self.resolved.media
}
}
#[derive(Default)]
pub struct BuildState {
pub page_config: crate::render::layout::page::PageConfig,
pub footnotes: crate::render::layout::fragment::FootnoteTracker,
pub endnote_counter: u32,
pub list_counters: HashMap<(model::NumId, u8), u32>,
pub outline: OutlineCollector,
pub field_ctx: crate::render::layout::fragment::FieldContext,
pub shape_default_text_color: Option<crate::render::resolve::color::RgbColor>,
pub shape_default_font_family: Option<String>,
pub shape_auto_fit: crate::render::layout::ShapeAutoFit,
pub warned_border_styles: std::collections::HashSet<model::BorderStyle>,
pub warned_row_cell_spacing: bool,
}
pub struct BuiltSection {
pub blocks: Vec<LayoutBlock>,
}
pub fn build_section_blocks(
section: &ResolvedSection,
config: &PageConfig,
ctx: &BuildContext,
state: &mut BuildState,
) -> BuiltSection {
let mut pending_dropcap: Option<crate::render::layout::paragraph::DropCapInfo> = None;
let blocks: Vec<LayoutBlock> = section
.blocks
.iter()
.filter_map(|block| {
build_block(
block,
config.content_width(),
ctx,
state,
&mut pending_dropcap,
)
})
.collect();
BuiltSection { blocks }
}
pub fn build_document_endnotes(
ctx: &BuildContext,
state: &mut BuildState,
) -> Vec<(String, Vec<Fragment>, ParagraphStyle)> {
let mut endnotes = Vec::new();
collect_endnotes(ctx, state, &mut endnotes);
endnotes
}
pub struct HeaderFooterContent {
pub blocks: Vec<LayoutBlock>,
pub absolute_position: Option<(Pt, Pt)>,
pub floating_images: Vec<crate::render::layout::section::FloatingImage>,
pub floating_shapes: Vec<crate::render::layout::section::FloatingShape>,
}
pub fn build_header_footer_content(
blocks: &[Block],
ctx: &BuildContext,
state: &mut BuildState,
) -> HeaderFooterContent {
let outer = std::mem::replace(&mut state.outline, OutlineCollector::Excluded);
let content = build_non_story_content(blocks, ctx, state);
state.outline = outer;
content
}
fn build_non_story_content(
blocks: &[Block],
ctx: &BuildContext,
state: &mut BuildState,
) -> HeaderFooterContent {
let mut layout_blocks = Vec::new();
let mut all_floating_images = Vec::new();
let mut all_page_anchored_shapes = Vec::new();
let mut absolute_position: Option<(Pt, Pt)> = None;
let available_width = state.page_config.content_width();
let block_count = blocks.len();
for (block_i, block) in blocks.iter().enumerate() {
match block {
Block::Paragraph(p) => {
let (mut frags, props) = build_fragments(p, ctx, state, None, None);
let _ = state.footnotes.take_pending();
let style = paragraph_style_from_props(
&props,
Pt::from(ctx.resolved.default_tab_stop),
state.shape_auto_fit,
convert::paragraph_locale(p, ctx.resolved),
convert::paragraph_outline(p, &props, state),
);
if absolute_position.is_none() {
for inline in &p.content {
if let Some(pos) = find_vml_absolute_position(inline) {
absolute_position = Some(pos);
break;
}
}
}
let para_floats = extract_floating_images(p, ctx, state, AnchorFrame::Page);
let has_float_images = !para_floats.is_empty();
all_floating_images.extend(para_floats);
let paragraph_shapes = floating::extract_floating_shapes(
p,
ctx,
state,
AnchorFrame::Stack,
floating::ShapeAnchorClass::ParagraphAnchored,
);
let page_anchored_shapes = floating::extract_floating_shapes(
p,
ctx,
state,
AnchorFrame::Page,
floating::ShapeAnchorClass::PageAnchored,
);
all_page_anchored_shapes.extend(page_anchored_shapes);
let has_floating_anchor = has_float_images || !paragraph_shapes.is_empty();
if frags.is_empty() && block_i + 1 < block_count && !has_floating_anchor {
let (family, mut size, ..) =
resolve_paragraph_defaults(p, ctx.resolved, false, None, None);
if let Some(ref mrp) = p.mark_run_properties {
if let Some(fs) = mrp.font_size {
size = Pt::from(fs);
}
}
let line_height = ctx.measurer.default_line_height(&family, size);
frags.push(Fragment::LineBreak { line_height });
}
layout_blocks.push(LayoutBlock::Paragraph {
fragments: frags,
style,
page_break_before: false,
footnotes: vec![],
floating_images: vec![], floating_shapes: paragraph_shapes,
});
}
Block::Table(t) => {
let built = build_table(t, available_width, ctx, state);
layout_blocks.push(LayoutBlock::Table {
rows: built.rows,
col_widths: built.col_widths,
cell_spacing: built.cell_spacing,
border_config: built.border_config,
indent: built.indent,
alignment: built.alignment,
float_info: built.float_info,
style_id: t.properties.style_id.clone(),
});
}
Block::SectionBreak(_) => {}
}
}
HeaderFooterContent {
blocks: layout_blocks,
absolute_position,
floating_images: all_floating_images,
floating_shapes: all_page_anchored_shapes,
}
}
pub fn default_line_height(ctx: &BuildContext) -> Pt {
let family = doc_font_family(ctx);
let size = doc_font_size(ctx);
ctx.measurer.default_line_height(&family, size)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::dimension::Dimension;
use crate::render::fonts::FontRegistry;
use crate::render::layout::measurer::TextMeasurer;
fn empty_resolved() -> ResolvedDocument {
ResolvedDocument {
sections: Vec::new(),
styles: HashMap::new(),
numbering: HashMap::new(),
font_families: Vec::new(),
media: HashMap::new(),
embedded_fonts: Vec::new(),
pic_bullets: HashMap::new(),
theme: None,
doc_defaults_paragraph: model::ParagraphProperties::default(),
doc_defaults_run: model::RunProperties::default(),
default_paragraph_style_id: None,
footnotes: HashMap::new(),
endnotes: HashMap::new(),
even_and_odd_headers: false,
default_tab_stop: Dimension::new(720),
}
}
fn empty_para() -> Block {
Block::Paragraph(Box::new(model::Paragraph {
style_id: None,
properties: model::ParagraphProperties::default(),
mark_run_properties: None,
content: Vec::new(),
rsids: model::ParagraphRevisionIds::default(),
}))
}
fn line_break_counts(blocks: &[LayoutBlock]) -> Vec<usize> {
blocks
.iter()
.map(|b| match b {
LayoutBlock::Paragraph { fragments, .. } => fragments
.iter()
.filter(|f| matches!(f, Fragment::LineBreak { .. }))
.count(),
_ => 0,
})
.collect()
}
#[test]
fn empty_header_paragraphs_hold_a_line_except_the_last() {
let resolved = empty_resolved();
let registry = FontRegistry::new(skia_safe::FontMgr::new());
let measurer = TextMeasurer::new(®istry);
let ctx = BuildContext {
measurer: &measurer,
resolved: &resolved,
};
let mut state = BuildState::default();
let blocks = vec![empty_para(), empty_para(), empty_para()];
let hf = build_header_footer_content(&blocks, &ctx, &mut state);
assert_eq!(hf.blocks.len(), 3);
assert_eq!(
line_break_counts(&hf.blocks),
vec![1, 1, 0],
"the trailing empty paragraph contributes no line"
);
}
#[test]
fn a_lone_empty_header_paragraph_holds_no_line() {
let resolved = empty_resolved();
let registry = FontRegistry::new(skia_safe::FontMgr::new());
let measurer = TextMeasurer::new(®istry);
let ctx = BuildContext {
measurer: &measurer,
resolved: &resolved,
};
let mut state = BuildState::default();
let hf = build_header_footer_content(&[empty_para()], &ctx, &mut state);
assert_eq!(line_break_counts(&hf.blocks), vec![0]);
}
#[test]
fn header_section_breaks_produce_no_blocks() {
let resolved = empty_resolved();
let registry = FontRegistry::new(skia_safe::FontMgr::new());
let measurer = TextMeasurer::new(®istry);
let ctx = BuildContext {
measurer: &measurer,
resolved: &resolved,
};
let mut state = BuildState::default();
let blocks = vec![Block::SectionBreak(Box::default()), empty_para()];
let hf = build_header_footer_content(&blocks, &ctx, &mut state);
assert_eq!(hf.blocks.len(), 1, "only the paragraph survives");
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OutlineCollector {
Collecting(i32),
Excluded,
}
impl Default for OutlineCollector {
fn default() -> Self {
Self::Collecting(0)
}
}
impl BuildState {
pub fn next_outline_node_id(&mut self) -> Option<i32> {
match &mut self.outline {
OutlineCollector::Collecting(count) => {
*count += 1;
Some(*count)
}
OutlineCollector::Excluded => None,
}
}
}