1use crate::block::{LayoutBlock, ParagraphBlock};
7use crate::font::FontManager;
8use crate::line::{LayoutLine, LineItem};
9use crate::output::{Color, GlyphRun, OutlineEntry, PageFrame, Point, PositionedElement, Rect};
10
11use rdocx_oxml::shared::{ST_Border, ST_Jc, ST_Underline};
12
13type BorderEdge = (f64, Color, Option<(f64, f64)>);
15
16#[derive(Debug, Clone, Copy)]
18pub struct PageGeometry {
19 pub page_width: f64,
20 pub page_height: f64,
21 pub margin_top: f64,
22 pub margin_right: f64,
23 pub margin_bottom: f64,
24 pub margin_left: f64,
25 pub header_distance: f64,
26 pub footer_distance: f64,
27}
28
29impl PageGeometry {
30 pub fn content_width(&self) -> f64 {
32 self.page_width - self.margin_left - self.margin_right
33 }
34
35 pub fn content_height(&self) -> f64 {
37 self.page_height - self.margin_top - self.margin_bottom
38 }
39}
40
41impl Default for PageGeometry {
42 fn default() -> Self {
43 PageGeometry {
45 page_width: 612.0,
46 page_height: 792.0,
47 margin_top: 72.0,
48 margin_right: 72.0,
49 margin_bottom: 72.0,
50 margin_left: 72.0,
51 header_distance: 36.0,
52 footer_distance: 36.0,
53 }
54 }
55}
56
57pub struct HeaderFooterContent {
59 pub header_blocks: Vec<ParagraphBlock>,
60 pub footer_blocks: Vec<ParagraphBlock>,
61 pub first_header_blocks: Vec<ParagraphBlock>,
63 pub first_footer_blocks: Vec<ParagraphBlock>,
65}
66
67pub struct Section {
69 pub blocks: Vec<LayoutBlock>,
70 pub geometry: PageGeometry,
71 pub header_footer: Option<HeaderFooterContent>,
72 pub title_pg: bool,
74}
75
76pub fn paginate_sections(
78 sections: &[Section],
79 fm: &FontManager,
80) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
81 if sections.is_empty() {
82 return (
83 vec![PageFrame {
84 page_number: 1,
85 width: 612.0,
86 height: 792.0,
87 elements: Vec::new(),
88 }],
89 Vec::new(),
90 );
91 }
92
93 if sections.len() == 1 {
95 let s = §ions[0];
96 return paginate(
97 &s.blocks,
98 s.geometry,
99 s.header_footer.as_ref(),
100 s.title_pg,
101 fm,
102 );
103 }
104
105 let mut all_pages = Vec::new();
107 let mut all_outlines = Vec::new();
108 let mut page_offset = 0;
109
110 for section in sections {
111 let (mut pages, mut outlines) = paginate(
112 §ion.blocks,
113 section.geometry,
114 section.header_footer.as_ref(),
115 section.title_pg,
116 fm,
117 );
118
119 for page in &mut pages {
121 page.page_number += page_offset;
122 }
123 for outline in &mut outlines {
124 outline.page_index += page_offset;
125 }
126
127 page_offset += pages.len();
128 all_pages.append(&mut pages);
129 all_outlines.append(&mut outlines);
130 }
131
132 for (i, page) in all_pages.iter_mut().enumerate() {
135 page.page_number = i + 1;
136 }
137
138 (all_pages, all_outlines)
139}
140
141pub fn paginate(
143 blocks: &[LayoutBlock],
144 geometry: PageGeometry,
145 header_footer: Option<&HeaderFooterContent>,
146 title_pg: bool,
147 _fm: &FontManager,
148) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
149 let mut pager = Pager::new(geometry, header_footer, title_pg);
150
151 for (block_idx, block) in blocks.iter().enumerate() {
152 if block.page_break_before() && pager.has_content() {
154 pager.finish_page();
155 }
156
157 match block {
158 LayoutBlock::Paragraph(para) => {
159 if let (Some(level), Some(title)) = (para.heading_level, ¶.heading_text) {
161 pager.outlines.push(OutlineEntry {
162 title: title.clone(),
163 level,
164 page_index: pager.page_number - 1,
165 y_position: pager.geometry.margin_top + pager.cursor_y,
166 });
167 }
168 paginate_paragraph(para, block_idx, blocks, &mut pager);
169 }
170 LayoutBlock::Table(table) => {
171 let table_x = geometry.margin_left + table.table_indent;
172 let tbl_borders = table.borders.as_ref();
173
174 for (row_idx, row) in table.rows.iter().enumerate() {
175 if pager.cursor_y + row.height > pager.content_height && pager.has_content() {
176 pager.finish_page();
177
178 for &hdr_idx in &table.header_row_indices {
180 if hdr_idx < row_idx {
181 let hdr_row = &table.rows[hdr_idx];
182 render_table_row(
183 hdr_row,
184 &table.col_widths,
185 table_x,
186 pager.geometry.margin_top + pager.cursor_y,
187 &pager.geometry,
188 tbl_borders,
189 &mut pager.elements,
190 );
191 pager.cursor_y += hdr_row.height;
192 pager.mark_content();
193 }
194 }
195 }
196
197 render_table_row(
198 row,
199 &table.col_widths,
200 table_x,
201 pager.geometry.margin_top + pager.cursor_y,
202 &pager.geometry,
203 tbl_borders,
204 &mut pager.elements,
205 );
206 pager.cursor_y += row.height;
207 pager.mark_content();
208 }
209 }
210 }
211 }
212
213 pager.flush()
214}
215
216struct Pager<'a> {
218 pages: Vec<PageFrame>,
219 elements: Vec<PositionedElement>,
220 cursor_y: f64,
221 page_number: usize,
222 content_height: f64,
223 geometry: PageGeometry,
224 header_footer: Option<&'a HeaderFooterContent>,
225 has_content_flag: bool,
226 outlines: Vec<OutlineEntry>,
227 is_first_page: bool,
229 title_pg: bool,
231}
232
233impl<'a> Pager<'a> {
234 fn new(
235 geometry: PageGeometry,
236 header_footer: Option<&'a HeaderFooterContent>,
237 title_pg: bool,
238 ) -> Self {
239 Pager {
240 pages: Vec::new(),
241 elements: Vec::new(),
242 cursor_y: 0.0,
243 page_number: 1,
244 content_height: geometry.content_height(),
245 geometry,
246 header_footer,
247 has_content_flag: false,
248 outlines: Vec::new(),
249 is_first_page: true,
250 title_pg,
251 }
252 }
253
254 fn has_content(&self) -> bool {
255 self.has_content_flag
256 }
257
258 fn mark_content(&mut self) {
259 self.has_content_flag = true;
260 }
261
262 fn finish_page(&mut self) {
263 let mut all_elements = Vec::new();
264
265 if let Some(hf) = self.header_footer {
266 let header_blocks = if self.is_first_page && self.title_pg {
268 &hf.first_header_blocks
269 } else {
270 &hf.header_blocks
271 };
272 if !header_blocks.is_empty() {
273 let header_y = self.geometry.header_distance;
274 render_hf_blocks(header_blocks, &self.geometry, header_y, &mut all_elements);
275 }
276 }
277
278 all_elements.append(&mut self.elements);
279
280 if let Some(hf) = self.header_footer {
281 let footer_blocks = if self.is_first_page && self.title_pg {
283 &hf.first_footer_blocks
284 } else {
285 &hf.footer_blocks
286 };
287 if !footer_blocks.is_empty() {
288 let footer_height: f64 = footer_blocks.iter().map(|b| b.content_height()).sum();
289 let footer_y =
290 self.geometry.page_height - self.geometry.footer_distance - footer_height;
291 render_hf_blocks(footer_blocks, &self.geometry, footer_y, &mut all_elements);
292 }
293 }
294
295 self.pages.push(PageFrame {
296 page_number: self.page_number,
297 width: self.geometry.page_width,
298 height: self.geometry.page_height,
299 elements: all_elements,
300 });
301 self.page_number += 1;
302 self.cursor_y = 0.0;
303 self.has_content_flag = false;
304 self.is_first_page = false;
305 }
306
307 fn flush(mut self) -> (Vec<PageFrame>, Vec<OutlineEntry>) {
308 if self.has_content() || self.pages.is_empty() {
310 self.finish_page();
311 }
312 (self.pages, self.outlines)
313 }
314}
315
316fn paginate_paragraph(
318 para: &ParagraphBlock,
319 block_idx: usize,
320 blocks: &[LayoutBlock],
321 pager: &mut Pager,
322) {
323 let space_before = if pager.cursor_y == 0.0 {
324 0.0
325 } else {
326 para.space_before
327 };
328
329 let total_needed = space_before + para.content_height();
331 let remaining = pager.content_height - pager.cursor_y;
332
333 if total_needed > remaining && pager.has_content() {
334 if para.keep_lines || para.lines.len() <= 2 {
336 pager.finish_page();
337 paginate_paragraph(para, block_idx, blocks, pager);
339 return;
340 }
341
342 let available_for_lines = remaining - space_before;
343 let lines_that_fit = count_lines_that_fit(¶.lines, available_for_lines);
344
345 if para.widow_control && lines_that_fit < 2 {
346 pager.finish_page();
348 paginate_paragraph(para, block_idx, blocks, pager);
349 return;
350 }
351
352 let lines_remaining = para.lines.len() - lines_that_fit;
353 if para.widow_control && lines_remaining < 2 && lines_that_fit >= 3 {
354 let split_at = lines_that_fit - 1;
356 render_para_split(para, split_at, space_before, pager);
357 return;
358 }
359
360 if lines_that_fit > 0 {
361 render_para_split(para, lines_that_fit, space_before, pager);
362 return;
363 }
364
365 pager.finish_page();
367 paginate_paragraph(para, block_idx, blocks, pager);
368 return;
369 }
370
371 if total_needed > pager.content_height && pager.cursor_y == 0.0 {
374 let lines_that_fit = count_lines_that_fit(¶.lines, pager.content_height);
376 if lines_that_fit > 0 && lines_that_fit < para.lines.len() {
377 render_para_split(para, lines_that_fit, 0.0, pager);
378 return;
379 }
380 }
381
382 if para.keep_next && block_idx + 1 < blocks.len() {
384 let next_first = match &blocks[block_idx + 1] {
385 LayoutBlock::Paragraph(p) => p.lines.first().map(|l| l.height).unwrap_or(0.0),
386 LayoutBlock::Table(t) => t.rows.first().map(|r| r.height).unwrap_or(0.0),
387 };
388 if pager.cursor_y + space_before + para.content_height() + next_first > pager.content_height
389 && pager.has_content()
390 {
391 pager.finish_page();
392 }
393 }
394
395 let space = if pager.cursor_y == 0.0 {
397 0.0
398 } else {
399 para.space_before
400 };
401 pager.cursor_y += space;
402
403 if let Some(shading) = para.shading {
404 pager.elements.push(PositionedElement::FilledRect {
405 rect: Rect {
406 x: pager.geometry.margin_left + para.indent_left,
407 y: pager.geometry.margin_top + pager.cursor_y,
408 width: pager.geometry.content_width() - para.indent_left - para.indent_right,
409 height: para.content_height(),
410 },
411 color: shading,
412 });
413 }
414
415 if let Some(ref borders) = para.borders {
417 let border_x = pager.geometry.margin_left + para.indent_left;
418 let border_y = pager.geometry.margin_top + pager.cursor_y;
419 let border_w = pager.geometry.content_width() - para.indent_left - para.indent_right;
420 let border_h = para.content_height();
421 render_border_edges(
422 borders,
423 border_x,
424 border_y,
425 border_w,
426 border_h,
427 &mut pager.elements,
428 );
429 }
430
431 render_paragraph_lines(
432 ¶.lines,
433 para,
434 &pager.geometry,
435 pager.cursor_y,
436 &mut pager.elements,
437 );
438 pager.cursor_y += para.content_height();
439 pager.cursor_y += para.space_after;
440 pager.mark_content();
441}
442
443fn render_para_split(para: &ParagraphBlock, split_at: usize, space_before: f64, pager: &mut Pager) {
446 pager.cursor_y += space_before;
448 render_paragraph_lines(
449 ¶.lines[..split_at],
450 para,
451 &pager.geometry,
452 pager.cursor_y,
453 &mut pager.elements,
454 );
455 pager.mark_content();
456 pager.finish_page();
457
458 let remaining_lines = ¶.lines[split_at..];
460 let remaining_height: f64 = remaining_lines.iter().map(|l| l.height).sum();
461
462 if remaining_height > pager.content_height {
463 let lines_that_fit = count_lines_that_fit(remaining_lines, pager.content_height);
465 if lines_that_fit > 0 && lines_that_fit < remaining_lines.len() {
466 let temp_para = ParagraphBlock {
468 lines: remaining_lines.to_vec(),
469 space_before: 0.0,
470 space_after: para.space_after,
471 borders: para.borders.clone(),
472 shading: para.shading,
473 indent_left: para.indent_left,
474 indent_right: para.indent_right,
475 jc: para.jc,
476 keep_next: para.keep_next,
477 keep_lines: false,
478 page_break_before: false,
479 widow_control: para.widow_control,
480 heading_level: None,
481 heading_text: None,
482 };
483 render_para_split(&temp_para, lines_that_fit, 0.0, pager);
484 return;
485 }
486 }
487
488 render_paragraph_lines(
490 remaining_lines,
491 para,
492 &pager.geometry,
493 0.0,
494 &mut pager.elements,
495 );
496 pager.cursor_y = remaining_height + para.space_after;
497 pager.mark_content();
498}
499
500fn count_lines_that_fit(lines: &[LayoutLine], available: f64) -> usize {
502 let mut used = 0.0;
503 for (i, line) in lines.iter().enumerate() {
504 used += line.height;
505 if used > available {
506 return i;
507 }
508 }
509 lines.len()
510}
511
512fn render_paragraph_lines(
514 lines: &[LayoutLine],
515 para: &ParagraphBlock,
516 geometry: &PageGeometry,
517 start_y: f64,
518 elements: &mut Vec<PositionedElement>,
519) {
520 let mut y = start_y;
521 for line in lines {
522 let baseline_y = geometry.margin_top + y + line.ascent;
523
524 let text_width: f64 = line.items.iter().map(|item| item.width()).sum();
526 let remaining_width = line.available_width - text_width;
527
528 let justify_extra =
530 if para.jc == Some(ST_Jc::Both) && !line.is_last && remaining_width > 0.0 {
531 let gap_count = count_word_gaps(&line.items);
533 if gap_count > 0 {
534 remaining_width / gap_count as f64
535 } else {
536 0.0
537 }
538 } else {
539 0.0
540 };
541
542 let x_offset = match para.jc {
543 Some(ST_Jc::Center) => geometry.margin_left + line.indent_left + remaining_width / 2.0,
544 Some(ST_Jc::Right) | Some(ST_Jc::End) => {
545 geometry.margin_left + line.indent_left + remaining_width
546 }
547 Some(ST_Jc::Both) if !line.is_last && justify_extra > 0.0 => {
548 geometry.margin_left + line.indent_left
550 }
551 _ => geometry.margin_left + line.indent_left,
552 };
553
554 let mut x = x_offset;
555 let mut _accumulated_extra = 0.0;
556
557 for item in &line.items {
558 match item {
559 LineItem::Text(seg) | LineItem::Marker(seg) => {
560 let adjusted_baseline = baseline_y - seg.baseline_offset;
561
562 let segment_spaces = if justify_extra > 0.0 {
564 seg.text.chars().filter(|c| *c == ' ').count()
565 } else {
566 0
567 };
568 let segment_extra = segment_spaces as f64 * justify_extra;
569 let effective_width = seg.width + segment_extra;
570
571 if let Some(hl_color) = seg.highlight {
573 elements.push(PositionedElement::FilledRect {
574 rect: Rect {
575 x,
576 y: geometry.margin_top + y,
577 width: effective_width,
578 height: line.height,
579 },
580 color: hl_color,
581 });
582 }
583
584 let advances = if justify_extra > 0.0 && segment_spaces > 0 {
586 distribute_justify_advances(&seg.text, &seg.advances, justify_extra)
588 } else {
589 seg.advances.clone()
590 };
591
592 elements.push(PositionedElement::Text(GlyphRun {
593 origin: Point {
594 x,
595 y: adjusted_baseline,
596 },
597 font_id: seg.font_id,
598 font_size: seg.font_size,
599 glyph_ids: seg.glyph_ids.clone(),
600 advances,
601 text: seg.text.clone(),
602 color: seg.color,
603 bold: seg.bold,
604 italic: seg.italic,
605 field_kind: seg.field_kind,
606 footnote_id: seg.footnote_id,
607 }));
608
609 if let Some(ul_style) = seg.underline
611 && ul_style != ST_Underline::None
612 {
613 let ul_y = adjusted_baseline + seg.descent * 0.3;
614 let ul_thickness = match ul_style {
615 ST_Underline::Thick => seg.font_size / 12.0,
616 ST_Underline::Double => seg.font_size / 24.0,
617 _ => seg.font_size / 18.0,
618 };
619 elements.push(PositionedElement::Line {
620 start: Point { x, y: ul_y },
621 end: Point {
622 x: x + effective_width,
623 y: ul_y,
624 },
625 width: ul_thickness,
626 color: seg.color,
627 dash_pattern: None,
628 });
629 if ul_style == ST_Underline::Double {
631 let ul_y2 = ul_y + ul_thickness * 2.5;
632 elements.push(PositionedElement::Line {
633 start: Point { x, y: ul_y2 },
634 end: Point {
635 x: x + effective_width,
636 y: ul_y2,
637 },
638 width: ul_thickness,
639 color: seg.color,
640 dash_pattern: None,
641 });
642 }
643 }
644
645 if seg.strike {
647 let strike_y = adjusted_baseline - seg.ascent * 0.3;
648 let strike_thickness = seg.font_size / 24.0;
649 elements.push(PositionedElement::Line {
650 start: Point { x, y: strike_y },
651 end: Point {
652 x: x + effective_width,
653 y: strike_y,
654 },
655 width: strike_thickness,
656 color: seg.color,
657 dash_pattern: None,
658 });
659 }
660
661 if seg.dstrike {
663 let strike_y = adjusted_baseline - seg.ascent * 0.3;
664 let strike_thickness = seg.font_size / 24.0;
665 let gap = strike_thickness * 2.0;
666 elements.push(PositionedElement::Line {
667 start: Point {
668 x,
669 y: strike_y - gap / 2.0,
670 },
671 end: Point {
672 x: x + effective_width,
673 y: strike_y - gap / 2.0,
674 },
675 width: strike_thickness,
676 color: seg.color,
677 dash_pattern: None,
678 });
679 elements.push(PositionedElement::Line {
680 start: Point {
681 x,
682 y: strike_y + gap / 2.0,
683 },
684 end: Point {
685 x: x + effective_width,
686 y: strike_y + gap / 2.0,
687 },
688 width: strike_thickness,
689 color: seg.color,
690 dash_pattern: None,
691 });
692 }
693
694 if let Some(ref url) = seg.hyperlink_url {
696 elements.push(PositionedElement::LinkAnnotation {
697 rect: Rect {
698 x,
699 y: geometry.margin_top + y,
700 width: effective_width,
701 height: line.height,
702 },
703 url: url.clone(),
704 });
705 }
706
707 _accumulated_extra += segment_extra;
708 x += effective_width;
709 }
710 LineItem::Tab { width, leader } => {
711 if let Some(leader_seg) = leader {
712 let baseline_y = geometry.margin_top + y + line.ascent;
714 elements.push(PositionedElement::Text(GlyphRun {
715 origin: Point { x, y: baseline_y },
716 font_id: leader_seg.font_id,
717 font_size: leader_seg.font_size,
718 glyph_ids: leader_seg.glyph_ids.clone(),
719 advances: leader_seg.advances.clone(),
720 text: leader_seg.text.clone(),
721 color: leader_seg.color,
722 bold: leader_seg.bold,
723 italic: leader_seg.italic,
724 field_kind: None,
725 footnote_id: None,
726 }));
727 }
728 x += width;
729 }
730 LineItem::Image {
731 width,
732 height,
733 embed_id,
734 } => {
735 elements.push(PositionedElement::Image {
737 rect: Rect {
738 x,
739 y: geometry.margin_top + y,
740 width: *width,
741 height: *height,
742 },
743 data: Vec::new(),
744 content_type: String::new(),
745 embed_id: Some(embed_id.clone()),
746 });
747 x += width;
748 }
749 }
750 }
751
752 y += line.height;
753 }
754}
755
756fn render_hf_blocks(
758 blocks: &[ParagraphBlock],
759 geometry: &PageGeometry,
760 start_y: f64,
761 elements: &mut Vec<PositionedElement>,
762) {
763 let mut y = start_y - geometry.margin_top; for para in blocks {
765 render_paragraph_lines(¶.lines, para, geometry, y, elements);
766 y += para.content_height();
767 }
768}
769
770fn render_table_row(
772 row: &crate::table::TableRow,
773 _col_widths: &[f64],
774 table_x: f64,
775 row_y: f64,
776 geometry: &PageGeometry,
777 table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
778 elements: &mut Vec<PositionedElement>,
779) {
780 let mut cell_x = table_x;
781 let num_cells = row.cells.len();
782
783 for (cell_idx, cell) in row.cells.iter().enumerate() {
784 if let Some(ref shading) = cell.shading {
786 elements.push(PositionedElement::FilledRect {
787 rect: Rect {
788 x: cell_x,
789 y: row_y,
790 width: cell.width,
791 height: cell.height,
792 },
793 color: *shading,
794 });
795 }
796
797 render_cell_borders(
799 cell_x,
800 row_y,
801 cell.width,
802 cell.height,
803 &cell.borders,
804 table_borders,
805 cell_idx,
806 num_cells,
807 cell.is_first_row,
808 cell.is_last_row,
809 elements,
810 );
811
812 if !cell.is_vmerge_continue {
813 let cell_margin_top = cell.margin_top;
815 let cell_margin_left = cell.margin_left;
816
817 let content_height: f64 = cell.paragraphs.iter().map(|p| p.total_height()).sum();
819 let v_offset = match cell.v_align {
820 Some(rdocx_oxml::table::ST_VerticalJc::Center) => {
821 ((cell.height - cell_margin_top - content_height) / 2.0).max(0.0)
822 }
823 Some(rdocx_oxml::table::ST_VerticalJc::Bottom) => {
824 (cell.height - cell_margin_top - content_height).max(0.0)
825 }
826 _ => 0.0, };
828
829 let mut para_y = row_y - geometry.margin_top + cell_margin_top + v_offset;
830 for para in &cell.paragraphs {
831 render_paragraph_lines(
832 ¶.lines,
833 para,
834 &PageGeometry {
835 margin_left: cell_x + cell_margin_left,
836 ..*geometry
837 },
838 para_y,
839 elements,
840 );
841 para_y += para.total_height();
842 }
843 }
844 cell_x += cell.width;
845 }
846}
847
848fn render_cell_borders(
850 x: f64,
851 y: f64,
852 w: f64,
853 h: f64,
854 cell_borders: &Option<rdocx_oxml::table::CT_TblBorders>,
855 table_borders: Option<&rdocx_oxml::table::CT_TblBorders>,
856 cell_idx: usize,
857 num_cells: usize,
858 is_first_row: bool,
859 is_last_row: bool,
860 elements: &mut Vec<PositionedElement>,
861) {
862 let get_edge = |cell_edge: Option<&rdocx_oxml::borders::CT_BorderEdge>,
864 table_edge: Option<&rdocx_oxml::borders::CT_BorderEdge>|
865 -> Option<BorderEdge> {
866 let edge = cell_edge.or(table_edge)?;
867 if edge.val == ST_Border::None {
868 return None;
869 }
870 let thickness = edge.sz.unwrap_or(4) as f64 / 8.0; let color = edge
872 .color
873 .as_ref()
874 .filter(|c| c.as_str() != "auto")
875 .map(|c| Color::from_hex(c))
876 .unwrap_or(Color::BLACK);
877 let dash = border_dash_pattern(edge.val, thickness);
878 Some((thickness, color, dash))
879 };
880
881 let table_top = table_borders.and_then(|b| {
883 if is_first_row {
884 b.top.as_ref()
885 } else {
886 b.inside_h.as_ref()
887 }
888 });
889 let cell_top = cell_borders.as_ref().and_then(|b| b.top.as_ref());
890 if let Some((thickness, color, dash_pattern)) = get_edge(cell_top, table_top) {
891 elements.push(PositionedElement::Line {
892 start: Point { x, y },
893 end: Point { x: x + w, y },
894 width: thickness,
895 color,
896 dash_pattern,
897 });
898 }
899
900 let table_bottom = table_borders.and_then(|b| {
902 if is_last_row {
903 b.bottom.as_ref()
904 } else {
905 b.inside_h.as_ref()
906 }
907 });
908 let cell_bottom = cell_borders.as_ref().and_then(|b| b.bottom.as_ref());
909 if let Some((thickness, color, dash_pattern)) = get_edge(cell_bottom, table_bottom) {
910 elements.push(PositionedElement::Line {
911 start: Point { x, y: y + h },
912 end: Point { x: x + w, y: y + h },
913 width: thickness,
914 color,
915 dash_pattern,
916 });
917 }
918
919 let table_left = table_borders.and_then(|b| {
921 if cell_idx == 0 {
922 b.left.as_ref()
923 } else {
924 b.inside_v.as_ref()
925 }
926 });
927 let cell_left = cell_borders.as_ref().and_then(|b| b.left.as_ref());
928 if let Some((thickness, color, dash_pattern)) = get_edge(cell_left, table_left) {
929 elements.push(PositionedElement::Line {
930 start: Point { x, y },
931 end: Point { x, y: y + h },
932 width: thickness,
933 color,
934 dash_pattern,
935 });
936 }
937
938 let table_right = table_borders.and_then(|b| {
940 if cell_idx == num_cells - 1 {
941 b.right.as_ref()
942 } else {
943 b.inside_v.as_ref()
944 }
945 });
946 let cell_right = cell_borders.as_ref().and_then(|b| b.right.as_ref());
947 if let Some((thickness, color, dash_pattern)) = get_edge(cell_right, table_right) {
948 elements.push(PositionedElement::Line {
949 start: Point { x: x + w, y },
950 end: Point { x: x + w, y: y + h },
951 width: thickness,
952 color,
953 dash_pattern,
954 });
955 }
956}
957
958fn render_border_edges(
960 borders: &rdocx_oxml::borders::CT_PBdr,
961 x: f64,
962 y: f64,
963 w: f64,
964 h: f64,
965 elements: &mut Vec<PositionedElement>,
966) {
967 let render_edge = |edge: &rdocx_oxml::borders::CT_BorderEdge,
968 start: Point,
969 end: Point,
970 elements: &mut Vec<PositionedElement>| {
971 if edge.val == ST_Border::None {
972 return;
973 }
974 let thickness = edge.sz.unwrap_or(4) as f64 / 8.0; let color = edge
976 .color
977 .as_ref()
978 .filter(|c| c.as_str() != "auto")
979 .map(|c| Color::from_hex(c))
980 .unwrap_or(Color::BLACK);
981 let dash_pattern = border_dash_pattern(edge.val, thickness);
982
983 if edge.val == ST_Border::Double {
984 let gap = thickness * 2.0;
986 let dx = end.x - start.x;
987 let dy = end.y - start.y;
988 let len = (dx * dx + dy * dy).sqrt();
989 let (nx, ny) = if len > 0.0 {
990 (-dy / len, dx / len)
991 } else {
992 (0.0, 1.0)
993 };
994 let offset = gap / 2.0;
995 elements.push(PositionedElement::Line {
996 start: Point {
997 x: start.x + nx * offset,
998 y: start.y + ny * offset,
999 },
1000 end: Point {
1001 x: end.x + nx * offset,
1002 y: end.y + ny * offset,
1003 },
1004 width: thickness,
1005 color,
1006 dash_pattern: None,
1007 });
1008 elements.push(PositionedElement::Line {
1009 start: Point {
1010 x: start.x - nx * offset,
1011 y: start.y - ny * offset,
1012 },
1013 end: Point {
1014 x: end.x - nx * offset,
1015 y: end.y - ny * offset,
1016 },
1017 width: thickness,
1018 color,
1019 dash_pattern: None,
1020 });
1021 } else {
1022 elements.push(PositionedElement::Line {
1023 start,
1024 end,
1025 width: thickness,
1026 color,
1027 dash_pattern,
1028 });
1029 }
1030 };
1031
1032 if let Some(ref edge) = borders.top {
1033 let space = edge.space.unwrap_or(0) as f64;
1034 render_edge(
1035 edge,
1036 Point { x, y: y - space },
1037 Point {
1038 x: x + w,
1039 y: y - space,
1040 },
1041 elements,
1042 );
1043 }
1044 if let Some(ref edge) = borders.bottom {
1045 let space = edge.space.unwrap_or(0) as f64;
1046 render_edge(
1047 edge,
1048 Point {
1049 x,
1050 y: y + h + space,
1051 },
1052 Point {
1053 x: x + w,
1054 y: y + h + space,
1055 },
1056 elements,
1057 );
1058 }
1059 if let Some(ref edge) = borders.left {
1060 let space = edge.space.unwrap_or(0) as f64;
1061 render_edge(
1062 edge,
1063 Point { x: x - space, y },
1064 Point {
1065 x: x - space,
1066 y: y + h,
1067 },
1068 elements,
1069 );
1070 }
1071 if let Some(ref edge) = borders.right {
1072 let space = edge.space.unwrap_or(0) as f64;
1073 render_edge(
1074 edge,
1075 Point {
1076 x: x + w + space,
1077 y,
1078 },
1079 Point {
1080 x: x + w + space,
1081 y: y + h,
1082 },
1083 elements,
1084 );
1085 }
1086}
1087
1088fn border_dash_pattern(style: ST_Border, thickness: f64) -> Option<(f64, f64)> {
1091 match style {
1092 ST_Border::Dashed => Some((3.0 * thickness, 2.0 * thickness)),
1093 ST_Border::Dotted => Some((thickness, thickness)),
1094 ST_Border::DotDash | ST_Border::DotDotDash => Some((3.0 * thickness, thickness)),
1095 _ => None,
1096 }
1097}
1098
1099fn count_word_gaps(items: &[LineItem]) -> usize {
1101 let mut count = 0;
1102 for item in items {
1103 match item {
1104 LineItem::Text(seg) | LineItem::Marker(seg) => {
1105 count += seg.text.chars().filter(|c| *c == ' ').count();
1106 }
1107 LineItem::Tab { .. } => {
1108 count += 1;
1109 }
1110 _ => {}
1111 }
1112 }
1113 count
1114}
1115
1116fn distribute_justify_advances(text: &str, advances: &[f64], extra_per_gap: f64) -> Vec<f64> {
1118 let chars: Vec<char> = text.chars().collect();
1119 let mut result = advances.to_vec();
1120
1121 if chars.len() == result.len() {
1122 for (i, &ch) in chars.iter().enumerate() {
1124 if ch == ' ' {
1125 result[i] += extra_per_gap;
1126 }
1127 }
1128 } else {
1129 let total_extra = extra_per_gap * text.chars().filter(|c| *c == ' ').count() as f64;
1131 if !result.is_empty() {
1132 let per_glyph = total_extra / result.len() as f64;
1133 for a in &mut result {
1134 *a += per_glyph;
1135 }
1136 }
1137 }
1138
1139 result
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144 use super::*;
1145 use crate::block::ParagraphBlock;
1146 use crate::line::LayoutLine;
1147
1148 fn make_line(height: f64) -> LayoutLine {
1149 LayoutLine {
1150 items: vec![],
1151 width: 100.0,
1152 ascent: height * 0.77,
1153 descent: height * 0.23,
1154 height,
1155 indent_left: 0.0,
1156 available_width: 468.0,
1157 is_last: true,
1158 }
1159 }
1160
1161 fn make_para(line_count: usize, line_height: f64) -> ParagraphBlock {
1162 let mut lines = Vec::new();
1163 for _ in 0..line_count {
1164 lines.push(make_line(line_height));
1165 }
1166 ParagraphBlock {
1167 lines,
1168 space_before: 0.0,
1169 space_after: 0.0,
1170 borders: None,
1171 shading: None,
1172 indent_left: 0.0,
1173 indent_right: 0.0,
1174 jc: None,
1175 keep_next: false,
1176 keep_lines: false,
1177 page_break_before: false,
1178 widow_control: true,
1179 heading_level: None,
1180 heading_text: None,
1181 }
1182 }
1183
1184 #[test]
1185 fn single_page_layout() {
1186 let fm = FontManager::new();
1187 let blocks = vec![LayoutBlock::Paragraph(make_para(3, 14.0))];
1188 let geom = PageGeometry::default();
1189 let (pages, _outlines) = paginate(&blocks, geom, None, false, &fm);
1190 assert_eq!(pages.len(), 1);
1191 assert_eq!(pages[0].page_number, 1);
1192 }
1193
1194 #[test]
1195 fn multi_page_overflow() {
1196 let fm = FontManager::new();
1197 let blocks = vec![LayoutBlock::Paragraph(make_para(100, 14.0))];
1199 let geom = PageGeometry::default();
1200 let (pages, _outlines) = paginate(&blocks, geom, None, false, &fm);
1201 assert!(pages.len() >= 2);
1202 }
1203
1204 #[test]
1205 fn forced_page_break() {
1206 let fm = FontManager::new();
1207 let mut para2 = make_para(3, 14.0);
1208 para2.page_break_before = true;
1209 let blocks = vec![
1210 LayoutBlock::Paragraph(make_para(3, 14.0)),
1211 LayoutBlock::Paragraph(para2),
1212 ];
1213 let geom = PageGeometry::default();
1214 let (pages, _outlines) = paginate(&blocks, geom, None, false, &fm);
1215 assert_eq!(pages.len(), 2);
1216 }
1217
1218 #[test]
1219 fn page_dimensions() {
1220 let fm = FontManager::new();
1221 let blocks = vec![LayoutBlock::Paragraph(make_para(1, 14.0))];
1222 let geom = PageGeometry::default();
1223 let (pages, _outlines) = paginate(&blocks, geom, None, false, &fm);
1224 assert!((pages[0].width - 612.0).abs() < 0.01);
1225 assert!((pages[0].height - 792.0).abs() < 0.01);
1226 }
1227
1228 fn make_text_line(height: f64, underline: Option<ST_Underline>, strike: bool) -> LayoutLine {
1229 use crate::line::TextSegment;
1230 let seg = TextSegment {
1231 text: "Hello".to_string(),
1232 font_id: crate::output::FontId(0),
1233 font_size: 12.0,
1234 glyph_ids: vec![1, 2, 3],
1235 advances: vec![6.0, 6.0, 6.0],
1236 width: 40.0,
1237 ascent: height * 0.77,
1238 descent: height * 0.23,
1239 color: Color::BLACK,
1240 bold: false,
1241 italic: false,
1242 underline,
1243 strike,
1244 dstrike: false,
1245 highlight: None,
1246 baseline_offset: 0.0,
1247 hyperlink_url: None,
1248 field_kind: None,
1249 footnote_id: None,
1250 };
1251 LayoutLine {
1252 items: vec![LineItem::Text(seg)],
1253 width: 40.0,
1254 ascent: height * 0.77,
1255 descent: height * 0.23,
1256 height,
1257 indent_left: 0.0,
1258 available_width: 468.0,
1259 is_last: true,
1260 }
1261 }
1262
1263 #[test]
1264 fn underline_renders_line_element() {
1265 let fm = FontManager::new();
1266 let para = ParagraphBlock {
1267 lines: vec![make_text_line(14.0, Some(ST_Underline::Single), false)],
1268 space_before: 0.0,
1269 space_after: 0.0,
1270 borders: None,
1271 shading: None,
1272 indent_left: 0.0,
1273 indent_right: 0.0,
1274 jc: None,
1275 keep_next: false,
1276 keep_lines: false,
1277 page_break_before: false,
1278 widow_control: true,
1279 heading_level: None,
1280 heading_text: None,
1281 };
1282 let blocks = vec![LayoutBlock::Paragraph(para)];
1283 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1284 let lines: Vec<_> = pages[0]
1286 .elements
1287 .iter()
1288 .filter(|e| matches!(e, PositionedElement::Line { .. }))
1289 .collect();
1290 assert_eq!(lines.len(), 1, "expected 1 underline line");
1291 }
1292
1293 #[test]
1294 fn strikethrough_renders_line_element() {
1295 let fm = FontManager::new();
1296 let para = ParagraphBlock {
1297 lines: vec![make_text_line(14.0, None, true)],
1298 space_before: 0.0,
1299 space_after: 0.0,
1300 borders: None,
1301 shading: None,
1302 indent_left: 0.0,
1303 indent_right: 0.0,
1304 jc: None,
1305 keep_next: false,
1306 keep_lines: false,
1307 page_break_before: false,
1308 widow_control: true,
1309 heading_level: None,
1310 heading_text: None,
1311 };
1312 let blocks = vec![LayoutBlock::Paragraph(para)];
1313 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1314 let lines: Vec<_> = pages[0]
1315 .elements
1316 .iter()
1317 .filter(|e| matches!(e, PositionedElement::Line { .. }))
1318 .collect();
1319 assert_eq!(lines.len(), 1, "expected 1 strikethrough line");
1320 }
1321
1322 #[test]
1323 fn highlight_renders_filled_rect() {
1324 use crate::line::TextSegment;
1325 let fm = FontManager::new();
1326 let seg = TextSegment {
1327 text: "Hi".to_string(),
1328 font_id: crate::output::FontId(0),
1329 font_size: 12.0,
1330 glyph_ids: vec![1],
1331 advances: vec![10.0],
1332 width: 20.0,
1333 ascent: 10.0,
1334 descent: 3.0,
1335 color: Color::BLACK,
1336 bold: false,
1337 italic: false,
1338 underline: None,
1339 strike: false,
1340 dstrike: false,
1341 highlight: Some(Color {
1342 r: 1.0,
1343 g: 1.0,
1344 b: 0.0,
1345 a: 1.0,
1346 }),
1347 baseline_offset: 0.0,
1348 hyperlink_url: None,
1349 field_kind: None,
1350 footnote_id: None,
1351 };
1352 let line = LayoutLine {
1353 items: vec![LineItem::Text(seg)],
1354 width: 20.0,
1355 ascent: 10.0,
1356 descent: 3.0,
1357 height: 13.0,
1358 indent_left: 0.0,
1359 available_width: 468.0,
1360 is_last: true,
1361 };
1362 let para = ParagraphBlock {
1363 lines: vec![line],
1364 space_before: 0.0,
1365 space_after: 0.0,
1366 borders: None,
1367 shading: None,
1368 indent_left: 0.0,
1369 indent_right: 0.0,
1370 jc: None,
1371 keep_next: false,
1372 keep_lines: false,
1373 page_break_before: false,
1374 widow_control: true,
1375 heading_level: None,
1376 heading_text: None,
1377 };
1378 let blocks = vec![LayoutBlock::Paragraph(para)];
1379 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1380 let rects: Vec<_> = pages[0]
1381 .elements
1382 .iter()
1383 .filter(|e| matches!(e, PositionedElement::FilledRect { .. }))
1384 .collect();
1385 assert_eq!(rects.len(), 1, "expected 1 highlight rect");
1386 }
1387
1388 #[test]
1389 fn paragraph_borders_render_lines() {
1390 use rdocx_oxml::borders::{CT_BorderEdge, CT_PBdr};
1391 let fm = FontManager::new();
1392 let para = ParagraphBlock {
1393 lines: vec![make_line(14.0)],
1394 space_before: 0.0,
1395 space_after: 0.0,
1396 borders: Some(CT_PBdr {
1397 top: Some(CT_BorderEdge {
1398 val: ST_Border::Single,
1399 sz: Some(4),
1400 space: Some(1),
1401 color: Some("000000".to_string()),
1402 }),
1403 bottom: Some(CT_BorderEdge {
1404 val: ST_Border::Single,
1405 sz: Some(4),
1406 space: Some(1),
1407 color: Some("000000".to_string()),
1408 }),
1409 ..Default::default()
1410 }),
1411 shading: None,
1412 indent_left: 0.0,
1413 indent_right: 0.0,
1414 jc: None,
1415 keep_next: false,
1416 keep_lines: false,
1417 page_break_before: false,
1418 widow_control: true,
1419 heading_level: None,
1420 heading_text: None,
1421 };
1422 let blocks = vec![LayoutBlock::Paragraph(para)];
1423 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1424 let lines: Vec<_> = pages[0]
1425 .elements
1426 .iter()
1427 .filter(|e| matches!(e, PositionedElement::Line { .. }))
1428 .collect();
1429 assert_eq!(lines.len(), 2, "expected 2 border lines (top + bottom)");
1430 }
1431
1432 #[test]
1433 fn paragraph_shading_renders_filled_rect() {
1434 let fm = FontManager::new();
1435 let para = ParagraphBlock {
1436 lines: vec![make_line(14.0)],
1437 space_before: 0.0,
1438 space_after: 0.0,
1439 borders: None,
1440 shading: Some(Color {
1441 r: 1.0,
1442 g: 1.0,
1443 b: 0.0,
1444 a: 1.0,
1445 }),
1446 indent_left: 0.0,
1447 indent_right: 0.0,
1448 jc: None,
1449 keep_next: false,
1450 keep_lines: false,
1451 page_break_before: false,
1452 widow_control: true,
1453 heading_level: None,
1454 heading_text: None,
1455 };
1456 let blocks = vec![LayoutBlock::Paragraph(para)];
1457 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1458 let rects: Vec<_> = pages[0]
1459 .elements
1460 .iter()
1461 .filter(|e| matches!(e, PositionedElement::FilledRect { .. }))
1462 .collect();
1463 assert_eq!(rects.len(), 1, "expected 1 paragraph shading rect");
1464 }
1465
1466 #[test]
1467 fn double_underline_renders_two_lines() {
1468 let fm = FontManager::new();
1469 let para = ParagraphBlock {
1470 lines: vec![make_text_line(14.0, Some(ST_Underline::Double), false)],
1471 space_before: 0.0,
1472 space_after: 0.0,
1473 borders: None,
1474 shading: None,
1475 indent_left: 0.0,
1476 indent_right: 0.0,
1477 jc: None,
1478 keep_next: false,
1479 keep_lines: false,
1480 page_break_before: false,
1481 widow_control: true,
1482 heading_level: None,
1483 heading_text: None,
1484 };
1485 let blocks = vec![LayoutBlock::Paragraph(para)];
1486 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1487 let lines: Vec<_> = pages[0]
1488 .elements
1489 .iter()
1490 .filter(|e| matches!(e, PositionedElement::Line { .. }))
1491 .collect();
1492 assert_eq!(lines.len(), 2, "expected 2 lines for double underline");
1493 }
1494
1495 fn make_justified_line(text: &str, seg_width: f64, is_last: bool) -> LayoutLine {
1496 use crate::line::TextSegment;
1497 let seg = TextSegment {
1498 text: text.to_string(),
1499 font_id: crate::output::FontId(0),
1500 font_size: 12.0,
1501 glyph_ids: vec![1; text.len()],
1502 advances: vec![seg_width / text.len() as f64; text.len()],
1503 width: seg_width,
1504 ascent: 10.0,
1505 descent: 3.0,
1506 color: Color::BLACK,
1507 bold: false,
1508 italic: false,
1509 underline: None,
1510 strike: false,
1511 dstrike: false,
1512 highlight: None,
1513 baseline_offset: 0.0,
1514 hyperlink_url: None,
1515 field_kind: None,
1516 footnote_id: None,
1517 };
1518 LayoutLine {
1519 items: vec![LineItem::Text(seg)],
1520 width: seg_width,
1521 ascent: 10.0,
1522 descent: 3.0,
1523 height: 13.0,
1524 indent_left: 0.0,
1525 available_width: 468.0,
1526 is_last,
1527 }
1528 }
1529
1530 #[test]
1531 fn hyperlink_emits_link_annotation() {
1532 use crate::line::TextSegment;
1533 let fm = FontManager::new();
1534 let seg = TextSegment {
1535 text: "Click me".to_string(),
1536 font_id: crate::output::FontId(0),
1537 font_size: 12.0,
1538 glyph_ids: vec![1, 2, 3],
1539 advances: vec![8.0, 8.0, 8.0],
1540 width: 60.0,
1541 ascent: 10.0,
1542 descent: 3.0,
1543 color: Color::BLACK,
1544 bold: false,
1545 italic: false,
1546 underline: None,
1547 strike: false,
1548 dstrike: false,
1549 highlight: None,
1550 baseline_offset: 0.0,
1551 hyperlink_url: Some("https://example.com".to_string()),
1552 field_kind: None,
1553 footnote_id: None,
1554 };
1555 let line = LayoutLine {
1556 items: vec![LineItem::Text(seg)],
1557 width: 60.0,
1558 ascent: 10.0,
1559 descent: 3.0,
1560 height: 13.0,
1561 indent_left: 0.0,
1562 available_width: 468.0,
1563 is_last: true,
1564 };
1565 let para = ParagraphBlock {
1566 lines: vec![line],
1567 space_before: 0.0,
1568 space_after: 0.0,
1569 borders: None,
1570 shading: None,
1571 indent_left: 0.0,
1572 indent_right: 0.0,
1573 jc: None,
1574 keep_next: false,
1575 keep_lines: false,
1576 page_break_before: false,
1577 widow_control: true,
1578 heading_level: None,
1579 heading_text: None,
1580 };
1581 let blocks = vec![LayoutBlock::Paragraph(para)];
1582 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1583 let annotations: Vec<_> = pages[0]
1584 .elements
1585 .iter()
1586 .filter(|e| matches!(e, PositionedElement::LinkAnnotation { .. }))
1587 .collect();
1588 assert_eq!(annotations.len(), 1, "expected 1 link annotation");
1589 if let PositionedElement::LinkAnnotation { url, .. } = annotations[0] {
1590 assert_eq!(url, "https://example.com");
1591 }
1592 }
1593
1594 #[test]
1595 fn justified_text_fills_line_width() {
1596 let fm = FontManager::new();
1597 let para = ParagraphBlock {
1599 lines: vec![
1600 make_justified_line("Hello World", 200.0, false),
1601 make_justified_line("End.", 40.0, true),
1602 ],
1603 space_before: 0.0,
1604 space_after: 0.0,
1605 borders: None,
1606 shading: None,
1607 indent_left: 0.0,
1608 indent_right: 0.0,
1609 jc: Some(ST_Jc::Both),
1610 keep_next: false,
1611 keep_lines: false,
1612 page_break_before: false,
1613 widow_control: true,
1614 heading_level: None,
1615 heading_text: None,
1616 };
1617
1618 let blocks = vec![LayoutBlock::Paragraph(para)];
1619 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1620
1621 let first_text = pages[0].elements.iter().find_map(|e| {
1623 if let PositionedElement::Text(run) = e {
1624 Some(run)
1625 } else {
1626 None
1627 }
1628 });
1629 assert!(first_text.is_some());
1630 let run = first_text.unwrap();
1631 let total_advance: f64 = run.advances.iter().sum();
1633 assert!(
1634 total_advance > 200.0,
1635 "justified text should be wider than original: {total_advance}"
1636 );
1637 }
1638
1639 #[test]
1640 fn justified_last_line_stays_left_aligned() {
1641 let fm = FontManager::new();
1642 let para = ParagraphBlock {
1643 lines: vec![
1644 make_justified_line("Hello World Test", 200.0, false),
1645 make_justified_line("End.", 40.0, true),
1646 ],
1647 space_before: 0.0,
1648 space_after: 0.0,
1649 borders: None,
1650 shading: None,
1651 indent_left: 0.0,
1652 indent_right: 0.0,
1653 jc: Some(ST_Jc::Both),
1654 keep_next: false,
1655 keep_lines: false,
1656 page_break_before: false,
1657 widow_control: true,
1658 heading_level: None,
1659 heading_text: None,
1660 };
1661
1662 let blocks = vec![LayoutBlock::Paragraph(para)];
1663 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1664
1665 let text_runs: Vec<_> = pages[0]
1667 .elements
1668 .iter()
1669 .filter_map(|e| {
1670 if let PositionedElement::Text(run) = e {
1671 Some(run)
1672 } else {
1673 None
1674 }
1675 })
1676 .collect();
1677
1678 assert!(text_runs.len() >= 2);
1679 let last_advance: f64 = text_runs[1].advances.iter().sum();
1681 assert!(
1682 (last_advance - 40.0).abs() < 0.1,
1683 "last line should stay at original width: {last_advance}"
1684 );
1685 }
1686
1687 #[test]
1688 fn justified_single_word_not_stretched() {
1689 let fm = FontManager::new();
1690 let para = ParagraphBlock {
1692 lines: vec![
1693 make_justified_line("Superlongword", 100.0, false),
1694 make_justified_line("End.", 40.0, true),
1695 ],
1696 space_before: 0.0,
1697 space_after: 0.0,
1698 borders: None,
1699 shading: None,
1700 indent_left: 0.0,
1701 indent_right: 0.0,
1702 jc: Some(ST_Jc::Both),
1703 keep_next: false,
1704 keep_lines: false,
1705 page_break_before: false,
1706 widow_control: true,
1707 heading_level: None,
1708 heading_text: None,
1709 };
1710
1711 let blocks = vec![LayoutBlock::Paragraph(para)];
1712 let (pages, _outlines) = paginate(&blocks, PageGeometry::default(), None, false, &fm);
1713
1714 let first_text = pages[0].elements.iter().find_map(|e| {
1715 if let PositionedElement::Text(run) = e {
1716 Some(run)
1717 } else {
1718 None
1719 }
1720 });
1721 assert!(first_text.is_some());
1722 let run = first_text.unwrap();
1723 let total_advance: f64 = run.advances.iter().sum();
1724 assert!(
1726 (total_advance - 100.0).abs() < 0.1,
1727 "single word should not be stretched: {total_advance}"
1728 );
1729 }
1730}