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