pub mod dimension;
pub(crate) mod emf;
pub mod error;
pub mod fonts;
pub mod geometry;
pub mod layout;
pub mod painter;
pub mod resolve;
pub mod skia_conv;
use crate::model::Document;
use crate::model::Block;
use crate::render::layout::build::{
build_section_blocks, default_line_height, BuildContext, BuildState,
};
use crate::render::layout::draw_command::LayoutedPage;
use crate::render::layout::header_footer::{render_headers_footers, HeaderFooterBlocks, PageRange};
use crate::render::layout::page::PageConfig;
use crate::render::layout::section::layout_section;
use crate::render::resolve::ResolvedDocument;
fn estimate_cursor_y(
page: &layout::draw_command::LayoutedPage,
config: &layout::page::PageConfig,
) -> dimension::Pt {
let mut max_y = config.margins.top;
for cmd in &page.commands {
let bottom = match cmd {
layout::draw_command::DrawCommand::Text {
position,
font_size,
..
} => position.y + *font_size,
layout::draw_command::DrawCommand::Image { rect, .. } => {
rect.origin.y + rect.size.height
}
layout::draw_command::DrawCommand::Rect { rect, .. } => {
rect.origin.y + rect.size.height
}
layout::draw_command::DrawCommand::Line { line, .. } => line.end.y,
_ => continue,
};
if bottom > max_y {
max_y = bottom;
}
}
max_y
}
pub fn render(doc: &Document) -> Result<Vec<u8>, error::RenderError> {
let font_mgr = skia_safe::FontMgr::new();
render_with_font_mgr(doc, &font_mgr)
}
pub fn render_with_font_mgr(
doc: &Document,
font_mgr: &skia_safe::FontMgr,
) -> Result<Vec<u8>, error::RenderError> {
let resolved = resolve::resolve(doc);
fonts::register_embedded_fonts(font_mgr, &doc.embedded_fonts);
fonts::preload_fonts(font_mgr, &resolved.font_families);
let pages = layout_document(&resolved, font_mgr);
painter::render_to_pdf(&pages, font_mgr)
}
pub fn resolve_and_layout(doc: &Document) -> (ResolvedDocument, Vec<LayoutedPage>) {
let font_mgr = skia_safe::FontMgr::new();
let resolved = resolve::resolve(doc);
fonts::preload_fonts(&font_mgr, &resolved.font_families);
let pages = layout_document(&resolved, &font_mgr);
(resolved, pages)
}
pub fn layout_document(
resolved: &ResolvedDocument,
font_mgr: &skia_safe::FontMgr,
) -> Vec<LayoutedPage> {
let measurer = layout::measurer::TextMeasurer::new(font_mgr.clone());
let ctx = BuildContext {
measurer: &measurer,
resolved,
};
let mut state = BuildState::default();
let dlh = default_line_height(&ctx);
let mut all_pages = Vec::new();
let mut all_endnotes = Vec::new();
let mut last_config = PageConfig::default();
struct SectionHfInfo<'a> {
page_range: std::ops::Range<usize>,
config: PageConfig,
header_blocks: Option<&'a [Block]>,
footer_blocks: Option<&'a [Block]>,
}
let mut section_hf: Vec<SectionHfInfo> = Vec::new();
let separator_indent = resolved
.default_paragraph_style_id
.as_ref()
.and_then(|id| resolved.styles.get(id))
.and_then(|s| s.paragraph.indentation)
.and_then(|ind| ind.first_line)
.map(|fl| match fl {
crate::model::FirstLineIndent::FirstLine(v) => dimension::Pt::from(v),
_ => dimension::Pt::ZERO,
})
.unwrap_or(dimension::Pt::ZERO);
let mut pending_continuation: Option<layout::section::ContinuationState> = None;
for section in &resolved.sections {
let config = adjust_margins_for_header_footer(
PageConfig::from_section(§ion.properties),
section,
&ctx,
&mut state,
dlh,
);
state.page_config = config.clone();
let built = build_section_blocks(section, &config, &ctx, &mut state);
let measure_fn = |text: &str,
font: &layout::fragment::FontProps|
-> (dimension::Pt, layout::fragment::TextMetrics) {
measurer.measure(text, font)
};
let continuation =
if section.properties.section_type == Some(crate::model::SectionType::Continuous) {
pending_continuation.take()
} else {
pending_continuation = None;
None
};
let mut pages = layout_section(
&built.blocks,
&config,
Some(&measure_fn),
separator_indent,
dlh,
continuation,
);
all_endnotes.extend(built.endnotes);
last_config = config.clone();
let next_is_continuous = {
let section_idx = resolved
.sections
.iter()
.position(|s| std::ptr::eq(s, section));
section_idx
.and_then(|i| resolved.sections.get(i + 1))
.is_some_and(|next| {
next.properties.section_type == Some(crate::model::SectionType::Continuous)
})
};
if next_is_continuous && !pages.is_empty() {
let last_page = pages.pop().unwrap();
let cursor_y = estimate_cursor_y(&last_page, &last_config);
pending_continuation = Some(layout::section::ContinuationState {
page: last_page,
cursor_y,
});
}
let page_start = all_pages.len();
all_pages.append(&mut pages);
section_hf.push(SectionHfInfo {
page_range: page_start..all_pages.len(),
config,
header_blocks: section.header.as_deref(),
footer_blocks: section.footer.as_deref(),
});
}
let total_pages = all_pages.len();
for info in §ion_hf {
state.page_config = info.config.clone();
render_headers_footers(
&mut all_pages[info.page_range.clone()],
&info.config,
&HeaderFooterBlocks {
header: info.header_blocks,
footer: info.footer_blocks,
},
&ctx,
&mut state,
dlh,
&PageRange {
page_base: info.page_range.start,
total_pages,
},
);
}
if !all_endnotes.is_empty() {
let measure_fn = |text: &str,
font: &layout::fragment::FontProps|
-> (dimension::Pt, layout::fragment::TextMetrics) {
measurer.measure(text, font)
};
let mut endnote_page = LayoutedPage::new(last_config.page_size);
let content_width = last_config.content_width();
let constraints =
layout::BoxConstraints::tight_width(content_width, dimension::Pt::INFINITY);
let mut cursor_y = last_config.margins.top;
let sep_width = content_width * 0.33;
let sep_x = last_config.margins.left + separator_indent;
endnote_page
.commands
.push(layout::draw_command::DrawCommand::Line {
line: crate::render::geometry::PtLineSegment::new(
crate::render::geometry::PtOffset::new(sep_x, cursor_y),
crate::render::geometry::PtOffset::new(sep_x + sep_width, cursor_y),
),
color: crate::render::resolve::color::RgbColor::BLACK,
width: dimension::Pt::new(0.5),
});
cursor_y += dimension::Pt::new(4.0);
for (_, frags, style) in &all_endnotes {
let para = layout::paragraph::layout_paragraph(
frags,
&constraints,
style,
dlh,
Some(&measure_fn),
);
for mut cmd in para.commands {
cmd.shift_y(cursor_y);
cmd.shift_x(last_config.margins.left);
endnote_page.commands.push(cmd);
}
cursor_y += para.size.height;
}
all_pages.push(endnote_page);
}
if all_pages.is_empty() {
all_pages.push(LayoutedPage::new(PageConfig::default().page_size));
}
all_pages
}
fn adjust_margins_for_header_footer(
mut config: PageConfig,
section: &crate::render::resolve::sections::ResolvedSection,
ctx: &layout::build::BuildContext,
state: &mut BuildState,
default_line_height: dimension::Pt,
) -> PageConfig {
let content_width = config.content_width();
if let Some(ref blocks) = section.header {
let hf = layout::build::build_header_footer_content(blocks, ctx, state);
let result =
layout::section::stack_blocks(&hf.blocks, content_width, default_line_height, None);
let blocks_bottom = config.header_margin + result.height;
let floats_bottom = hf
.floating_images
.iter()
.filter(|fi| fi.wrap_top_and_bottom)
.map(|fi| {
let y = match fi.y {
layout::section::FloatingImageY::Absolute(y) => y,
layout::section::FloatingImageY::RelativeToParagraph(off) => {
config.header_margin + off
}
};
y + fi.size.height
})
.fold(dimension::Pt::ZERO, |a, b| a.max(b));
let header_bottom = blocks_bottom.max(floats_bottom);
if header_bottom > config.margins.top {
config.margins.top = header_bottom;
}
}
if let Some(ref blocks) = section.footer {
let hf = layout::build::build_header_footer_content(blocks, ctx, state);
let result =
layout::section::stack_blocks(&hf.blocks, content_width, default_line_height, None);
let blocks_extent = config.footer_margin + result.height;
let floats_extent = hf
.floating_images
.iter()
.filter(|fi| fi.wrap_top_and_bottom)
.map(|fi| match fi.y {
layout::section::FloatingImageY::Absolute(y) => config.page_size.height - y,
layout::section::FloatingImageY::RelativeToParagraph(off) => {
config.footer_margin + off + fi.size.height
}
})
.fold(dimension::Pt::ZERO, |a, b| a.max(b));
let footer_extent = blocks_extent.max(floats_extent);
if footer_extent > config.margins.bottom {
config.margins.bottom = footer_extent;
}
}
config
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::*;
use std::collections::HashMap;
fn empty_doc() -> Document {
Document {
settings: DocumentSettings::default(),
theme: None,
styles: StyleSheet::default(),
numbering: NumberingDefinitions::default(),
body: vec![],
final_section: SectionProperties::default(),
headers: HashMap::new(),
footers: HashMap::new(),
footnotes: HashMap::new(),
endnotes: HashMap::new(),
media: HashMap::new(),
embedded_fonts: vec![],
}
}
fn para(text: &str) -> Block {
Block::Paragraph(Box::new(Paragraph {
style_id: None,
properties: ParagraphProperties::default(),
mark_run_properties: None,
content: vec![Inline::TextRun(Box::new(TextRun {
style_id: None,
properties: RunProperties::default(),
content: vec![RunElement::Text(text.to_string())],
rsids: RevisionIds::default(),
}))],
rsids: ParagraphRevisionIds::default(),
}))
}
#[test]
fn resolve_and_layout_empty_doc() {
let doc = empty_doc();
let (resolved, pages) = resolve_and_layout(&doc);
assert_eq!(resolved.sections.len(), 1);
assert_eq!(pages.len(), 1);
assert!(pages[0].commands.is_empty());
}
#[test]
fn resolve_and_layout_with_paragraphs() {
let mut doc = empty_doc();
doc.body = vec![para("hello"), para("world")];
let (_, pages) = resolve_and_layout(&doc);
assert_eq!(pages.len(), 1);
let text_count = pages[0]
.commands
.iter()
.filter(|c| matches!(c, layout::draw_command::DrawCommand::Text { .. }))
.count();
assert_eq!(text_count, 2);
}
#[test]
fn resolve_and_layout_with_table() {
let mut doc = empty_doc();
doc.body = vec![Block::Table(Box::new(Table {
properties: TableProperties::default(),
grid: vec![
GridColumn {
width: crate::model::dimension::Dimension::new(4680),
},
GridColumn {
width: crate::model::dimension::Dimension::new(4680),
},
],
rows: vec![TableRow {
properties: TableRowProperties::default(),
cells: vec![
TableCell {
properties: TableCellProperties::default(),
content: vec![para("A")],
},
TableCell {
properties: TableCellProperties::default(),
content: vec![para("B")],
},
],
rsids: TableRowRevisionIds::default(),
}],
}))];
let (_, pages) = resolve_and_layout(&doc);
assert_eq!(pages.len(), 1);
let text_count = pages[0]
.commands
.iter()
.filter(|c| matches!(c, layout::draw_command::DrawCommand::Text { .. }))
.count();
assert_eq!(text_count, 2, "two cells = two text commands");
}
#[test]
fn layout_respects_page_size() {
let mut doc = empty_doc();
doc.final_section = SectionProperties {
page_size: Some(PageSize {
width: Some(crate::model::dimension::Dimension::new(12240)),
height: Some(crate::model::dimension::Dimension::new(15840)),
orientation: None,
}),
..Default::default()
};
let (_, pages) = resolve_and_layout(&doc);
assert_eq!(pages[0].page_size.width.raw(), 612.0);
assert_eq!(pages[0].page_size.height.raw(), 792.0);
}
}