1use crate::block::{AnchoredContent, AnchoredDrawing, LayoutBlock, ParagraphBlock, ShapePreset};
7use std::collections::HashMap;
8
9use oxml_layout::{
10 Align, Color, FontManager, GlyphRun, LayoutLine, LineItem, MediaId, OutlineEntry, PageFrame,
11 Point, PositionedElement, Rect, Underline,
12};
13
14use rdocx_oxml::drawing::{ST_RelativeFromH, ST_RelativeFromV};
15use rdocx_oxml::shared::ST_Border;
16
17use crate::input::{ImageData, MediaRegistry};
18
19type BorderEdge = (f64, Color, Option<(f64, f64)>);
21
22#[derive(Debug, Clone, Copy)]
24pub struct PageGeometry {
25 pub page_width: f64,
26 pub page_height: f64,
27 pub margin_top: f64,
28 pub margin_right: f64,
29 pub margin_bottom: f64,
30 pub margin_left: f64,
31 pub header_distance: f64,
32 pub footer_distance: f64,
33}
34
35impl PageGeometry {
36 pub fn content_width(&self) -> f64 {
38 self.page_width - self.margin_left - self.margin_right
39 }
40
41 pub fn content_height(&self) -> f64 {
43 self.page_height - self.margin_top - self.margin_bottom
44 }
45}
46
47impl Default for PageGeometry {
48 fn default() -> Self {
49 PageGeometry {
51 page_width: 612.0,
52 page_height: 792.0,
53 margin_top: 72.0,
54 margin_right: 72.0,
55 margin_bottom: 72.0,
56 margin_left: 72.0,
57 header_distance: 36.0,
58 footer_distance: 36.0,
59 }
60 }
61}
62
63pub struct HeaderFooterContent {
65 pub header_blocks: Vec<ParagraphBlock>,
66 pub footer_blocks: Vec<ParagraphBlock>,
67 pub first_header_blocks: Vec<ParagraphBlock>,
69 pub first_footer_blocks: Vec<ParagraphBlock>,
71}
72
73pub struct Section {
75 pub blocks: Vec<LayoutBlock>,
76 pub geometry: PageGeometry,
77 pub header_footer: Option<HeaderFooterContent>,
78 pub title_pg: bool,
80}
81
82pub fn paginate_sections(
84 sections: &[Section],
85 fm: &FontManager,
86 media: &MediaRegistry,
87) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
88 let media = media.media();
89 if sections.is_empty() {
90 return (
91 vec![PageFrame::new(1, 612.0, 792.0, Vec::new())],
92 Vec::new(),
93 );
94 }
95
96 if sections.len() == 1 {
98 let s = §ions[0];
99 return paginate_with_media(
100 &s.blocks,
101 s.geometry,
102 s.header_footer.as_ref(),
103 s.title_pg,
104 fm,
105 media,
106 );
107 }
108
109 let mut all_pages = Vec::new();
111 let mut all_outlines = Vec::new();
112 let mut page_offset = 0;
113
114 for section in sections {
115 let (mut pages, mut outlines) = paginate_with_media(
116 §ion.blocks,
117 section.geometry,
118 section.header_footer.as_ref(),
119 section.title_pg,
120 fm,
121 media,
122 );
123
124 for page in &mut pages {
126 page.page_number += page_offset;
127 }
128 for outline in &mut outlines {
129 outline.page_index += page_offset;
130 }
131
132 page_offset += pages.len();
133 all_pages.append(&mut pages);
134 all_outlines.append(&mut outlines);
135 }
136
137 for (i, page) in all_pages.iter_mut().enumerate() {
140 page.page_number = i + 1;
141 }
142
143 (all_pages, all_outlines)
144}
145
146pub fn paginate(
148 blocks: &[LayoutBlock],
149 geometry: PageGeometry,
150 header_footer: Option<&HeaderFooterContent>,
151 title_pg: bool,
152 _fm: &FontManager,
153 media: &MediaRegistry,
154) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
155 paginate_with_media(
156 blocks,
157 geometry,
158 header_footer,
159 title_pg,
160 _fm,
161 media.media(),
162 )
163}
164
165fn paginate_with_media(
166 blocks: &[LayoutBlock],
167 geometry: PageGeometry,
168 header_footer: Option<&HeaderFooterContent>,
169 title_pg: bool,
170 _fm: &FontManager,
171 media: &HashMap<MediaId, ImageData>,
172) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
173 let mut pager = Pager::new(geometry, header_footer, title_pg, media);
174
175 for (block_idx, block) in blocks.iter().enumerate() {
176 if block.page_break_before() && pager.has_content() {
178 pager.finish_page();
179 }
180
181 match block {
182 LayoutBlock::Paragraph(para) => {
183 if let (Some(level), Some(title)) = (para.heading_level, ¶.heading_text) {
185 pager.outlines.push(OutlineEntry {
186 title: title.clone(),
187 level,
188 page_index: pager.page_number - 1,
189 y_position: pager.geometry.margin_top + pager.cursor_y,
190 });
191 }
192 paginate_paragraph(para, block_idx, blocks, &mut pager);
193 }
194 LayoutBlock::Table(table) => {
195 let table_x = geometry.margin_left + table.table_indent;
196 let tbl_borders = table.borders.as_ref();
197
198 for (row_idx, row) in table.rows.iter().enumerate() {
199 if pager.cursor_y + row.height > pager.content_height && pager.has_content() {
200 pager.finish_page();
201
202 for &hdr_idx in &table.header_row_indices {
204 if hdr_idx < row_idx {
205 let hdr_row = &table.rows[hdr_idx];
206 render_table_row(
207 hdr_row,
208 &table.col_widths,
209 table_x,
210 pager.geometry.margin_top + pager.cursor_y,
211 &pager.geometry,
212 tbl_borders,
213 &mut pager.elements,
214 pager.media,
215 );
216 pager.cursor_y += hdr_row.height;
217 pager.mark_content();
218 }
219 }
220 }
221
222 render_table_row(
223 row,
224 &table.col_widths,
225 table_x,
226 pager.geometry.margin_top + pager.cursor_y,
227 &pager.geometry,
228 tbl_borders,
229 &mut pager.elements,
230 pager.media,
231 );
232 pager.cursor_y += row.height;
233 pager.mark_content();
234 }
235 }
236 }
237 }
238
239 pager.flush()
240}
241
242struct Pager<'a> {
244 pages: Vec<PageFrame>,
245 elements: Vec<PositionedElement>,
246 behind_elements: Vec<PositionedElement>,
250 cursor_y: f64,
251 page_number: usize,
252 content_height: f64,
253 geometry: PageGeometry,
254 header_footer: Option<&'a HeaderFooterContent>,
255 has_content_flag: bool,
256 outlines: Vec<OutlineEntry>,
257 is_first_page: bool,
259 title_pg: bool,
261 media: &'a HashMap<MediaId, ImageData>,
262}
263
264impl<'a> Pager<'a> {
265 fn new(
266 geometry: PageGeometry,
267 header_footer: Option<&'a HeaderFooterContent>,
268 title_pg: bool,
269 media: &'a HashMap<MediaId, ImageData>,
270 ) -> Self {
271 Pager {
272 pages: Vec::new(),
273 elements: Vec::new(),
274 behind_elements: Vec::new(),
275 cursor_y: 0.0,
276 page_number: 1,
277 content_height: geometry.content_height(),
278 geometry,
279 header_footer,
280 has_content_flag: false,
281 outlines: Vec::new(),
282 is_first_page: true,
283 title_pg,
284 media,
285 }
286 }
287
288 fn has_content(&self) -> bool {
289 self.has_content_flag
290 }
291
292 fn mark_content(&mut self) {
293 self.has_content_flag = true;
294 }
295
296 fn place_anchored(&mut self, anchored: &[AnchoredDrawing], para_top: f64, indent_left: f64) {
299 for a in anchored {
300 let x = resolve_anchor_h(a.rel_h, a.off_h, &self.geometry, indent_left);
301 let y = resolve_anchor_v(a.rel_v, a.off_v, &self.geometry, para_top);
302 let rect = Rect {
303 x,
304 y,
305 width: a.width,
306 height: a.height,
307 };
308
309 let mut produced: Vec<PositionedElement> = Vec::new();
310
311 match &a.content {
312 AnchoredContent::Image { media_id } => {
313 let image = self.media.get(media_id);
314 produced.push(PositionedElement::Image {
315 rect,
316 data: image.map_or_else(Vec::new, |image| image.data.clone()),
317 content_type: image
318 .map_or_else(String::new, |image| image.content_type.clone()),
319 media_id: *media_id,
320 });
321 }
322 AnchoredContent::Shape { preset, fill, text } => {
323 match (preset, fill) {
326 (ShapePreset::Rect, Some(color)) => {
327 produced.push(PositionedElement::FilledRect {
328 rect,
329 color: *color,
330 });
331 }
332 (ShapePreset::Line, Some(color)) => {
333 produced.push(PositionedElement::Line {
336 start: Point { x, y },
337 end: Point {
338 x: x + a.width,
339 y: y + a.height,
340 },
341 width: 1.0,
342 color: *color,
343 dash_pattern: None,
344 });
345 }
346 _ => {}
347 }
348 produced.extend(render_shape_text(text, &self.geometry, rect, self.media));
349 }
350 }
351
352 if a.behind_doc {
353 self.behind_elements.append(&mut produced);
354 } else {
355 self.elements.append(&mut produced);
356 }
357 }
358 }
359
360 fn finish_page(&mut self) {
361 let mut all_elements = Vec::new();
362
363 all_elements.append(&mut self.behind_elements);
365
366 if let Some(hf) = self.header_footer {
367 let header_blocks = if self.is_first_page && self.title_pg {
369 &hf.first_header_blocks
370 } else {
371 &hf.header_blocks
372 };
373 if !header_blocks.is_empty() {
374 let header_y = self.geometry.header_distance;
375 render_hf_blocks(
376 header_blocks,
377 &self.geometry,
378 header_y,
379 &mut all_elements,
380 self.media,
381 );
382 }
383 }
384
385 all_elements.append(&mut self.elements);
386
387 if let Some(hf) = self.header_footer {
388 let footer_blocks = if self.is_first_page && self.title_pg {
390 &hf.first_footer_blocks
391 } else {
392 &hf.footer_blocks
393 };
394 if !footer_blocks.is_empty() {
395 let footer_height: f64 = footer_blocks.iter().map(|b| b.content_height()).sum();
396 let footer_y =
397 self.geometry.page_height - self.geometry.footer_distance - footer_height;
398 render_hf_blocks(
399 footer_blocks,
400 &self.geometry,
401 footer_y,
402 &mut all_elements,
403 self.media,
404 );
405 }
406 }
407
408 self.pages.push(PageFrame::new(
409 self.page_number,
410 self.geometry.page_width,
411 self.geometry.page_height,
412 all_elements,
413 ));
414 self.page_number += 1;
415 self.cursor_y = 0.0;
416 self.has_content_flag = false;
417 self.is_first_page = false;
418 }
419
420 fn flush(mut self) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
421 if self.has_content() || self.pages.is_empty() {
423 self.finish_page();
424 }
425 (self.pages, self.outlines)
426 }
427}
428
429fn translate_element(element: &mut PositionedElement, dx: f64, dy: f64) {
435 match element {
436 PositionedElement::Text(run) => {
437 run.origin.x += dx;
438 run.origin.y += dy;
439 }
440 PositionedElement::Line { start, end, .. } => {
441 start.x += dx;
442 start.y += dy;
443 end.x += dx;
444 end.y += dy;
445 }
446 PositionedElement::FilledRect { rect, .. }
447 | PositionedElement::Image { rect, .. }
448 | PositionedElement::LinkAnnotation { rect, .. } => {
449 rect.x += dx;
450 rect.y += dy;
451 }
452 _ => {}
453 }
454}
455
456fn render_shape_text(
462 text: &[ParagraphBlock],
463 geometry: &PageGeometry,
464 rect: Rect,
465 media: &HashMap<MediaId, ImageData>,
466) -> Vec<PositionedElement> {
467 if text.is_empty() {
468 return Vec::new();
469 }
470
471 let mut local = Vec::new();
472 let mut y = 0.0;
473 for para in text {
474 render_paragraph_lines(¶.lines, para, geometry, y, &mut local, media);
475 y += para.content_height();
476 }
477
478 let dx = rect.x - geometry.margin_left;
481 let dy = rect.y - geometry.margin_top;
482 for element in &mut local {
483 translate_element(element, dx, dy);
484 }
485 local
486}
487
488fn resolve_anchor_h(rel: ST_RelativeFromH, off: f64, g: &PageGeometry, indent_left: f64) -> f64 {
494 match rel {
495 ST_RelativeFromH::Page | ST_RelativeFromH::LeftMargin => off,
496 ST_RelativeFromH::RightMargin | ST_RelativeFromH::OutsideMargin => {
497 g.page_width - g.margin_right + off
498 }
499 ST_RelativeFromH::InsideMargin => g.margin_left + off,
500 ST_RelativeFromH::Character => g.margin_left + indent_left + off,
502 ST_RelativeFromH::Margin | ST_RelativeFromH::Column => g.margin_left + off,
505 }
506}
507
508fn resolve_anchor_v(rel: ST_RelativeFromV, off: f64, g: &PageGeometry, para_top: f64) -> f64 {
513 match rel {
514 ST_RelativeFromV::Page | ST_RelativeFromV::TopMargin => off,
515 ST_RelativeFromV::BottomMargin | ST_RelativeFromV::OutsideMargin => {
516 g.page_height - g.margin_bottom + off
517 }
518 ST_RelativeFromV::Margin | ST_RelativeFromV::InsideMargin => g.margin_top + off,
519 ST_RelativeFromV::Paragraph | ST_RelativeFromV::Line => g.margin_top + para_top + off,
523 }
524}
525
526fn paginate_paragraph(
527 para: &ParagraphBlock,
528 block_idx: usize,
529 blocks: &[LayoutBlock],
530 pager: &mut Pager,
531) {
532 let space_before = if pager.cursor_y == 0.0 {
533 0.0
534 } else {
535 para.space_before
536 };
537
538 let total_needed = space_before + para.content_height();
540 let remaining = pager.content_height - pager.cursor_y;
541
542 if total_needed > remaining && pager.has_content() {
543 if para.keep_lines || para.lines.len() <= 2 {
545 pager.finish_page();
546 paginate_paragraph(para, block_idx, blocks, pager);
548 return;
549 }
550
551 let available_for_lines = remaining - space_before;
552 let lines_that_fit = count_lines_that_fit(¶.lines, available_for_lines);
553
554 if para.widow_control && lines_that_fit < 2 {
555 pager.finish_page();
557 paginate_paragraph(para, block_idx, blocks, pager);
558 return;
559 }
560
561 let lines_remaining = para.lines.len() - lines_that_fit;
562 if para.widow_control && lines_remaining < 2 && lines_that_fit >= 3 {
563 let split_at = lines_that_fit - 1;
565 render_para_split(para, split_at, space_before, pager);
566 return;
567 }
568
569 if lines_that_fit > 0 {
570 render_para_split(para, lines_that_fit, space_before, pager);
571 return;
572 }
573
574 pager.finish_page();
576 paginate_paragraph(para, block_idx, blocks, pager);
577 return;
578 }
579
580 if total_needed > pager.content_height && pager.cursor_y == 0.0 {
583 let lines_that_fit = count_lines_that_fit(¶.lines, pager.content_height);
585 if lines_that_fit > 0 && lines_that_fit < para.lines.len() {
586 render_para_split(para, lines_that_fit, 0.0, pager);
587 return;
588 }
589 }
590
591 if para.keep_next && block_idx + 1 < blocks.len() {
593 let next_first = match &blocks[block_idx + 1] {
594 LayoutBlock::Paragraph(p) => p.lines.first().map(|l| l.height).unwrap_or(0.0),
595 LayoutBlock::Table(t) => t.rows.first().map(|r| r.height).unwrap_or(0.0),
596 };
597 if pager.cursor_y + space_before + para.content_height() + next_first > pager.content_height
598 && pager.has_content()
599 {
600 pager.finish_page();
601 }
602 }
603
604 let space = if pager.cursor_y == 0.0 {
606 0.0
607 } else {
608 para.space_before
609 };
610 pager.cursor_y += space;
611
612 if let Some(shading) = para.shading {
613 pager.elements.push(PositionedElement::FilledRect {
614 rect: Rect {
615 x: pager.geometry.margin_left + para.indent_left,
616 y: pager.geometry.margin_top + pager.cursor_y,
617 width: pager.geometry.content_width() - para.indent_left - para.indent_right,
618 height: para.content_height(),
619 },
620 color: shading,
621 });
622 }
623
624 if let Some(ref borders) = para.borders {
626 let border_x = pager.geometry.margin_left + para.indent_left;
627 let border_y = pager.geometry.margin_top + pager.cursor_y;
628 let border_w = pager.geometry.content_width() - para.indent_left - para.indent_right;
629 let border_h = para.content_height();
630 render_border_edges(
631 borders,
632 border_x,
633 border_y,
634 border_w,
635 border_h,
636 &mut pager.elements,
637 );
638 }
639
640 pager.place_anchored(¶.anchored, pager.cursor_y, para.indent_left);
643
644 render_paragraph_lines(
645 ¶.lines,
646 para,
647 &pager.geometry,
648 pager.cursor_y,
649 &mut pager.elements,
650 pager.media,
651 );
652 pager.cursor_y += para.content_height();
653 pager.cursor_y += para.space_after;
654 pager.mark_content();
655}
656
657fn render_para_split(para: &ParagraphBlock, split_at: usize, space_before: f64, pager: &mut Pager) {
660 pager.cursor_y += space_before;
662 pager.place_anchored(¶.anchored, pager.cursor_y, para.indent_left);
664 render_paragraph_lines(
665 ¶.lines[..split_at],
666 para,
667 &pager.geometry,
668 pager.cursor_y,
669 &mut pager.elements,
670 pager.media,
671 );
672 pager.mark_content();
673 pager.finish_page();
674
675 let remaining_lines = ¶.lines[split_at..];
677 let remaining_height: f64 = remaining_lines.iter().map(|l| l.height).sum();
678
679 if remaining_height > pager.content_height {
680 let lines_that_fit = count_lines_that_fit(remaining_lines, pager.content_height);
682 if lines_that_fit > 0 && lines_that_fit < remaining_lines.len() {
683 let temp_para = ParagraphBlock {
685 anchored: Vec::new(),
688 lines: remaining_lines.to_vec(),
689 space_before: 0.0,
690 space_after: para.space_after,
691 borders: para.borders.clone(),
692 shading: para.shading,
693 indent_left: para.indent_left,
694 indent_right: para.indent_right,
695 jc: para.jc,
696 keep_next: para.keep_next,
697 keep_lines: false,
698 page_break_before: false,
699 widow_control: para.widow_control,
700 heading_level: None,
701 heading_text: None,
702 };
703 render_para_split(&temp_para, lines_that_fit, 0.0, pager);
704 return;
705 }
706 }
707
708 render_paragraph_lines(
710 remaining_lines,
711 para,
712 &pager.geometry,
713 0.0,
714 &mut pager.elements,
715 pager.media,
716 );
717 pager.cursor_y = remaining_height + para.space_after;
718 pager.mark_content();
719}
720
721fn count_lines_that_fit(lines: &[LayoutLine], available: f64) -> usize {
723 let mut used = 0.0;
724 for (i, line) in lines.iter().enumerate() {
725 used += line.height;
726 if used > available {
727 return i;
728 }
729 }
730 lines.len()
731}
732
733fn render_paragraph_lines(
735 lines: &[LayoutLine],
736 para: &ParagraphBlock,
737 geometry: &PageGeometry,
738 start_y: f64,
739 elements: &mut Vec<PositionedElement>,
740 media: &HashMap<MediaId, ImageData>,
741) {
742 let mut y = start_y;
743 for line in lines {
744 let baseline_y = geometry.margin_top + y + line.ascent;
745
746 let text_width: f64 = line.items.iter().map(|item| item.width()).sum();
748 let remaining_width = line.available_width - text_width;
749
750 let justify_extra =
752 if para.jc == Some(Align::Justify) && !line.is_last && remaining_width > 0.0 {
753 let gap_count = count_word_gaps(&line.items);
755 if gap_count > 0 {
756 remaining_width / gap_count as f64
757 } else {
758 0.0
759 }
760 } else {
761 0.0
762 };
763
764 let x_offset = match para.jc {
765 Some(Align::Center) => geometry.margin_left + line.indent_left + remaining_width / 2.0,
766 Some(Align::End) => geometry.margin_left + line.indent_left + remaining_width,
767 Some(Align::Justify) if !line.is_last && justify_extra > 0.0 => {
768 geometry.margin_left + line.indent_left
770 }
771 _ => geometry.margin_left + line.indent_left,
772 };
773
774 let mut x = x_offset;
775 let mut _accumulated_extra = 0.0;
776
777 for item in &line.items {
778 match item {
779 LineItem::Text(seg) | LineItem::Marker(seg) => {
780 let adjusted_baseline = baseline_y - seg.baseline_offset;
781
782 let segment_spaces = if justify_extra > 0.0 {
784 seg.text.chars().filter(|c| *c == ' ').count()
785 } else {
786 0
787 };
788 let segment_extra = segment_spaces as f64 * justify_extra;
789 let effective_width = seg.width + segment_extra;
790
791 if let Some(hl_color) = seg.highlight {
793 elements.push(PositionedElement::FilledRect {
794 rect: Rect {
795 x,
796 y: geometry.margin_top + y,
797 width: effective_width,
798 height: line.height,
799 },
800 color: hl_color,
801 });
802 }
803
804 let advances = if justify_extra > 0.0 && segment_spaces > 0 {
806 distribute_justify_advances(&seg.text, &seg.advances, justify_extra)
808 } else {
809 seg.advances.clone()
810 };
811
812 elements.push(PositionedElement::Text(GlyphRun {
813 origin: Point {
814 x,
815 y: adjusted_baseline,
816 },
817 font_id: seg.font_id,
818 font_size: seg.font_size,
819 glyph_ids: seg.glyph_ids.clone(),
820 advances,
821 text: seg.text.clone(),
822 color: seg.color,
823 bold: seg.bold,
824 italic: seg.italic,
825 field_kind: seg.field_kind,
826 footnote_id: seg.footnote_id,
827 }));
828
829 if let Some(ul_style) = seg.underline {
831 let ul_y = adjusted_baseline + seg.descent * 0.3;
832 let ul_thickness = match ul_style {
833 Underline::Thick => seg.font_size / 12.0,
834 Underline::Double => seg.font_size / 24.0,
835 _ => seg.font_size / 18.0,
836 };
837 elements.push(PositionedElement::Line {
838 start: Point { x, y: ul_y },
839 end: Point {
840 x: x + effective_width,
841 y: ul_y,
842 },
843 width: ul_thickness,
844 color: seg.color,
845 dash_pattern: None,
846 });
847 if ul_style == Underline::Double {
849 let ul_y2 = ul_y + ul_thickness * 2.5;
850 elements.push(PositionedElement::Line {
851 start: Point { x, y: ul_y2 },
852 end: Point {
853 x: x + effective_width,
854 y: ul_y2,
855 },
856 width: ul_thickness,
857 color: seg.color,
858 dash_pattern: None,
859 });
860 }
861 }
862
863 if seg.strike {
865 let strike_y = adjusted_baseline - seg.ascent * 0.3;
866 let strike_thickness = seg.font_size / 24.0;
867 elements.push(PositionedElement::Line {
868 start: Point { x, y: strike_y },
869 end: Point {
870 x: x + effective_width,
871 y: strike_y,
872 },
873 width: strike_thickness,
874 color: seg.color,
875 dash_pattern: None,
876 });
877 }
878
879 if seg.dstrike {
881 let strike_y = adjusted_baseline - seg.ascent * 0.3;
882 let strike_thickness = seg.font_size / 24.0;
883 let gap = strike_thickness * 2.0;
884 elements.push(PositionedElement::Line {
885 start: Point {
886 x,
887 y: strike_y - gap / 2.0,
888 },
889 end: Point {
890 x: x + effective_width,
891 y: strike_y - gap / 2.0,
892 },
893 width: strike_thickness,
894 color: seg.color,
895 dash_pattern: None,
896 });
897 elements.push(PositionedElement::Line {
898 start: Point {
899 x,
900 y: strike_y + gap / 2.0,
901 },
902 end: Point {
903 x: x + effective_width,
904 y: strike_y + gap / 2.0,
905 },
906 width: strike_thickness,
907 color: seg.color,
908 dash_pattern: None,
909 });
910 }
911
912 if let Some(ref url) = seg.hyperlink_url {
914 elements.push(PositionedElement::LinkAnnotation {
915 rect: Rect {
916 x,
917 y: geometry.margin_top + y,
918 width: effective_width,
919 height: line.height,
920 },
921 url: url.clone(),
922 });
923 }
924
925 _accumulated_extra += segment_extra;
926 x += effective_width;
927 }
928 LineItem::Tab { width, leader } => {
929 if let Some(leader_seg) = leader {
930 let baseline_y = geometry.margin_top + y + line.ascent;
932 elements.push(PositionedElement::Text(GlyphRun {
933 origin: Point { x, y: baseline_y },
934 font_id: leader_seg.font_id,
935 font_size: leader_seg.font_size,
936 glyph_ids: leader_seg.glyph_ids.clone(),
937 advances: leader_seg.advances.clone(),
938 text: leader_seg.text.clone(),
939 color: leader_seg.color,
940 bold: leader_seg.bold,
941 italic: leader_seg.italic,
942 field_kind: None,
943 footnote_id: None,
944 }));
945 }
946 x += width;
947 }
948 LineItem::Image {
949 width,
950 height,
951 media_id,
952 } => {
953 let image = media.get(media_id);
954 elements.push(PositionedElement::Image {
956 rect: Rect {
957 x,
958 y: geometry.margin_top + y,
959 width: *width,
960 height: *height,
961 },
962 data: image.map_or_else(Vec::new, |image| image.data.clone()),
963 content_type: image
964 .map_or_else(String::new, |image| image.content_type.clone()),
965 media_id: *media_id,
966 });
967 x += width;
968 }
969 }
970 }
971
972 y += line.height;
973 }
974}
975
976fn render_hf_blocks(
978 blocks: &[ParagraphBlock],
979 geometry: &PageGeometry,
980 start_y: f64,
981 elements: &mut Vec<PositionedElement>,
982 media: &HashMap<MediaId, ImageData>,
983) {
984 let mut y = start_y - geometry.margin_top; for para in blocks {
986 render_paragraph_lines(¶.lines, para, geometry, y, elements, media);
987 y += para.content_height();
988 }
989}
990
991fn render_table_row(
993 row: &crate::table::TableRow,
994 _col_widths: &[f64],
995 table_x: f64,
996 row_y: f64,
997 geometry: &PageGeometry,
998 table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
999 elements: &mut Vec<PositionedElement>,
1000 media: &HashMap<MediaId, ImageData>,
1001) {
1002 let mut cell_x = table_x;
1003 let num_cells = row.cells.len();
1004
1005 for (cell_idx, cell) in row.cells.iter().enumerate() {
1006 if let Some(ref shading) = cell.shading {
1008 elements.push(PositionedElement::FilledRect {
1009 rect: Rect {
1010 x: cell_x,
1011 y: row_y,
1012 width: cell.width,
1013 height: cell.height,
1014 },
1015 color: *shading,
1016 });
1017 }
1018
1019 render_cell_borders(
1021 cell_x,
1022 row_y,
1023 cell.width,
1024 cell.height,
1025 &cell.borders,
1026 table_borders,
1027 cell_idx,
1028 num_cells,
1029 cell.is_first_row,
1030 cell.is_last_row,
1031 elements,
1032 );
1033
1034 if !cell.is_vmerge_continue {
1035 let cell_margin_top = cell.margin_top;
1037 let cell_margin_left = cell.margin_left;
1038
1039 let content_height: f64 = cell.paragraphs.iter().map(|p| p.total_height()).sum();
1041 let v_offset = match cell.v_align {
1042 Some(rdocx_oxml::table::ST_VerticalJc::Center) => {
1043 ((cell.height - cell_margin_top - content_height) / 2.0).max(0.0)
1044 }
1045 Some(rdocx_oxml::table::ST_VerticalJc::Bottom) => {
1046 (cell.height - cell_margin_top - content_height).max(0.0)
1047 }
1048 _ => 0.0, };
1050
1051 let mut para_y = row_y - geometry.margin_top + cell_margin_top + v_offset;
1052 for para in &cell.paragraphs {
1053 render_paragraph_lines(
1054 ¶.lines,
1055 para,
1056 &PageGeometry {
1057 margin_left: cell_x + cell_margin_left,
1058 ..*geometry
1059 },
1060 para_y,
1061 elements,
1062 media,
1063 );
1064 para_y += para.total_height();
1065 }
1066 }
1067 cell_x += cell.width;
1068 }
1069}
1070
1071fn render_cell_borders(
1073 x: f64,
1074 y: f64,
1075 w: f64,
1076 h: f64,
1077 cell_borders: &Option<rdocx_oxml::table::CT_TblBorders>,
1078 table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
1079 cell_idx: usize,
1080 num_cells: usize,
1081 is_first_row: bool,
1082 is_last_row: bool,
1083 elements: &mut Vec<PositionedElement>,
1084) {
1085 let get_edge = |cell_edge: Option<&rdocx_oxml::borders::CT_BorderEdge>,
1087 table_edge: Option<&rdocx_oxml::borders::CT_BorderEdge>|
1088 -> Option<BorderEdge> {
1089 let edge = cell_edge.or(table_edge)?;
1090 if edge.val == ST_Border::None {
1091 return None;
1092 }
1093 let thickness = edge.sz.unwrap_or(4) as f64 / 8.0; let color = edge
1095 .color
1096 .as_ref()
1097 .filter(|c| c.as_str() != "auto")
1098 .map(|c| Color::from_hex(c))
1099 .unwrap_or(Color::BLACK);
1100 let dash = border_dash_pattern(edge.val, thickness);
1101 Some((thickness, color, dash))
1102 };
1103
1104 let table_top = table_borders.and_then(|b| {
1106 if is_first_row {
1107 b.top.as_ref()
1108 } else {
1109 b.inside_h.as_ref()
1110 }
1111 });
1112 let cell_top = cell_borders.as_ref().and_then(|b| b.top.as_ref());
1113 if let Some((thickness, color, dash_pattern)) = get_edge(cell_top, table_top) {
1114 elements.push(PositionedElement::Line {
1115 start: Point { x, y },
1116 end: Point { x: x + w, y },
1117 width: thickness,
1118 color,
1119 dash_pattern,
1120 });
1121 }
1122
1123 let table_bottom = table_borders.and_then(|b| {
1125 if is_last_row {
1126 b.bottom.as_ref()
1127 } else {
1128 b.inside_h.as_ref()
1129 }
1130 });
1131 let cell_bottom = cell_borders.as_ref().and_then(|b| b.bottom.as_ref());
1132 if let Some((thickness, color, dash_pattern)) = get_edge(cell_bottom, table_bottom) {
1133 elements.push(PositionedElement::Line {
1134 start: Point { x, y: y + h },
1135 end: Point { x: x + w, y: y + h },
1136 width: thickness,
1137 color,
1138 dash_pattern,
1139 });
1140 }
1141
1142 let table_left = table_borders.and_then(|b| {
1144 if cell_idx == 0 {
1145 b.left.as_ref()
1146 } else {
1147 b.inside_v.as_ref()
1148 }
1149 });
1150 let cell_left = cell_borders.as_ref().and_then(|b| b.left.as_ref());
1151 if let Some((thickness, color, dash_pattern)) = get_edge(cell_left, table_left) {
1152 elements.push(PositionedElement::Line {
1153 start: Point { x, y },
1154 end: Point { x, y: y + h },
1155 width: thickness,
1156 color,
1157 dash_pattern,
1158 });
1159 }
1160
1161 let table_right = table_borders.and_then(|b| {
1163 if cell_idx == num_cells - 1 {
1164 b.right.as_ref()
1165 } else {
1166 b.inside_v.as_ref()
1167 }
1168 });
1169 let cell_right = cell_borders.as_ref().and_then(|b| b.right.as_ref());
1170 if let Some((thickness, color, dash_pattern)) = get_edge(cell_right, table_right) {
1171 elements.push(PositionedElement::Line {
1172 start: Point { x: x + w, y },
1173 end: Point { x: x + w, y: y + h },
1174 width: thickness,
1175 color,
1176 dash_pattern,
1177 });
1178 }
1179}
1180
1181fn render_border_edges(
1183 borders: &rdocx_oxml::borders::CT_PBdr,
1184 x: f64,
1185 y: f64,
1186 w: f64,
1187 h: f64,
1188 elements: &mut Vec<PositionedElement>,
1189) {
1190 let render_edge = |edge: &rdocx_oxml::borders::CT_BorderEdge,
1191 start: Point,
1192 end: Point,
1193 elements: &mut Vec<PositionedElement>| {
1194 if edge.val == ST_Border::None {
1195 return;
1196 }
1197 let thickness = edge.sz.unwrap_or(4) as f64 / 8.0; let color = edge
1199 .color
1200 .as_ref()
1201 .filter(|c| c.as_str() != "auto")
1202 .map(|c| Color::from_hex(c))
1203 .unwrap_or(Color::BLACK);
1204 let dash_pattern = border_dash_pattern(edge.val, thickness);
1205
1206 if edge.val == ST_Border::Double {
1207 let gap = thickness * 2.0;
1209 let dx = end.x - start.x;
1210 let dy = end.y - start.y;
1211 let len = (dx * dx + dy * dy).sqrt();
1212 let (nx, ny) = if len > 0.0 {
1213 (-dy / len, dx / len)
1214 } else {
1215 (0.0, 1.0)
1216 };
1217 let offset = gap / 2.0;
1218 elements.push(PositionedElement::Line {
1219 start: Point {
1220 x: start.x + nx * offset,
1221 y: start.y + ny * offset,
1222 },
1223 end: Point {
1224 x: end.x + nx * offset,
1225 y: end.y + ny * offset,
1226 },
1227 width: thickness,
1228 color,
1229 dash_pattern: None,
1230 });
1231 elements.push(PositionedElement::Line {
1232 start: Point {
1233 x: start.x - nx * offset,
1234 y: start.y - ny * offset,
1235 },
1236 end: Point {
1237 x: end.x - nx * offset,
1238 y: end.y - ny * offset,
1239 },
1240 width: thickness,
1241 color,
1242 dash_pattern: None,
1243 });
1244 } else {
1245 elements.push(PositionedElement::Line {
1246 start,
1247 end,
1248 width: thickness,
1249 color,
1250 dash_pattern,
1251 });
1252 }
1253 };
1254
1255 if let Some(ref edge) = borders.top {
1256 let space = edge.space.unwrap_or(0) as f64;
1257 render_edge(
1258 edge,
1259 Point { x, y: y - space },
1260 Point {
1261 x: x + w,
1262 y: y - space,
1263 },
1264 elements,
1265 );
1266 }
1267 if let Some(ref edge) = borders.bottom {
1268 let space = edge.space.unwrap_or(0) as f64;
1269 render_edge(
1270 edge,
1271 Point {
1272 x,
1273 y: y + h + space,
1274 },
1275 Point {
1276 x: x + w,
1277 y: y + h + space,
1278 },
1279 elements,
1280 );
1281 }
1282 if let Some(ref edge) = borders.left {
1283 let space = edge.space.unwrap_or(0) as f64;
1284 render_edge(
1285 edge,
1286 Point { x: x - space, y },
1287 Point {
1288 x: x - space,
1289 y: y + h,
1290 },
1291 elements,
1292 );
1293 }
1294 if let Some(ref edge) = borders.right {
1295 let space = edge.space.unwrap_or(0) as f64;
1296 render_edge(
1297 edge,
1298 Point {
1299 x: x + w + space,
1300 y,
1301 },
1302 Point {
1303 x: x + w + space,
1304 y: y + h,
1305 },
1306 elements,
1307 );
1308 }
1309}
1310
1311fn border_dash_pattern(style: ST_Border, thickness: f64) -> Option<(f64, f64)> {
1314 match style {
1315 ST_Border::Dashed => Some((3.0 * thickness, 2.0 * thickness)),
1316 ST_Border::Dotted => Some((thickness, thickness)),
1317 ST_Border::DotDash | ST_Border::DotDotDash => Some((3.0 * thickness, thickness)),
1318 _ => None,
1319 }
1320}
1321
1322fn count_word_gaps(items: &[LineItem]) -> usize {
1324 let mut count = 0;
1325 for item in items {
1326 match item {
1327 LineItem::Text(seg) | LineItem::Marker(seg) => {
1328 count += seg.text.chars().filter(|c| *c == ' ').count();
1329 }
1330 LineItem::Tab { .. } => {
1331 count += 1;
1332 }
1333 _ => {}
1334 }
1335 }
1336 count
1337}
1338
1339fn distribute_justify_advances(text: &str, advances: &[f64], extra_per_gap: f64) -> Vec<f64> {
1341 let chars: Vec<char> = text.chars().collect();
1342 let mut result = advances.to_vec();
1343
1344 if chars.len() == result.len() {
1345 for (i, &ch) in chars.iter().enumerate() {
1347 if ch == ' ' {
1348 result[i] += extra_per_gap;
1349 }
1350 }
1351 } else {
1352 let total_extra = extra_per_gap * text.chars().filter(|c| *c == ' ').count() as f64;
1354 if !result.is_empty() {
1355 let per_glyph = total_extra / result.len() as f64;
1356 for a in &mut result {
1357 *a += per_glyph;
1358 }
1359 }
1360 }
1361
1362 result
1363}
1364
1365#[cfg(test)]
1366mod tests {
1367 use super::*;
1368 use crate::block::ParagraphBlock;
1369 use oxml_layout::LayoutLine;
1370
1371 fn empty_media() -> MediaRegistry {
1372 MediaRegistry::new(&HashMap::new())
1373 }
1374
1375 fn make_line(height: f64) -> LayoutLine {
1376 LayoutLine {
1377 items: vec![],
1378 width: 100.0,
1379 ascent: height * 0.77,
1380 descent: height * 0.23,
1381 line_gap: 0.0,
1382 height,
1383 indent_left: 0.0,
1384 available_width: 468.0,
1385 is_last: true,
1386 }
1387 }
1388
1389 fn make_para(line_count: usize, line_height: f64) -> ParagraphBlock {
1390 let mut lines = Vec::new();
1391 for _ in 0..line_count {
1392 lines.push(make_line(line_height));
1393 }
1394 ParagraphBlock {
1395 anchored: Vec::new(),
1396 lines,
1397 space_before: 0.0,
1398 space_after: 0.0,
1399 borders: None,
1400 shading: None,
1401 indent_left: 0.0,
1402 indent_right: 0.0,
1403 jc: None,
1404 keep_next: false,
1405 keep_lines: false,
1406 page_break_before: false,
1407 widow_control: true,
1408 heading_level: None,
1409 heading_text: None,
1410 }
1411 }
1412
1413 #[test]
1414 fn single_page_layout() {
1415 let fm = FontManager::new();
1416 let blocks = vec![LayoutBlock::Paragraph(make_para(3, 14.0))];
1417 let geom = PageGeometry::default();
1418 let (pages, _outlines) = paginate(&blocks, geom, None, false, &fm, &empty_media());
1419 assert_eq!(pages.len(), 1);
1420 assert_eq!(pages[0].page_number, 1);
1421 }
1422
1423 #[test]
1424 fn multi_page_overflow() {
1425 let fm = FontManager::new();
1426 let blocks = vec![LayoutBlock::Paragraph(make_para(100, 14.0))];
1428 let geom = PageGeometry::default();
1429 let (pages, _outlines) = paginate(&blocks, geom, None, false, &fm, &empty_media());
1430 assert!(pages.len() >= 2);
1431 }
1432
1433 #[test]
1434 fn forced_page_break() {
1435 let fm = FontManager::new();
1436 let mut para2 = make_para(3, 14.0);
1437 para2.page_break_before = true;
1438 let blocks = vec![
1439 LayoutBlock::Paragraph(make_para(3, 14.0)),
1440 LayoutBlock::Paragraph(para2),
1441 ];
1442 let geom = PageGeometry::default();
1443 let (pages, _outlines) = paginate(&blocks, geom, None, false, &fm, &empty_media());
1444 assert_eq!(pages.len(), 2);
1445 }
1446
1447 #[test]
1448 fn page_dimensions() {
1449 let fm = FontManager::new();
1450 let blocks = vec![LayoutBlock::Paragraph(make_para(1, 14.0))];
1451 let geom = PageGeometry::default();
1452 let (pages, _outlines) = paginate(&blocks, geom, None, false, &fm, &empty_media());
1453 assert!((pages[0].width - 612.0).abs() < 0.01);
1454 assert!((pages[0].height - 792.0).abs() < 0.01);
1455 }
1456
1457 fn make_text_line(height: f64, underline: Option<Underline>, strike: bool) -> LayoutLine {
1458 use oxml_layout::TextSegment;
1459 let seg = TextSegment {
1460 text: "Hello".to_string(),
1461 font_id: oxml_layout::FontId(0),
1462 font_size: 12.0,
1463 glyph_ids: vec![1, 2, 3],
1464 advances: vec![6.0, 6.0, 6.0],
1465 width: 40.0,
1466 ascent: height * 0.77,
1467 descent: height * 0.23,
1468 line_gap: 0.0,
1469 color: Color::BLACK,
1470 bold: false,
1471 italic: false,
1472 underline,
1473 strike,
1474 dstrike: false,
1475 highlight: None,
1476 baseline_offset: 0.0,
1477 hyperlink_url: None,
1478 field_kind: None,
1479 footnote_id: None,
1480 };
1481 LayoutLine {
1482 items: vec![LineItem::Text(seg)],
1483 width: 40.0,
1484 ascent: height * 0.77,
1485 descent: height * 0.23,
1486 line_gap: 0.0,
1487 height,
1488 indent_left: 0.0,
1489 available_width: 468.0,
1490 is_last: true,
1491 }
1492 }
1493
1494 #[test]
1495 fn underline_renders_line_element() {
1496 let fm = FontManager::new();
1497 let para = ParagraphBlock {
1498 anchored: Vec::new(),
1499 lines: vec![make_text_line(14.0, Some(Underline::Single), false)],
1500 space_before: 0.0,
1501 space_after: 0.0,
1502 borders: None,
1503 shading: None,
1504 indent_left: 0.0,
1505 indent_right: 0.0,
1506 jc: None,
1507 keep_next: false,
1508 keep_lines: false,
1509 page_break_before: false,
1510 widow_control: true,
1511 heading_level: None,
1512 heading_text: None,
1513 };
1514 let blocks = vec![LayoutBlock::Paragraph(para)];
1515 let (pages, _outlines) = paginate(
1516 &blocks,
1517 PageGeometry::default(),
1518 None,
1519 false,
1520 &fm,
1521 &empty_media(),
1522 );
1523 let lines: Vec<_> = pages[0]
1525 .elements
1526 .iter()
1527 .filter(|e| matches!(e, PositionedElement::Line { .. }))
1528 .collect();
1529 assert_eq!(lines.len(), 1, "expected 1 underline line");
1530 }
1531
1532 #[test]
1533 fn strikethrough_renders_line_element() {
1534 let fm = FontManager::new();
1535 let para = ParagraphBlock {
1536 anchored: Vec::new(),
1537 lines: vec![make_text_line(14.0, None, true)],
1538 space_before: 0.0,
1539 space_after: 0.0,
1540 borders: None,
1541 shading: None,
1542 indent_left: 0.0,
1543 indent_right: 0.0,
1544 jc: None,
1545 keep_next: false,
1546 keep_lines: false,
1547 page_break_before: false,
1548 widow_control: true,
1549 heading_level: None,
1550 heading_text: None,
1551 };
1552 let blocks = vec![LayoutBlock::Paragraph(para)];
1553 let (pages, _outlines) = paginate(
1554 &blocks,
1555 PageGeometry::default(),
1556 None,
1557 false,
1558 &fm,
1559 &empty_media(),
1560 );
1561 let lines: Vec<_> = pages[0]
1562 .elements
1563 .iter()
1564 .filter(|e| matches!(e, PositionedElement::Line { .. }))
1565 .collect();
1566 assert_eq!(lines.len(), 1, "expected 1 strikethrough line");
1567 }
1568
1569 #[test]
1570 fn highlight_renders_filled_rect() {
1571 use oxml_layout::TextSegment;
1572 let fm = FontManager::new();
1573 let seg = TextSegment {
1574 text: "Hi".to_string(),
1575 font_id: oxml_layout::FontId(0),
1576 font_size: 12.0,
1577 glyph_ids: vec![1],
1578 advances: vec![10.0],
1579 width: 20.0,
1580 ascent: 10.0,
1581 descent: 3.0,
1582 line_gap: 0.0,
1583 color: Color::BLACK,
1584 bold: false,
1585 italic: false,
1586 underline: None,
1587 strike: false,
1588 dstrike: false,
1589 highlight: Some(Color {
1590 r: 1.0,
1591 g: 1.0,
1592 b: 0.0,
1593 a: 1.0,
1594 }),
1595 baseline_offset: 0.0,
1596 hyperlink_url: None,
1597 field_kind: None,
1598 footnote_id: None,
1599 };
1600 let line = LayoutLine {
1601 items: vec![LineItem::Text(seg)],
1602 width: 20.0,
1603 ascent: 10.0,
1604 descent: 3.0,
1605 line_gap: 0.0,
1606 height: 13.0,
1607 indent_left: 0.0,
1608 available_width: 468.0,
1609 is_last: true,
1610 };
1611 let para = ParagraphBlock {
1612 anchored: Vec::new(),
1613 lines: vec![line],
1614 space_before: 0.0,
1615 space_after: 0.0,
1616 borders: None,
1617 shading: None,
1618 indent_left: 0.0,
1619 indent_right: 0.0,
1620 jc: None,
1621 keep_next: false,
1622 keep_lines: false,
1623 page_break_before: false,
1624 widow_control: true,
1625 heading_level: None,
1626 heading_text: None,
1627 };
1628 let blocks = vec![LayoutBlock::Paragraph(para)];
1629 let (pages, _outlines) = paginate(
1630 &blocks,
1631 PageGeometry::default(),
1632 None,
1633 false,
1634 &fm,
1635 &empty_media(),
1636 );
1637 let rects: Vec<_> = pages[0]
1638 .elements
1639 .iter()
1640 .filter(|e| matches!(e, PositionedElement::FilledRect { .. }))
1641 .collect();
1642 assert_eq!(rects.len(), 1, "expected 1 highlight rect");
1643 }
1644
1645 #[test]
1646 fn paragraph_borders_render_lines() {
1647 use rdocx_oxml::borders::{CT_BorderEdge, CT_PBdr};
1648 let fm = FontManager::new();
1649 let para = ParagraphBlock {
1650 anchored: Vec::new(),
1651 lines: vec![make_line(14.0)],
1652 space_before: 0.0,
1653 space_after: 0.0,
1654 borders: Some(CT_PBdr {
1655 top: Some(CT_BorderEdge {
1656 val: ST_Border::Single,
1657 sz: Some(4),
1658 space: Some(1),
1659 color: Some("000000".to_string()),
1660 }),
1661 bottom: Some(CT_BorderEdge {
1662 val: ST_Border::Single,
1663 sz: Some(4),
1664 space: Some(1),
1665 color: Some("000000".to_string()),
1666 }),
1667 ..Default::default()
1668 }),
1669 shading: None,
1670 indent_left: 0.0,
1671 indent_right: 0.0,
1672 jc: None,
1673 keep_next: false,
1674 keep_lines: false,
1675 page_break_before: false,
1676 widow_control: true,
1677 heading_level: None,
1678 heading_text: None,
1679 };
1680 let blocks = vec![LayoutBlock::Paragraph(para)];
1681 let (pages, _outlines) = paginate(
1682 &blocks,
1683 PageGeometry::default(),
1684 None,
1685 false,
1686 &fm,
1687 &empty_media(),
1688 );
1689 let lines: Vec<_> = pages[0]
1690 .elements
1691 .iter()
1692 .filter(|e| matches!(e, PositionedElement::Line { .. }))
1693 .collect();
1694 assert_eq!(lines.len(), 2, "expected 2 border lines (top + bottom)");
1695 }
1696
1697 #[test]
1698 fn paragraph_shading_renders_filled_rect() {
1699 let fm = FontManager::new();
1700 let para = ParagraphBlock {
1701 anchored: Vec::new(),
1702 lines: vec![make_line(14.0)],
1703 space_before: 0.0,
1704 space_after: 0.0,
1705 borders: None,
1706 shading: Some(Color {
1707 r: 1.0,
1708 g: 1.0,
1709 b: 0.0,
1710 a: 1.0,
1711 }),
1712 indent_left: 0.0,
1713 indent_right: 0.0,
1714 jc: None,
1715 keep_next: false,
1716 keep_lines: false,
1717 page_break_before: false,
1718 widow_control: true,
1719 heading_level: None,
1720 heading_text: None,
1721 };
1722 let blocks = vec![LayoutBlock::Paragraph(para)];
1723 let (pages, _outlines) = paginate(
1724 &blocks,
1725 PageGeometry::default(),
1726 None,
1727 false,
1728 &fm,
1729 &empty_media(),
1730 );
1731 let rects: Vec<_> = pages[0]
1732 .elements
1733 .iter()
1734 .filter(|e| matches!(e, PositionedElement::FilledRect { .. }))
1735 .collect();
1736 assert_eq!(rects.len(), 1, "expected 1 paragraph shading rect");
1737 }
1738
1739 #[test]
1740 fn double_underline_renders_two_lines() {
1741 let fm = FontManager::new();
1742 let para = ParagraphBlock {
1743 anchored: Vec::new(),
1744 lines: vec![make_text_line(14.0, Some(Underline::Double), false)],
1745 space_before: 0.0,
1746 space_after: 0.0,
1747 borders: None,
1748 shading: None,
1749 indent_left: 0.0,
1750 indent_right: 0.0,
1751 jc: None,
1752 keep_next: false,
1753 keep_lines: false,
1754 page_break_before: false,
1755 widow_control: true,
1756 heading_level: None,
1757 heading_text: None,
1758 };
1759 let blocks = vec![LayoutBlock::Paragraph(para)];
1760 let (pages, _outlines) = paginate(
1761 &blocks,
1762 PageGeometry::default(),
1763 None,
1764 false,
1765 &fm,
1766 &empty_media(),
1767 );
1768 let lines: Vec<_> = pages[0]
1769 .elements
1770 .iter()
1771 .filter(|e| matches!(e, PositionedElement::Line { .. }))
1772 .collect();
1773 assert_eq!(lines.len(), 2, "expected 2 lines for double underline");
1774 }
1775
1776 fn make_justified_line(text: &str, seg_width: f64, is_last: bool) -> LayoutLine {
1777 use oxml_layout::TextSegment;
1778 let seg = TextSegment {
1779 text: text.to_string(),
1780 font_id: oxml_layout::FontId(0),
1781 font_size: 12.0,
1782 glyph_ids: vec![1; text.len()],
1783 advances: vec![seg_width / text.len() as f64; text.len()],
1784 width: seg_width,
1785 ascent: 10.0,
1786 descent: 3.0,
1787 line_gap: 0.0,
1788 color: Color::BLACK,
1789 bold: false,
1790 italic: false,
1791 underline: None,
1792 strike: false,
1793 dstrike: false,
1794 highlight: None,
1795 baseline_offset: 0.0,
1796 hyperlink_url: None,
1797 field_kind: None,
1798 footnote_id: None,
1799 };
1800 LayoutLine {
1801 items: vec![LineItem::Text(seg)],
1802 width: seg_width,
1803 ascent: 10.0,
1804 descent: 3.0,
1805 line_gap: 0.0,
1806 height: 13.0,
1807 indent_left: 0.0,
1808 available_width: 468.0,
1809 is_last,
1810 }
1811 }
1812
1813 #[test]
1814 fn hyperlink_emits_link_annotation() {
1815 use oxml_layout::TextSegment;
1816 let fm = FontManager::new();
1817 let seg = TextSegment {
1818 text: "Click me".to_string(),
1819 font_id: oxml_layout::FontId(0),
1820 font_size: 12.0,
1821 glyph_ids: vec![1, 2, 3],
1822 advances: vec![8.0, 8.0, 8.0],
1823 width: 60.0,
1824 ascent: 10.0,
1825 descent: 3.0,
1826 line_gap: 0.0,
1827 color: Color::BLACK,
1828 bold: false,
1829 italic: false,
1830 underline: None,
1831 strike: false,
1832 dstrike: false,
1833 highlight: None,
1834 baseline_offset: 0.0,
1835 hyperlink_url: Some("https://example.com".to_string()),
1836 field_kind: None,
1837 footnote_id: None,
1838 };
1839 let line = LayoutLine {
1840 items: vec![LineItem::Text(seg)],
1841 width: 60.0,
1842 ascent: 10.0,
1843 descent: 3.0,
1844 line_gap: 0.0,
1845 height: 13.0,
1846 indent_left: 0.0,
1847 available_width: 468.0,
1848 is_last: true,
1849 };
1850 let para = ParagraphBlock {
1851 anchored: Vec::new(),
1852 lines: vec![line],
1853 space_before: 0.0,
1854 space_after: 0.0,
1855 borders: None,
1856 shading: None,
1857 indent_left: 0.0,
1858 indent_right: 0.0,
1859 jc: None,
1860 keep_next: false,
1861 keep_lines: false,
1862 page_break_before: false,
1863 widow_control: true,
1864 heading_level: None,
1865 heading_text: None,
1866 };
1867 let blocks = vec![LayoutBlock::Paragraph(para)];
1868 let (pages, _outlines) = paginate(
1869 &blocks,
1870 PageGeometry::default(),
1871 None,
1872 false,
1873 &fm,
1874 &empty_media(),
1875 );
1876 let annotations: Vec<_> = pages[0]
1877 .elements
1878 .iter()
1879 .filter(|e| matches!(e, PositionedElement::LinkAnnotation { .. }))
1880 .collect();
1881 assert_eq!(annotations.len(), 1, "expected 1 link annotation");
1882 if let PositionedElement::LinkAnnotation { url, .. } = annotations[0] {
1883 assert_eq!(url, "https://example.com");
1884 }
1885 }
1886
1887 #[test]
1888 fn justified_text_fills_line_width() {
1889 let fm = FontManager::new();
1890 let para = ParagraphBlock {
1892 anchored: Vec::new(),
1893 lines: vec![
1894 make_justified_line("Hello World", 200.0, false),
1895 make_justified_line("End.", 40.0, true),
1896 ],
1897 space_before: 0.0,
1898 space_after: 0.0,
1899 borders: None,
1900 shading: None,
1901 indent_left: 0.0,
1902 indent_right: 0.0,
1903 jc: Some(Align::Justify),
1904 keep_next: false,
1905 keep_lines: false,
1906 page_break_before: false,
1907 widow_control: true,
1908 heading_level: None,
1909 heading_text: None,
1910 };
1911
1912 let blocks = vec![LayoutBlock::Paragraph(para)];
1913 let (pages, _outlines) = paginate(
1914 &blocks,
1915 PageGeometry::default(),
1916 None,
1917 false,
1918 &fm,
1919 &empty_media(),
1920 );
1921
1922 let first_text = pages[0].elements.iter().find_map(|e| {
1924 if let PositionedElement::Text(run) = e {
1925 Some(run)
1926 } else {
1927 None
1928 }
1929 });
1930 assert!(first_text.is_some());
1931 let run = first_text.unwrap();
1932 let total_advance: f64 = run.advances.iter().sum();
1934 assert!(
1935 total_advance > 200.0,
1936 "justified text should be wider than original: {total_advance}"
1937 );
1938 }
1939
1940 #[test]
1941 fn justified_last_line_stays_left_aligned() {
1942 let fm = FontManager::new();
1943 let para = ParagraphBlock {
1944 anchored: Vec::new(),
1945 lines: vec![
1946 make_justified_line("Hello World Test", 200.0, false),
1947 make_justified_line("End.", 40.0, true),
1948 ],
1949 space_before: 0.0,
1950 space_after: 0.0,
1951 borders: None,
1952 shading: None,
1953 indent_left: 0.0,
1954 indent_right: 0.0,
1955 jc: Some(Align::Justify),
1956 keep_next: false,
1957 keep_lines: false,
1958 page_break_before: false,
1959 widow_control: true,
1960 heading_level: None,
1961 heading_text: None,
1962 };
1963
1964 let blocks = vec![LayoutBlock::Paragraph(para)];
1965 let (pages, _outlines) = paginate(
1966 &blocks,
1967 PageGeometry::default(),
1968 None,
1969 false,
1970 &fm,
1971 &empty_media(),
1972 );
1973
1974 let text_runs: Vec<_> = pages[0]
1976 .elements
1977 .iter()
1978 .filter_map(|e| {
1979 if let PositionedElement::Text(run) = e {
1980 Some(run)
1981 } else {
1982 None
1983 }
1984 })
1985 .collect();
1986
1987 assert!(text_runs.len() >= 2);
1988 let last_advance: f64 = text_runs[1].advances.iter().sum();
1990 assert!(
1991 (last_advance - 40.0).abs() < 0.1,
1992 "last line should stay at original width: {last_advance}"
1993 );
1994 }
1995
1996 #[test]
1997 fn justified_single_word_not_stretched() {
1998 let fm = FontManager::new();
1999 let para = ParagraphBlock {
2001 anchored: Vec::new(),
2002 lines: vec![
2003 make_justified_line("Superlongword", 100.0, false),
2004 make_justified_line("End.", 40.0, true),
2005 ],
2006 space_before: 0.0,
2007 space_after: 0.0,
2008 borders: None,
2009 shading: None,
2010 indent_left: 0.0,
2011 indent_right: 0.0,
2012 jc: Some(Align::Justify),
2013 keep_next: false,
2014 keep_lines: false,
2015 page_break_before: false,
2016 widow_control: true,
2017 heading_level: None,
2018 heading_text: None,
2019 };
2020
2021 let blocks = vec![LayoutBlock::Paragraph(para)];
2022 let (pages, _outlines) = paginate(
2023 &blocks,
2024 PageGeometry::default(),
2025 None,
2026 false,
2027 &fm,
2028 &empty_media(),
2029 );
2030
2031 let first_text = pages[0].elements.iter().find_map(|e| {
2032 if let PositionedElement::Text(run) = e {
2033 Some(run)
2034 } else {
2035 None
2036 }
2037 });
2038 assert!(first_text.is_some());
2039 let run = first_text.unwrap();
2040 let total_advance: f64 = run.advances.iter().sum();
2041 assert!(
2043 (total_advance - 100.0).abs() < 0.1,
2044 "single word should not be stretched: {total_advance}"
2045 );
2046 }
2047
2048 #[test]
2052 fn anchor_offsets_resolve_against_their_frame() {
2053 let g = PageGeometry::default(); let para_top = 100.0;
2055 let off = 10.0;
2056
2057 assert_eq!(resolve_anchor_h(ST_RelativeFromH::Page, off, &g, 0.0), 10.0);
2058 assert_eq!(
2059 resolve_anchor_h(ST_RelativeFromH::LeftMargin, off, &g, 0.0),
2060 10.0
2061 );
2062 assert_eq!(
2063 resolve_anchor_h(ST_RelativeFromH::Margin, off, &g, 0.0),
2064 82.0,
2065 "margin-relative starts at the left margin"
2066 );
2067 assert_eq!(
2068 resolve_anchor_h(ST_RelativeFromH::Column, off, &g, 0.0),
2069 82.0,
2070 "column-relative starts at the text area"
2071 );
2072 assert_eq!(
2073 resolve_anchor_h(ST_RelativeFromH::RightMargin, off, &g, 0.0),
2074 550.0,
2075 "right-margin-relative starts at the right margin edge"
2076 );
2077 assert_eq!(
2078 resolve_anchor_h(ST_RelativeFromH::Character, off, &g, 36.0),
2079 118.0,
2080 "character-relative includes the paragraph indent"
2081 );
2082
2083 assert_eq!(
2084 resolve_anchor_v(ST_RelativeFromV::Page, off, &g, para_top),
2085 10.0
2086 );
2087 assert_eq!(
2088 resolve_anchor_v(ST_RelativeFromV::TopMargin, off, &g, para_top),
2089 10.0
2090 );
2091 assert_eq!(
2092 resolve_anchor_v(ST_RelativeFromV::Margin, off, &g, para_top),
2093 82.0
2094 );
2095 assert_eq!(
2096 resolve_anchor_v(ST_RelativeFromV::Paragraph, off, &g, para_top),
2097 182.0,
2098 "paragraph-relative follows the paragraph down the page"
2099 );
2100 assert_eq!(
2101 resolve_anchor_v(ST_RelativeFromV::Line, off, &g, para_top),
2102 182.0
2103 );
2104 assert_eq!(
2105 resolve_anchor_v(ST_RelativeFromV::BottomMargin, off, &g, para_top),
2106 730.0
2107 );
2108 }
2109
2110 #[test]
2113 fn paragraph_relative_anchor_tracks_the_paragraph() {
2114 let g = PageGeometry::default();
2115 let near_top = resolve_anchor_v(ST_RelativeFromV::Paragraph, 5.0, &g, 0.0);
2116 let further_down = resolve_anchor_v(ST_RelativeFromV::Paragraph, 5.0, &g, 300.0);
2117 assert_eq!(near_top, 77.0);
2118 assert_eq!(further_down, 377.0);
2119 assert!(further_down > near_top);
2120 }
2121}