use crate::block::{
AnchoredContent, AnchoredDrawing, CellBlockSemantics, LayoutBlock, LayoutBlockLike,
ParagraphBlock, ParagraphView, ShapePreset, SharedLayoutBlock,
};
use std::collections::HashMap;
use oxml_layout::{
Align, Color, FontManager, GlyphRun, GroupElement, LayoutLine, LineItem, MediaId, NoteRef,
NoteStream, OutlineEntry, PageFrame, Path, Point, PositionedElement, Rect, Transform,
Underline, break_into_lines,
};
use rdocx_oxml::drawing::{
AnchorAlignH, AnchorAlignV, ST_RelativeFromH, ST_RelativeFromV, WrapType,
};
use rdocx_oxml::shared::ST_Border;
use crate::input::{ImageData, MediaRegistry};
use crate::notes::{
NOTE_INDENT, NOTE_SEPARATOR_OFFSET, NoteLayout, NoteRegistry, SEPARATOR_WIDTH_FRACTION,
};
#[derive(Debug, Clone, Copy)]
struct PlacedWrap {
rect: Rect,
wrap: WrapType,
dist_top: f64,
dist_bottom: f64,
dist_left: f64,
dist_right: f64,
}
impl PlacedWrap {
fn keep_out_top(&self) -> f64 {
self.rect.y - self.dist_top
}
fn keep_out_bottom(&self) -> f64 {
self.rect.y + self.rect.height + self.dist_bottom
}
}
type BorderEdge = (f64, Color, Option<(f64, f64)>);
#[derive(Debug, Clone, Copy)]
pub struct PageGeometry {
pub page_width: f64,
pub page_height: f64,
pub margin_top: f64,
pub margin_right: f64,
pub margin_bottom: f64,
pub margin_left: f64,
pub header_distance: f64,
pub footer_distance: f64,
}
impl PageGeometry {
pub fn content_width(&self) -> f64 {
self.page_width - self.margin_left - self.margin_right
}
pub fn content_height(&self) -> f64 {
self.page_height - self.margin_top - self.margin_bottom
}
}
impl Default for PageGeometry {
fn default() -> Self {
PageGeometry {
page_width: 612.0,
page_height: 792.0,
margin_top: 72.0,
margin_right: 72.0,
margin_bottom: 72.0,
margin_left: 72.0,
header_distance: 36.0,
footer_distance: 36.0,
}
}
}
pub struct HeaderFooterContent {
pub header_blocks: Vec<ParagraphBlock>,
pub footer_blocks: Vec<ParagraphBlock>,
pub first_header_blocks: Vec<ParagraphBlock>,
pub first_footer_blocks: Vec<ParagraphBlock>,
pub even_header_blocks: Vec<ParagraphBlock>,
pub even_footer_blocks: Vec<ParagraphBlock>,
pub even_headers_active: bool,
pub watermark: Option<oxml_layout::GroupElement>,
pub first_watermark: Option<oxml_layout::GroupElement>,
pub even_watermark: Option<oxml_layout::GroupElement>,
}
pub struct Section {
pub blocks: Vec<LayoutBlock>,
pub geometry: PageGeometry,
pub header_footer: Option<HeaderFooterContent>,
pub title_pg: bool,
pub page_number_start: Option<usize>,
}
pub(crate) struct SharedSection {
pub blocks: Vec<SharedLayoutBlock>,
pub geometry: PageGeometry,
pub header_footer: Option<HeaderFooterContent>,
pub title_pg: bool,
pub page_number_start: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PaginationCheckpoint {
pub next_block_index: usize,
pub page_count: usize,
pub next_header_page_number: usize,
}
pub(crate) struct RecordedPagination {
pub pages: Vec<PageFrame>,
pub outlines: Vec<OutlineEntry>,
pub checkpoints: Vec<PaginationCheckpoint>,
pub stopped_at: Option<PaginationCheckpoint>,
}
pub fn paginate_sections(
sections: &[Section],
fm: &FontManager,
media: &MediaRegistry,
notes: &NoteRegistry,
) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
let media = media.media();
if sections.is_empty() {
return (
vec![PageFrame::new(1, 612.0, 792.0, Vec::new())],
Vec::new(),
);
}
if sections.len() == 1 {
let s = §ions[0];
return paginate_with_media(
&s.blocks,
s.geometry,
s.header_footer.as_ref(),
s.title_pg,
fm,
media,
notes,
1,
s.page_number_start.unwrap_or(1),
);
}
let mut all_pages = Vec::new();
let mut all_outlines = Vec::new();
let mut page_offset = 0;
let mut next_section_page_number = 1usize;
for section in sections {
let section_page_number = section
.page_number_start
.unwrap_or(next_section_page_number);
let (mut pages, mut outlines) = paginate_with_media(
§ion.blocks,
section.geometry,
section.header_footer.as_ref(),
section.title_pg,
fm,
media,
notes,
page_offset + 1,
section_page_number,
);
next_section_page_number = section_page_number.saturating_add(pages.len());
page_offset += pages.len();
all_pages.append(&mut pages);
all_outlines.append(&mut outlines);
}
for (i, page) in all_pages.iter_mut().enumerate() {
page.page_number = i + 1;
}
(all_pages, all_outlines)
}
pub(crate) fn paginate_shared_sections(
sections: &[SharedSection],
fm: &FontManager,
media: &MediaRegistry,
notes: &NoteRegistry,
) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
let media = media.media();
if sections.is_empty() {
return (
vec![PageFrame::new(1, 612.0, 792.0, Vec::new())],
Vec::new(),
);
}
if sections.len() == 1 {
let section = §ions[0];
return paginate_with_media(
§ion.blocks,
section.geometry,
section.header_footer.as_ref(),
section.title_pg,
fm,
media,
notes,
1,
section.page_number_start.unwrap_or(1),
);
}
let mut pages = Vec::new();
let mut outlines = Vec::new();
let mut page_offset = 0;
let mut next_section_page_number = 1usize;
for section in sections {
let section_page_number = section
.page_number_start
.unwrap_or(next_section_page_number);
let (mut section_pages, mut section_outlines) = paginate_with_media(
§ion.blocks,
section.geometry,
section.header_footer.as_ref(),
section.title_pg,
fm,
media,
notes,
page_offset + 1,
section_page_number,
);
next_section_page_number = section_page_number.saturating_add(section_pages.len());
page_offset += section_pages.len();
pages.append(&mut section_pages);
outlines.append(&mut section_outlines);
}
for (index, page) in pages.iter_mut().enumerate() {
page.page_number = index + 1;
}
(pages, outlines)
}
pub(crate) fn paginate_shared_single_section_recorded(
section: &SharedSection,
fm: &FontManager,
media: &MediaRegistry,
notes: &NoteRegistry,
restart: Option<PaginationCheckpoint>,
stop_at: Option<PaginationCheckpoint>,
) -> RecordedPagination {
let checkpoint = restart.unwrap_or(PaginationCheckpoint {
next_block_index: 0,
page_count: 0,
next_header_page_number: section.page_number_start.unwrap_or(1),
});
let context = PassContext {
geometry: section.geometry,
header_footer: section.header_footer.as_ref(),
title_pg: section.title_pg,
fm,
media: media.media(),
notes,
first_page_number: checkpoint.page_count + 1,
first_header_page_number: checkpoint.next_header_page_number,
};
let result = paginate_pass_from(
§ion.blocks,
&context,
&ResolvedWraps::new(),
checkpoint.next_block_index,
checkpoint.page_count == 0,
stop_at,
);
let mut checkpoints = result.checkpoints;
if checkpoint.page_count == 0 {
checkpoints.insert(0, checkpoint);
}
RecordedPagination {
pages: result.pages,
outlines: result.outlines,
checkpoints,
stopped_at: result.stopped_at,
}
}
pub fn paginate(
blocks: &[LayoutBlock],
geometry: PageGeometry,
header_footer: Option<&HeaderFooterContent>,
title_pg: bool,
_fm: &FontManager,
media: &MediaRegistry,
notes: &NoteRegistry,
) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
paginate_with_media(
blocks,
geometry,
header_footer,
title_pg,
_fm,
media.media(),
notes,
1,
1,
)
}
type ResolvedWraps = HashMap<(usize, usize), (usize, PlacedWrap)>;
fn has_paragraph_relative_wrap<B: LayoutBlockLike>(blocks: &[B]) -> bool {
blocks.iter().any(|block| {
let Some(para) = block.paragraph() else {
return false;
};
para.anchored.iter().any(is_paragraph_relative_wrap)
})
}
fn is_paragraph_relative_wrap(anchored: &AnchoredDrawing) -> bool {
anchored.wrap != WrapType::None
&& matches!(
anchored.rel_v,
ST_RelativeFromV::Paragraph | ST_RelativeFromV::Line
)
}
fn paginate_with_media<B: LayoutBlockLike>(
blocks: &[B],
geometry: PageGeometry,
header_footer: Option<&HeaderFooterContent>,
title_pg: bool,
_fm: &FontManager,
media: &HashMap<MediaId, ImageData>,
notes: &NoteRegistry,
first_page_number: usize,
first_header_page_number: usize,
) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
let context = PassContext {
geometry,
header_footer,
title_pg,
fm: _fm,
media,
notes,
first_page_number,
first_header_page_number,
};
let first = paginate_pass(blocks, &context, &ResolvedWraps::new());
if !has_paragraph_relative_wrap(blocks) {
return (first.pages, first.outlines);
}
let second = paginate_pass(blocks, &context, &first.resolved);
(second.pages, second.outlines)
}
struct PassResult {
pages: Vec<PageFrame>,
outlines: Vec<OutlineEntry>,
resolved: ResolvedWraps,
checkpoints: Vec<PaginationCheckpoint>,
stopped_at: Option<PaginationCheckpoint>,
}
struct PassContext<'a> {
geometry: PageGeometry,
header_footer: Option<&'a HeaderFooterContent>,
title_pg: bool,
fm: &'a FontManager,
media: &'a HashMap<MediaId, ImageData>,
notes: &'a NoteRegistry,
first_page_number: usize,
first_header_page_number: usize,
}
fn paginate_pass<B: LayoutBlockLike>(
blocks: &[B],
context: &PassContext,
resolved_in: &ResolvedWraps,
) -> PassResult {
paginate_pass_from(blocks, context, resolved_in, 0, true, None)
}
fn paginate_pass_from<B: LayoutBlockLike>(
blocks: &[B],
context: &PassContext,
resolved_in: &ResolvedWraps,
first_block_index: usize,
is_first_page: bool,
stop_at: Option<PaginationCheckpoint>,
) -> PassResult {
if first_block_index >= blocks.len() && !is_first_page {
return PassResult {
pages: Vec::new(),
outlines: Vec::new(),
resolved: resolved_in.clone(),
checkpoints: Vec::new(),
stopped_at: None,
};
}
let geometry = context.geometry;
let mut pager = Pager::new(
geometry,
context.header_footer,
context.title_pg,
context.media,
context.notes,
context.fm,
resolved_in,
context.first_page_number,
context.first_header_page_number,
is_first_page,
stop_at,
);
for (block_idx, block) in blocks.iter().enumerate().skip(first_block_index) {
if block.page_break_before() && pager.has_content() {
pager.finish_page_before(block_idx);
if pager.stopped_at.is_some() {
break;
}
}
if let Some(para) = block.paragraph() {
if let (Some(level), Some(title)) = (para.heading_level, ¶.heading_text) {
pager.outlines.push(OutlineEntry {
title: title.clone(),
level,
page_index: pager.page_number - 1,
y_position: pager.geometry.margin_top + pager.cursor_y,
});
}
paginate_paragraph(para, block_idx, blocks, &mut pager);
if pager.stopped_at.is_some() {
break;
}
} else if let Some(table) = block.table() {
let table_x = geometry.margin_left + table.table_indent;
let tbl_borders = table.borders.as_ref();
for (row_idx, row) in table.rows.iter().enumerate() {
let row_semantics = table
.semantics
.and_then(|semantics| semantics.rows.get(row_idx));
if pager.cursor_y + row.height > pager.available_height() && pager.has_content() {
pager.finish_page();
for &hdr_idx in &table.header_row_indices {
if hdr_idx < row_idx {
let hdr_row = &table.rows[hdr_idx];
render_table_row(
hdr_row,
table
.semantics
.and_then(|semantics| semantics.rows.get(hdr_idx)),
&table.col_widths,
table_x,
pager.geometry.margin_top + pager.cursor_y,
&pager.geometry,
pager.page_number,
tbl_borders,
&mut pager.elements,
&mut pager.behind_elements,
pager.media,
);
pager.cursor_y += hdr_row.height;
pager.mark_content();
}
}
}
render_table_row(
row,
row_semantics,
&table.col_widths,
table_x,
pager.geometry.margin_top + pager.cursor_y,
&pager.geometry,
pager.page_number,
tbl_borders,
&mut pager.elements,
&mut pager.behind_elements,
pager.media,
);
pager.cursor_y += row.height;
pager.mark_content();
}
}
}
let resolved = std::mem::take(&mut pager.resolved_out);
let checkpoints = std::mem::take(&mut pager.checkpoints);
let stopped_at = pager.stopped_at;
let (pages, outlines) = if stopped_at.is_some() {
(
std::mem::take(&mut pager.pages),
std::mem::take(&mut pager.outlines),
)
} else {
pager.flush()
};
PassResult {
pages,
outlines,
resolved,
checkpoints,
stopped_at,
}
}
struct Pager<'a> {
pages: Vec<PageFrame>,
elements: Vec<PositionedElement>,
behind_elements: Vec<PositionedElement>,
cursor_y: f64,
page_number: usize,
header_page_number: usize,
content_height: f64,
geometry: PageGeometry,
header_footer: Option<&'a HeaderFooterContent>,
has_content_flag: bool,
outlines: Vec<OutlineEntry>,
is_first_page: bool,
title_pg: bool,
media: &'a HashMap<MediaId, ImageData>,
notes: &'a NoteRegistry,
page_note_ids: Vec<NoteRef>,
pending_notes: Vec<(NoteRef, usize)>,
fm: &'a FontManager,
page_wraps: Vec<PlacedWrap>,
ink_bottom: f64,
resolved_in: &'a ResolvedWraps,
resolved_out: ResolvedWraps,
checkpoints: Vec<PaginationCheckpoint>,
stop_at: Option<PaginationCheckpoint>,
stopped_at: Option<PaginationCheckpoint>,
}
impl<'a> Pager<'a> {
fn new(
geometry: PageGeometry,
header_footer: Option<&'a HeaderFooterContent>,
title_pg: bool,
media: &'a HashMap<MediaId, ImageData>,
notes: &'a NoteRegistry,
fm: &'a FontManager,
resolved_in: &'a ResolvedWraps,
first_page_number: usize,
first_header_page_number: usize,
is_first_page: bool,
stop_at: Option<PaginationCheckpoint>,
) -> Self {
Pager {
pages: Vec::new(),
elements: Vec::new(),
behind_elements: Vec::new(),
cursor_y: 0.0,
page_number: first_page_number,
header_page_number: first_header_page_number,
content_height: geometry.content_height(),
geometry,
header_footer,
has_content_flag: false,
outlines: Vec::new(),
is_first_page,
title_pg,
media,
notes,
page_note_ids: Vec::new(),
pending_notes: Vec::new(),
fm,
page_wraps: Vec::new(),
ink_bottom: 0.0,
resolved_in,
resolved_out: ResolvedWraps::new(),
checkpoints: Vec::new(),
stop_at,
stopped_at: None,
}
}
fn has_content(&self) -> bool {
self.has_content_flag
}
fn reserve_for(&self, carried: &[(NoteRef, usize)], fresh: &[NoteRef]) -> f64 {
if carried.is_empty() && fresh.is_empty() {
return 0.0;
}
let carried_height: f64 = carried
.iter()
.filter_map(|(id, first)| {
self.notes
.get(*id, self.geometry.content_width())
.map(|note| note.height_from(*first))
})
.sum();
let fresh_height: f64 = fresh
.iter()
.filter_map(|id| {
self.notes
.get(*id, self.geometry.content_width())
.map(NoteLayout::height)
})
.sum();
NOTE_SEPARATOR_OFFSET + carried_height + fresh_height
}
fn reserved_height(&self) -> f64 {
self.reserve_for(&self.pending_notes, &self.page_note_ids)
}
fn available_height(&self) -> f64 {
(self.content_height - self.reserved_height()).max(0.0)
}
fn available_height_for(&self, lines: &[LayoutLine]) -> f64 {
let mut fresh = self.page_note_ids.clone();
for line in lines {
for id in page_foot_notes_in_line(line) {
if self.notes.get(id, self.geometry.content_width()).is_some()
&& !fresh.contains(&id)
&& !self.pending_notes.iter().any(|(pending, _)| *pending == id)
{
fresh.push(id);
}
}
}
(self.content_height - self.reserve_for(&self.pending_notes, &fresh)).max(0.0)
}
fn claim_notes(&mut self, lines: &[LayoutLine]) {
for id in lines.iter().flat_map(page_foot_notes_in_line) {
{
if self.notes.get(id, self.geometry.content_width()).is_some()
&& !self.page_note_ids.contains(&id)
&& !self.pending_notes.iter().any(|(pending, _)| *pending == id)
{
self.page_note_ids.push(id);
}
}
}
}
fn count_lines_that_fit_with_notes(&self, lines: &[LayoutLine], start_y: f64) -> usize {
let mut fresh = self.page_note_ids.clone();
let mut used = 0.0;
for (index, line) in lines.iter().enumerate() {
for id in page_foot_notes_in_line(line) {
if self.notes.get(id, self.geometry.content_width()).is_some()
&& !fresh.contains(&id)
&& !self.pending_notes.iter().any(|(pending, _)| *pending == id)
{
fresh.push(id);
}
}
let reserve = self.reserve_for(&self.pending_notes, &fresh);
if start_y + used + line.height > self.content_height - reserve + 0.01 {
let page_is_empty = !self.has_content() && used == 0.0 && index == 0;
if !page_is_empty {
return index;
}
}
used += line.height;
}
lines.len()
}
fn mark_content(&mut self) {
self.has_content_flag = true;
}
fn wrap_rects_for(
&self,
anchored: &[AnchoredDrawing],
para_top: f64,
indent_left: f64,
) -> Vec<PlacedWrap> {
anchored
.iter()
.filter(|a| a.wrap != WrapType::None)
.map(|a| PlacedWrap {
rect: Rect {
x: resolve_anchor_h(
a.rel_h,
a.off_h,
a.align_h,
a.width,
&self.geometry,
indent_left,
),
y: resolve_anchor_v(
a.rel_v,
a.off_v,
a.align_v,
a.height,
&self.geometry,
para_top,
),
width: a.width,
height: a.height,
},
wrap: a.wrap,
dist_top: a.dist_top,
dist_bottom: a.dist_bottom,
dist_left: a.dist_left,
dist_right: a.dist_right,
})
.collect()
}
fn lookahead_wraps<B: LayoutBlockLike>(
&self,
block_idx: usize,
blocks: &[B],
) -> Vec<PlacedWrap> {
let mut out = Vec::new();
let mut height = self.cursor_y;
for (offset, block) in blocks.iter().enumerate().skip(block_idx + 1) {
if block.page_break_before() || height > self.content_height {
break;
}
height += block.space_before() + block.content_height() + block.space_after();
let Some(para) = block.paragraph() else {
continue;
};
for (anchor_idx, a) in para.anchored.iter().enumerate() {
if a.wrap == WrapType::None {
continue;
}
if is_paragraph_relative_wrap(a) {
if let Some((page, placed)) = self.resolved_in.get(&(offset, anchor_idx))
&& *page == self.page_number
{
out.push(*placed);
}
continue;
}
out.extend(self.wrap_rects_for(std::slice::from_ref(a), 0.0, para.indent_left));
}
}
out
}
fn place_anchored(
&mut self,
anchored: &[AnchoredDrawing],
para_top: f64,
indent_left: f64,
block_idx: usize,
) {
for (anchor_idx, a) in anchored.iter().enumerate() {
let x = resolve_anchor_h(
a.rel_h,
a.off_h,
a.align_h,
a.width,
&self.geometry,
indent_left,
);
let y = resolve_anchor_v(
a.rel_v,
a.off_v,
a.align_v,
a.height,
&self.geometry,
para_top,
);
let rect = Rect {
x,
y,
width: a.width,
height: a.height,
};
if a.wrap != WrapType::None {
let placed = PlacedWrap {
rect,
wrap: a.wrap,
dist_top: a.dist_top,
dist_bottom: a.dist_bottom,
dist_left: a.dist_left,
dist_right: a.dist_right,
};
if is_paragraph_relative_wrap(a) {
self.resolved_out
.insert((block_idx, anchor_idx), (self.page_number, placed));
}
self.page_wraps.push(placed);
}
let mut produced = anchored_elements(a, rect, &self.geometry, self.media);
if a.behind_doc {
self.behind_elements.append(&mut produced);
} else {
self.elements.append(&mut produced);
}
}
}
fn place_page_notes(&mut self) {
let mut queue: Vec<(NoteRef, usize, bool)> = self
.pending_notes
.drain(..)
.map(|(id, first)| (id, first, true))
.collect();
queue.extend(self.page_note_ids.drain(..).map(|id| (id, 0usize, false)));
if queue.is_empty() {
return;
}
let opens_with_continuation = queue[0].2;
let available = (self.content_height - self.ink_bottom - NOTE_SEPARATOR_OFFSET).max(0.0);
let mut placed: Vec<(NoteRef, usize, usize, bool)> = Vec::new();
let mut used = 0.0;
let mut carried: Vec<(NoteRef, usize)> = Vec::new();
for (id, first, continued) in queue {
let Some(note) = self.notes.get(id, self.geometry.content_width()) else {
continue;
};
if !carried.is_empty() {
carried.push((id, first));
continue;
}
let mut count = 0;
for line in note.lines.iter().skip(first) {
if used + line.height > available + 0.01 {
break;
}
used += line.height;
count += 1;
}
if count > 0 {
placed.push((id, first, count, continued));
}
if first + count < note.lines.len() {
carried.push((id, first + count));
}
}
self.pending_notes = carried;
if placed.is_empty() {
return;
}
let total: f64 = placed
.iter()
.filter_map(|(id, first, count, _)| {
self.notes
.get(*id, self.geometry.content_width())
.map(|n| n.height_of(*first, *count))
})
.sum();
let separator_y =
self.geometry.page_height - self.geometry.margin_bottom - total - NOTE_SEPARATOR_OFFSET;
let separator_width = if opens_with_continuation && self.notes.has_continuation_separator()
{
self.geometry.content_width()
} else {
self.geometry.content_width() * SEPARATOR_WIDTH_FRACTION
};
self.elements.push(PositionedElement::Line {
start: Point {
x: self.geometry.margin_left,
y: separator_y,
},
end: Point {
x: self.geometry.margin_left + separator_width,
y: separator_y,
},
width: 0.5,
color: Color::BLACK,
dash_pattern: None,
});
let mut cursor_y = separator_y + NOTE_SEPARATOR_OFFSET;
for (id, first, count, continued) in placed {
let Some(note) = self.notes.get(id, self.geometry.content_width()) else {
continue;
};
cursor_y += draw_note(
&mut self.elements,
&self.geometry,
note,
first,
count,
continued,
cursor_y,
self.page_number,
);
}
}
fn finish_page(&mut self) {
self.place_page_notes();
let mut all_elements = Vec::new();
if let Some(hf) = self.header_footer {
let watermark = if self.is_first_page && self.title_pg {
hf.first_watermark.as_ref()
} else if hf.even_headers_active && self.header_page_number.is_multiple_of(2) {
hf.even_watermark.as_ref()
} else {
hf.watermark.as_ref()
};
if let Some(watermark) = watermark {
all_elements.push(PositionedElement::Group(watermark.clone()));
}
}
all_elements.append(&mut self.behind_elements);
if let Some(hf) = self.header_footer {
let header_blocks = if self.is_first_page && self.title_pg {
&hf.first_header_blocks
} else if hf.even_headers_active && self.header_page_number.is_multiple_of(2) {
&hf.even_header_blocks
} else {
&hf.header_blocks
};
if !header_blocks.is_empty() {
let header_y = self.geometry.header_distance;
render_hf_blocks(
header_blocks,
&self.geometry,
header_y,
self.page_number,
&mut all_elements,
self.media,
);
}
}
all_elements.append(&mut self.elements);
if let Some(hf) = self.header_footer {
let footer_blocks = if self.is_first_page && self.title_pg {
&hf.first_footer_blocks
} else if hf.even_headers_active && self.header_page_number.is_multiple_of(2) {
&hf.even_footer_blocks
} else {
&hf.footer_blocks
};
if !footer_blocks.is_empty() {
let footer_height: f64 = footer_blocks.iter().map(|b| b.content_height()).sum();
let footer_y =
self.geometry.page_height - self.geometry.footer_distance - footer_height;
render_hf_blocks(
footer_blocks,
&self.geometry,
footer_y,
self.page_number,
&mut all_elements,
self.media,
);
}
}
self.pages.push(PageFrame::new(
self.page_number,
self.geometry.page_width,
self.geometry.page_height,
all_elements,
));
self.page_number += 1;
self.header_page_number += 1;
self.cursor_y = 0.0;
self.page_wraps.clear();
self.ink_bottom = 0.0;
self.has_content_flag = false;
self.is_first_page = false;
}
fn finish_page_before(&mut self, next_block_index: usize) {
self.finish_page();
if self.pending_notes.is_empty()
&& self.page_note_ids.is_empty()
&& self.page_wraps.is_empty()
&& self.resolved_out.is_empty()
{
self.checkpoints.push(PaginationCheckpoint {
next_block_index,
page_count: self.page_number - 1,
next_header_page_number: self.header_page_number,
});
if self.stop_at == self.checkpoints.last().copied() {
self.stopped_at = self.checkpoints.last().copied();
}
}
}
fn flush(mut self) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
if self.has_content() || self.pages.is_empty() {
self.finish_page();
}
while !self.pending_notes.is_empty() {
let before = self.pending_notes.clone();
self.finish_page();
if self.pending_notes == before {
debug_assert!(
false,
"a page placed no note content, dropping {:?}",
self.pending_notes
);
break;
}
}
(self.pages, self.outlines)
}
}
fn anchored_elements(
anchor: &AnchoredDrawing,
rect: Rect,
geometry: &PageGeometry,
media: &HashMap<MediaId, ImageData>,
) -> Vec<PositionedElement> {
let mut produced = Vec::new();
match &anchor.content {
AnchoredContent::Image { media_id } => {
let image = media.get(media_id);
produced.push(PositionedElement::Image {
rect,
data: image.map_or_else(Vec::new, |image| image.data.clone()),
content_type: image.map_or_else(String::new, |image| image.content_type.clone()),
media_id: *media_id,
});
}
AnchoredContent::Group(group) => {
let mut positioned = group.clone();
positioned.transform = positioned.transform.then(oxml_layout::Transform {
e: rect.x,
f: rect.y,
..oxml_layout::Transform::IDENTITY
});
produced.push(PositionedElement::Group(positioned));
}
AnchoredContent::Shape { preset, fill, text } => {
match (preset, fill) {
(ShapePreset::Rect, Some(color)) => {
produced.push(PositionedElement::FilledRect {
rect,
color: *color,
});
}
(ShapePreset::Line, Some(color)) => {
produced.push(PositionedElement::Line {
start: Point {
x: rect.x,
y: rect.y,
},
end: Point {
x: rect.x + anchor.width,
y: rect.y + anchor.height,
},
width: 1.0,
color: *color,
dash_pattern: None,
});
}
_ => {}
}
produced.extend(render_shape_text(text, geometry, rect, media));
}
}
produced
.into_iter()
.map(|element| PositionedElement::MarkedContent {
structure: anchor.structure_id,
children: vec![element],
})
.collect()
}
fn draw_note(
elements: &mut Vec<PositionedElement>,
geometry: &PageGeometry,
note: &NoteLayout,
first: usize,
count: usize,
continued: bool,
top: f64,
page_number: usize,
) -> f64 {
let baseline = top + note.lines.get(first).map_or(0.0, |line| line.ascent);
if !continued {
elements.push(PositionedElement::Text(GlyphRun {
origin: Point {
x: geometry.margin_left,
y: baseline - note.marker_rise,
},
font_id: note.marker.font_id,
font_size: note.marker.font_size,
glyph_ids: note.marker.glyph_ids.clone(),
advances: note.marker.advances.clone(),
text: note.marker.text.clone(),
source: None,
color: note.marker.color,
bold: note.marker.bold,
italic: note.marker.italic,
field_kind: None,
note: None,
}));
}
let mut cursor_y = top;
for line in note.lines.iter().skip(first).take(count) {
let line_baseline = cursor_y + line.ascent;
let mut x = geometry.margin_left + NOTE_INDENT;
for item in &line.items {
let (segment, advance) = match item {
LineItem::Text(seg) | LineItem::Marker(seg) => (Some(seg), seg.width),
LineItem::Tab { width, .. }
| LineItem::Image { width, .. }
| LineItem::Group { width, .. } => (None, *width),
_ => (None, item.width()),
};
if let Some(seg) = segment {
let adjusted_baseline = line_baseline - seg.baseline_offset;
if let Some(color) = seg.highlight {
elements.push(PositionedElement::FilledRect {
rect: Rect {
x,
y: cursor_y,
width: seg.width,
height: line.height,
},
color,
});
}
elements.push(PositionedElement::Text(GlyphRun {
origin: Point {
x,
y: adjusted_baseline,
},
font_id: seg.font_id,
font_size: seg.font_size,
glyph_ids: seg.glyph_ids.clone(),
advances: seg.advances.clone(),
text: seg.text.clone(),
source: seg.source,
color: seg.color,
bold: seg.bold,
italic: seg.italic,
field_kind: None,
note: None,
}));
if let Some(underline) = seg.underline {
let underline_y = adjusted_baseline + seg.descent * 0.3;
let thickness = match underline {
Underline::Thick => seg.font_size / 12.0,
Underline::Double => seg.font_size / 24.0,
_ => seg.font_size / 18.0,
};
elements.push(PositionedElement::Line {
start: Point { x, y: underline_y },
end: Point {
x: x + seg.width,
y: underline_y,
},
width: thickness,
color: seg.color,
dash_pattern: None,
});
if underline == Underline::Double {
let second_y = underline_y + thickness * 2.5;
elements.push(PositionedElement::Line {
start: Point { x, y: second_y },
end: Point {
x: x + seg.width,
y: second_y,
},
width: thickness,
color: seg.color,
dash_pattern: None,
});
}
}
if seg.strike {
let strike_y = adjusted_baseline - seg.ascent * 0.3;
let thickness = seg.font_size / 24.0;
elements.push(PositionedElement::Line {
start: Point { x, y: strike_y },
end: Point {
x: x + seg.width,
y: strike_y,
},
width: thickness,
color: seg.color,
dash_pattern: None,
});
}
if seg.dstrike {
let strike_y = adjusted_baseline - seg.ascent * 0.3;
let thickness = seg.font_size / 24.0;
let gap = thickness * 2.0;
for y in [strike_y - gap / 2.0, strike_y + gap / 2.0] {
elements.push(PositionedElement::Line {
start: Point { x, y },
end: Point {
x: x + seg.width,
y,
},
width: thickness,
color: seg.color,
dash_pattern: None,
});
}
}
}
x += advance;
}
cursor_y += line.height;
}
for range in ¬e.revision_ranges {
let visible_start = range.start.max(first);
let visible_end = range.end.min(first + count);
if visible_start >= visible_end {
continue;
}
let offset = note
.lines
.iter()
.skip(first)
.take(visible_start - first)
.map(|line| line.height)
.sum::<f64>();
let height = note
.lines
.iter()
.skip(visible_start)
.take(visible_end - visible_start)
.map(|line| line.height)
.sum::<f64>();
render_change_bar_at(top + offset, height, geometry, page_number, elements);
}
cursor_y - top
}
pub fn append_endnote_pages(
pages: &mut Vec<PageFrame>,
notes: &NoteRegistry,
geometry: PageGeometry,
) {
let mut ordered: Vec<NoteRef> = Vec::new();
for page in pages.iter() {
oxml_layout::walk(&page.elements, &mut |element, _| {
if let PositionedElement::Text(run) = element
&& let Some(note) = run.note
&& note.stream == NoteStream::Endnote
&& notes.get(note, geometry.content_width()).is_some()
&& !ordered.contains(¬e)
{
ordered.push(note);
}
});
}
if ordered.is_empty() {
return;
}
let content_height = geometry.content_height();
let mut elements: Vec<PositionedElement> = Vec::new();
let mut cursor_y = 0.0;
let mut page_number = pages.len() + 1;
let mut flush = |elements: &mut Vec<PositionedElement>, page_number: &mut usize| {
pages.push(PageFrame::new(
*page_number,
geometry.page_width,
geometry.page_height,
std::mem::take(elements),
));
*page_number += 1;
};
for note_ref in ordered {
let Some(note) = notes.get(note_ref, geometry.content_width()) else {
continue;
};
let mut first = 0;
let mut continued = false;
while first < note.lines.len() {
let mut count = 0;
let mut used = cursor_y;
for line in note.lines.iter().skip(first) {
if used + line.height > content_height + 0.01 {
break;
}
used += line.height;
count += 1;
}
if count == 0 {
if cursor_y == 0.0 {
count = 1;
} else {
flush(&mut elements, &mut page_number);
cursor_y = 0.0;
continue;
}
}
cursor_y += draw_note(
&mut elements,
&geometry,
note,
first,
count,
continued,
geometry.margin_top + cursor_y,
page_number,
);
first += count;
continued = true;
}
}
if !elements.is_empty() {
flush(&mut elements, &mut page_number);
}
}
fn notes_in_line(line: &LayoutLine) -> impl Iterator<Item = NoteRef> + '_ {
line.items.iter().filter_map(|item| match item {
LineItem::Text(seg) | LineItem::Marker(seg) => seg.note,
_ => None,
})
}
fn page_foot_notes_in_line(line: &LayoutLine) -> impl Iterator<Item = NoteRef> + '_ {
notes_in_line(line).filter(|note| note.stream == NoteStream::Footnote)
}
fn translate_element(element: &mut PositionedElement, dx: f64, dy: f64) {
match element {
PositionedElement::Text(run) => {
run.origin.x += dx;
run.origin.y += dy;
}
PositionedElement::Line { start, end, .. } => {
start.x += dx;
start.y += dy;
end.x += dx;
end.y += dy;
}
PositionedElement::FilledRect { rect, .. }
| PositionedElement::Image { rect, .. }
| PositionedElement::LinkAnnotation { rect, .. } => {
rect.x += dx;
rect.y += dy;
}
_ => {}
}
}
fn render_shape_text(
text: &[ParagraphBlock],
geometry: &PageGeometry,
rect: Rect,
media: &HashMap<MediaId, ImageData>,
) -> Vec<PositionedElement> {
if text.is_empty() {
return Vec::new();
}
let mut local = Vec::new();
let mut y = 0.0;
for para in text {
render_paragraph_lines(
¶.lines,
ParagraphView {
block: para,
semantics: None,
},
geometry,
y,
&mut local,
media,
);
y += para.content_height();
}
let dx = rect.x - geometry.margin_left;
let dy = rect.y - geometry.margin_top;
for element in &mut local {
translate_element(element, dx, dy);
}
local
}
fn frame_h(rel: ST_RelativeFromH, g: &PageGeometry, indent_left: f64) -> (f64, f64) {
let text_width = g.page_width - g.margin_left - g.margin_right;
match rel {
ST_RelativeFromH::Page | ST_RelativeFromH::LeftMargin => (0.0, g.page_width),
ST_RelativeFromH::RightMargin | ST_RelativeFromH::OutsideMargin => {
(g.page_width - g.margin_right, g.margin_right)
}
ST_RelativeFromH::InsideMargin => (g.margin_left, g.margin_left),
ST_RelativeFromH::Character => (g.margin_left + indent_left, text_width),
ST_RelativeFromH::Margin | ST_RelativeFromH::Column => (g.margin_left, text_width),
}
}
fn resolve_anchor_h(
rel: ST_RelativeFromH,
off: f64,
align: Option<AnchorAlignH>,
width: f64,
g: &PageGeometry,
indent_left: f64,
) -> f64 {
let (start, size) = frame_h(rel, g, indent_left);
match align {
Some(AnchorAlignH::Left | AnchorAlignH::Inside) => start,
Some(AnchorAlignH::Center) => start + (size - width) / 2.0,
Some(AnchorAlignH::Right | AnchorAlignH::Outside) => start + size - width,
None => start + off,
}
}
fn frame_v(rel: ST_RelativeFromV, g: &PageGeometry, para_top: f64) -> (f64, f64) {
let text_height = g.page_height - g.margin_top - g.margin_bottom;
match rel {
ST_RelativeFromV::Page | ST_RelativeFromV::TopMargin => (0.0, g.page_height),
ST_RelativeFromV::BottomMargin | ST_RelativeFromV::OutsideMargin => {
(g.page_height - g.margin_bottom, g.margin_bottom)
}
ST_RelativeFromV::Margin | ST_RelativeFromV::InsideMargin => (g.margin_top, text_height),
ST_RelativeFromV::Paragraph | ST_RelativeFromV::Line => {
(g.margin_top + para_top, text_height)
}
}
}
fn resolve_anchor_v(
rel: ST_RelativeFromV,
off: f64,
align: Option<AnchorAlignV>,
height: f64,
g: &PageGeometry,
para_top: f64,
) -> f64 {
let (start, size) = frame_v(rel, g, para_top);
match align {
Some(AnchorAlignV::Top | AnchorAlignV::Inside) => start,
Some(AnchorAlignV::Center) => start + (size - height) / 2.0,
Some(AnchorAlignV::Bottom | AnchorAlignV::Outside) => start + size - height,
None => start + off,
}
}
fn reflow_around_wraps(
para: &ParagraphBlock,
wraps: &[PlacedWrap],
para_top: f64,
geometry: &PageGeometry,
fm: &FontManager,
) -> Option<ParagraphBlock> {
let reflow = para.reflow.as_ref()?;
if wraps.is_empty() {
return None;
}
let mut lines = para.lines.clone();
let mut offset_top = 0.0;
for _ in 0..2 {
let mut prefix: Vec<f64> = Vec::new();
let mut suffix: Vec<f64> = Vec::new();
let paragraph_top = geometry.margin_top + para_top;
let mut next_offset_top: f64 = 0.0;
for wrap in wraps.iter().filter(|w| w.wrap == WrapType::TopAndBottom) {
if wrap.keep_out_top() <= paragraph_top + 1.0 && wrap.keep_out_bottom() > paragraph_top
{
next_offset_top = next_offset_top.max(wrap.keep_out_bottom() - paragraph_top);
}
}
for wrap in wraps.iter().filter(|w| w.wrap != WrapType::TopAndBottom) {
let text_left = geometry.margin_left + para.indent_left;
let text_right = geometry.page_width - geometry.margin_right - para.indent_right;
let drawing_centre = wrap.rect.x + wrap.rect.width / 2.0;
let on_the_left = drawing_centre < (text_left + text_right) / 2.0;
let reserve = if on_the_left {
(wrap.rect.x + wrap.rect.width + wrap.dist_right - text_left).max(0.0)
} else {
(text_right - (wrap.rect.x - wrap.dist_left)).max(0.0)
};
if reserve <= 0.0 {
continue;
}
let mut line_top = geometry.margin_top + para_top + next_offset_top;
for (index, line) in lines.iter().enumerate() {
let line_bottom = line_top + line.height;
if line_bottom > wrap.keep_out_top() && line_top < wrap.keep_out_bottom() {
let target = if on_the_left {
&mut prefix
} else {
&mut suffix
};
if target.len() <= index {
target.resize(index + 1, 0.0);
}
target[index] += reserve;
}
line_top = line_bottom;
}
}
if prefix.is_empty() && suffix.is_empty() && next_offset_top == 0.0 {
return None;
}
let mut params = reflow.params.clone();
params.line_prefix_widths = prefix;
params.line_suffix_widths = suffix;
let Ok(reflowed) = break_into_lines(&reflow.items, ¶ms, fm) else {
return None;
};
lines = reflowed;
offset_top = next_offset_top;
}
let mut adjusted = para.clone();
adjusted.lines = lines;
adjusted.content_offset_top = offset_top;
adjusted.reflow = None;
Some(adjusted)
}
fn paginate_paragraph<B: LayoutBlockLike>(
para: ParagraphView<'_>,
block_idx: usize,
blocks: &[B],
pager: &mut Pager,
) {
let space_before = if pager.cursor_y == 0.0 {
0.0
} else {
para.space_before
};
let reflowed = {
let para_top = pager.cursor_y + space_before;
let mut wraps = pager.page_wraps.clone();
wraps.extend(pager.wrap_rects_for(¶.anchored, para_top, para.indent_left));
wraps.extend(pager.lookahead_wraps(block_idx, blocks));
reflow_around_wraps(para.block, &wraps, para_top, &pager.geometry, pager.fm)
};
let para = reflowed.as_ref().map_or(para, |block| ParagraphView {
block,
semantics: para.semantics,
});
let total_needed = space_before + para.content_height();
let remaining = pager.available_height_for(¶.lines) - pager.cursor_y;
if total_needed > remaining && pager.has_content() {
if para.keep_lines || para.lines.len() <= 2 {
pager.finish_page_before(block_idx);
if pager.stopped_at.is_some() {
return;
}
paginate_paragraph(para, block_idx, blocks, pager);
return;
}
let lines_that_fit = pager.count_lines_that_fit_with_notes(
¶.lines,
pager.cursor_y + space_before + para.content_offset_top,
);
if para.widow_control && lines_that_fit < 2 {
pager.finish_page_before(block_idx);
if pager.stopped_at.is_some() {
return;
}
paginate_paragraph(para, block_idx, blocks, pager);
return;
}
let lines_remaining = para.lines.len() - lines_that_fit;
if para.widow_control && lines_remaining < 2 && lines_that_fit >= 3 {
let split_at = lines_that_fit - 1;
render_para_split(para, split_at, space_before, pager, block_idx);
return;
}
if lines_that_fit > 0 {
render_para_split(para, lines_that_fit, space_before, pager, block_idx);
return;
}
pager.finish_page_before(block_idx);
if pager.stopped_at.is_some() {
return;
}
paginate_paragraph(para, block_idx, blocks, pager);
return;
}
if total_needed > pager.available_height_for(¶.lines) && pager.cursor_y == 0.0 {
let lines_that_fit =
pager.count_lines_that_fit_with_notes(¶.lines, para.content_offset_top);
if lines_that_fit > 0 && lines_that_fit < para.lines.len() {
render_para_split(para, lines_that_fit, 0.0, pager, block_idx);
return;
}
}
if para.keep_next && block_idx + 1 < blocks.len() {
let next = &blocks[block_idx + 1];
let next_first = next.paragraph().map_or_else(
|| {
next.table().map_or(0.0, |table| {
table.rows.first().map_or(0.0, |row| row.height)
})
},
|paragraph| paragraph.lines.first().map_or(0.0, |line| line.height),
);
if pager.cursor_y + space_before + para.content_height() + next_first
> pager.available_height_for(¶.lines)
&& pager.has_content()
{
pager.finish_page_before(block_idx);
if pager.stopped_at.is_some() {
return;
}
}
}
let space = if pager.cursor_y == 0.0 {
0.0
} else {
para.space_before
};
pager.cursor_y += space;
if let Some(shading) = para.shading {
pager.elements.push(PositionedElement::FilledRect {
rect: Rect {
x: pager.geometry.margin_left + para.indent_left,
y: pager.geometry.margin_top + pager.cursor_y,
width: pager.geometry.content_width() - para.indent_left - para.indent_right,
height: para.content_height(),
},
color: shading,
});
}
if let Some(ref borders) = para.borders {
let border_x = pager.geometry.margin_left + para.indent_left;
let border_y = pager.geometry.margin_top + pager.cursor_y;
let border_w = pager.geometry.content_width() - para.indent_left - para.indent_right;
let border_h = para.content_height();
render_border_edges(
borders,
border_x,
border_y,
border_w,
border_h,
&mut pager.elements,
);
}
pager.place_anchored(¶.anchored, pager.cursor_y, para.indent_left, block_idx);
render_paragraph_lines(
¶.lines,
para,
&pager.geometry,
pager.cursor_y,
&mut pager.elements,
pager.media,
);
render_change_bar(
para.block,
pager.cursor_y,
para.content_height(),
&pager.geometry,
pager.page_number,
&mut pager.elements,
);
pager.claim_notes(¶.lines);
pager.cursor_y += para.content_height();
pager.ink_bottom = pager.cursor_y;
pager.cursor_y += para.space_after;
pager.mark_content();
}
fn render_para_split(
para: ParagraphView<'_>,
split_at: usize,
space_before: f64,
pager: &mut Pager,
block_idx: usize,
) {
pager.cursor_y += space_before;
pager.place_anchored(¶.anchored, pager.cursor_y, para.indent_left, block_idx);
render_paragraph_lines(
¶.lines[..split_at],
para,
&pager.geometry,
pager.cursor_y,
&mut pager.elements,
pager.media,
);
let first_height = para.content_offset_top
+ para.lines[..split_at]
.iter()
.map(|line| line.height)
.sum::<f64>();
render_change_bar(
para.block,
pager.cursor_y,
first_height,
&pager.geometry,
pager.page_number,
&mut pager.elements,
);
pager.claim_notes(¶.lines[..split_at]);
pager.ink_bottom = pager.cursor_y
+ para.content_offset_top
+ para.lines[..split_at].iter().map(|l| l.height).sum::<f64>();
pager.mark_content();
pager.finish_page();
let remaining_lines = ¶.lines[split_at..];
let remaining_height: f64 = remaining_lines.iter().map(|l| l.height).sum();
if remaining_height > pager.available_height_for(remaining_lines) {
let lines_that_fit = pager.count_lines_that_fit_with_notes(remaining_lines, 0.0);
if lines_that_fit > 0 && lines_that_fit < remaining_lines.len() {
let temp_para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: para.has_visible_revision,
lines: remaining_lines.to_vec(),
space_before: 0.0,
space_after: para.space_after,
borders: para.borders.clone(),
shading: para.shading,
indent_left: para.indent_left,
indent_right: para.indent_right,
jc: para.jc,
keep_next: para.keep_next,
keep_lines: false,
page_break_before: false,
widow_control: para.widow_control,
heading_level: None,
heading_text: None,
list: para.list,
structure_id: para.structure_id(),
reflow: None,
content_offset_top: 0.0,
};
render_para_split(
ParagraphView {
block: &temp_para,
semantics: para.semantics,
},
lines_that_fit,
0.0,
pager,
block_idx,
);
return;
}
}
render_paragraph_lines(
remaining_lines,
para,
&pager.geometry,
0.0,
&mut pager.elements,
pager.media,
);
render_change_bar(
para.block,
0.0,
remaining_height,
&pager.geometry,
pager.page_number,
&mut pager.elements,
);
pager.claim_notes(remaining_lines);
pager.ink_bottom = remaining_height;
pager.cursor_y = remaining_height + para.space_after;
pager.mark_content();
}
fn render_paragraph_lines(
lines: &[LayoutLine],
para: ParagraphView<'_>,
geometry: &PageGeometry,
start_y: f64,
elements: &mut Vec<PositionedElement>,
media: &HashMap<MediaId, ImageData>,
) {
let first_element = elements.len();
let mut y = start_y + para.content_offset_top;
for line in lines {
let baseline_y = geometry.margin_top + y + line.ascent;
let text_width: f64 = line.items.iter().map(|item| item.width()).sum();
let remaining_width = line.available_width - text_width;
let justify_extra =
if para.jc == Some(Align::Justify) && !line.is_last && remaining_width > 0.0 {
let gap_count = count_word_gaps(&line.items);
if gap_count > 0 {
remaining_width / gap_count as f64
} else {
0.0
}
} else {
0.0
};
let x_offset = match para.jc {
Some(Align::Center) => geometry.margin_left + line.indent_left + remaining_width / 2.0,
Some(Align::End) => geometry.margin_left + line.indent_left + remaining_width,
Some(Align::Justify) if !line.is_last && justify_extra > 0.0 => {
geometry.margin_left + line.indent_left
}
_ => geometry.margin_left + line.indent_left,
};
let mut x = x_offset;
let mut _accumulated_extra = 0.0;
for item in &line.items {
match item {
LineItem::Text(seg) | LineItem::Marker(seg) => {
let adjusted_baseline = baseline_y - seg.baseline_offset;
let segment_spaces = if justify_extra > 0.0 {
seg.text.chars().filter(|c| *c == ' ').count()
} else {
0
};
let segment_extra = segment_spaces as f64 * justify_extra;
let effective_width = seg.width + segment_extra;
if let Some(hl_color) = seg.highlight {
elements.push(PositionedElement::FilledRect {
rect: Rect {
x,
y: geometry.margin_top + y,
width: effective_width,
height: line.height,
},
color: hl_color,
});
}
let advances = if justify_extra > 0.0 && segment_spaces > 0 {
distribute_justify_advances(&seg.text, &seg.advances, justify_extra)
} else {
seg.advances.clone()
};
elements.push(PositionedElement::Text(GlyphRun {
origin: Point {
x,
y: adjusted_baseline,
},
font_id: seg.font_id,
font_size: seg.font_size,
glyph_ids: seg.glyph_ids.clone(),
advances,
text: seg.text.clone(),
source: match para.source_node() {
Some(source_node) => seg.source.and_then(|mut source| {
source.node = source_node?;
Some(source)
}),
None => seg.source,
},
color: seg.color,
bold: seg.bold,
italic: seg.italic,
field_kind: seg.field_kind,
note: seg.note,
}));
if let Some(ul_style) = seg.underline {
let ul_y = adjusted_baseline + seg.descent * 0.3;
let ul_thickness = match ul_style {
Underline::Thick => seg.font_size / 12.0,
Underline::Double => seg.font_size / 24.0,
_ => seg.font_size / 18.0,
};
elements.push(PositionedElement::Line {
start: Point { x, y: ul_y },
end: Point {
x: x + effective_width,
y: ul_y,
},
width: ul_thickness,
color: seg.color,
dash_pattern: None,
});
if ul_style == Underline::Double {
let ul_y2 = ul_y + ul_thickness * 2.5;
elements.push(PositionedElement::Line {
start: Point { x, y: ul_y2 },
end: Point {
x: x + effective_width,
y: ul_y2,
},
width: ul_thickness,
color: seg.color,
dash_pattern: None,
});
}
}
if seg.strike {
let strike_y = adjusted_baseline - seg.ascent * 0.3;
let strike_thickness = seg.font_size / 24.0;
elements.push(PositionedElement::Line {
start: Point { x, y: strike_y },
end: Point {
x: x + effective_width,
y: strike_y,
},
width: strike_thickness,
color: seg.color,
dash_pattern: None,
});
}
if seg.dstrike {
let strike_y = adjusted_baseline - seg.ascent * 0.3;
let strike_thickness = seg.font_size / 24.0;
let gap = strike_thickness * 2.0;
elements.push(PositionedElement::Line {
start: Point {
x,
y: strike_y - gap / 2.0,
},
end: Point {
x: x + effective_width,
y: strike_y - gap / 2.0,
},
width: strike_thickness,
color: seg.color,
dash_pattern: None,
});
elements.push(PositionedElement::Line {
start: Point {
x,
y: strike_y + gap / 2.0,
},
end: Point {
x: x + effective_width,
y: strike_y + gap / 2.0,
},
width: strike_thickness,
color: seg.color,
dash_pattern: None,
});
}
if let Some(ref url) = seg.hyperlink_url {
elements.push(PositionedElement::LinkAnnotation {
rect: Rect {
x,
y: geometry.margin_top + y,
width: effective_width,
height: line.height,
},
url: url.clone(),
});
}
_accumulated_extra += segment_extra;
x += effective_width;
}
LineItem::Tab { width, leader } => {
if let Some(leader_seg) = leader {
let baseline_y = geometry.margin_top + y + line.ascent;
elements.push(PositionedElement::Text(GlyphRun {
origin: Point { x, y: baseline_y },
font_id: leader_seg.font_id,
font_size: leader_seg.font_size,
glyph_ids: leader_seg.glyph_ids.clone(),
advances: leader_seg.advances.clone(),
text: leader_seg.text.clone(),
source: None,
color: leader_seg.color,
bold: leader_seg.bold,
italic: leader_seg.italic,
field_kind: None,
note: None,
}));
}
x += width;
}
LineItem::Image {
width,
height,
media_id,
} => {
let image = media.get(media_id);
let image = PositionedElement::Image {
rect: Rect {
x,
y: geometry.margin_top + y,
width: *width,
height: *height,
},
data: image.map_or_else(Vec::new, |image| image.data.clone()),
content_type: image
.map_or_else(String::new, |image| image.content_type.clone()),
media_id: *media_id,
};
elements.push(image);
x += width;
}
LineItem::Group { width, group, .. } => {
let mut positioned = group.clone();
positioned.transform = positioned.transform.then(oxml_layout::Transform {
e: x,
f: geometry.margin_top + y,
..oxml_layout::Transform::IDENTITY
});
let group = PositionedElement::Group(positioned);
elements.push(group);
x += width;
}
LineItem::Figure {
item, structure_id, ..
} => {
let figure = match item.as_ref() {
LineItem::Image {
width,
height,
media_id,
} => {
let image = media.get(media_id);
PositionedElement::Image {
rect: Rect {
x,
y: geometry.margin_top + y,
width: *width,
height: *height,
},
data: image.map_or_else(Vec::new, |image| image.data.clone()),
content_type: image
.map_or_else(String::new, |image| image.content_type.clone()),
media_id: *media_id,
}
}
LineItem::Group { group, .. } => {
let mut positioned = group.clone();
positioned.transform =
positioned.transform.then(oxml_layout::Transform {
e: x,
f: geometry.margin_top + y,
..oxml_layout::Transform::IDENTITY
});
PositionedElement::Group(positioned)
}
_ => {
x += item.width();
continue;
}
};
elements.push(PositionedElement::MarkedContent {
structure: *structure_id,
children: vec![figure],
});
x += item.width();
}
_ => x += item.width(),
}
}
y += line.height;
}
if let Some(structure_id) = para.structure_id() {
let produced = elements.split_off(first_element);
elements.extend(produced.into_iter().map(|element| match &element {
PositionedElement::Text(run) if !(run.text.is_empty() && run.glyph_ids.is_empty()) => {
PositionedElement::MarkedContent {
structure: Some(structure_id),
children: vec![element],
}
}
PositionedElement::Image { .. } | PositionedElement::Group(_) => {
PositionedElement::MarkedContent {
structure: None,
children: vec![element],
}
}
PositionedElement::MarkedContent { .. } => element,
PositionedElement::LinkAnnotation { .. } => element,
_ => PositionedElement::MarkedContent {
structure: None,
children: vec![element],
},
}));
}
}
fn render_change_bar(
para: &ParagraphBlock,
start_y: f64,
height: f64,
geometry: &PageGeometry,
page_number: usize,
elements: &mut Vec<PositionedElement>,
) {
if !para.has_visible_revision || !height.is_finite() || height <= 0.0 {
return;
}
render_change_bar_at(
geometry.margin_top + start_y,
height,
geometry,
page_number,
elements,
);
}
fn render_change_bar_at(
start_y: f64,
height: f64,
geometry: &PageGeometry,
page_number: usize,
elements: &mut Vec<PositionedElement>,
) {
if !height.is_finite() || height <= 0.0 {
return;
}
let x = if page_number.is_multiple_of(2) {
geometry.margin_left / 2.0
} else {
geometry.page_width - geometry.margin_right / 2.0
};
if !x.is_finite() || !start_y.is_finite() || !(start_y + height).is_finite() {
return;
}
elements.push(PositionedElement::Line {
start: Point { x, y: start_y },
end: Point {
x,
y: start_y + height,
},
width: 1.5,
color: Color::BLACK,
dash_pattern: None,
});
}
fn render_hf_blocks(
blocks: &[ParagraphBlock],
geometry: &PageGeometry,
start_y: f64,
page_number: usize,
elements: &mut Vec<PositionedElement>,
media: &HashMap<MediaId, ImageData>,
) {
let mut y = start_y - geometry.margin_top; for para in blocks {
render_paragraph_lines(
¶.lines,
ParagraphView {
block: para,
semantics: None,
},
geometry,
y,
elements,
media,
);
render_change_bar(
para,
y,
para.content_height(),
geometry,
page_number,
elements,
);
y += para.content_height();
}
}
fn render_table_row(
row: &crate::table::TableRow,
row_semantics: Option<&crate::block::RowSemantics>,
_col_widths: &[f64],
table_x: f64,
row_y: f64,
geometry: &PageGeometry,
page_number: usize,
table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
elements: &mut Vec<PositionedElement>,
behind_elements: &mut Vec<PositionedElement>,
media: &HashMap<MediaId, ImageData>,
) {
let mut cell_x = table_x;
let num_cells = row.cells.len();
for (cell_idx, cell) in row.cells.iter().enumerate() {
let cell_semantics = row_semantics.and_then(|row| row.cells.get(cell_idx));
if cell.is_vmerge_continue {
cell_x += cell.width;
continue;
}
let paint_height = cell.merged_height;
if let Some(ref shading) = cell.shading {
elements.push(PositionedElement::FilledRect {
rect: Rect {
x: cell_x,
y: row_y,
width: cell.width,
height: paint_height,
},
color: *shading,
});
}
render_cell_borders(
cell_x,
row_y,
cell.width,
paint_height,
&cell.borders,
table_borders,
cell_idx,
num_cells,
cell.is_first_row,
cell.is_last_row,
elements,
);
let content_element_start = elements.len();
let behind_element_start = behind_elements.len();
let content_height = cell
.blocks
.iter()
.map(crate::table::CellBlock::total_height)
.sum::<f64>();
let v_offset = match cell.v_align {
Some(rdocx_oxml::table::ST_VerticalJc::Center) => {
((paint_height - cell.margin_top - content_height) / 2.0).max(0.0)
}
Some(rdocx_oxml::table::ST_VerticalJc::Bottom) => {
(paint_height - cell.margin_top - content_height).max(0.0)
}
_ => 0.0,
};
let mut content_y = row_y - geometry.margin_top + cell.margin_top + v_offset;
for (block_index, block) in cell.blocks.iter().enumerate() {
let block_semantics = cell_semantics.and_then(|cell| cell.blocks.get(block_index));
match block {
crate::table::CellBlock::Paragraph(paragraph) => {
let semantics = match block_semantics {
Some(CellBlockSemantics::Paragraph(semantics)) => Some(semantics),
_ => None,
};
let cell_geometry = PageGeometry {
margin_left: cell_x + cell.margin_left,
margin_right: 0.0,
page_width: cell_x + cell.width - cell.margin_right,
..*geometry
};
render_paragraph_lines(
¶graph.lines,
ParagraphView {
block: paragraph,
semantics,
},
&cell_geometry,
content_y,
elements,
media,
);
place_cell_anchored(
¶graph.anchored,
geometry,
&cell_geometry,
content_y,
paragraph.indent_left,
elements,
behind_elements,
media,
);
render_change_bar(
paragraph,
content_y,
paragraph.content_height(),
geometry,
page_number,
elements,
);
}
crate::table::CellBlock::Table(table) => {
let semantics = match block_semantics {
Some(CellBlockSemantics::Table(semantics)) => Some(semantics),
_ => None,
};
let nested_x = cell_x + cell.margin_left + table.table_indent;
let mut nested_y = geometry.margin_top + content_y;
for (nested_row_index, nested_row) in table.rows.iter().enumerate() {
render_table_row(
nested_row,
semantics.and_then(|semantics| semantics.rows.get(nested_row_index)),
&table.col_widths,
nested_x,
nested_y,
geometry,
page_number,
table.borders.as_ref(),
elements,
behind_elements,
media,
);
nested_y += nested_row.height;
}
}
}
content_y += block.total_height();
}
if cell.clip_content {
let clip = Some(Path::rect(Rect {
x: cell_x,
y: row_y,
width: cell.width,
height: paint_height,
}));
let children = elements.split_off(content_element_start);
elements.push(PositionedElement::Group(GroupElement {
transform: Transform::IDENTITY,
clip: clip.clone(),
opacity: 1.0,
effects: Vec::new(),
children,
}));
let children = behind_elements.split_off(behind_element_start);
if !children.is_empty() {
behind_elements.push(PositionedElement::Group(GroupElement {
transform: Transform::IDENTITY,
clip,
opacity: 1.0,
effects: Vec::new(),
children,
}));
}
}
cell_x += cell.width;
}
}
fn place_cell_anchored(
anchors: &[AnchoredDrawing],
page_geometry: &PageGeometry,
cell_geometry: &PageGeometry,
paragraph_top: f64,
paragraph_indent: f64,
elements: &mut Vec<PositionedElement>,
behind_elements: &mut Vec<PositionedElement>,
media: &HashMap<MediaId, ImageData>,
) {
for anchor in anchors {
let horizontal_geometry = match anchor.rel_h {
ST_RelativeFromH::Column | ST_RelativeFromH::Character => cell_geometry,
_ => page_geometry,
};
let x = resolve_anchor_h(
anchor.rel_h,
anchor.off_h,
anchor.align_h,
anchor.width,
horizontal_geometry,
paragraph_indent,
);
let y = resolve_anchor_v(
anchor.rel_v,
anchor.off_v,
anchor.align_v,
anchor.height,
page_geometry,
paragraph_top,
);
let rect = Rect {
x,
y,
width: anchor.width,
height: anchor.height,
};
let mut produced = anchored_elements(anchor, rect, page_geometry, media);
if anchor.behind_doc {
behind_elements.append(&mut produced);
} else {
elements.append(&mut produced);
}
}
}
fn render_cell_borders(
x: f64,
y: f64,
w: f64,
h: f64,
cell_borders: &Option<rdocx_oxml::table::CT_TblBorders>,
table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
cell_idx: usize,
num_cells: usize,
is_first_row: bool,
is_last_row: bool,
elements: &mut Vec<PositionedElement>,
) {
let get_edge = |cell_edge: Option<&rdocx_oxml::borders::CT_BorderEdge>,
table_edge: Option<&rdocx_oxml::borders::CT_BorderEdge>,
outer_edge: bool|
-> Option<BorderEdge> {
let edge = match cell_edge {
Some(edge) if edge.val == ST_Border::None && outer_edge => table_edge?,
Some(edge) => edge,
None => table_edge?,
};
if edge.val == ST_Border::None {
return None;
}
let thickness = edge.sz.unwrap_or(4) as f64 / 8.0; let color = edge
.color
.as_ref()
.filter(|c| c.as_str() != "auto")
.map(|c| Color::from_hex(c))
.unwrap_or(Color::BLACK);
let dash = border_dash_pattern(edge.val, thickness);
Some((thickness, color, dash))
};
let table_top = table_borders.and_then(|b| {
if is_first_row {
b.top.as_ref()
} else {
b.inside_h.as_ref()
}
});
let cell_top = cell_borders.as_ref().and_then(|b| b.top.as_ref());
if let Some((thickness, color, dash_pattern)) = get_edge(cell_top, table_top, is_first_row) {
elements.push(PositionedElement::Line {
start: Point { x, y },
end: Point { x: x + w, y },
width: thickness,
color,
dash_pattern,
});
}
let table_bottom = table_borders.and_then(|b| {
if is_last_row {
b.bottom.as_ref()
} else {
b.inside_h.as_ref()
}
});
let cell_bottom = cell_borders.as_ref().and_then(|b| b.bottom.as_ref());
if let Some((thickness, color, dash_pattern)) = get_edge(cell_bottom, table_bottom, is_last_row)
{
elements.push(PositionedElement::Line {
start: Point { x, y: y + h },
end: Point { x: x + w, y: y + h },
width: thickness,
color,
dash_pattern,
});
}
let table_left = table_borders.and_then(|b| {
if cell_idx == 0 {
b.left.as_ref()
} else {
b.inside_v.as_ref()
}
});
let cell_left = cell_borders.as_ref().and_then(|b| b.left.as_ref());
if let Some((thickness, color, dash_pattern)) = get_edge(cell_left, table_left, cell_idx == 0) {
elements.push(PositionedElement::Line {
start: Point { x, y },
end: Point { x, y: y + h },
width: thickness,
color,
dash_pattern,
});
}
let table_right = table_borders.and_then(|b| {
if cell_idx == num_cells - 1 {
b.right.as_ref()
} else {
b.inside_v.as_ref()
}
});
let cell_right = cell_borders.as_ref().and_then(|b| b.right.as_ref());
if let Some((thickness, color, dash_pattern)) =
get_edge(cell_right, table_right, cell_idx == num_cells - 1)
{
elements.push(PositionedElement::Line {
start: Point { x: x + w, y },
end: Point { x: x + w, y: y + h },
width: thickness,
color,
dash_pattern,
});
}
}
fn render_border_edges(
borders: &rdocx_oxml::borders::CT_PBdr,
x: f64,
y: f64,
w: f64,
h: f64,
elements: &mut Vec<PositionedElement>,
) {
let render_edge = |edge: &rdocx_oxml::borders::CT_BorderEdge,
start: Point,
end: Point,
elements: &mut Vec<PositionedElement>| {
if edge.val == ST_Border::None {
return;
}
let thickness = edge.sz.unwrap_or(4) as f64 / 8.0; let color = edge
.color
.as_ref()
.filter(|c| c.as_str() != "auto")
.map(|c| Color::from_hex(c))
.unwrap_or(Color::BLACK);
let dash_pattern = border_dash_pattern(edge.val, thickness);
if edge.val == ST_Border::Double {
let gap = thickness * 2.0;
let dx = end.x - start.x;
let dy = end.y - start.y;
let len = (dx * dx + dy * dy).sqrt();
let (nx, ny) = if len > 0.0 {
(-dy / len, dx / len)
} else {
(0.0, 1.0)
};
let offset = gap / 2.0;
elements.push(PositionedElement::Line {
start: Point {
x: start.x + nx * offset,
y: start.y + ny * offset,
},
end: Point {
x: end.x + nx * offset,
y: end.y + ny * offset,
},
width: thickness,
color,
dash_pattern: None,
});
elements.push(PositionedElement::Line {
start: Point {
x: start.x - nx * offset,
y: start.y - ny * offset,
},
end: Point {
x: end.x - nx * offset,
y: end.y - ny * offset,
},
width: thickness,
color,
dash_pattern: None,
});
} else {
elements.push(PositionedElement::Line {
start,
end,
width: thickness,
color,
dash_pattern,
});
}
};
if let Some(ref edge) = borders.top {
let space = edge.space.unwrap_or(0) as f64;
render_edge(
edge,
Point { x, y: y - space },
Point {
x: x + w,
y: y - space,
},
elements,
);
}
if let Some(ref edge) = borders.bottom {
let space = edge.space.unwrap_or(0) as f64;
render_edge(
edge,
Point {
x,
y: y + h + space,
},
Point {
x: x + w,
y: y + h + space,
},
elements,
);
}
if let Some(ref edge) = borders.left {
let space = edge.space.unwrap_or(0) as f64;
render_edge(
edge,
Point { x: x - space, y },
Point {
x: x - space,
y: y + h,
},
elements,
);
}
if let Some(ref edge) = borders.right {
let space = edge.space.unwrap_or(0) as f64;
render_edge(
edge,
Point {
x: x + w + space,
y,
},
Point {
x: x + w + space,
y: y + h,
},
elements,
);
}
}
fn border_dash_pattern(style: ST_Border, thickness: f64) -> Option<(f64, f64)> {
match style {
ST_Border::Dashed => Some((3.0 * thickness, 2.0 * thickness)),
ST_Border::Dotted => Some((thickness, thickness)),
ST_Border::DotDash | ST_Border::DotDotDash => Some((3.0 * thickness, thickness)),
_ => None,
}
}
fn count_word_gaps(items: &[LineItem]) -> usize {
let mut count = 0;
for item in items {
match item {
LineItem::Text(seg) | LineItem::Marker(seg) => {
count += seg.text.chars().filter(|c| *c == ' ').count();
}
LineItem::Tab { .. } => {
count += 1;
}
_ => {}
}
}
count
}
fn distribute_justify_advances(text: &str, advances: &[f64], extra_per_gap: f64) -> Vec<f64> {
let chars: Vec<char> = text.chars().collect();
let mut result = advances.to_vec();
if chars.len() == result.len() {
for (i, &ch) in chars.iter().enumerate() {
if ch == ' ' {
result[i] += extra_per_gap;
}
}
} else {
let total_extra = extra_per_gap * text.chars().filter(|c| *c == ' ').count() as f64;
if !result.is_empty() {
let per_glyph = total_extra / result.len() as f64;
for a in &mut result {
*a += per_glyph;
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::ParagraphBlock;
use oxml_layout::LayoutLine;
fn empty_media() -> MediaRegistry {
MediaRegistry::new(&HashMap::new())
}
fn make_line(height: f64) -> LayoutLine {
LayoutLine {
items: vec![],
width: 100.0,
ascent: height * 0.77,
descent: height * 0.23,
line_gap: 0.0,
height,
indent_left: 0.0,
available_width: 468.0,
is_last: true,
}
}
fn make_para(line_count: usize, line_height: f64) -> ParagraphBlock {
let mut lines = Vec::new();
for _ in 0..line_count {
lines.push(make_line(line_height));
}
ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines,
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
}
}
#[test]
fn descriptionless_inline_drawings_are_paragraph_artifacts() {
let mut paragraph = make_para(1, 14.0);
paragraph.structure_id = oxml_layout::StructureId::new(1);
paragraph.lines[0].items = vec![
LineItem::Image {
width: 10.0,
height: 10.0,
media_id: MediaId(1),
},
LineItem::Group {
width: 10.0,
height: 10.0,
group: oxml_layout::GroupElement {
transform: oxml_layout::Transform::IDENTITY,
clip: None,
opacity: 1.0,
effects: Vec::new(),
children: vec![PositionedElement::FilledRect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
color: Color::BLACK,
}],
},
},
];
let mut elements = Vec::new();
let media = HashMap::new();
render_paragraph_lines(
¶graph.lines,
ParagraphView {
block: ¶graph,
semantics: None,
},
&PageGeometry::default(),
0.0,
&mut elements,
&media,
);
assert_eq!(elements.len(), 2);
assert!(elements.iter().all(|element| matches!(
element,
PositionedElement::MarkedContent {
structure: None,
children,
} if matches!(
children.as_slice(),
[PositionedElement::Image { .. }] | [PositionedElement::Group(_)]
)
)));
}
#[test]
fn single_page_layout() {
let fm = FontManager::new();
let blocks = vec![LayoutBlock::Paragraph(make_para(3, 14.0))];
let geom = PageGeometry::default();
let (pages, _outlines) = paginate(
&blocks,
geom,
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
assert_eq!(pages.len(), 1);
assert_eq!(pages[0].page_number, 1);
}
#[test]
fn multi_page_overflow() {
let fm = FontManager::new();
let blocks = vec![LayoutBlock::Paragraph(make_para(100, 14.0))];
let geom = PageGeometry::default();
let (pages, _outlines) = paginate(
&blocks,
geom,
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
assert!(pages.len() >= 2);
}
#[test]
fn forced_page_break() {
let fm = FontManager::new();
let mut para2 = make_para(3, 14.0);
para2.page_break_before = true;
let blocks = vec![
LayoutBlock::Paragraph(make_para(3, 14.0)),
LayoutBlock::Paragraph(para2),
];
let geom = PageGeometry::default();
let (pages, _outlines) = paginate(
&blocks,
geom,
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
assert_eq!(pages.len(), 2);
}
#[test]
fn page_dimensions() {
let fm = FontManager::new();
let blocks = vec![LayoutBlock::Paragraph(make_para(1, 14.0))];
let geom = PageGeometry::default();
let (pages, _outlines) = paginate(
&blocks,
geom,
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
assert!((pages[0].width - 612.0).abs() < 0.01);
assert!((pages[0].height - 792.0).abs() < 0.01);
}
fn make_text_line(height: f64, underline: Option<Underline>, strike: bool) -> LayoutLine {
use oxml_layout::TextSegment;
let seg = TextSegment {
text: "Hello".to_string(),
source: None,
font_id: oxml_layout::FontId(0),
font_size: 12.0,
glyph_ids: vec![1, 2, 3],
advances: vec![6.0, 6.0, 6.0],
width: 40.0,
ascent: height * 0.77,
descent: height * 0.23,
line_gap: 0.0,
color: Color::BLACK,
bold: false,
italic: false,
underline,
strike,
dstrike: false,
highlight: None,
baseline_offset: 0.0,
hyperlink_url: None,
field_kind: None,
note: None,
};
LayoutLine {
items: vec![LineItem::Text(seg)],
width: 40.0,
ascent: height * 0.77,
descent: height * 0.23,
line_gap: 0.0,
height,
indent_left: 0.0,
available_width: 468.0,
is_last: true,
}
}
#[test]
fn underline_renders_line_element() {
let fm = FontManager::new();
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![make_text_line(14.0, Some(Underline::Single), false)],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let lines: Vec<_> = pages[0]
.elements
.iter()
.filter(|e| matches!(e, PositionedElement::Line { .. }))
.collect();
assert_eq!(lines.len(), 1, "expected 1 underline line");
}
#[test]
fn strikethrough_renders_line_element() {
let fm = FontManager::new();
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![make_text_line(14.0, None, true)],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let lines: Vec<_> = pages[0]
.elements
.iter()
.filter(|e| matches!(e, PositionedElement::Line { .. }))
.collect();
assert_eq!(lines.len(), 1, "expected 1 strikethrough line");
}
#[test]
fn highlight_renders_filled_rect() {
use oxml_layout::TextSegment;
let fm = FontManager::new();
let seg = TextSegment {
text: "Hi".to_string(),
source: None,
font_id: oxml_layout::FontId(0),
font_size: 12.0,
glyph_ids: vec![1],
advances: vec![10.0],
width: 20.0,
ascent: 10.0,
descent: 3.0,
line_gap: 0.0,
color: Color::BLACK,
bold: false,
italic: false,
underline: None,
strike: false,
dstrike: false,
highlight: Some(Color {
r: 1.0,
g: 1.0,
b: 0.0,
a: 1.0,
}),
baseline_offset: 0.0,
hyperlink_url: None,
field_kind: None,
note: None,
};
let line = LayoutLine {
items: vec![LineItem::Text(seg)],
width: 20.0,
ascent: 10.0,
descent: 3.0,
line_gap: 0.0,
height: 13.0,
indent_left: 0.0,
available_width: 468.0,
is_last: true,
};
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![line],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let rects: Vec<_> = pages[0]
.elements
.iter()
.filter(|e| matches!(e, PositionedElement::FilledRect { .. }))
.collect();
assert_eq!(rects.len(), 1, "expected 1 highlight rect");
}
#[test]
fn paragraph_borders_render_lines() {
use rdocx_oxml::borders::{CT_BorderEdge, CT_PBdr};
let fm = FontManager::new();
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![make_line(14.0)],
space_before: 0.0,
space_after: 0.0,
borders: Some(CT_PBdr {
top: Some(CT_BorderEdge {
val: ST_Border::Single,
sz: Some(4),
space: Some(1),
color: Some("000000".to_string()),
}),
bottom: Some(CT_BorderEdge {
val: ST_Border::Single,
sz: Some(4),
space: Some(1),
color: Some("000000".to_string()),
}),
..Default::default()
}),
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let lines: Vec<_> = pages[0]
.elements
.iter()
.filter(|e| matches!(e, PositionedElement::Line { .. }))
.collect();
assert_eq!(lines.len(), 2, "expected 2 border lines (top + bottom)");
}
#[test]
fn paragraph_shading_renders_filled_rect() {
let fm = FontManager::new();
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![make_line(14.0)],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: Some(Color {
r: 1.0,
g: 1.0,
b: 0.0,
a: 1.0,
}),
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let rects: Vec<_> = pages[0]
.elements
.iter()
.filter(|e| matches!(e, PositionedElement::FilledRect { .. }))
.collect();
assert_eq!(rects.len(), 1, "expected 1 paragraph shading rect");
}
#[test]
fn double_underline_renders_two_lines() {
let fm = FontManager::new();
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![make_text_line(14.0, Some(Underline::Double), false)],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let lines: Vec<_> = pages[0]
.elements
.iter()
.filter(|e| matches!(e, PositionedElement::Line { .. }))
.collect();
assert_eq!(lines.len(), 2, "expected 2 lines for double underline");
}
fn make_justified_line(text: &str, seg_width: f64, is_last: bool) -> LayoutLine {
use oxml_layout::TextSegment;
let seg = TextSegment {
text: text.to_string(),
source: None,
font_id: oxml_layout::FontId(0),
font_size: 12.0,
glyph_ids: vec![1; text.len()],
advances: vec![seg_width / text.len() as f64; text.len()],
width: seg_width,
ascent: 10.0,
descent: 3.0,
line_gap: 0.0,
color: Color::BLACK,
bold: false,
italic: false,
underline: None,
strike: false,
dstrike: false,
highlight: None,
baseline_offset: 0.0,
hyperlink_url: None,
field_kind: None,
note: None,
};
LayoutLine {
items: vec![LineItem::Text(seg)],
width: seg_width,
ascent: 10.0,
descent: 3.0,
line_gap: 0.0,
height: 13.0,
indent_left: 0.0,
available_width: 468.0,
is_last,
}
}
#[test]
fn hyperlink_emits_link_annotation() {
use oxml_layout::TextSegment;
let fm = FontManager::new();
let seg = TextSegment {
text: "Click me".to_string(),
source: None,
font_id: oxml_layout::FontId(0),
font_size: 12.0,
glyph_ids: vec![1, 2, 3],
advances: vec![8.0, 8.0, 8.0],
width: 60.0,
ascent: 10.0,
descent: 3.0,
line_gap: 0.0,
color: Color::BLACK,
bold: false,
italic: false,
underline: None,
strike: false,
dstrike: false,
highlight: None,
baseline_offset: 0.0,
hyperlink_url: Some("https://example.com".to_string()),
field_kind: None,
note: None,
};
let line = LayoutLine {
items: vec![LineItem::Text(seg)],
width: 60.0,
ascent: 10.0,
descent: 3.0,
line_gap: 0.0,
height: 13.0,
indent_left: 0.0,
available_width: 468.0,
is_last: true,
};
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![line],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let annotations: Vec<_> = pages[0]
.elements
.iter()
.filter(|e| matches!(e, PositionedElement::LinkAnnotation { .. }))
.collect();
assert_eq!(annotations.len(), 1, "expected 1 link annotation");
if let PositionedElement::LinkAnnotation { url, .. } = annotations[0] {
assert_eq!(url, "https://example.com");
}
}
#[test]
fn justified_text_fills_line_width() {
let fm = FontManager::new();
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![
make_justified_line("Hello World", 200.0, false),
make_justified_line("End.", 40.0, true),
],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: Some(Align::Justify),
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let first_text = pages[0].elements.iter().find_map(|e| {
if let PositionedElement::Text(run) = e {
Some(run)
} else {
None
}
});
assert!(first_text.is_some());
let run = first_text.unwrap();
let total_advance: f64 = run.advances.iter().sum();
assert!(
total_advance > 200.0,
"justified text should be wider than original: {total_advance}"
);
}
#[test]
fn justified_last_line_stays_left_aligned() {
let fm = FontManager::new();
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![
make_justified_line("Hello World Test", 200.0, false),
make_justified_line("End.", 40.0, true),
],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: Some(Align::Justify),
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let text_runs: Vec<_> = pages[0]
.elements
.iter()
.filter_map(|e| {
if let PositionedElement::Text(run) = e {
Some(run)
} else {
None
}
})
.collect();
assert!(text_runs.len() >= 2);
let last_advance: f64 = text_runs[1].advances.iter().sum();
assert!(
(last_advance - 40.0).abs() < 0.1,
"last line should stay at original width: {last_advance}"
);
}
#[test]
fn justified_single_word_not_stretched() {
let fm = FontManager::new();
let para = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![
make_justified_line("Superlongword", 100.0, false),
make_justified_line("End.", 40.0, true),
],
space_before: 0.0,
space_after: 0.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: Some(Align::Justify),
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
let blocks = vec![LayoutBlock::Paragraph(para)];
let (pages, _outlines) = paginate(
&blocks,
PageGeometry::default(),
None,
false,
&fm,
&empty_media(),
&NoteRegistry::default(),
);
let first_text = pages[0].elements.iter().find_map(|e| {
if let PositionedElement::Text(run) = e {
Some(run)
} else {
None
}
});
assert!(first_text.is_some());
let run = first_text.unwrap();
let total_advance: f64 = run.advances.iter().sum();
assert!(
(total_advance - 100.0).abs() < 0.1,
"single word should not be stretched: {total_advance}"
);
}
#[test]
fn anchor_offsets_resolve_against_their_frame() {
let g = PageGeometry::default(); let para_top = 100.0;
let off = 10.0;
assert_eq!(
resolve_anchor_h(ST_RelativeFromH::Page, off, None, 0.0, &g, 0.0),
10.0
);
assert_eq!(
resolve_anchor_h(ST_RelativeFromH::LeftMargin, off, None, 0.0, &g, 0.0),
10.0
);
assert_eq!(
resolve_anchor_h(ST_RelativeFromH::Margin, off, None, 0.0, &g, 0.0),
82.0,
"margin-relative starts at the left margin"
);
assert_eq!(
resolve_anchor_h(ST_RelativeFromH::Column, off, None, 0.0, &g, 0.0),
82.0,
"column-relative starts at the text area"
);
assert_eq!(
resolve_anchor_h(ST_RelativeFromH::RightMargin, off, None, 0.0, &g, 0.0),
550.0,
"right-margin-relative starts at the right margin edge"
);
assert_eq!(
resolve_anchor_h(ST_RelativeFromH::Character, off, None, 0.0, &g, 36.0),
118.0,
"character-relative includes the paragraph indent"
);
assert_eq!(
resolve_anchor_v(ST_RelativeFromV::Page, off, None, 0.0, &g, para_top),
10.0
);
assert_eq!(
resolve_anchor_v(ST_RelativeFromV::TopMargin, off, None, 0.0, &g, para_top),
10.0
);
assert_eq!(
resolve_anchor_v(ST_RelativeFromV::Margin, off, None, 0.0, &g, para_top),
82.0
);
assert_eq!(
resolve_anchor_v(ST_RelativeFromV::Paragraph, off, None, 0.0, &g, para_top),
182.0,
"paragraph-relative follows the paragraph down the page"
);
assert_eq!(
resolve_anchor_v(ST_RelativeFromV::Line, off, None, 0.0, &g, para_top),
182.0
);
assert_eq!(
resolve_anchor_v(ST_RelativeFromV::BottomMargin, off, None, 0.0, &g, para_top),
730.0
);
}
#[test]
fn cell_anchors_use_cell_coordinates_and_page_behind_order() {
let page = PageGeometry::default();
let cell = PageGeometry {
margin_left: 200.0,
margin_right: 0.0,
page_width: 300.0,
..page
};
let anchor = |behind_doc| AnchoredDrawing {
behind_doc,
rel_h: ST_RelativeFromH::Column,
off_h: 5.0,
rel_v: ST_RelativeFromV::Paragraph,
off_v: 4.0,
width: 20.0,
height: 10.0,
wrap: WrapType::None,
dist_top: 0.0,
dist_bottom: 0.0,
dist_left: 0.0,
dist_right: 0.0,
align_h: None,
align_v: None,
content: AnchoredContent::Shape {
preset: ShapePreset::Rect,
fill: Some(Color::from_hex("CC0000")),
text: Vec::new(),
},
alternate_text: Some("cell stamp".to_owned()),
structure_id: None,
};
let mut foreground = Vec::new();
let mut behind = Vec::new();
place_cell_anchored(
&[anchor(false)],
&page,
&cell,
30.0,
0.0,
&mut foreground,
&mut behind,
&HashMap::new(),
);
let PositionedElement::MarkedContent { children, .. } = &foreground[0] else {
panic!("foreground anchor remains marked content");
};
let PositionedElement::FilledRect { rect, .. } = children[0] else {
panic!("foreground stamp rectangle");
};
assert_eq!(rect.x, 205.0);
assert_eq!(rect.y, page.margin_top + 34.0);
assert!(behind.is_empty());
let mut character_anchor = anchor(false);
character_anchor.rel_h = ST_RelativeFromH::Character;
let mut character_elements = Vec::new();
place_cell_anchored(
&[character_anchor],
&page,
&cell,
30.0,
12.0,
&mut character_elements,
&mut behind,
&HashMap::new(),
);
let PositionedElement::MarkedContent { children, .. } = &character_elements[0] else {
panic!("character anchor remains marked content");
};
let PositionedElement::FilledRect { rect, .. } = children[0] else {
panic!("character stamp rectangle");
};
assert_eq!(rect.x, 217.0, "character origin includes paragraph indent");
place_cell_anchored(
&[anchor(true)],
&page,
&cell,
30.0,
0.0,
&mut foreground,
&mut behind,
&HashMap::new(),
);
assert_eq!(foreground.len(), 1);
assert_eq!(behind.len(), 1);
let mut page_order = behind;
page_order.extend(foreground);
let PositionedElement::MarkedContent { children, .. } = &page_order[0] else {
panic!("behind anchor remains marked content");
};
assert!(matches!(children[0], PositionedElement::FilledRect { .. }));
}
#[test]
fn exact_height_cell_content_is_group_clipped_to_the_row() {
let mut paragraph = make_para(2, 12.0);
paragraph.lines = vec![
make_text_line(12.0, None, false),
make_text_line(12.0, None, false),
];
let row = crate::table::TableRow {
structure_id: None,
cells: vec![crate::table::TableCell {
structure_id: None,
blocks: vec![crate::table::CellBlock::Paragraph(paragraph)],
width: 40.0,
height: 10.0,
grid_span: 1,
is_vmerge_continue: false,
starts_vmerge: false,
merged_height: 10.0,
merge_with_below: false,
clip_content: true,
col_index: 0,
borders: None,
shading: None,
margin_left: 0.0,
margin_right: 0.0,
margin_top: 0.0,
margin_bottom: 0.0,
is_first_row: true,
is_last_row: true,
v_align: None,
}],
height: 10.0,
is_header: false,
};
let mut elements = Vec::new();
render_table_row(
&row,
None,
&[40.0],
10.0,
20.0,
&PageGeometry::default(),
0,
None,
&mut elements,
&mut Vec::new(),
&HashMap::new(),
);
let [PositionedElement::Group(group)] = elements.as_slice() else {
panic!("exact cell content is one clipped group: {elements:?}");
};
assert!(group.clip.is_some());
assert_eq!(
group.children.len(),
2,
"both overflow lines remain in the clip"
);
}
#[test]
fn outer_nil_border_matches_word_without_changing_interior_nil() {
use rdocx_oxml::borders::CT_BorderEdge;
use rdocx_oxml::table::CT_TblBorders;
let mut visible = CT_BorderEdge::new(ST_Border::Single);
visible.sz = Some(8);
visible.color = Some("112233".to_owned());
let nil = CT_BorderEdge::new(ST_Border::None);
let table = CT_TblBorders {
top: Some(visible.clone()),
bottom: Some(visible.clone()),
left: Some(visible.clone()),
right: Some(visible.clone()),
inside_h: Some(visible.clone()),
inside_v: Some(visible),
};
let cell = Some(CT_TblBorders {
top: Some(nil.clone()),
bottom: Some(nil.clone()),
left: Some(nil.clone()),
right: Some(nil),
inside_h: None,
inside_v: None,
});
let mut outer = Vec::new();
render_cell_borders(
10.0,
20.0,
30.0,
40.0,
&cell,
Some(&table),
0,
1,
true,
true,
&mut outer,
);
assert_eq!(outer.len(), 4, "four outer edges fall back to the table");
let mut interior = Vec::new();
render_cell_borders(
10.0,
20.0,
30.0,
40.0,
&cell,
Some(&table),
1,
3,
false,
false,
&mut interior,
);
assert!(interior.is_empty(), "interior nil remains suppressive");
}
#[test]
fn paragraph_relative_anchor_tracks_the_paragraph() {
let g = PageGeometry::default();
let near_top = resolve_anchor_v(ST_RelativeFromV::Paragraph, 5.0, None, 0.0, &g, 0.0);
let further_down = resolve_anchor_v(ST_RelativeFromV::Paragraph, 5.0, None, 0.0, &g, 300.0);
assert_eq!(near_top, 77.0);
assert_eq!(further_down, 377.0);
assert!(further_down > near_top);
}
#[test]
fn an_aligned_anchor_resolves_against_its_frame() {
let g = PageGeometry::default();
let width = 100.0;
let height = 50.0;
let text_left = g.margin_left;
let text_width = g.page_width - g.margin_left - g.margin_right;
assert_eq!(
resolve_anchor_h(
ST_RelativeFromH::Margin,
999.0,
Some(AnchorAlignH::Left),
width,
&g,
0.0
),
text_left,
"an alignment replaces the offset rather than adding to it"
);
assert_eq!(
resolve_anchor_h(
ST_RelativeFromH::Margin,
0.0,
Some(AnchorAlignH::Right),
width,
&g,
0.0
),
text_left + text_width - width
);
assert_eq!(
resolve_anchor_h(
ST_RelativeFromH::Margin,
0.0,
Some(AnchorAlignH::Center),
width,
&g,
0.0
),
text_left + (text_width - width) / 2.0
);
assert_eq!(
resolve_anchor_v(
ST_RelativeFromV::Page,
0.0,
Some(AnchorAlignV::Top),
height,
&g,
0.0
),
0.0
);
assert_eq!(
resolve_anchor_v(
ST_RelativeFromV::Page,
0.0,
Some(AnchorAlignV::Bottom),
height,
&g,
0.0
),
g.page_height - height
);
}
#[test]
fn an_anchor_without_an_alignment_still_uses_its_offset() {
let g = PageGeometry::default();
assert_eq!(
resolve_anchor_h(ST_RelativeFromH::Page, 10.0, None, 100.0, &g, 0.0),
10.0
);
assert_eq!(
resolve_anchor_h(ST_RelativeFromH::Margin, 10.0, None, 100.0, &g, 0.0),
g.margin_left + 10.0
);
assert_eq!(
resolve_anchor_v(ST_RelativeFromV::Paragraph, 5.0, None, 50.0, &g, 300.0),
g.margin_top + 300.0 + 5.0
);
}
fn wrapping_drawing(rel_v: ST_RelativeFromV) -> AnchoredDrawing {
AnchoredDrawing {
behind_doc: false,
rel_h: ST_RelativeFromH::Margin,
off_h: 0.0,
rel_v,
off_v: 0.0,
width: 100.0,
height: 50.0,
wrap: WrapType::Square,
dist_top: 0.0,
dist_bottom: 0.0,
dist_left: 0.0,
dist_right: 0.0,
align_h: None,
align_v: None,
content: AnchoredContent::Image {
media_id: MediaId(1),
},
alternate_text: None,
structure_id: None,
}
}
fn para_anchoring(rel_v: ST_RelativeFromV) -> LayoutBlock {
let mut para = make_para(1, 14.0);
para.anchored = vec![wrapping_drawing(rel_v)];
LayoutBlock::Paragraph(para)
}
#[test]
fn the_two_pass_predicate_matches_only_paragraph_relative_wraps() {
assert!(!has_paragraph_relative_wrap(&[LayoutBlock::Paragraph(
make_para(3, 14.0)
)]));
assert!(!has_paragraph_relative_wrap(&[para_anchoring(
ST_RelativeFromV::Page
)]));
assert!(has_paragraph_relative_wrap(&[para_anchoring(
ST_RelativeFromV::Paragraph
)]));
assert!(has_paragraph_relative_wrap(&[para_anchoring(
ST_RelativeFromV::Line
)]));
let mut still = wrapping_drawing(ST_RelativeFromV::Paragraph);
still.wrap = WrapType::None;
let mut para = make_para(1, 14.0);
para.anchored = vec![still];
assert!(!has_paragraph_relative_wrap(&[LayoutBlock::Paragraph(
para
)]));
}
#[test]
fn pass_one_ignores_paragraph_relative_anchors() {
let fm = FontManager::new();
let media = HashMap::new();
let notes = NoteRegistry::default();
let empty = ResolvedWraps::new();
let blocks = vec![
LayoutBlock::Paragraph(make_para(3, 14.0)),
para_anchoring(ST_RelativeFromV::Paragraph),
para_anchoring(ST_RelativeFromV::Page),
];
let pager = Pager::new(
PageGeometry::default(),
None,
false,
&media,
¬es,
&fm,
&empty,
1,
1,
true,
None,
);
assert_eq!(pager.lookahead_wraps(0, &blocks).len(), 1);
}
#[test]
fn the_lookahead_offers_a_resolved_rect_only_on_its_own_page() {
let fm = FontManager::new();
let media = HashMap::new();
let notes = NoteRegistry::default();
let blocks = vec![
LayoutBlock::Paragraph(make_para(3, 14.0)),
para_anchoring(ST_RelativeFromV::Paragraph),
];
let placed = PlacedWrap {
rect: Rect {
x: 100.0,
y: 200.0,
width: 100.0,
height: 50.0,
},
wrap: WrapType::Square,
dist_top: 0.0,
dist_bottom: 0.0,
dist_left: 0.0,
dist_right: 0.0,
};
for (recorded_page, expected) in [(1usize, 1usize), (2, 0)] {
let mut resolved = ResolvedWraps::new();
resolved.insert((1, 0), (recorded_page, placed));
let pager = Pager::new(
PageGeometry::default(),
None,
false,
&media,
¬es,
&fm,
&resolved,
1,
1,
true,
None,
);
assert_eq!(
pager.lookahead_wraps(0, &blocks).len(),
expected,
"recorded on page {recorded_page}"
);
}
}
#[test]
fn a_placed_paragraph_relative_wrap_is_recorded_for_the_next_pass() {
let fm = FontManager::new();
let media = HashMap::new();
let notes = NoteRegistry::default();
let empty = ResolvedWraps::new();
let blocks = vec![
LayoutBlock::Paragraph(make_para(3, 14.0)),
para_anchoring(ST_RelativeFromV::Paragraph),
para_anchoring(ST_RelativeFromV::Page),
];
let context = PassContext {
geometry: PageGeometry::default(),
header_footer: None,
title_pg: false,
fm: &fm,
media: &media,
notes: ¬es,
first_page_number: 1,
first_header_page_number: 1,
};
let pass = paginate_pass(&blocks, &context, &empty);
assert_eq!(pass.resolved.len(), 1);
let (page, placed) = pass.resolved.get(&(1, 0)).expect("block one, anchor zero");
assert_eq!(*page, 1);
assert_eq!(placed.wrap, WrapType::Square);
}
}