1use crate::block::{AnchoredContent, AnchoredDrawing, LayoutBlock, ParagraphBlock, ShapePreset};
7use std::collections::HashMap;
8
9use oxml_layout::{
10 Align, Color, FontManager, GlyphRun, LayoutLine, LineItem, MediaId, NoteRef, NoteStream,
11 OutlineEntry, PageFrame, Point, PositionedElement, Rect, Underline, break_into_lines,
12};
13
14use rdocx_oxml::drawing::{
15 AnchorAlignH, AnchorAlignV, ST_RelativeFromH, ST_RelativeFromV, WrapType,
16};
17use rdocx_oxml::shared::ST_Border;
18
19use crate::input::{ImageData, MediaRegistry};
20use crate::notes::{
21 NOTE_INDENT, NOTE_SEPARATOR_OFFSET, NoteLayout, NoteRegistry, SEPARATOR_WIDTH_FRACTION,
22};
23
24#[derive(Debug, Clone, Copy)]
26struct PlacedWrap {
27 rect: Rect,
28 wrap: WrapType,
29 dist_top: f64,
30 dist_bottom: f64,
31 dist_left: f64,
32 dist_right: f64,
33}
34
35impl PlacedWrap {
36 fn keep_out_top(&self) -> f64 {
38 self.rect.y - self.dist_top
39 }
40
41 fn keep_out_bottom(&self) -> f64 {
43 self.rect.y + self.rect.height + self.dist_bottom
44 }
45}
46
47type BorderEdge = (f64, Color, Option<(f64, f64)>);
49
50#[derive(Debug, Clone, Copy)]
52pub struct PageGeometry {
53 pub page_width: f64,
54 pub page_height: f64,
55 pub margin_top: f64,
56 pub margin_right: f64,
57 pub margin_bottom: f64,
58 pub margin_left: f64,
59 pub header_distance: f64,
60 pub footer_distance: f64,
61}
62
63impl PageGeometry {
64 pub fn content_width(&self) -> f64 {
66 self.page_width - self.margin_left - self.margin_right
67 }
68
69 pub fn content_height(&self) -> f64 {
71 self.page_height - self.margin_top - self.margin_bottom
72 }
73}
74
75impl Default for PageGeometry {
76 fn default() -> Self {
77 PageGeometry {
79 page_width: 612.0,
80 page_height: 792.0,
81 margin_top: 72.0,
82 margin_right: 72.0,
83 margin_bottom: 72.0,
84 margin_left: 72.0,
85 header_distance: 36.0,
86 footer_distance: 36.0,
87 }
88 }
89}
90
91pub struct HeaderFooterContent {
93 pub header_blocks: Vec<ParagraphBlock>,
94 pub footer_blocks: Vec<ParagraphBlock>,
95 pub first_header_blocks: Vec<ParagraphBlock>,
97 pub first_footer_blocks: Vec<ParagraphBlock>,
99}
100
101pub struct Section {
103 pub blocks: Vec<LayoutBlock>,
104 pub geometry: PageGeometry,
105 pub header_footer: Option<HeaderFooterContent>,
106 pub title_pg: bool,
108}
109
110pub fn paginate_sections(
112 sections: &[Section],
113 fm: &FontManager,
114 media: &MediaRegistry,
115 notes: &NoteRegistry,
116) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
117 let media = media.media();
118 if sections.is_empty() {
119 return (
120 vec![PageFrame::new(1, 612.0, 792.0, Vec::new())],
121 Vec::new(),
122 );
123 }
124
125 if sections.len() == 1 {
127 let s = §ions[0];
128 return paginate_with_media(
129 &s.blocks,
130 s.geometry,
131 s.header_footer.as_ref(),
132 s.title_pg,
133 fm,
134 media,
135 notes,
136 );
137 }
138
139 let mut all_pages = Vec::new();
141 let mut all_outlines = Vec::new();
142 let mut page_offset = 0;
143
144 for section in sections {
145 let (mut pages, mut outlines) = paginate_with_media(
146 §ion.blocks,
147 section.geometry,
148 section.header_footer.as_ref(),
149 section.title_pg,
150 fm,
151 media,
152 notes,
153 );
154
155 for page in &mut pages {
157 page.page_number += page_offset;
158 }
159 for outline in &mut outlines {
160 outline.page_index += page_offset;
161 }
162
163 page_offset += pages.len();
164 all_pages.append(&mut pages);
165 all_outlines.append(&mut outlines);
166 }
167
168 for (i, page) in all_pages.iter_mut().enumerate() {
171 page.page_number = i + 1;
172 }
173
174 (all_pages, all_outlines)
175}
176
177pub fn paginate(
179 blocks: &[LayoutBlock],
180 geometry: PageGeometry,
181 header_footer: Option<&HeaderFooterContent>,
182 title_pg: bool,
183 _fm: &FontManager,
184 media: &MediaRegistry,
185 notes: &NoteRegistry,
186) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
187 paginate_with_media(
188 blocks,
189 geometry,
190 header_footer,
191 title_pg,
192 _fm,
193 media.media(),
194 notes,
195 )
196}
197
198fn paginate_with_media(
199 blocks: &[LayoutBlock],
200 geometry: PageGeometry,
201 header_footer: Option<&HeaderFooterContent>,
202 title_pg: bool,
203 _fm: &FontManager,
204 media: &HashMap<MediaId, ImageData>,
205 notes: &NoteRegistry,
206) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
207 let mut pager = Pager::new(geometry, header_footer, title_pg, media, notes, _fm);
208
209 for (block_idx, block) in blocks.iter().enumerate() {
210 if block.page_break_before() && pager.has_content() {
212 pager.finish_page();
213 }
214
215 match block {
216 LayoutBlock::Paragraph(para) => {
217 if let (Some(level), Some(title)) = (para.heading_level, ¶.heading_text) {
219 pager.outlines.push(OutlineEntry {
220 title: title.clone(),
221 level,
222 page_index: pager.page_number - 1,
223 y_position: pager.geometry.margin_top + pager.cursor_y,
224 });
225 }
226 paginate_paragraph(para, block_idx, blocks, &mut pager);
227 }
228 LayoutBlock::Table(table) => {
229 let table_x = geometry.margin_left + table.table_indent;
230 let tbl_borders = table.borders.as_ref();
231
232 for (row_idx, row) in table.rows.iter().enumerate() {
233 if pager.cursor_y + row.height > pager.available_height() && pager.has_content()
234 {
235 pager.finish_page();
236
237 for &hdr_idx in &table.header_row_indices {
239 if hdr_idx < row_idx {
240 let hdr_row = &table.rows[hdr_idx];
241 render_table_row(
242 hdr_row,
243 &table.col_widths,
244 table_x,
245 pager.geometry.margin_top + pager.cursor_y,
246 &pager.geometry,
247 tbl_borders,
248 &mut pager.elements,
249 pager.media,
250 );
251 pager.cursor_y += hdr_row.height;
252 pager.mark_content();
253 }
254 }
255 }
256
257 render_table_row(
258 row,
259 &table.col_widths,
260 table_x,
261 pager.geometry.margin_top + pager.cursor_y,
262 &pager.geometry,
263 tbl_borders,
264 &mut pager.elements,
265 pager.media,
266 );
267 pager.cursor_y += row.height;
268 pager.mark_content();
269 }
270 }
271 }
272 }
273
274 pager.flush()
275}
276
277struct Pager<'a> {
279 pages: Vec<PageFrame>,
280 elements: Vec<PositionedElement>,
281 behind_elements: Vec<PositionedElement>,
285 cursor_y: f64,
286 page_number: usize,
287 content_height: f64,
288 geometry: PageGeometry,
289 header_footer: Option<&'a HeaderFooterContent>,
290 has_content_flag: bool,
291 outlines: Vec<OutlineEntry>,
292 is_first_page: bool,
294 title_pg: bool,
296 media: &'a HashMap<MediaId, ImageData>,
297 notes: &'a NoteRegistry,
299 page_note_ids: Vec<NoteRef>,
303 pending_notes: Vec<(NoteRef, usize)>,
306 fm: &'a FontManager,
308 page_wraps: Vec<PlacedWrap>,
311 ink_bottom: f64,
318}
319
320impl<'a> Pager<'a> {
321 fn new(
322 geometry: PageGeometry,
323 header_footer: Option<&'a HeaderFooterContent>,
324 title_pg: bool,
325 media: &'a HashMap<MediaId, ImageData>,
326 notes: &'a NoteRegistry,
327 fm: &'a FontManager,
328 ) -> Self {
329 Pager {
330 pages: Vec::new(),
331 elements: Vec::new(),
332 behind_elements: Vec::new(),
333 cursor_y: 0.0,
334 page_number: 1,
335 content_height: geometry.content_height(),
336 geometry,
337 header_footer,
338 has_content_flag: false,
339 outlines: Vec::new(),
340 is_first_page: true,
341 title_pg,
342 media,
343 notes,
344 page_note_ids: Vec::new(),
345 pending_notes: Vec::new(),
346 fm,
347 page_wraps: Vec::new(),
348 ink_bottom: 0.0,
349 }
350 }
351
352 fn has_content(&self) -> bool {
353 self.has_content_flag
354 }
355
356 fn reserve_for(&self, carried: &[(NoteRef, usize)], fresh: &[NoteRef]) -> f64 {
361 if carried.is_empty() && fresh.is_empty() {
362 return 0.0;
363 }
364 let carried_height: f64 = carried
365 .iter()
366 .filter_map(|(id, first)| self.notes.get(*id).map(|note| note.height_from(*first)))
367 .sum();
368 let fresh_height: f64 = fresh
369 .iter()
370 .filter_map(|id| self.notes.get(*id).map(NoteLayout::height))
371 .sum();
372 NOTE_SEPARATOR_OFFSET + carried_height + fresh_height
373 }
374
375 fn reserved_height(&self) -> f64 {
377 self.reserve_for(&self.pending_notes, &self.page_note_ids)
378 }
379
380 fn available_height(&self) -> f64 {
382 (self.content_height - self.reserved_height()).max(0.0)
383 }
384
385 fn available_height_for(&self, lines: &[LayoutLine]) -> f64 {
393 let mut fresh = self.page_note_ids.clone();
394 for line in lines {
395 for id in page_foot_notes_in_line(line) {
396 if self.notes.get(id).is_some()
397 && !fresh.contains(&id)
398 && !self.pending_notes.iter().any(|(pending, _)| *pending == id)
399 {
400 fresh.push(id);
401 }
402 }
403 }
404 (self.content_height - self.reserve_for(&self.pending_notes, &fresh)).max(0.0)
405 }
406
407 fn claim_notes(&mut self, lines: &[LayoutLine]) {
412 for id in lines.iter().flat_map(page_foot_notes_in_line) {
413 {
414 if self.notes.get(id).is_some()
415 && !self.page_note_ids.contains(&id)
416 && !self.pending_notes.iter().any(|(pending, _)| *pending == id)
417 {
418 self.page_note_ids.push(id);
419 }
420 }
421 }
422 }
423
424 fn count_lines_that_fit_with_notes(&self, lines: &[LayoutLine], start_y: f64) -> usize {
433 let mut fresh = self.page_note_ids.clone();
434 let mut used = 0.0;
435
436 for (index, line) in lines.iter().enumerate() {
437 for id in page_foot_notes_in_line(line) {
438 if self.notes.get(id).is_some()
439 && !fresh.contains(&id)
440 && !self.pending_notes.iter().any(|(pending, _)| *pending == id)
441 {
442 fresh.push(id);
443 }
444 }
445
446 let reserve = self.reserve_for(&self.pending_notes, &fresh);
447 if start_y + used + line.height > self.content_height - reserve + 0.01 {
448 let page_is_empty = !self.has_content() && used == 0.0 && index == 0;
449 if !page_is_empty {
450 return index;
451 }
452 }
453 used += line.height;
454 }
455
456 lines.len()
457 }
458
459 fn mark_content(&mut self) {
460 self.has_content_flag = true;
461 }
462
463 fn wrap_rects_for(
467 &self,
468 anchored: &[AnchoredDrawing],
469 para_top: f64,
470 indent_left: f64,
471 ) -> Vec<PlacedWrap> {
472 anchored
473 .iter()
474 .filter(|a| a.wrap != WrapType::None)
475 .map(|a| PlacedWrap {
476 rect: Rect {
477 x: resolve_anchor_h(
478 a.rel_h,
479 a.off_h,
480 a.align_h,
481 a.width,
482 &self.geometry,
483 indent_left,
484 ),
485 y: resolve_anchor_v(
486 a.rel_v,
487 a.off_v,
488 a.align_v,
489 a.height,
490 &self.geometry,
491 para_top,
492 ),
493 width: a.width,
494 height: a.height,
495 },
496 wrap: a.wrap,
497 dist_top: a.dist_top,
498 dist_bottom: a.dist_bottom,
499 dist_left: a.dist_left,
500 dist_right: a.dist_right,
501 })
502 .collect()
503 }
504
505 fn lookahead_wraps(&self, block_idx: usize, blocks: &[LayoutBlock]) -> Vec<PlacedWrap> {
516 let mut out = Vec::new();
517 let mut height = self.cursor_y;
518
519 for block in blocks.iter().skip(block_idx + 1) {
520 if block.page_break_before() || height > self.content_height {
521 break;
522 }
523 height += block.space_before() + block.content_height() + block.space_after();
524
525 let LayoutBlock::Paragraph(para) = block else {
526 continue;
527 };
528 let absolute = para.anchored.iter().filter(|a| {
529 a.wrap != WrapType::None
530 && !matches!(
531 a.rel_v,
532 ST_RelativeFromV::Paragraph | ST_RelativeFromV::Line
533 )
534 });
535 for a in absolute {
536 out.extend(self.wrap_rects_for(std::slice::from_ref(a), 0.0, para.indent_left));
537 }
538 }
539
540 out
541 }
542
543 fn place_anchored(&mut self, anchored: &[AnchoredDrawing], para_top: f64, indent_left: f64) {
546 for a in anchored {
547 let x = resolve_anchor_h(
548 a.rel_h,
549 a.off_h,
550 a.align_h,
551 a.width,
552 &self.geometry,
553 indent_left,
554 );
555 let y = resolve_anchor_v(
556 a.rel_v,
557 a.off_v,
558 a.align_v,
559 a.height,
560 &self.geometry,
561 para_top,
562 );
563 let rect = Rect {
564 x,
565 y,
566 width: a.width,
567 height: a.height,
568 };
569
570 if a.wrap != WrapType::None {
571 self.page_wraps.push(PlacedWrap {
572 rect,
573 wrap: a.wrap,
574 dist_top: a.dist_top,
575 dist_bottom: a.dist_bottom,
576 dist_left: a.dist_left,
577 dist_right: a.dist_right,
578 });
579 }
580
581 let mut produced: Vec<PositionedElement> = Vec::new();
582
583 match &a.content {
584 AnchoredContent::Image { media_id } => {
585 let image = self.media.get(media_id);
586 produced.push(PositionedElement::Image {
587 rect,
588 data: image.map_or_else(Vec::new, |image| image.data.clone()),
589 content_type: image
590 .map_or_else(String::new, |image| image.content_type.clone()),
591 media_id: *media_id,
592 });
593 }
594 AnchoredContent::Shape { preset, fill, text } => {
595 match (preset, fill) {
598 (ShapePreset::Rect, Some(color)) => {
599 produced.push(PositionedElement::FilledRect {
600 rect,
601 color: *color,
602 });
603 }
604 (ShapePreset::Line, Some(color)) => {
605 produced.push(PositionedElement::Line {
608 start: Point { x, y },
609 end: Point {
610 x: x + a.width,
611 y: y + a.height,
612 },
613 width: 1.0,
614 color: *color,
615 dash_pattern: None,
616 });
617 }
618 _ => {}
619 }
620 produced.extend(render_shape_text(text, &self.geometry, rect, self.media));
621 }
622 }
623
624 if a.behind_doc {
625 self.behind_elements.append(&mut produced);
626 } else {
627 self.elements.append(&mut produced);
628 }
629 }
630 }
631
632 fn place_page_notes(&mut self) {
638 let mut queue: Vec<(NoteRef, usize, bool)> = self
639 .pending_notes
640 .drain(..)
641 .map(|(id, first)| (id, first, true))
642 .collect();
643 queue.extend(self.page_note_ids.drain(..).map(|id| (id, 0usize, false)));
644
645 if queue.is_empty() {
646 return;
647 }
648
649 let opens_with_continuation = queue[0].2;
650 let available = (self.content_height - self.ink_bottom - NOTE_SEPARATOR_OFFSET).max(0.0);
651
652 let mut placed: Vec<(NoteRef, usize, usize, bool)> = Vec::new();
654 let mut used = 0.0;
655 let mut carried: Vec<(NoteRef, usize)> = Vec::new();
656
657 for (id, first, continued) in queue {
658 let Some(note) = self.notes.get(id) else {
659 continue;
660 };
661 if !carried.is_empty() {
662 carried.push((id, first));
665 continue;
666 }
667
668 let mut count = 0;
669 for line in note.lines.iter().skip(first) {
670 if used + line.height > available + 0.01 {
671 break;
672 }
673 used += line.height;
674 count += 1;
675 }
676
677 if count > 0 {
678 placed.push((id, first, count, continued));
679 }
680 if first + count < note.lines.len() {
681 carried.push((id, first + count));
682 }
683 }
684
685 self.pending_notes = carried;
686
687 if placed.is_empty() {
688 return;
689 }
690
691 let total: f64 = placed
692 .iter()
693 .filter_map(|(id, first, count, _)| {
694 self.notes.get(*id).map(|n| n.height_of(*first, *count))
695 })
696 .sum();
697
698 let separator_y =
699 self.geometry.page_height - self.geometry.margin_bottom - total - NOTE_SEPARATOR_OFFSET;
700
701 let separator_width = if opens_with_continuation && self.notes.has_continuation_separator()
705 {
706 self.geometry.content_width()
707 } else {
708 self.geometry.content_width() * SEPARATOR_WIDTH_FRACTION
709 };
710
711 self.elements.push(PositionedElement::Line {
712 start: Point {
713 x: self.geometry.margin_left,
714 y: separator_y,
715 },
716 end: Point {
717 x: self.geometry.margin_left + separator_width,
718 y: separator_y,
719 },
720 width: 0.5,
721 color: Color::BLACK,
722 dash_pattern: None,
723 });
724
725 let mut cursor_y = separator_y + NOTE_SEPARATOR_OFFSET;
726 for (id, first, count, continued) in placed {
727 let Some(note) = self.notes.get(id) else {
728 continue;
729 };
730 cursor_y += draw_note(
731 &mut self.elements,
732 &self.geometry,
733 note,
734 first,
735 count,
736 continued,
737 cursor_y,
738 );
739 }
740 }
741
742 fn finish_page(&mut self) {
743 self.place_page_notes();
744 let mut all_elements = Vec::new();
745
746 all_elements.append(&mut self.behind_elements);
748
749 if let Some(hf) = self.header_footer {
750 let header_blocks = if self.is_first_page && self.title_pg {
752 &hf.first_header_blocks
753 } else {
754 &hf.header_blocks
755 };
756 if !header_blocks.is_empty() {
757 let header_y = self.geometry.header_distance;
758 render_hf_blocks(
759 header_blocks,
760 &self.geometry,
761 header_y,
762 &mut all_elements,
763 self.media,
764 );
765 }
766 }
767
768 all_elements.append(&mut self.elements);
769
770 if let Some(hf) = self.header_footer {
771 let footer_blocks = if self.is_first_page && self.title_pg {
773 &hf.first_footer_blocks
774 } else {
775 &hf.footer_blocks
776 };
777 if !footer_blocks.is_empty() {
778 let footer_height: f64 = footer_blocks.iter().map(|b| b.content_height()).sum();
779 let footer_y =
780 self.geometry.page_height - self.geometry.footer_distance - footer_height;
781 render_hf_blocks(
782 footer_blocks,
783 &self.geometry,
784 footer_y,
785 &mut all_elements,
786 self.media,
787 );
788 }
789 }
790
791 self.pages.push(PageFrame::new(
792 self.page_number,
793 self.geometry.page_width,
794 self.geometry.page_height,
795 all_elements,
796 ));
797 self.page_number += 1;
798 self.cursor_y = 0.0;
799 self.page_wraps.clear();
800 self.ink_bottom = 0.0;
801 self.has_content_flag = false;
802 self.is_first_page = false;
803 }
804
805 fn flush(mut self) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
806 if self.has_content() || self.pages.is_empty() {
808 self.finish_page();
809 }
810 while !self.pending_notes.is_empty() {
814 let before = self.pending_notes.clone();
815 self.finish_page();
816 if self.pending_notes == before {
817 debug_assert!(
822 false,
823 "a page placed no note content, dropping {:?}",
824 self.pending_notes
825 );
826 break;
827 }
828 }
829 (self.pages, self.outlines)
830 }
831}
832
833fn draw_note(
838 elements: &mut Vec<PositionedElement>,
839 geometry: &PageGeometry,
840 note: &NoteLayout,
841 first: usize,
842 count: usize,
843 continued: bool,
844 top: f64,
845) -> f64 {
846 let baseline = top + note.lines.get(first).map_or(0.0, |line| line.ascent);
847
848 if !continued {
850 elements.push(PositionedElement::Text(GlyphRun {
851 origin: Point {
852 x: geometry.margin_left,
853 y: baseline - note.marker_rise,
854 },
855 font_id: note.marker.font_id,
856 font_size: note.marker.font_size,
857 glyph_ids: note.marker.glyph_ids.clone(),
858 advances: note.marker.advances.clone(),
859 text: note.marker.text.clone(),
860 color: note.marker.color,
861 bold: note.marker.bold,
862 italic: note.marker.italic,
863 field_kind: None,
864 note: None,
865 }));
866 }
867
868 let mut cursor_y = top;
869 for line in note.lines.iter().skip(first).take(count) {
870 let line_baseline = cursor_y + line.ascent;
871 let mut x = geometry.margin_left + NOTE_INDENT;
872 for item in &line.items {
873 let (segment, advance) = match item {
874 LineItem::Text(seg) | LineItem::Marker(seg) => (Some(seg), seg.width),
875 LineItem::Tab { width, .. } | LineItem::Image { width, .. } => (None, *width),
876 };
877 if let Some(seg) = segment {
878 elements.push(PositionedElement::Text(GlyphRun {
879 origin: Point {
880 x,
881 y: line_baseline - seg.baseline_offset,
882 },
883 font_id: seg.font_id,
884 font_size: seg.font_size,
885 glyph_ids: seg.glyph_ids.clone(),
886 advances: seg.advances.clone(),
887 text: seg.text.clone(),
888 color: seg.color,
889 bold: seg.bold,
890 italic: seg.italic,
891 field_kind: None,
892 note: None,
893 }));
894 }
895 x += advance;
896 }
897 cursor_y += line.height;
898 }
899
900 cursor_y - top
901}
902
903pub fn append_endnote_pages(
909 pages: &mut Vec<PageFrame>,
910 notes: &NoteRegistry,
911 geometry: PageGeometry,
912) {
913 let mut ordered: Vec<NoteRef> = Vec::new();
916 for page in pages.iter() {
917 for element in &page.elements {
918 if let PositionedElement::Text(run) = element
919 && let Some(note) = run.note
920 && note.stream == NoteStream::Endnote
921 && notes.get(note).is_some()
922 && !ordered.contains(¬e)
923 {
924 ordered.push(note);
925 }
926 }
927 }
928
929 if ordered.is_empty() {
930 return;
931 }
932
933 let content_height = geometry.content_height();
934 let mut elements: Vec<PositionedElement> = Vec::new();
935 let mut cursor_y = 0.0;
936 let mut page_number = pages.len() + 1;
937
938 let mut flush = |elements: &mut Vec<PositionedElement>, page_number: &mut usize| {
939 pages.push(PageFrame::new(
940 *page_number,
941 geometry.page_width,
942 geometry.page_height,
943 std::mem::take(elements),
944 ));
945 *page_number += 1;
946 };
947
948 for note_ref in ordered {
949 let Some(note) = notes.get(note_ref) else {
950 continue;
951 };
952
953 let mut first = 0;
954 let mut continued = false;
955 while first < note.lines.len() {
956 let mut count = 0;
957 let mut used = cursor_y;
958 for line in note.lines.iter().skip(first) {
959 if used + line.height > content_height + 0.01 {
960 break;
961 }
962 used += line.height;
963 count += 1;
964 }
965
966 if count == 0 {
967 if cursor_y == 0.0 {
974 count = 1;
975 } else {
976 flush(&mut elements, &mut page_number);
977 cursor_y = 0.0;
978 continue;
979 }
980 }
981
982 cursor_y += draw_note(
983 &mut elements,
984 &geometry,
985 note,
986 first,
987 count,
988 continued,
989 geometry.margin_top + cursor_y,
990 );
991 first += count;
992 continued = true;
993 }
994 }
995
996 if !elements.is_empty() {
997 flush(&mut elements, &mut page_number);
998 }
999}
1000
1001fn notes_in_line(line: &LayoutLine) -> impl Iterator<Item = NoteRef> + '_ {
1003 line.items.iter().filter_map(|item| match item {
1004 LineItem::Text(seg) | LineItem::Marker(seg) => seg.note,
1005 _ => None,
1006 })
1007}
1008
1009fn page_foot_notes_in_line(line: &LayoutLine) -> impl Iterator<Item = NoteRef> + '_ {
1014 notes_in_line(line).filter(|note| note.stream == NoteStream::Footnote)
1015}
1016
1017fn translate_element(element: &mut PositionedElement, dx: f64, dy: f64) {
1023 match element {
1024 PositionedElement::Text(run) => {
1025 run.origin.x += dx;
1026 run.origin.y += dy;
1027 }
1028 PositionedElement::Line { start, end, .. } => {
1029 start.x += dx;
1030 start.y += dy;
1031 end.x += dx;
1032 end.y += dy;
1033 }
1034 PositionedElement::FilledRect { rect, .. }
1035 | PositionedElement::Image { rect, .. }
1036 | PositionedElement::LinkAnnotation { rect, .. } => {
1037 rect.x += dx;
1038 rect.y += dy;
1039 }
1040 _ => {}
1041 }
1042}
1043
1044fn render_shape_text(
1050 text: &[ParagraphBlock],
1051 geometry: &PageGeometry,
1052 rect: Rect,
1053 media: &HashMap<MediaId, ImageData>,
1054) -> Vec<PositionedElement> {
1055 if text.is_empty() {
1056 return Vec::new();
1057 }
1058
1059 let mut local = Vec::new();
1060 let mut y = 0.0;
1061 for para in text {
1062 render_paragraph_lines(¶.lines, para, geometry, y, &mut local, media);
1063 y += para.content_height();
1064 }
1065
1066 let dx = rect.x - geometry.margin_left;
1069 let dy = rect.y - geometry.margin_top;
1070 for element in &mut local {
1071 translate_element(element, dx, dy);
1072 }
1073 local
1074}
1075
1076fn frame_h(rel: ST_RelativeFromH, g: &PageGeometry, indent_left: f64) -> (f64, f64) {
1082 let text_width = g.page_width - g.margin_left - g.margin_right;
1083 match rel {
1084 ST_RelativeFromH::Page | ST_RelativeFromH::LeftMargin => (0.0, g.page_width),
1085 ST_RelativeFromH::RightMargin | ST_RelativeFromH::OutsideMargin => {
1086 (g.page_width - g.margin_right, g.margin_right)
1087 }
1088 ST_RelativeFromH::InsideMargin => (g.margin_left, g.margin_left),
1089 ST_RelativeFromH::Character => (g.margin_left + indent_left, text_width),
1091 ST_RelativeFromH::Margin | ST_RelativeFromH::Column => (g.margin_left, text_width),
1094 }
1095}
1096
1097fn resolve_anchor_h(
1098 rel: ST_RelativeFromH,
1099 off: f64,
1100 align: Option<AnchorAlignH>,
1101 width: f64,
1102 g: &PageGeometry,
1103 indent_left: f64,
1104) -> f64 {
1105 let (start, size) = frame_h(rel, g, indent_left);
1106 match align {
1107 Some(AnchorAlignH::Left | AnchorAlignH::Inside) => start,
1111 Some(AnchorAlignH::Center) => start + (size - width) / 2.0,
1112 Some(AnchorAlignH::Right | AnchorAlignH::Outside) => start + size - width,
1113 None => start + off,
1114 }
1115}
1116
1117fn frame_v(rel: ST_RelativeFromV, g: &PageGeometry, para_top: f64) -> (f64, f64) {
1122 let text_height = g.page_height - g.margin_top - g.margin_bottom;
1123 match rel {
1124 ST_RelativeFromV::Page | ST_RelativeFromV::TopMargin => (0.0, g.page_height),
1125 ST_RelativeFromV::BottomMargin | ST_RelativeFromV::OutsideMargin => {
1126 (g.page_height - g.margin_bottom, g.margin_bottom)
1127 }
1128 ST_RelativeFromV::Margin | ST_RelativeFromV::InsideMargin => (g.margin_top, text_height),
1129 ST_RelativeFromV::Paragraph | ST_RelativeFromV::Line => {
1133 (g.margin_top + para_top, text_height)
1134 }
1135 }
1136}
1137
1138fn resolve_anchor_v(
1139 rel: ST_RelativeFromV,
1140 off: f64,
1141 align: Option<AnchorAlignV>,
1142 height: f64,
1143 g: &PageGeometry,
1144 para_top: f64,
1145) -> f64 {
1146 let (start, size) = frame_v(rel, g, para_top);
1147 match align {
1148 Some(AnchorAlignV::Top | AnchorAlignV::Inside) => start,
1149 Some(AnchorAlignV::Center) => start + (size - height) / 2.0,
1150 Some(AnchorAlignV::Bottom | AnchorAlignV::Outside) => start + size - height,
1151 None => start + off,
1152 }
1153}
1154
1155fn reflow_around_wraps(
1162 para: &ParagraphBlock,
1163 wraps: &[PlacedWrap],
1164 para_top: f64,
1165 geometry: &PageGeometry,
1166 fm: &FontManager,
1167) -> Option<ParagraphBlock> {
1168 let reflow = para.reflow.as_ref()?;
1169 if wraps.is_empty() {
1170 return None;
1171 }
1172
1173 let mut lines = para.lines.clone();
1174 let mut offset_top = 0.0;
1175
1176 for _ in 0..2 {
1180 let mut prefix: Vec<f64> = Vec::new();
1181 let mut suffix: Vec<f64> = Vec::new();
1182
1183 let paragraph_top = geometry.margin_top + para_top;
1186 let mut next_offset_top: f64 = 0.0;
1187 for wrap in wraps.iter().filter(|w| w.wrap == WrapType::TopAndBottom) {
1188 if wrap.keep_out_top() <= paragraph_top + 1.0 && wrap.keep_out_bottom() > paragraph_top
1189 {
1190 next_offset_top = next_offset_top.max(wrap.keep_out_bottom() - paragraph_top);
1191 }
1192 }
1193
1194 for wrap in wraps.iter().filter(|w| w.wrap != WrapType::TopAndBottom) {
1195 let text_left = geometry.margin_left + para.indent_left;
1197 let text_right = geometry.page_width - geometry.margin_right - para.indent_right;
1198 let drawing_centre = wrap.rect.x + wrap.rect.width / 2.0;
1199 let on_the_left = drawing_centre < (text_left + text_right) / 2.0;
1200
1201 let reserve = if on_the_left {
1202 (wrap.rect.x + wrap.rect.width + wrap.dist_right - text_left).max(0.0)
1203 } else {
1204 (text_right - (wrap.rect.x - wrap.dist_left)).max(0.0)
1205 };
1206 if reserve <= 0.0 {
1207 continue;
1208 }
1209
1210 let mut line_top = geometry.margin_top + para_top + next_offset_top;
1211 for (index, line) in lines.iter().enumerate() {
1212 let line_bottom = line_top + line.height;
1213 if line_bottom > wrap.keep_out_top() && line_top < wrap.keep_out_bottom() {
1214 let target = if on_the_left {
1215 &mut prefix
1216 } else {
1217 &mut suffix
1218 };
1219 if target.len() <= index {
1220 target.resize(index + 1, 0.0);
1221 }
1222 target[index] += reserve;
1223 }
1224 line_top = line_bottom;
1225 }
1226 }
1227
1228 if prefix.is_empty() && suffix.is_empty() && next_offset_top == 0.0 {
1229 return None;
1230 }
1231
1232 let mut params = reflow.params.clone();
1233 params.line_prefix_widths = prefix;
1234 params.line_suffix_widths = suffix;
1235
1236 let Ok(reflowed) = break_into_lines(&reflow.items, ¶ms, fm) else {
1237 return None;
1238 };
1239 lines = reflowed;
1240 offset_top = next_offset_top;
1241 }
1242
1243 let mut adjusted = para.clone();
1244 adjusted.lines = lines;
1245 adjusted.content_offset_top = offset_top;
1246 adjusted.reflow = None;
1248 Some(adjusted)
1249}
1250
1251fn paginate_paragraph(
1252 para: &ParagraphBlock,
1253 block_idx: usize,
1254 blocks: &[LayoutBlock],
1255 pager: &mut Pager,
1256) {
1257 let space_before = if pager.cursor_y == 0.0 {
1258 0.0
1259 } else {
1260 para.space_before
1261 };
1262
1263 let reflowed = {
1267 let para_top = pager.cursor_y + space_before;
1268 let mut wraps = pager.page_wraps.clone();
1269 wraps.extend(pager.wrap_rects_for(¶.anchored, para_top, para.indent_left));
1270 wraps.extend(pager.lookahead_wraps(block_idx, blocks));
1271 reflow_around_wraps(para, &wraps, para_top, &pager.geometry, pager.fm)
1272 };
1273 let para = reflowed.as_ref().unwrap_or(para);
1274
1275 let total_needed = space_before + para.content_height();
1279 let remaining = pager.available_height_for(¶.lines) - pager.cursor_y;
1280
1281 if total_needed > remaining && pager.has_content() {
1282 if para.keep_lines || para.lines.len() <= 2 {
1284 pager.finish_page();
1285 paginate_paragraph(para, block_idx, blocks, pager);
1287 return;
1288 }
1289
1290 let lines_that_fit = pager.count_lines_that_fit_with_notes(
1293 ¶.lines,
1294 pager.cursor_y + space_before + para.content_offset_top,
1295 );
1296
1297 if para.widow_control && lines_that_fit < 2 {
1298 pager.finish_page();
1300 paginate_paragraph(para, block_idx, blocks, pager);
1301 return;
1302 }
1303
1304 let lines_remaining = para.lines.len() - lines_that_fit;
1305 if para.widow_control && lines_remaining < 2 && lines_that_fit >= 3 {
1306 let split_at = lines_that_fit - 1;
1308 render_para_split(para, split_at, space_before, pager);
1309 return;
1310 }
1311
1312 if lines_that_fit > 0 {
1313 render_para_split(para, lines_that_fit, space_before, pager);
1314 return;
1315 }
1316
1317 pager.finish_page();
1319 paginate_paragraph(para, block_idx, blocks, pager);
1320 return;
1321 }
1322
1323 if total_needed > pager.available_height_for(¶.lines) && pager.cursor_y == 0.0 {
1326 let lines_that_fit =
1328 pager.count_lines_that_fit_with_notes(¶.lines, para.content_offset_top);
1329 if lines_that_fit > 0 && lines_that_fit < para.lines.len() {
1330 render_para_split(para, lines_that_fit, 0.0, pager);
1331 return;
1332 }
1333 }
1334
1335 if para.keep_next && block_idx + 1 < blocks.len() {
1337 let next_first = match &blocks[block_idx + 1] {
1338 LayoutBlock::Paragraph(p) => p.lines.first().map(|l| l.height).unwrap_or(0.0),
1339 LayoutBlock::Table(t) => t.rows.first().map(|r| r.height).unwrap_or(0.0),
1340 };
1341 if pager.cursor_y + space_before + para.content_height() + next_first
1342 > pager.available_height_for(¶.lines)
1343 && pager.has_content()
1344 {
1345 pager.finish_page();
1346 }
1347 }
1348
1349 let space = if pager.cursor_y == 0.0 {
1351 0.0
1352 } else {
1353 para.space_before
1354 };
1355 pager.cursor_y += space;
1356
1357 if let Some(shading) = para.shading {
1358 pager.elements.push(PositionedElement::FilledRect {
1359 rect: Rect {
1360 x: pager.geometry.margin_left + para.indent_left,
1361 y: pager.geometry.margin_top + pager.cursor_y,
1362 width: pager.geometry.content_width() - para.indent_left - para.indent_right,
1363 height: para.content_height(),
1364 },
1365 color: shading,
1366 });
1367 }
1368
1369 if let Some(ref borders) = para.borders {
1371 let border_x = pager.geometry.margin_left + para.indent_left;
1372 let border_y = pager.geometry.margin_top + pager.cursor_y;
1373 let border_w = pager.geometry.content_width() - para.indent_left - para.indent_right;
1374 let border_h = para.content_height();
1375 render_border_edges(
1376 borders,
1377 border_x,
1378 border_y,
1379 border_w,
1380 border_h,
1381 &mut pager.elements,
1382 );
1383 }
1384
1385 pager.place_anchored(¶.anchored, pager.cursor_y, para.indent_left);
1388
1389 render_paragraph_lines(
1390 ¶.lines,
1391 para,
1392 &pager.geometry,
1393 pager.cursor_y,
1394 &mut pager.elements,
1395 pager.media,
1396 );
1397 pager.claim_notes(¶.lines);
1398 pager.cursor_y += para.content_height();
1399 pager.ink_bottom = pager.cursor_y;
1400 pager.cursor_y += para.space_after;
1401 pager.mark_content();
1402}
1403
1404fn render_para_split(para: &ParagraphBlock, split_at: usize, space_before: f64, pager: &mut Pager) {
1407 pager.cursor_y += space_before;
1409 pager.place_anchored(¶.anchored, pager.cursor_y, para.indent_left);
1411 render_paragraph_lines(
1412 ¶.lines[..split_at],
1413 para,
1414 &pager.geometry,
1415 pager.cursor_y,
1416 &mut pager.elements,
1417 pager.media,
1418 );
1419 pager.claim_notes(¶.lines[..split_at]);
1422 pager.ink_bottom = pager.cursor_y
1423 + para.content_offset_top
1424 + para.lines[..split_at].iter().map(|l| l.height).sum::<f64>();
1425 pager.mark_content();
1426 pager.finish_page();
1427
1428 let remaining_lines = ¶.lines[split_at..];
1430 let remaining_height: f64 = remaining_lines.iter().map(|l| l.height).sum();
1431
1432 if remaining_height > pager.available_height_for(remaining_lines) {
1433 let lines_that_fit = pager.count_lines_that_fit_with_notes(remaining_lines, 0.0);
1435 if lines_that_fit > 0 && lines_that_fit < remaining_lines.len() {
1436 let temp_para = ParagraphBlock {
1438 anchored: Vec::new(),
1441 lines: remaining_lines.to_vec(),
1442 space_before: 0.0,
1443 space_after: para.space_after,
1444 borders: para.borders.clone(),
1445 shading: para.shading,
1446 indent_left: para.indent_left,
1447 indent_right: para.indent_right,
1448 jc: para.jc,
1449 keep_next: para.keep_next,
1450 keep_lines: false,
1451 page_break_before: false,
1452 widow_control: para.widow_control,
1453 heading_level: None,
1454 heading_text: None,
1455 reflow: None,
1458 content_offset_top: 0.0,
1459 };
1460 render_para_split(&temp_para, lines_that_fit, 0.0, pager);
1461 return;
1462 }
1463 }
1464
1465 render_paragraph_lines(
1467 remaining_lines,
1468 para,
1469 &pager.geometry,
1470 0.0,
1471 &mut pager.elements,
1472 pager.media,
1473 );
1474 pager.claim_notes(remaining_lines);
1475 pager.ink_bottom = remaining_height;
1476 pager.cursor_y = remaining_height + para.space_after;
1477 pager.mark_content();
1478}
1479
1480fn render_paragraph_lines(
1482 lines: &[LayoutLine],
1483 para: &ParagraphBlock,
1484 geometry: &PageGeometry,
1485 start_y: f64,
1486 elements: &mut Vec<PositionedElement>,
1487 media: &HashMap<MediaId, ImageData>,
1488) {
1489 let mut y = start_y + para.content_offset_top;
1492 for line in lines {
1493 let baseline_y = geometry.margin_top + y + line.ascent;
1494
1495 let text_width: f64 = line.items.iter().map(|item| item.width()).sum();
1497 let remaining_width = line.available_width - text_width;
1498
1499 let justify_extra =
1501 if para.jc == Some(Align::Justify) && !line.is_last && remaining_width > 0.0 {
1502 let gap_count = count_word_gaps(&line.items);
1504 if gap_count > 0 {
1505 remaining_width / gap_count as f64
1506 } else {
1507 0.0
1508 }
1509 } else {
1510 0.0
1511 };
1512
1513 let x_offset = match para.jc {
1514 Some(Align::Center) => geometry.margin_left + line.indent_left + remaining_width / 2.0,
1515 Some(Align::End) => geometry.margin_left + line.indent_left + remaining_width,
1516 Some(Align::Justify) if !line.is_last && justify_extra > 0.0 => {
1517 geometry.margin_left + line.indent_left
1519 }
1520 _ => geometry.margin_left + line.indent_left,
1521 };
1522
1523 let mut x = x_offset;
1524 let mut _accumulated_extra = 0.0;
1525
1526 for item in &line.items {
1527 match item {
1528 LineItem::Text(seg) | LineItem::Marker(seg) => {
1529 let adjusted_baseline = baseline_y - seg.baseline_offset;
1530
1531 let segment_spaces = if justify_extra > 0.0 {
1533 seg.text.chars().filter(|c| *c == ' ').count()
1534 } else {
1535 0
1536 };
1537 let segment_extra = segment_spaces as f64 * justify_extra;
1538 let effective_width = seg.width + segment_extra;
1539
1540 if let Some(hl_color) = seg.highlight {
1542 elements.push(PositionedElement::FilledRect {
1543 rect: Rect {
1544 x,
1545 y: geometry.margin_top + y,
1546 width: effective_width,
1547 height: line.height,
1548 },
1549 color: hl_color,
1550 });
1551 }
1552
1553 let advances = if justify_extra > 0.0 && segment_spaces > 0 {
1555 distribute_justify_advances(&seg.text, &seg.advances, justify_extra)
1557 } else {
1558 seg.advances.clone()
1559 };
1560
1561 elements.push(PositionedElement::Text(GlyphRun {
1562 origin: Point {
1563 x,
1564 y: adjusted_baseline,
1565 },
1566 font_id: seg.font_id,
1567 font_size: seg.font_size,
1568 glyph_ids: seg.glyph_ids.clone(),
1569 advances,
1570 text: seg.text.clone(),
1571 color: seg.color,
1572 bold: seg.bold,
1573 italic: seg.italic,
1574 field_kind: seg.field_kind,
1575 note: seg.note,
1576 }));
1577
1578 if let Some(ul_style) = seg.underline {
1580 let ul_y = adjusted_baseline + seg.descent * 0.3;
1581 let ul_thickness = match ul_style {
1582 Underline::Thick => seg.font_size / 12.0,
1583 Underline::Double => seg.font_size / 24.0,
1584 _ => seg.font_size / 18.0,
1585 };
1586 elements.push(PositionedElement::Line {
1587 start: Point { x, y: ul_y },
1588 end: Point {
1589 x: x + effective_width,
1590 y: ul_y,
1591 },
1592 width: ul_thickness,
1593 color: seg.color,
1594 dash_pattern: None,
1595 });
1596 if ul_style == Underline::Double {
1598 let ul_y2 = ul_y + ul_thickness * 2.5;
1599 elements.push(PositionedElement::Line {
1600 start: Point { x, y: ul_y2 },
1601 end: Point {
1602 x: x + effective_width,
1603 y: ul_y2,
1604 },
1605 width: ul_thickness,
1606 color: seg.color,
1607 dash_pattern: None,
1608 });
1609 }
1610 }
1611
1612 if seg.strike {
1614 let strike_y = adjusted_baseline - seg.ascent * 0.3;
1615 let strike_thickness = seg.font_size / 24.0;
1616 elements.push(PositionedElement::Line {
1617 start: Point { x, y: strike_y },
1618 end: Point {
1619 x: x + effective_width,
1620 y: strike_y,
1621 },
1622 width: strike_thickness,
1623 color: seg.color,
1624 dash_pattern: None,
1625 });
1626 }
1627
1628 if seg.dstrike {
1630 let strike_y = adjusted_baseline - seg.ascent * 0.3;
1631 let strike_thickness = seg.font_size / 24.0;
1632 let gap = strike_thickness * 2.0;
1633 elements.push(PositionedElement::Line {
1634 start: Point {
1635 x,
1636 y: strike_y - gap / 2.0,
1637 },
1638 end: Point {
1639 x: x + effective_width,
1640 y: strike_y - gap / 2.0,
1641 },
1642 width: strike_thickness,
1643 color: seg.color,
1644 dash_pattern: None,
1645 });
1646 elements.push(PositionedElement::Line {
1647 start: Point {
1648 x,
1649 y: strike_y + gap / 2.0,
1650 },
1651 end: Point {
1652 x: x + effective_width,
1653 y: strike_y + gap / 2.0,
1654 },
1655 width: strike_thickness,
1656 color: seg.color,
1657 dash_pattern: None,
1658 });
1659 }
1660
1661 if let Some(ref url) = seg.hyperlink_url {
1663 elements.push(PositionedElement::LinkAnnotation {
1664 rect: Rect {
1665 x,
1666 y: geometry.margin_top + y,
1667 width: effective_width,
1668 height: line.height,
1669 },
1670 url: url.clone(),
1671 });
1672 }
1673
1674 _accumulated_extra += segment_extra;
1675 x += effective_width;
1676 }
1677 LineItem::Tab { width, leader } => {
1678 if let Some(leader_seg) = leader {
1679 let baseline_y = geometry.margin_top + y + line.ascent;
1681 elements.push(PositionedElement::Text(GlyphRun {
1682 origin: Point { x, y: baseline_y },
1683 font_id: leader_seg.font_id,
1684 font_size: leader_seg.font_size,
1685 glyph_ids: leader_seg.glyph_ids.clone(),
1686 advances: leader_seg.advances.clone(),
1687 text: leader_seg.text.clone(),
1688 color: leader_seg.color,
1689 bold: leader_seg.bold,
1690 italic: leader_seg.italic,
1691 field_kind: None,
1692 note: None,
1693 }));
1694 }
1695 x += width;
1696 }
1697 LineItem::Image {
1698 width,
1699 height,
1700 media_id,
1701 } => {
1702 let image = media.get(media_id);
1703 elements.push(PositionedElement::Image {
1705 rect: Rect {
1706 x,
1707 y: geometry.margin_top + y,
1708 width: *width,
1709 height: *height,
1710 },
1711 data: image.map_or_else(Vec::new, |image| image.data.clone()),
1712 content_type: image
1713 .map_or_else(String::new, |image| image.content_type.clone()),
1714 media_id: *media_id,
1715 });
1716 x += width;
1717 }
1718 }
1719 }
1720
1721 y += line.height;
1722 }
1723}
1724
1725fn render_hf_blocks(
1727 blocks: &[ParagraphBlock],
1728 geometry: &PageGeometry,
1729 start_y: f64,
1730 elements: &mut Vec<PositionedElement>,
1731 media: &HashMap<MediaId, ImageData>,
1732) {
1733 let mut y = start_y - geometry.margin_top; for para in blocks {
1735 render_paragraph_lines(¶.lines, para, geometry, y, elements, media);
1736 y += para.content_height();
1737 }
1738}
1739
1740fn render_table_row(
1742 row: &crate::table::TableRow,
1743 _col_widths: &[f64],
1744 table_x: f64,
1745 row_y: f64,
1746 geometry: &PageGeometry,
1747 table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
1748 elements: &mut Vec<PositionedElement>,
1749 media: &HashMap<MediaId, ImageData>,
1750) {
1751 let mut cell_x = table_x;
1752 let num_cells = row.cells.len();
1753
1754 for (cell_idx, cell) in row.cells.iter().enumerate() {
1755 if let Some(ref shading) = cell.shading {
1757 elements.push(PositionedElement::FilledRect {
1758 rect: Rect {
1759 x: cell_x,
1760 y: row_y,
1761 width: cell.width,
1762 height: cell.height,
1763 },
1764 color: *shading,
1765 });
1766 }
1767
1768 render_cell_borders(
1770 cell_x,
1771 row_y,
1772 cell.width,
1773 cell.height,
1774 &cell.borders,
1775 table_borders,
1776 cell_idx,
1777 num_cells,
1778 cell.is_first_row,
1779 cell.is_last_row,
1780 elements,
1781 );
1782
1783 if !cell.is_vmerge_continue {
1784 let cell_margin_top = cell.margin_top;
1786 let cell_margin_left = cell.margin_left;
1787
1788 let content_height: f64 = cell.paragraphs.iter().map(|p| p.total_height()).sum();
1790 let v_offset = match cell.v_align {
1791 Some(rdocx_oxml::table::ST_VerticalJc::Center) => {
1792 ((cell.height - cell_margin_top - content_height) / 2.0).max(0.0)
1793 }
1794 Some(rdocx_oxml::table::ST_VerticalJc::Bottom) => {
1795 (cell.height - cell_margin_top - content_height).max(0.0)
1796 }
1797 _ => 0.0, };
1799
1800 let mut para_y = row_y - geometry.margin_top + cell_margin_top + v_offset;
1801 for para in &cell.paragraphs {
1802 render_paragraph_lines(
1803 ¶.lines,
1804 para,
1805 &PageGeometry {
1806 margin_left: cell_x + cell_margin_left,
1807 ..*geometry
1808 },
1809 para_y,
1810 elements,
1811 media,
1812 );
1813 para_y += para.total_height();
1814 }
1815 }
1816 cell_x += cell.width;
1817 }
1818}
1819
1820fn render_cell_borders(
1822 x: f64,
1823 y: f64,
1824 w: f64,
1825 h: f64,
1826 cell_borders: &Option<rdocx_oxml::table::CT_TblBorders>,
1827 table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
1828 cell_idx: usize,
1829 num_cells: usize,
1830 is_first_row: bool,
1831 is_last_row: bool,
1832 elements: &mut Vec<PositionedElement>,
1833) {
1834 let get_edge = |cell_edge: Option<&rdocx_oxml::borders::CT_BorderEdge>,
1836 table_edge: Option<&rdocx_oxml::borders::CT_BorderEdge>|
1837 -> Option<BorderEdge> {
1838 let edge = cell_edge.or(table_edge)?;
1839 if edge.val == ST_Border::None {
1840 return None;
1841 }
1842 let thickness = edge.sz.unwrap_or(4) as f64 / 8.0; let color = edge
1844 .color
1845 .as_ref()
1846 .filter(|c| c.as_str() != "auto")
1847 .map(|c| Color::from_hex(c))
1848 .unwrap_or(Color::BLACK);
1849 let dash = border_dash_pattern(edge.val, thickness);
1850 Some((thickness, color, dash))
1851 };
1852
1853 let table_top = table_borders.and_then(|b| {
1855 if is_first_row {
1856 b.top.as_ref()
1857 } else {
1858 b.inside_h.as_ref()
1859 }
1860 });
1861 let cell_top = cell_borders.as_ref().and_then(|b| b.top.as_ref());
1862 if let Some((thickness, color, dash_pattern)) = get_edge(cell_top, table_top) {
1863 elements.push(PositionedElement::Line {
1864 start: Point { x, y },
1865 end: Point { x: x + w, y },
1866 width: thickness,
1867 color,
1868 dash_pattern,
1869 });
1870 }
1871
1872 let table_bottom = table_borders.and_then(|b| {
1874 if is_last_row {
1875 b.bottom.as_ref()
1876 } else {
1877 b.inside_h.as_ref()
1878 }
1879 });
1880 let cell_bottom = cell_borders.as_ref().and_then(|b| b.bottom.as_ref());
1881 if let Some((thickness, color, dash_pattern)) = get_edge(cell_bottom, table_bottom) {
1882 elements.push(PositionedElement::Line {
1883 start: Point { x, y: y + h },
1884 end: Point { x: x + w, y: y + h },
1885 width: thickness,
1886 color,
1887 dash_pattern,
1888 });
1889 }
1890
1891 let table_left = table_borders.and_then(|b| {
1893 if cell_idx == 0 {
1894 b.left.as_ref()
1895 } else {
1896 b.inside_v.as_ref()
1897 }
1898 });
1899 let cell_left = cell_borders.as_ref().and_then(|b| b.left.as_ref());
1900 if let Some((thickness, color, dash_pattern)) = get_edge(cell_left, table_left) {
1901 elements.push(PositionedElement::Line {
1902 start: Point { x, y },
1903 end: Point { x, y: y + h },
1904 width: thickness,
1905 color,
1906 dash_pattern,
1907 });
1908 }
1909
1910 let table_right = table_borders.and_then(|b| {
1912 if cell_idx == num_cells - 1 {
1913 b.right.as_ref()
1914 } else {
1915 b.inside_v.as_ref()
1916 }
1917 });
1918 let cell_right = cell_borders.as_ref().and_then(|b| b.right.as_ref());
1919 if let Some((thickness, color, dash_pattern)) = get_edge(cell_right, table_right) {
1920 elements.push(PositionedElement::Line {
1921 start: Point { x: x + w, y },
1922 end: Point { x: x + w, y: y + h },
1923 width: thickness,
1924 color,
1925 dash_pattern,
1926 });
1927 }
1928}
1929
1930fn render_border_edges(
1932 borders: &rdocx_oxml::borders::CT_PBdr,
1933 x: f64,
1934 y: f64,
1935 w: f64,
1936 h: f64,
1937 elements: &mut Vec<PositionedElement>,
1938) {
1939 let render_edge = |edge: &rdocx_oxml::borders::CT_BorderEdge,
1940 start: Point,
1941 end: Point,
1942 elements: &mut Vec<PositionedElement>| {
1943 if edge.val == ST_Border::None {
1944 return;
1945 }
1946 let thickness = edge.sz.unwrap_or(4) as f64 / 8.0; let color = edge
1948 .color
1949 .as_ref()
1950 .filter(|c| c.as_str() != "auto")
1951 .map(|c| Color::from_hex(c))
1952 .unwrap_or(Color::BLACK);
1953 let dash_pattern = border_dash_pattern(edge.val, thickness);
1954
1955 if edge.val == ST_Border::Double {
1956 let gap = thickness * 2.0;
1958 let dx = end.x - start.x;
1959 let dy = end.y - start.y;
1960 let len = (dx * dx + dy * dy).sqrt();
1961 let (nx, ny) = if len > 0.0 {
1962 (-dy / len, dx / len)
1963 } else {
1964 (0.0, 1.0)
1965 };
1966 let offset = gap / 2.0;
1967 elements.push(PositionedElement::Line {
1968 start: Point {
1969 x: start.x + nx * offset,
1970 y: start.y + ny * offset,
1971 },
1972 end: Point {
1973 x: end.x + nx * offset,
1974 y: end.y + ny * offset,
1975 },
1976 width: thickness,
1977 color,
1978 dash_pattern: None,
1979 });
1980 elements.push(PositionedElement::Line {
1981 start: Point {
1982 x: start.x - nx * offset,
1983 y: start.y - ny * offset,
1984 },
1985 end: Point {
1986 x: end.x - nx * offset,
1987 y: end.y - ny * offset,
1988 },
1989 width: thickness,
1990 color,
1991 dash_pattern: None,
1992 });
1993 } else {
1994 elements.push(PositionedElement::Line {
1995 start,
1996 end,
1997 width: thickness,
1998 color,
1999 dash_pattern,
2000 });
2001 }
2002 };
2003
2004 if let Some(ref edge) = borders.top {
2005 let space = edge.space.unwrap_or(0) as f64;
2006 render_edge(
2007 edge,
2008 Point { x, y: y - space },
2009 Point {
2010 x: x + w,
2011 y: y - space,
2012 },
2013 elements,
2014 );
2015 }
2016 if let Some(ref edge) = borders.bottom {
2017 let space = edge.space.unwrap_or(0) as f64;
2018 render_edge(
2019 edge,
2020 Point {
2021 x,
2022 y: y + h + space,
2023 },
2024 Point {
2025 x: x + w,
2026 y: y + h + space,
2027 },
2028 elements,
2029 );
2030 }
2031 if let Some(ref edge) = borders.left {
2032 let space = edge.space.unwrap_or(0) as f64;
2033 render_edge(
2034 edge,
2035 Point { x: x - space, y },
2036 Point {
2037 x: x - space,
2038 y: y + h,
2039 },
2040 elements,
2041 );
2042 }
2043 if let Some(ref edge) = borders.right {
2044 let space = edge.space.unwrap_or(0) as f64;
2045 render_edge(
2046 edge,
2047 Point {
2048 x: x + w + space,
2049 y,
2050 },
2051 Point {
2052 x: x + w + space,
2053 y: y + h,
2054 },
2055 elements,
2056 );
2057 }
2058}
2059
2060fn border_dash_pattern(style: ST_Border, thickness: f64) -> Option<(f64, f64)> {
2063 match style {
2064 ST_Border::Dashed => Some((3.0 * thickness, 2.0 * thickness)),
2065 ST_Border::Dotted => Some((thickness, thickness)),
2066 ST_Border::DotDash | ST_Border::DotDotDash => Some((3.0 * thickness, thickness)),
2067 _ => None,
2068 }
2069}
2070
2071fn count_word_gaps(items: &[LineItem]) -> usize {
2073 let mut count = 0;
2074 for item in items {
2075 match item {
2076 LineItem::Text(seg) | LineItem::Marker(seg) => {
2077 count += seg.text.chars().filter(|c| *c == ' ').count();
2078 }
2079 LineItem::Tab { .. } => {
2080 count += 1;
2081 }
2082 _ => {}
2083 }
2084 }
2085 count
2086}
2087
2088fn distribute_justify_advances(text: &str, advances: &[f64], extra_per_gap: f64) -> Vec<f64> {
2090 let chars: Vec<char> = text.chars().collect();
2091 let mut result = advances.to_vec();
2092
2093 if chars.len() == result.len() {
2094 for (i, &ch) in chars.iter().enumerate() {
2096 if ch == ' ' {
2097 result[i] += extra_per_gap;
2098 }
2099 }
2100 } else {
2101 let total_extra = extra_per_gap * text.chars().filter(|c| *c == ' ').count() as f64;
2103 if !result.is_empty() {
2104 let per_glyph = total_extra / result.len() as f64;
2105 for a in &mut result {
2106 *a += per_glyph;
2107 }
2108 }
2109 }
2110
2111 result
2112}
2113
2114#[cfg(test)]
2115mod tests {
2116 use super::*;
2117 use crate::block::ParagraphBlock;
2118 use oxml_layout::LayoutLine;
2119
2120 fn empty_media() -> MediaRegistry {
2121 MediaRegistry::new(&HashMap::new())
2122 }
2123
2124 fn make_line(height: f64) -> LayoutLine {
2125 LayoutLine {
2126 items: vec![],
2127 width: 100.0,
2128 ascent: height * 0.77,
2129 descent: height * 0.23,
2130 line_gap: 0.0,
2131 height,
2132 indent_left: 0.0,
2133 available_width: 468.0,
2134 is_last: true,
2135 }
2136 }
2137
2138 fn make_para(line_count: usize, line_height: f64) -> ParagraphBlock {
2139 let mut lines = Vec::new();
2140 for _ in 0..line_count {
2141 lines.push(make_line(line_height));
2142 }
2143 ParagraphBlock {
2144 anchored: Vec::new(),
2145 lines,
2146 space_before: 0.0,
2147 space_after: 0.0,
2148 borders: None,
2149 shading: None,
2150 indent_left: 0.0,
2151 indent_right: 0.0,
2152 jc: None,
2153 keep_next: false,
2154 keep_lines: false,
2155 page_break_before: false,
2156 widow_control: true,
2157 heading_level: None,
2158 heading_text: None,
2159 reflow: None,
2160 content_offset_top: 0.0,
2161 }
2162 }
2163
2164 #[test]
2165 fn single_page_layout() {
2166 let fm = FontManager::new();
2167 let blocks = vec![LayoutBlock::Paragraph(make_para(3, 14.0))];
2168 let geom = PageGeometry::default();
2169 let (pages, _outlines) = paginate(
2170 &blocks,
2171 geom,
2172 None,
2173 false,
2174 &fm,
2175 &empty_media(),
2176 &NoteRegistry::default(),
2177 );
2178 assert_eq!(pages.len(), 1);
2179 assert_eq!(pages[0].page_number, 1);
2180 }
2181
2182 #[test]
2183 fn multi_page_overflow() {
2184 let fm = FontManager::new();
2185 let blocks = vec![LayoutBlock::Paragraph(make_para(100, 14.0))];
2187 let geom = PageGeometry::default();
2188 let (pages, _outlines) = paginate(
2189 &blocks,
2190 geom,
2191 None,
2192 false,
2193 &fm,
2194 &empty_media(),
2195 &NoteRegistry::default(),
2196 );
2197 assert!(pages.len() >= 2);
2198 }
2199
2200 #[test]
2201 fn forced_page_break() {
2202 let fm = FontManager::new();
2203 let mut para2 = make_para(3, 14.0);
2204 para2.page_break_before = true;
2205 let blocks = vec![
2206 LayoutBlock::Paragraph(make_para(3, 14.0)),
2207 LayoutBlock::Paragraph(para2),
2208 ];
2209 let geom = PageGeometry::default();
2210 let (pages, _outlines) = paginate(
2211 &blocks,
2212 geom,
2213 None,
2214 false,
2215 &fm,
2216 &empty_media(),
2217 &NoteRegistry::default(),
2218 );
2219 assert_eq!(pages.len(), 2);
2220 }
2221
2222 #[test]
2223 fn page_dimensions() {
2224 let fm = FontManager::new();
2225 let blocks = vec![LayoutBlock::Paragraph(make_para(1, 14.0))];
2226 let geom = PageGeometry::default();
2227 let (pages, _outlines) = paginate(
2228 &blocks,
2229 geom,
2230 None,
2231 false,
2232 &fm,
2233 &empty_media(),
2234 &NoteRegistry::default(),
2235 );
2236 assert!((pages[0].width - 612.0).abs() < 0.01);
2237 assert!((pages[0].height - 792.0).abs() < 0.01);
2238 }
2239
2240 fn make_text_line(height: f64, underline: Option<Underline>, strike: bool) -> LayoutLine {
2241 use oxml_layout::TextSegment;
2242 let seg = TextSegment {
2243 text: "Hello".to_string(),
2244 font_id: oxml_layout::FontId(0),
2245 font_size: 12.0,
2246 glyph_ids: vec![1, 2, 3],
2247 advances: vec![6.0, 6.0, 6.0],
2248 width: 40.0,
2249 ascent: height * 0.77,
2250 descent: height * 0.23,
2251 line_gap: 0.0,
2252 color: Color::BLACK,
2253 bold: false,
2254 italic: false,
2255 underline,
2256 strike,
2257 dstrike: false,
2258 highlight: None,
2259 baseline_offset: 0.0,
2260 hyperlink_url: None,
2261 field_kind: None,
2262 note: None,
2263 };
2264 LayoutLine {
2265 items: vec![LineItem::Text(seg)],
2266 width: 40.0,
2267 ascent: height * 0.77,
2268 descent: height * 0.23,
2269 line_gap: 0.0,
2270 height,
2271 indent_left: 0.0,
2272 available_width: 468.0,
2273 is_last: true,
2274 }
2275 }
2276
2277 #[test]
2278 fn underline_renders_line_element() {
2279 let fm = FontManager::new();
2280 let para = ParagraphBlock {
2281 anchored: Vec::new(),
2282 lines: vec![make_text_line(14.0, Some(Underline::Single), false)],
2283 space_before: 0.0,
2284 space_after: 0.0,
2285 borders: None,
2286 shading: None,
2287 indent_left: 0.0,
2288 indent_right: 0.0,
2289 jc: None,
2290 keep_next: false,
2291 keep_lines: false,
2292 page_break_before: false,
2293 widow_control: true,
2294 heading_level: None,
2295 heading_text: None,
2296 reflow: None,
2297 content_offset_top: 0.0,
2298 };
2299 let blocks = vec![LayoutBlock::Paragraph(para)];
2300 let (pages, _outlines) = paginate(
2301 &blocks,
2302 PageGeometry::default(),
2303 None,
2304 false,
2305 &fm,
2306 &empty_media(),
2307 &NoteRegistry::default(),
2308 );
2309 let lines: Vec<_> = pages[0]
2311 .elements
2312 .iter()
2313 .filter(|e| matches!(e, PositionedElement::Line { .. }))
2314 .collect();
2315 assert_eq!(lines.len(), 1, "expected 1 underline line");
2316 }
2317
2318 #[test]
2319 fn strikethrough_renders_line_element() {
2320 let fm = FontManager::new();
2321 let para = ParagraphBlock {
2322 anchored: Vec::new(),
2323 lines: vec![make_text_line(14.0, None, true)],
2324 space_before: 0.0,
2325 space_after: 0.0,
2326 borders: None,
2327 shading: None,
2328 indent_left: 0.0,
2329 indent_right: 0.0,
2330 jc: None,
2331 keep_next: false,
2332 keep_lines: false,
2333 page_break_before: false,
2334 widow_control: true,
2335 heading_level: None,
2336 heading_text: None,
2337 reflow: None,
2338 content_offset_top: 0.0,
2339 };
2340 let blocks = vec![LayoutBlock::Paragraph(para)];
2341 let (pages, _outlines) = paginate(
2342 &blocks,
2343 PageGeometry::default(),
2344 None,
2345 false,
2346 &fm,
2347 &empty_media(),
2348 &NoteRegistry::default(),
2349 );
2350 let lines: Vec<_> = pages[0]
2351 .elements
2352 .iter()
2353 .filter(|e| matches!(e, PositionedElement::Line { .. }))
2354 .collect();
2355 assert_eq!(lines.len(), 1, "expected 1 strikethrough line");
2356 }
2357
2358 #[test]
2359 fn highlight_renders_filled_rect() {
2360 use oxml_layout::TextSegment;
2361 let fm = FontManager::new();
2362 let seg = TextSegment {
2363 text: "Hi".to_string(),
2364 font_id: oxml_layout::FontId(0),
2365 font_size: 12.0,
2366 glyph_ids: vec![1],
2367 advances: vec![10.0],
2368 width: 20.0,
2369 ascent: 10.0,
2370 descent: 3.0,
2371 line_gap: 0.0,
2372 color: Color::BLACK,
2373 bold: false,
2374 italic: false,
2375 underline: None,
2376 strike: false,
2377 dstrike: false,
2378 highlight: Some(Color {
2379 r: 1.0,
2380 g: 1.0,
2381 b: 0.0,
2382 a: 1.0,
2383 }),
2384 baseline_offset: 0.0,
2385 hyperlink_url: None,
2386 field_kind: None,
2387 note: None,
2388 };
2389 let line = LayoutLine {
2390 items: vec![LineItem::Text(seg)],
2391 width: 20.0,
2392 ascent: 10.0,
2393 descent: 3.0,
2394 line_gap: 0.0,
2395 height: 13.0,
2396 indent_left: 0.0,
2397 available_width: 468.0,
2398 is_last: true,
2399 };
2400 let para = ParagraphBlock {
2401 anchored: Vec::new(),
2402 lines: vec![line],
2403 space_before: 0.0,
2404 space_after: 0.0,
2405 borders: None,
2406 shading: None,
2407 indent_left: 0.0,
2408 indent_right: 0.0,
2409 jc: None,
2410 keep_next: false,
2411 keep_lines: false,
2412 page_break_before: false,
2413 widow_control: true,
2414 heading_level: None,
2415 heading_text: None,
2416 reflow: None,
2417 content_offset_top: 0.0,
2418 };
2419 let blocks = vec![LayoutBlock::Paragraph(para)];
2420 let (pages, _outlines) = paginate(
2421 &blocks,
2422 PageGeometry::default(),
2423 None,
2424 false,
2425 &fm,
2426 &empty_media(),
2427 &NoteRegistry::default(),
2428 );
2429 let rects: Vec<_> = pages[0]
2430 .elements
2431 .iter()
2432 .filter(|e| matches!(e, PositionedElement::FilledRect { .. }))
2433 .collect();
2434 assert_eq!(rects.len(), 1, "expected 1 highlight rect");
2435 }
2436
2437 #[test]
2438 fn paragraph_borders_render_lines() {
2439 use rdocx_oxml::borders::{CT_BorderEdge, CT_PBdr};
2440 let fm = FontManager::new();
2441 let para = ParagraphBlock {
2442 anchored: Vec::new(),
2443 lines: vec![make_line(14.0)],
2444 space_before: 0.0,
2445 space_after: 0.0,
2446 borders: Some(CT_PBdr {
2447 top: Some(CT_BorderEdge {
2448 val: ST_Border::Single,
2449 sz: Some(4),
2450 space: Some(1),
2451 color: Some("000000".to_string()),
2452 }),
2453 bottom: Some(CT_BorderEdge {
2454 val: ST_Border::Single,
2455 sz: Some(4),
2456 space: Some(1),
2457 color: Some("000000".to_string()),
2458 }),
2459 ..Default::default()
2460 }),
2461 shading: None,
2462 indent_left: 0.0,
2463 indent_right: 0.0,
2464 jc: None,
2465 keep_next: false,
2466 keep_lines: false,
2467 page_break_before: false,
2468 widow_control: true,
2469 heading_level: None,
2470 heading_text: None,
2471 reflow: None,
2472 content_offset_top: 0.0,
2473 };
2474 let blocks = vec![LayoutBlock::Paragraph(para)];
2475 let (pages, _outlines) = paginate(
2476 &blocks,
2477 PageGeometry::default(),
2478 None,
2479 false,
2480 &fm,
2481 &empty_media(),
2482 &NoteRegistry::default(),
2483 );
2484 let lines: Vec<_> = pages[0]
2485 .elements
2486 .iter()
2487 .filter(|e| matches!(e, PositionedElement::Line { .. }))
2488 .collect();
2489 assert_eq!(lines.len(), 2, "expected 2 border lines (top + bottom)");
2490 }
2491
2492 #[test]
2493 fn paragraph_shading_renders_filled_rect() {
2494 let fm = FontManager::new();
2495 let para = ParagraphBlock {
2496 anchored: Vec::new(),
2497 lines: vec![make_line(14.0)],
2498 space_before: 0.0,
2499 space_after: 0.0,
2500 borders: None,
2501 shading: Some(Color {
2502 r: 1.0,
2503 g: 1.0,
2504 b: 0.0,
2505 a: 1.0,
2506 }),
2507 indent_left: 0.0,
2508 indent_right: 0.0,
2509 jc: None,
2510 keep_next: false,
2511 keep_lines: false,
2512 page_break_before: false,
2513 widow_control: true,
2514 heading_level: None,
2515 heading_text: None,
2516 reflow: None,
2517 content_offset_top: 0.0,
2518 };
2519 let blocks = vec![LayoutBlock::Paragraph(para)];
2520 let (pages, _outlines) = paginate(
2521 &blocks,
2522 PageGeometry::default(),
2523 None,
2524 false,
2525 &fm,
2526 &empty_media(),
2527 &NoteRegistry::default(),
2528 );
2529 let rects: Vec<_> = pages[0]
2530 .elements
2531 .iter()
2532 .filter(|e| matches!(e, PositionedElement::FilledRect { .. }))
2533 .collect();
2534 assert_eq!(rects.len(), 1, "expected 1 paragraph shading rect");
2535 }
2536
2537 #[test]
2538 fn double_underline_renders_two_lines() {
2539 let fm = FontManager::new();
2540 let para = ParagraphBlock {
2541 anchored: Vec::new(),
2542 lines: vec![make_text_line(14.0, Some(Underline::Double), false)],
2543 space_before: 0.0,
2544 space_after: 0.0,
2545 borders: None,
2546 shading: None,
2547 indent_left: 0.0,
2548 indent_right: 0.0,
2549 jc: None,
2550 keep_next: false,
2551 keep_lines: false,
2552 page_break_before: false,
2553 widow_control: true,
2554 heading_level: None,
2555 heading_text: None,
2556 reflow: None,
2557 content_offset_top: 0.0,
2558 };
2559 let blocks = vec![LayoutBlock::Paragraph(para)];
2560 let (pages, _outlines) = paginate(
2561 &blocks,
2562 PageGeometry::default(),
2563 None,
2564 false,
2565 &fm,
2566 &empty_media(),
2567 &NoteRegistry::default(),
2568 );
2569 let lines: Vec<_> = pages[0]
2570 .elements
2571 .iter()
2572 .filter(|e| matches!(e, PositionedElement::Line { .. }))
2573 .collect();
2574 assert_eq!(lines.len(), 2, "expected 2 lines for double underline");
2575 }
2576
2577 fn make_justified_line(text: &str, seg_width: f64, is_last: bool) -> LayoutLine {
2578 use oxml_layout::TextSegment;
2579 let seg = TextSegment {
2580 text: text.to_string(),
2581 font_id: oxml_layout::FontId(0),
2582 font_size: 12.0,
2583 glyph_ids: vec![1; text.len()],
2584 advances: vec![seg_width / text.len() as f64; text.len()],
2585 width: seg_width,
2586 ascent: 10.0,
2587 descent: 3.0,
2588 line_gap: 0.0,
2589 color: Color::BLACK,
2590 bold: false,
2591 italic: false,
2592 underline: None,
2593 strike: false,
2594 dstrike: false,
2595 highlight: None,
2596 baseline_offset: 0.0,
2597 hyperlink_url: None,
2598 field_kind: None,
2599 note: None,
2600 };
2601 LayoutLine {
2602 items: vec![LineItem::Text(seg)],
2603 width: seg_width,
2604 ascent: 10.0,
2605 descent: 3.0,
2606 line_gap: 0.0,
2607 height: 13.0,
2608 indent_left: 0.0,
2609 available_width: 468.0,
2610 is_last,
2611 }
2612 }
2613
2614 #[test]
2615 fn hyperlink_emits_link_annotation() {
2616 use oxml_layout::TextSegment;
2617 let fm = FontManager::new();
2618 let seg = TextSegment {
2619 text: "Click me".to_string(),
2620 font_id: oxml_layout::FontId(0),
2621 font_size: 12.0,
2622 glyph_ids: vec![1, 2, 3],
2623 advances: vec![8.0, 8.0, 8.0],
2624 width: 60.0,
2625 ascent: 10.0,
2626 descent: 3.0,
2627 line_gap: 0.0,
2628 color: Color::BLACK,
2629 bold: false,
2630 italic: false,
2631 underline: None,
2632 strike: false,
2633 dstrike: false,
2634 highlight: None,
2635 baseline_offset: 0.0,
2636 hyperlink_url: Some("https://example.com".to_string()),
2637 field_kind: None,
2638 note: None,
2639 };
2640 let line = LayoutLine {
2641 items: vec![LineItem::Text(seg)],
2642 width: 60.0,
2643 ascent: 10.0,
2644 descent: 3.0,
2645 line_gap: 0.0,
2646 height: 13.0,
2647 indent_left: 0.0,
2648 available_width: 468.0,
2649 is_last: true,
2650 };
2651 let para = ParagraphBlock {
2652 anchored: Vec::new(),
2653 lines: vec![line],
2654 space_before: 0.0,
2655 space_after: 0.0,
2656 borders: None,
2657 shading: None,
2658 indent_left: 0.0,
2659 indent_right: 0.0,
2660 jc: None,
2661 keep_next: false,
2662 keep_lines: false,
2663 page_break_before: false,
2664 widow_control: true,
2665 heading_level: None,
2666 heading_text: None,
2667 reflow: None,
2668 content_offset_top: 0.0,
2669 };
2670 let blocks = vec![LayoutBlock::Paragraph(para)];
2671 let (pages, _outlines) = paginate(
2672 &blocks,
2673 PageGeometry::default(),
2674 None,
2675 false,
2676 &fm,
2677 &empty_media(),
2678 &NoteRegistry::default(),
2679 );
2680 let annotations: Vec<_> = pages[0]
2681 .elements
2682 .iter()
2683 .filter(|e| matches!(e, PositionedElement::LinkAnnotation { .. }))
2684 .collect();
2685 assert_eq!(annotations.len(), 1, "expected 1 link annotation");
2686 if let PositionedElement::LinkAnnotation { url, .. } = annotations[0] {
2687 assert_eq!(url, "https://example.com");
2688 }
2689 }
2690
2691 #[test]
2692 fn justified_text_fills_line_width() {
2693 let fm = FontManager::new();
2694 let para = ParagraphBlock {
2696 anchored: Vec::new(),
2697 lines: vec![
2698 make_justified_line("Hello World", 200.0, false),
2699 make_justified_line("End.", 40.0, true),
2700 ],
2701 space_before: 0.0,
2702 space_after: 0.0,
2703 borders: None,
2704 shading: None,
2705 indent_left: 0.0,
2706 indent_right: 0.0,
2707 jc: Some(Align::Justify),
2708 keep_next: false,
2709 keep_lines: false,
2710 page_break_before: false,
2711 widow_control: true,
2712 heading_level: None,
2713 heading_text: None,
2714 reflow: None,
2715 content_offset_top: 0.0,
2716 };
2717
2718 let blocks = vec![LayoutBlock::Paragraph(para)];
2719 let (pages, _outlines) = paginate(
2720 &blocks,
2721 PageGeometry::default(),
2722 None,
2723 false,
2724 &fm,
2725 &empty_media(),
2726 &NoteRegistry::default(),
2727 );
2728
2729 let first_text = pages[0].elements.iter().find_map(|e| {
2731 if let PositionedElement::Text(run) = e {
2732 Some(run)
2733 } else {
2734 None
2735 }
2736 });
2737 assert!(first_text.is_some());
2738 let run = first_text.unwrap();
2739 let total_advance: f64 = run.advances.iter().sum();
2741 assert!(
2742 total_advance > 200.0,
2743 "justified text should be wider than original: {total_advance}"
2744 );
2745 }
2746
2747 #[test]
2748 fn justified_last_line_stays_left_aligned() {
2749 let fm = FontManager::new();
2750 let para = ParagraphBlock {
2751 anchored: Vec::new(),
2752 lines: vec![
2753 make_justified_line("Hello World Test", 200.0, false),
2754 make_justified_line("End.", 40.0, true),
2755 ],
2756 space_before: 0.0,
2757 space_after: 0.0,
2758 borders: None,
2759 shading: None,
2760 indent_left: 0.0,
2761 indent_right: 0.0,
2762 jc: Some(Align::Justify),
2763 keep_next: false,
2764 keep_lines: false,
2765 page_break_before: false,
2766 widow_control: true,
2767 heading_level: None,
2768 heading_text: None,
2769 reflow: None,
2770 content_offset_top: 0.0,
2771 };
2772
2773 let blocks = vec![LayoutBlock::Paragraph(para)];
2774 let (pages, _outlines) = paginate(
2775 &blocks,
2776 PageGeometry::default(),
2777 None,
2778 false,
2779 &fm,
2780 &empty_media(),
2781 &NoteRegistry::default(),
2782 );
2783
2784 let text_runs: Vec<_> = pages[0]
2786 .elements
2787 .iter()
2788 .filter_map(|e| {
2789 if let PositionedElement::Text(run) = e {
2790 Some(run)
2791 } else {
2792 None
2793 }
2794 })
2795 .collect();
2796
2797 assert!(text_runs.len() >= 2);
2798 let last_advance: f64 = text_runs[1].advances.iter().sum();
2800 assert!(
2801 (last_advance - 40.0).abs() < 0.1,
2802 "last line should stay at original width: {last_advance}"
2803 );
2804 }
2805
2806 #[test]
2807 fn justified_single_word_not_stretched() {
2808 let fm = FontManager::new();
2809 let para = ParagraphBlock {
2811 anchored: Vec::new(),
2812 lines: vec![
2813 make_justified_line("Superlongword", 100.0, false),
2814 make_justified_line("End.", 40.0, true),
2815 ],
2816 space_before: 0.0,
2817 space_after: 0.0,
2818 borders: None,
2819 shading: None,
2820 indent_left: 0.0,
2821 indent_right: 0.0,
2822 jc: Some(Align::Justify),
2823 keep_next: false,
2824 keep_lines: false,
2825 page_break_before: false,
2826 widow_control: true,
2827 heading_level: None,
2828 heading_text: None,
2829 reflow: None,
2830 content_offset_top: 0.0,
2831 };
2832
2833 let blocks = vec![LayoutBlock::Paragraph(para)];
2834 let (pages, _outlines) = paginate(
2835 &blocks,
2836 PageGeometry::default(),
2837 None,
2838 false,
2839 &fm,
2840 &empty_media(),
2841 &NoteRegistry::default(),
2842 );
2843
2844 let first_text = pages[0].elements.iter().find_map(|e| {
2845 if let PositionedElement::Text(run) = e {
2846 Some(run)
2847 } else {
2848 None
2849 }
2850 });
2851 assert!(first_text.is_some());
2852 let run = first_text.unwrap();
2853 let total_advance: f64 = run.advances.iter().sum();
2854 assert!(
2856 (total_advance - 100.0).abs() < 0.1,
2857 "single word should not be stretched: {total_advance}"
2858 );
2859 }
2860
2861 #[test]
2865 fn anchor_offsets_resolve_against_their_frame() {
2866 let g = PageGeometry::default(); let para_top = 100.0;
2868 let off = 10.0;
2869
2870 assert_eq!(
2871 resolve_anchor_h(ST_RelativeFromH::Page, off, None, 0.0, &g, 0.0),
2872 10.0
2873 );
2874 assert_eq!(
2875 resolve_anchor_h(ST_RelativeFromH::LeftMargin, off, None, 0.0, &g, 0.0),
2876 10.0
2877 );
2878 assert_eq!(
2879 resolve_anchor_h(ST_RelativeFromH::Margin, off, None, 0.0, &g, 0.0),
2880 82.0,
2881 "margin-relative starts at the left margin"
2882 );
2883 assert_eq!(
2884 resolve_anchor_h(ST_RelativeFromH::Column, off, None, 0.0, &g, 0.0),
2885 82.0,
2886 "column-relative starts at the text area"
2887 );
2888 assert_eq!(
2889 resolve_anchor_h(ST_RelativeFromH::RightMargin, off, None, 0.0, &g, 0.0),
2890 550.0,
2891 "right-margin-relative starts at the right margin edge"
2892 );
2893 assert_eq!(
2894 resolve_anchor_h(ST_RelativeFromH::Character, off, None, 0.0, &g, 36.0),
2895 118.0,
2896 "character-relative includes the paragraph indent"
2897 );
2898
2899 assert_eq!(
2900 resolve_anchor_v(ST_RelativeFromV::Page, off, None, 0.0, &g, para_top),
2901 10.0
2902 );
2903 assert_eq!(
2904 resolve_anchor_v(ST_RelativeFromV::TopMargin, off, None, 0.0, &g, para_top),
2905 10.0
2906 );
2907 assert_eq!(
2908 resolve_anchor_v(ST_RelativeFromV::Margin, off, None, 0.0, &g, para_top),
2909 82.0
2910 );
2911 assert_eq!(
2912 resolve_anchor_v(ST_RelativeFromV::Paragraph, off, None, 0.0, &g, para_top),
2913 182.0,
2914 "paragraph-relative follows the paragraph down the page"
2915 );
2916 assert_eq!(
2917 resolve_anchor_v(ST_RelativeFromV::Line, off, None, 0.0, &g, para_top),
2918 182.0
2919 );
2920 assert_eq!(
2921 resolve_anchor_v(ST_RelativeFromV::BottomMargin, off, None, 0.0, &g, para_top),
2922 730.0
2923 );
2924 }
2925
2926 #[test]
2929 fn paragraph_relative_anchor_tracks_the_paragraph() {
2930 let g = PageGeometry::default();
2931 let near_top = resolve_anchor_v(ST_RelativeFromV::Paragraph, 5.0, None, 0.0, &g, 0.0);
2932 let further_down = resolve_anchor_v(ST_RelativeFromV::Paragraph, 5.0, None, 0.0, &g, 300.0);
2933 assert_eq!(near_top, 77.0);
2934 assert_eq!(further_down, 377.0);
2935 assert!(further_down > near_top);
2936 }
2937
2938 #[test]
2941 fn an_aligned_anchor_resolves_against_its_frame() {
2942 let g = PageGeometry::default();
2943 let width = 100.0;
2944 let height = 50.0;
2945
2946 let text_left = g.margin_left;
2948 let text_width = g.page_width - g.margin_left - g.margin_right;
2949
2950 assert_eq!(
2951 resolve_anchor_h(
2952 ST_RelativeFromH::Margin,
2953 999.0,
2954 Some(AnchorAlignH::Left),
2955 width,
2956 &g,
2957 0.0
2958 ),
2959 text_left,
2960 "an alignment replaces the offset rather than adding to it"
2961 );
2962 assert_eq!(
2963 resolve_anchor_h(
2964 ST_RelativeFromH::Margin,
2965 0.0,
2966 Some(AnchorAlignH::Right),
2967 width,
2968 &g,
2969 0.0
2970 ),
2971 text_left + text_width - width
2972 );
2973 assert_eq!(
2974 resolve_anchor_h(
2975 ST_RelativeFromH::Margin,
2976 0.0,
2977 Some(AnchorAlignH::Center),
2978 width,
2979 &g,
2980 0.0
2981 ),
2982 text_left + (text_width - width) / 2.0
2983 );
2984
2985 assert_eq!(
2987 resolve_anchor_v(
2988 ST_RelativeFromV::Page,
2989 0.0,
2990 Some(AnchorAlignV::Top),
2991 height,
2992 &g,
2993 0.0
2994 ),
2995 0.0
2996 );
2997 assert_eq!(
2998 resolve_anchor_v(
2999 ST_RelativeFromV::Page,
3000 0.0,
3001 Some(AnchorAlignV::Bottom),
3002 height,
3003 &g,
3004 0.0
3005 ),
3006 g.page_height - height
3007 );
3008 }
3009
3010 #[test]
3011 fn an_anchor_without_an_alignment_still_uses_its_offset() {
3012 let g = PageGeometry::default();
3014 assert_eq!(
3015 resolve_anchor_h(ST_RelativeFromH::Page, 10.0, None, 100.0, &g, 0.0),
3016 10.0
3017 );
3018 assert_eq!(
3019 resolve_anchor_h(ST_RelativeFromH::Margin, 10.0, None, 100.0, &g, 0.0),
3020 g.margin_left + 10.0
3021 );
3022 assert_eq!(
3023 resolve_anchor_v(ST_RelativeFromV::Paragraph, 5.0, None, 50.0, &g, 300.0),
3024 g.margin_top + 300.0 + 5.0
3025 );
3026 }
3027}