use crate::block::{AnchoredContent, AnchoredDrawing, LayoutBlock, ParagraphBlock, ShapePreset};
use std::collections::HashMap;
use oxml_layout::{
Align, Color, FontManager, GlyphRun, LayoutLine, LineItem, MediaId, NoteRef, NoteStream,
OutlineEntry, PageFrame, Point, PositionedElement, Rect, 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 struct Section {
pub blocks: Vec<LayoutBlock>,
pub geometry: PageGeometry,
pub header_footer: Option<HeaderFooterContent>,
pub title_pg: bool,
}
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,
);
}
let mut all_pages = Vec::new();
let mut all_outlines = Vec::new();
let mut page_offset = 0;
for section in sections {
let (mut pages, mut outlines) = paginate_with_media(
§ion.blocks,
section.geometry,
section.header_footer.as_ref(),
section.title_pg,
fm,
media,
notes,
);
for page in &mut pages {
page.page_number += page_offset;
}
for outline in &mut outlines {
outline.page_index += page_offset;
}
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 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,
)
}
fn paginate_with_media(
blocks: &[LayoutBlock],
geometry: PageGeometry,
header_footer: Option<&HeaderFooterContent>,
title_pg: bool,
_fm: &FontManager,
media: &HashMap<MediaId, ImageData>,
notes: &NoteRegistry,
) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
let mut pager = Pager::new(geometry, header_footer, title_pg, media, notes, _fm);
for (block_idx, block) in blocks.iter().enumerate() {
if block.page_break_before() && pager.has_content() {
pager.finish_page();
}
match block {
LayoutBlock::Paragraph(para) => {
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);
}
LayoutBlock::Table(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() {
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.col_widths,
table_x,
pager.geometry.margin_top + pager.cursor_y,
&pager.geometry,
tbl_borders,
&mut pager.elements,
pager.media,
);
pager.cursor_y += hdr_row.height;
pager.mark_content();
}
}
}
render_table_row(
row,
&table.col_widths,
table_x,
pager.geometry.margin_top + pager.cursor_y,
&pager.geometry,
tbl_borders,
&mut pager.elements,
pager.media,
);
pager.cursor_y += row.height;
pager.mark_content();
}
}
}
}
pager.flush()
}
struct Pager<'a> {
pages: Vec<PageFrame>,
elements: Vec<PositionedElement>,
behind_elements: Vec<PositionedElement>,
cursor_y: f64,
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,
}
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,
) -> Self {
Pager {
pages: Vec::new(),
elements: Vec::new(),
behind_elements: Vec::new(),
cursor_y: 0.0,
page_number: 1,
content_height: geometry.content_height(),
geometry,
header_footer,
has_content_flag: false,
outlines: Vec::new(),
is_first_page: true,
title_pg,
media,
notes,
page_note_ids: Vec::new(),
pending_notes: Vec::new(),
fm,
page_wraps: Vec::new(),
ink_bottom: 0.0,
}
}
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).map(|note| note.height_from(*first)))
.sum();
let fresh_height: f64 = fresh
.iter()
.filter_map(|id| self.notes.get(*id).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).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).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).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(&self, block_idx: usize, blocks: &[LayoutBlock]) -> Vec<PlacedWrap> {
let mut out = Vec::new();
let mut height = self.cursor_y;
for block in blocks.iter().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 LayoutBlock::Paragraph(para) = block else {
continue;
};
let absolute = para.anchored.iter().filter(|a| {
a.wrap != WrapType::None
&& !matches!(
a.rel_v,
ST_RelativeFromV::Paragraph | ST_RelativeFromV::Line
)
});
for a in absolute {
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) {
for a in anchored {
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 {
self.page_wraps.push(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,
});
}
let mut produced: Vec<PositionedElement> = Vec::new();
match &a.content {
AnchoredContent::Image { media_id } => {
let image = self.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::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, y },
end: Point {
x: x + a.width,
y: y + a.height,
},
width: 1.0,
color: *color,
dash_pattern: None,
});
}
_ => {}
}
produced.extend(render_shape_text(text, &self.geometry, rect, 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) 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).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) else {
continue;
};
cursor_y += draw_note(
&mut self.elements,
&self.geometry,
note,
first,
count,
continued,
cursor_y,
);
}
}
fn finish_page(&mut self) {
self.place_page_notes();
let mut all_elements = Vec::new();
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 {
&hf.header_blocks
};
if !header_blocks.is_empty() {
let header_y = self.geometry.header_distance;
render_hf_blocks(
header_blocks,
&self.geometry,
header_y,
&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 {
&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,
&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.cursor_y = 0.0;
self.page_wraps.clear();
self.ink_bottom = 0.0;
self.has_content_flag = false;
self.is_first_page = false;
}
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 draw_note(
elements: &mut Vec<PositionedElement>,
geometry: &PageGeometry,
note: &NoteLayout,
first: usize,
count: usize,
continued: bool,
top: f64,
) -> 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(),
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, .. } => (None, *width),
};
if let Some(seg) = segment {
elements.push(PositionedElement::Text(GlyphRun {
origin: Point {
x,
y: line_baseline - seg.baseline_offset,
},
font_id: seg.font_id,
font_size: seg.font_size,
glyph_ids: seg.glyph_ids.clone(),
advances: seg.advances.clone(),
text: seg.text.clone(),
color: seg.color,
bold: seg.bold,
italic: seg.italic,
field_kind: None,
note: None,
}));
}
x += advance;
}
cursor_y += line.height;
}
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() {
for element in &page.elements {
if let PositionedElement::Text(run) = element
&& let Some(note) = run.note
&& note.stream == NoteStream::Endnote
&& notes.get(note).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) 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,
);
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, para, 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(
para: &ParagraphBlock,
block_idx: usize,
blocks: &[LayoutBlock],
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, &wraps, para_top, &pager.geometry, pager.fm)
};
let para = reflowed.as_ref().unwrap_or(para);
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();
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();
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);
return;
}
if lines_that_fit > 0 {
render_para_split(para, lines_that_fit, space_before, pager);
return;
}
pager.finish_page();
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);
return;
}
}
if para.keep_next && block_idx + 1 < blocks.len() {
let next_first = match &blocks[block_idx + 1] {
LayoutBlock::Paragraph(p) => p.lines.first().map(|l| l.height).unwrap_or(0.0),
LayoutBlock::Table(t) => t.rows.first().map(|r| r.height).unwrap_or(0.0),
};
if pager.cursor_y + space_before + para.content_height() + next_first
> pager.available_height_for(¶.lines)
&& pager.has_content()
{
pager.finish_page();
}
}
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);
render_paragraph_lines(
¶.lines,
para,
&pager.geometry,
pager.cursor_y,
&mut pager.elements,
pager.media,
);
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: &ParagraphBlock, split_at: usize, space_before: f64, pager: &mut Pager) {
pager.cursor_y += space_before;
pager.place_anchored(¶.anchored, pager.cursor_y, para.indent_left);
render_paragraph_lines(
¶.lines[..split_at],
para,
&pager.geometry,
pager.cursor_y,
&mut pager.elements,
pager.media,
);
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(),
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,
reflow: None,
content_offset_top: 0.0,
};
render_para_split(&temp_para, lines_that_fit, 0.0, pager);
return;
}
}
render_paragraph_lines(
remaining_lines,
para,
&pager.geometry,
0.0,
&mut pager.elements,
pager.media,
);
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: &ParagraphBlock,
geometry: &PageGeometry,
start_y: f64,
elements: &mut Vec<PositionedElement>,
media: &HashMap<MediaId, ImageData>,
) {
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(),
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(),
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);
elements.push(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,
});
x += width;
}
}
}
y += line.height;
}
}
fn render_hf_blocks(
blocks: &[ParagraphBlock],
geometry: &PageGeometry,
start_y: f64,
elements: &mut Vec<PositionedElement>,
media: &HashMap<MediaId, ImageData>,
) {
let mut y = start_y - geometry.margin_top; for para in blocks {
render_paragraph_lines(¶.lines, para, geometry, y, elements, media);
y += para.content_height();
}
}
fn render_table_row(
row: &crate::table::TableRow,
_col_widths: &[f64],
table_x: f64,
row_y: f64,
geometry: &PageGeometry,
table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
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() {
if let Some(ref shading) = cell.shading {
elements.push(PositionedElement::FilledRect {
rect: Rect {
x: cell_x,
y: row_y,
width: cell.width,
height: cell.height,
},
color: *shading,
});
}
render_cell_borders(
cell_x,
row_y,
cell.width,
cell.height,
&cell.borders,
table_borders,
cell_idx,
num_cells,
cell.is_first_row,
cell.is_last_row,
elements,
);
if !cell.is_vmerge_continue {
let cell_margin_top = cell.margin_top;
let cell_margin_left = cell.margin_left;
let content_height: f64 = cell.paragraphs.iter().map(|p| p.total_height()).sum();
let v_offset = match cell.v_align {
Some(rdocx_oxml::table::ST_VerticalJc::Center) => {
((cell.height - cell_margin_top - content_height) / 2.0).max(0.0)
}
Some(rdocx_oxml::table::ST_VerticalJc::Bottom) => {
(cell.height - cell_margin_top - content_height).max(0.0)
}
_ => 0.0, };
let mut para_y = row_y - geometry.margin_top + cell_margin_top + v_offset;
for para in &cell.paragraphs {
render_paragraph_lines(
¶.lines,
para,
&PageGeometry {
margin_left: cell_x + cell_margin_left,
..*geometry
},
para_y,
elements,
media,
);
para_y += para.total_height();
}
}
cell_x += cell.width;
}
}
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>|
-> Option<BorderEdge> {
let edge = cell_edge.or(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) {
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) {
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) {
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) {
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(),
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,
reflow: None,
content_offset_top: 0.0,
}
}
#[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(),
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(),
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,
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(),
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,
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(),
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(),
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,
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(),
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,
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(),
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,
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(),
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,
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(),
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(),
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(),
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,
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(),
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,
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(),
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,
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(),
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,
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 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
);
}
}