1use rdocx_oxml::document::{BodyContent, CT_SectPr};
4use rdocx_oxml::header_footer::HdrFtrType;
5use rdocx_oxml::properties::CT_PPr;
6use rdocx_oxml::shared::ST_HighlightColor;
7use rdocx_oxml::styles::CT_Styles;
8use rdocx_oxml::text::{BreakType, CT_P, FieldType, RunContent};
9
10use crate::block::{self, LayoutBlock, ParagraphBlock};
11use crate::error::Result;
12use crate::font::FontManager;
13use crate::input::LayoutInput;
14use crate::line::{self, InlineItem, LineBreakParams, LineItem, TextSegment};
15use crate::output::{
16 Color, DocumentMetadata, FieldKind, GlyphRun, LayoutResult, PageFrame, Point,
17 PositionedElement, Rect,
18};
19use crate::paginator::{self, HeaderFooterContent, PageGeometry};
20use crate::style_resolver::{self, NumberingState};
21use crate::table;
22
23pub struct Engine {
25 font_manager: FontManager,
26}
27
28impl Default for Engine {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl Engine {
35 pub fn new() -> Self {
36 Engine {
37 font_manager: FontManager::new(),
38 }
39 }
40
41 pub fn layout(&mut self, input: &LayoutInput) -> Result<LayoutResult> {
43 if !input.fonts.is_empty() {
45 self.font_manager.load_additional_fonts(&input.fonts);
46 }
47
48 let styles = &input.styles;
49 let mut num_state = NumberingState::new();
50
51 let final_sect_pr = input
53 .document
54 .body
55 .sect_pr
56 .as_ref()
57 .cloned()
58 .unwrap_or_else(CT_SectPr::default_letter);
59
60 let mut sections: Vec<paginator::Section> = Vec::new();
62 let mut current_blocks: Vec<LayoutBlock> = Vec::new();
63 let mut current_sect_pr: Option<CT_SectPr> = None; for content in &input.document.body.content {
66 match content {
67 BodyContent::Paragraph(para) => {
68 let para_sect_pr = para.properties.as_ref().and_then(|p| p.sect_pr.clone());
70
71 let sect_pr_for_layout = para_sect_pr
72 .as_ref()
73 .or(current_sect_pr.as_ref())
74 .unwrap_or(&final_sect_pr);
75 let geometry = sect_pr_to_geometry(sect_pr_for_layout);
76
77 let mut para_block = layout_paragraph(
78 para,
79 geometry.content_width(),
80 styles,
81 input,
82 &mut self.font_manager,
83 &mut num_state,
84 )?;
85
86 if let Some(level) = detect_heading_level(para, styles) {
88 para_block.heading_level = Some(level);
89 para_block.heading_text = Some(para.text());
90 }
91
92 current_blocks.push(LayoutBlock::Paragraph(para_block));
93
94 if let Some(sect_pr) = para_sect_pr {
96 let geometry = sect_pr_to_geometry(§_pr);
97 let header_footer = layout_header_footer(
98 §_pr,
99 input,
100 styles,
101 &mut self.font_manager,
102 &mut num_state,
103 )?;
104 let title_pg = sect_pr.title_pg.unwrap_or(false);
105 sections.push(paginator::Section {
106 blocks: std::mem::take(&mut current_blocks),
107 geometry,
108 header_footer,
109 title_pg,
110 });
111 current_sect_pr = Some(sect_pr);
112 }
113 }
114 BodyContent::Table(tbl) => {
115 let sect_pr_for_layout = current_sect_pr.as_ref().unwrap_or(&final_sect_pr);
116 let geometry = sect_pr_to_geometry(sect_pr_for_layout);
117
118 let table_block = table::layout_table(
119 tbl,
120 geometry.content_width(),
121 styles,
122 input,
123 &mut self.font_manager,
124 &mut num_state,
125 )?;
126 current_blocks.push(LayoutBlock::Table(table_block));
127 }
128 _ => {} }
130 }
131
132 let final_geometry = sect_pr_to_geometry(&final_sect_pr);
134 let final_hf = layout_header_footer(
135 &final_sect_pr,
136 input,
137 styles,
138 &mut self.font_manager,
139 &mut num_state,
140 )?;
141 let final_title_pg = final_sect_pr.title_pg.unwrap_or(false);
142 sections.push(paginator::Section {
143 blocks: current_blocks,
144 geometry: final_geometry,
145 header_footer: final_hf,
146 title_pg: final_title_pg,
147 });
148
149 let (mut pages, outlines) = paginator::paginate_sections(§ions, &self.font_manager);
151
152 let total_pages = pages.len();
154 for page in &mut pages {
155 let page_num = page.page_number;
156 substitute_fields(
157 &mut page.elements,
158 page_num,
159 total_pages,
160 &mut self.font_manager,
161 );
162 }
163
164 apply_page_background(&mut pages, input);
166
167 resolve_anchor_images(&mut pages, input);
169
170 resolve_inline_images(&mut pages, input);
172
173 if input.footnotes.is_some() || input.endnotes.is_some() {
175 render_page_footnotes(
176 &mut pages,
177 input,
178 styles,
179 &final_geometry,
180 &mut self.font_manager,
181 &mut num_state,
182 )?;
183 }
184
185 let fonts = self.font_manager.all_font_data();
187
188 let metadata = input.core_properties.as_ref().map(|cp| DocumentMetadata {
190 title: cp.title.clone(),
191 author: cp.creator.clone(),
192 subject: cp.subject.clone(),
193 keywords: cp.keywords.clone(),
194 creator: Some("rdocx".to_string()),
195 });
196
197 Ok(LayoutResult {
198 pages,
199 fonts,
200 metadata,
201 outlines,
202 })
203 }
204}
205
206fn apply_page_background(pages: &mut [PageFrame], input: &LayoutInput) {
208 let bg_xml = match &input.document.background_xml {
209 Some(xml) => xml,
210 None => return,
211 };
212
213 let xml_str = std::str::from_utf8(bg_xml).unwrap_or("");
215 let color = extract_background_color(xml_str);
216 let color = match color {
217 Some(c) => c,
218 None => return,
219 };
220
221 for page in pages.iter_mut() {
223 page.elements.insert(
224 0,
225 PositionedElement::FilledRect {
226 rect: Rect {
227 x: 0.0,
228 y: 0.0,
229 width: page.width,
230 height: page.height,
231 },
232 color,
233 },
234 );
235 }
236}
237
238fn extract_background_color(xml: &str) -> Option<Color> {
240 for attr in ["w:color=\"", "color=\""] {
242 if let Some(start) = xml.find(attr) {
243 let val_start = start + attr.len();
244 if let Some(end) = xml[val_start..].find('"') {
245 let hex = &xml[val_start..val_start + end];
246 if hex.len() == 6 && hex != "auto" {
247 return Some(Color::from_hex(hex));
248 }
249 }
250 }
251 }
252 None
253}
254
255fn resolve_anchor_images(pages: &mut [PageFrame], input: &LayoutInput) {
260 use crate::output::Rect;
261 use rdocx_oxml::text::RunContent;
262
263 let mut anchor_images: Vec<(bool, f64, f64, f64, f64, String)> = Vec::new();
265
266 for content in &input.document.body.content {
267 if let BodyContent::Paragraph(p) = content {
268 for run in &p.runs {
269 for rc in &run.content {
270 if let RunContent::Drawing(drawing) = rc
271 && let Some(ref anchor) = drawing.anchor
272 {
273 let behind = anchor.behind_doc;
274 let x = anchor.pos_h_offset.to_pt();
276 let y = anchor.pos_v_offset.to_pt();
277 let w = anchor.extent_cx.to_pt();
278 let h = anchor.extent_cy.to_pt();
279 anchor_images.push((behind, x, y, w, h, anchor.embed_id.clone()));
280 }
281 }
282 }
283 }
284 }
285
286 if anchor_images.is_empty() {
287 return;
288 }
289
290 for (behind, x, y, w, h, embed_id) in &anchor_images {
292 let (data, content_type) = if let Some(img) = input.images.get(embed_id) {
293 (img.data.clone(), img.content_type.clone())
294 } else {
295 continue;
296 };
297
298 let element = PositionedElement::Image {
299 rect: Rect {
300 x: *x,
301 y: *y,
302 width: *w,
303 height: *h,
304 },
305 data,
306 content_type,
307 embed_id: None, };
309
310 if *behind {
311 if let Some(page) = pages.first_mut() {
314 page.elements.insert(0, element);
315 }
316 } else if let Some(page) = pages.first_mut() {
317 page.elements.push(element);
319 }
320 }
321}
322
323fn resolve_inline_images(pages: &mut [PageFrame], input: &LayoutInput) {
328 for page in pages.iter_mut() {
329 for element in &mut page.elements {
330 if let PositionedElement::Image {
331 data,
332 content_type,
333 embed_id: Some(eid),
334 ..
335 } = element
336 && data.is_empty()
337 && let Some(img) = input.images.get(eid.as_str())
338 {
339 *data = img.data.clone();
340 *content_type = img.content_type.clone();
341 }
342 }
343 }
344}
345
346fn substitute_fields(
348 elements: &mut [PositionedElement],
349 page_number: usize,
350 total_pages: usize,
351 fm: &mut crate::font::FontManager,
352) {
353 for element in elements.iter_mut() {
354 if let PositionedElement::Text(run) = element
355 && let Some(fk) = run.field_kind
356 {
357 let value = match fk {
358 FieldKind::Page => page_number.to_string(),
359 FieldKind::NumPages => total_pages.to_string(),
360 };
361 if let Ok(shaped) = fm.shape_text(run.font_id, &value, run.font_size) {
363 run.text = value;
364 run.glyph_ids = shaped.glyph_ids;
365 run.advances = shaped.advances;
366 }
367 }
368 }
369}
370
371fn render_page_footnotes(
376 pages: &mut [PageFrame],
377 input: &LayoutInput,
378 styles: &CT_Styles,
379 geometry: &paginator::PageGeometry,
380 fm: &mut FontManager,
381 num_state: &mut NumberingState,
382) -> Result<()> {
383 let footnote_font_size = 8.0; let separator_offset = 6.0; let separator_width_frac = 0.33; for page in pages.iter_mut() {
388 let mut footnote_ids: Vec<i32> = Vec::new();
390 for element in &page.elements {
391 if let PositionedElement::Text(run) = element
392 && let Some(fn_id) = run.footnote_id
393 && !footnote_ids.contains(&fn_id)
394 {
395 footnote_ids.push(fn_id);
396 }
397 }
398
399 if footnote_ids.is_empty() {
400 continue;
401 }
402
403 let mut footnote_blocks: Vec<(i32, Vec<block::ParagraphBlock>)> = Vec::new();
405 for &fn_id in &footnote_ids {
406 let paragraphs = input
408 .footnotes
409 .as_ref()
410 .and_then(|fns| fns.get_by_id(fn_id))
411 .or_else(|| input.endnotes.as_ref().and_then(|ens| ens.get_by_id(fn_id)));
412
413 if let Some(footnote) = paragraphs {
414 let mut fn_blocks = Vec::new();
415 for para in &footnote.paragraphs {
416 if let Ok(pb) = layout_paragraph(
417 para,
418 geometry.content_width(),
419 styles,
420 input,
421 fm,
422 num_state,
423 ) {
424 fn_blocks.push(pb);
425 }
426 }
427 footnote_blocks.push((fn_id, fn_blocks));
428 }
429 }
430
431 if footnote_blocks.is_empty() {
432 continue;
433 }
434
435 let total_fn_height: f64 = footnote_blocks
437 .iter()
438 .flat_map(|(_, blocks)| blocks.iter())
439 .map(|b| b.content_height())
440 .sum();
441
442 let footnote_area_top =
444 page.height - geometry.margin_bottom - total_fn_height - separator_offset;
445
446 let sep_y = footnote_area_top;
448 let sep_width = geometry.content_width() * separator_width_frac;
449 page.elements.push(PositionedElement::Line {
450 start: Point {
451 x: geometry.margin_left,
452 y: sep_y,
453 },
454 end: Point {
455 x: geometry.margin_left + sep_width,
456 y: sep_y,
457 },
458 width: 0.5,
459 color: Color::BLACK,
460 dash_pattern: None,
461 });
462
463 let mut cursor_y = sep_y + separator_offset;
465 for (fn_id, blocks) in &footnote_blocks {
466 for pb in blocks {
467 let baseline_y = cursor_y + pb.lines.first().map(|l| l.ascent).unwrap_or(0.0);
468
469 let marker_text = fn_id.to_string();
471 let marker_size = footnote_font_size * 0.58;
472 if let Ok(font_id) = fm.resolve_font(Some("serif"), false, false)
473 && let Ok(shaped) = fm.shape_text(font_id, &marker_text, marker_size)
474 {
475 page.elements.push(PositionedElement::Text(GlyphRun {
476 origin: Point {
477 x: geometry.margin_left,
478 y: baseline_y - footnote_font_size * 0.33,
479 },
480 font_id,
481 font_size: marker_size,
482 glyph_ids: shaped.glyph_ids,
483 advances: shaped.advances,
484 text: marker_text,
485 color: Color::BLACK,
486 bold: false,
487 italic: false,
488 field_kind: None,
489 footnote_id: None,
490 }));
491 }
492
493 let indent = 12.0; for line in &pb.lines {
496 let line_baseline = cursor_y + line.ascent;
497 for item in &line.items {
498 if let LineItem::Text(seg) | LineItem::Marker(seg) = item {
499 page.elements.push(PositionedElement::Text(GlyphRun {
500 origin: Point {
501 x: geometry.margin_left + indent,
502 y: line_baseline - seg.baseline_offset,
503 },
504 font_id: seg.font_id,
505 font_size: seg.font_size,
506 glyph_ids: seg.glyph_ids.clone(),
507 advances: seg.advances.clone(),
508 text: seg.text.clone(),
509 color: seg.color,
510 bold: seg.bold,
511 italic: seg.italic,
512 field_kind: None,
513 footnote_id: None,
514 }));
515 }
516 }
517 cursor_y += line.height;
518 }
519 }
520 }
521 }
522
523 Ok(())
524}
525
526fn detect_heading_level(para: &CT_P, styles: &CT_Styles) -> Option<u32> {
528 let style_id = para.properties.as_ref()?.style_id.as_deref()?;
529 if let Some(rest) = style_id.strip_prefix("Heading") {
531 return rest.parse::<u32>().ok().filter(|n| (1..=9).contains(n));
532 }
533 if let Some(style_def) = styles.get_by_id(style_id)
535 && let Some(ref name) = style_def.name
536 && let Some(rest) = name.strip_prefix("heading ")
537 {
538 return rest.parse::<u32>().ok().filter(|n| (1..=9).contains(n));
539 }
540 None
541}
542
543pub fn layout_paragraph(
545 para: &CT_P,
546 available_width: f64,
547 styles: &CT_Styles,
548 input: &LayoutInput,
549 fm: &mut FontManager,
550 num_state: &mut NumberingState,
551) -> Result<ParagraphBlock> {
552 let para_style_id = para.properties.as_ref().and_then(|p| p.style_id.as_deref());
554
555 let resolved_ppr = style_resolver::resolve_paragraph_properties(para_style_id, styles);
556
557 let mut effective_ppr = resolved_ppr;
559 if let Some(ref direct_ppr) = para.properties {
560 merge_direct_ppr(&mut effective_ppr, direct_ppr);
561 }
562
563 let space_before = effective_ppr.space_before.map(|t| t.to_pt()).unwrap_or(0.0);
565 let space_after = effective_ppr.space_after.map(|t| t.to_pt()).unwrap_or(0.0);
566 let ind_left = effective_ppr.ind_left.map(|t| t.to_pt()).unwrap_or(0.0);
567 let ind_right = effective_ppr.ind_right.map(|t| t.to_pt()).unwrap_or(0.0);
568 let ind_first_line = effective_ppr
569 .ind_first_line
570 .map(|t| t.to_pt())
571 .unwrap_or(0.0);
572 let ind_hanging = effective_ppr.ind_hanging.map(|t| t.to_pt()).unwrap_or(0.0);
573
574 let keep_next = effective_ppr.keep_next.unwrap_or(false);
575 let keep_lines = effective_ppr.keep_lines.unwrap_or(false);
576 let page_break_before = effective_ppr.page_break_before.unwrap_or(false);
577 let widow_control = effective_ppr.widow_control.unwrap_or(true);
578 let jc = effective_ppr.jc;
579
580 let tab_stops = effective_ppr
582 .tabs
583 .as_ref()
584 .map(|t| t.tabs.clone())
585 .unwrap_or_default();
586
587 let shading = effective_ppr
589 .shading
590 .as_ref()
591 .and_then(|shd| shd.fill.as_ref())
592 .filter(|f| f != &"auto")
593 .map(|f| Color::from_hex(f));
594
595 let mut inline_items = Vec::new();
597
598 if let (Some(num_id), Some(numbering)) = (effective_ppr.num_id, input.numbering.as_ref()) {
600 let ilvl = effective_ppr.num_ilvl.unwrap_or(0);
601 if let Some(marker) = style_resolver::generate_marker(num_id, ilvl, numbering, num_state) {
602 let marker_rpr = marker.marker_rpr;
604 let marker_font_size = marker_rpr.sz.map(|hp| hp.to_pt()).unwrap_or_else(|| {
605 style_resolver::resolve_run_properties(para_style_id, None, styles)
606 .sz
607 .map(|hp| hp.to_pt())
608 .unwrap_or(11.0)
609 });
610 let marker_bold = marker_rpr.bold.unwrap_or(false);
611 let marker_italic = marker_rpr.italic.unwrap_or(false);
612 let marker_font_family = marker_rpr.font_ascii.as_deref();
613
614 if let Ok(font_id) = fm.resolve_font(marker_font_family, marker_bold, marker_italic)
615 && let Ok(shaped) = fm.shape_text(font_id, &marker.marker_text, marker_font_size)
616 {
617 let metrics = fm.metrics(font_id, marker_font_size)?;
618 let color = marker_rpr
619 .color
620 .as_ref()
621 .map(|c| Color::from_hex(c))
622 .unwrap_or(Color::BLACK);
623
624 inline_items.push(InlineItem::Marker(TextSegment {
625 text: marker.marker_text,
626 font_id,
627 font_size: marker_font_size,
628 glyph_ids: shaped.glyph_ids,
629 advances: shaped.advances,
630 width: shaped.width,
631 ascent: metrics.ascent,
632 descent: metrics.descent,
633 color,
634 bold: marker_bold,
635 italic: marker_italic,
636 underline: None,
637 strike: false,
638 dstrike: false,
639 highlight: None,
640 baseline_offset: 0.0,
641 hyperlink_url: None,
642 field_kind: None,
643 footnote_id: None,
644 }));
645
646 inline_items.push(InlineItem::Tab);
648 }
649 }
650 }
651
652 let mut run_hyperlink_url: std::collections::HashMap<usize, String> =
654 std::collections::HashMap::new();
655 for hl in ¶.hyperlinks {
656 if let Some(ref rel_id) = hl.rel_id
657 && let Some(url) = input.hyperlink_urls.get(rel_id)
658 {
659 for run_idx in hl.run_start..hl.run_end {
660 run_hyperlink_url.insert(run_idx, url.clone());
661 }
662 }
663 }
664
665 for (run_idx, run) in para.runs.iter().enumerate() {
667 let current_hyperlink_url = run_hyperlink_url.get(&run_idx).cloned();
668
669 let run_style_id = run.properties.as_ref().and_then(|p| p.style_id.as_deref());
670
671 let resolved_rpr =
672 style_resolver::resolve_run_properties(para_style_id, run_style_id, styles);
673
674 let mut effective_rpr = resolved_rpr;
676 if let Some(ref direct_rpr) = run.properties {
677 effective_rpr.merge_from(direct_rpr);
678 }
679
680 if effective_rpr.vanish == Some(true) {
682 continue;
683 }
684
685 let mut font_size = effective_rpr.sz.map(|hp| hp.to_pt()).unwrap_or(11.0);
686 let bold = effective_rpr.bold.unwrap_or(false);
687 let italic = effective_rpr.italic.unwrap_or(false);
688
689 let font_family = resolve_font_family(&effective_rpr, input.theme.as_ref());
691
692 let color = resolve_run_color(&effective_rpr, input.theme.as_ref());
694
695 let underline = effective_rpr.underline;
697 let strike = effective_rpr.strike.unwrap_or(false);
698 let dstrike = effective_rpr.dstrike.unwrap_or(false);
699 let highlight = effective_rpr.highlight.and_then(highlight_to_color);
700
701 let mut baseline_offset = 0.0;
703 if let Some(ref va) = effective_rpr.vert_align {
704 match va.as_str() {
705 "superscript" => {
706 let original_size = font_size;
708 font_size *= 0.58;
709 baseline_offset = original_size * 0.33; }
711 "subscript" => {
712 let original_size = font_size;
714 font_size *= 0.58;
715 baseline_offset = -(original_size * 0.14); }
717 _ => {}
718 }
719 }
720
721 if let Some(pos) = effective_rpr.position {
723 baseline_offset += pos as f64 / 2.0; }
725
726 let font_id = fm.resolve_font(font_family.as_deref(), bold, italic)?;
727 let metrics = fm.metrics(font_id, font_size)?;
728
729 for content in &run.content {
730 match content {
731 RunContent::Text(ct_text) => {
732 let text = if effective_rpr.caps == Some(true) {
733 ct_text.text.to_uppercase()
734 } else {
735 ct_text.text.clone()
736 };
737
738 if text.is_empty() {
739 continue;
740 }
741
742 let mut shaped = fm.shape_text(font_id, &text, font_size)?;
743
744 if let Some(spacing) = effective_rpr.spacing {
746 let extra = spacing.to_pt();
747 for advance in &mut shaped.advances {
748 *advance += extra;
749 }
750 shaped.width += extra * shaped.advances.len() as f64;
751 }
752
753 inline_items.push(InlineItem::Text(TextSegment {
754 text,
755 font_id,
756 font_size,
757 glyph_ids: shaped.glyph_ids,
758 advances: shaped.advances,
759 width: shaped.width,
760 ascent: metrics.ascent,
761 descent: metrics.descent,
762 color,
763 bold,
764 italic,
765 underline,
766 strike,
767 dstrike,
768 highlight,
769 baseline_offset,
770 hyperlink_url: current_hyperlink_url.clone(),
771 field_kind: None,
772 footnote_id: None,
773 }));
774 }
775 RunContent::Tab => {
776 inline_items.push(InlineItem::Tab);
777 }
778 RunContent::Break(bt) => match bt {
779 BreakType::Line => inline_items.push(InlineItem::LineBreak),
780 BreakType::Page => inline_items.push(InlineItem::PageBreak),
781 BreakType::Column => inline_items.push(InlineItem::ColumnBreak),
782 },
783 RunContent::Drawing(drawing) => {
784 if let Some(ref inline) = drawing.inline {
785 let width = inline.extent_cx.to_pt();
786 let height = inline.extent_cy.to_pt();
787 inline_items.push(InlineItem::Image {
788 width,
789 height,
790 embed_id: inline.embed_id.clone(),
791 });
792 }
793 }
794 RunContent::Field { field_type } => {
795 let placeholder = "99";
797 let fk = match field_type {
798 FieldType::Page => FieldKind::Page,
799 FieldType::NumPages => FieldKind::NumPages,
800 FieldType::Other(_) => continue, };
802 let shaped = fm.shape_text(font_id, placeholder, font_size)?;
803 inline_items.push(InlineItem::Text(TextSegment {
804 text: placeholder.to_string(),
805 font_id,
806 font_size,
807 glyph_ids: shaped.glyph_ids,
808 advances: shaped.advances,
809 width: shaped.width,
810 ascent: metrics.ascent,
811 descent: metrics.descent,
812 color,
813 bold,
814 italic,
815 underline: None,
816 strike: false,
817 dstrike: false,
818 highlight: None,
819 baseline_offset,
820 hyperlink_url: None,
821 field_kind: Some(fk),
822 footnote_id: None,
823 }));
824 }
825 RunContent::FootnoteRef { id } | RunContent::EndnoteRef { id } => {
826 let marker = id.to_string();
828 let sup_size = font_size * 0.58;
829 let sup_offset = font_size * 0.33; let shaped = fm.shape_text(font_id, &marker, sup_size)?;
831 let sup_metrics = fm.metrics(font_id, sup_size)?;
832 inline_items.push(InlineItem::Text(TextSegment {
833 text: marker,
834 font_id,
835 font_size: sup_size,
836 glyph_ids: shaped.glyph_ids,
837 advances: shaped.advances,
838 width: shaped.width,
839 ascent: sup_metrics.ascent,
840 descent: sup_metrics.descent,
841 color,
842 bold,
843 italic,
844 underline: None,
845 strike: false,
846 dstrike: false,
847 highlight: None,
848 baseline_offset: sup_offset,
849 hyperlink_url: None,
850 field_kind: None,
851 footnote_id: Some(*id),
852 }));
853 }
854 }
855 }
856 }
857
858 let line_params = LineBreakParams {
860 available_width,
861 ind_left,
862 ind_right,
863 ind_first_line,
864 ind_hanging,
865 tab_stops,
866 line_spacing: effective_ppr.line_spacing,
867 line_rule: effective_ppr.line_rule,
868 jc,
869 };
870
871 let lines = line::break_into_lines(&inline_items, &line_params, fm)?;
872
873 Ok(block::build_paragraph_block(
874 lines,
875 space_before,
876 space_after,
877 effective_ppr.borders,
878 shading,
879 ind_left,
880 ind_right,
881 jc,
882 keep_next,
883 keep_lines,
884 page_break_before,
885 widow_control,
886 ))
887}
888
889fn merge_direct_ppr(effective: &mut CT_PPr, direct: &CT_PPr) {
891 if direct.jc.is_some() {
893 effective.jc = direct.jc;
894 }
895 if direct.space_before.is_some() {
896 effective.space_before = direct.space_before;
897 }
898 if direct.space_after.is_some() {
899 effective.space_after = direct.space_after;
900 }
901 if direct.line_spacing.is_some() {
902 effective.line_spacing = direct.line_spacing;
903 }
904 if direct.line_rule.is_some() {
905 effective.line_rule = direct.line_rule.clone();
906 }
907 if direct.ind_left.is_some() {
908 effective.ind_left = direct.ind_left;
909 }
910 if direct.ind_right.is_some() {
911 effective.ind_right = direct.ind_right;
912 }
913 if direct.ind_first_line.is_some() {
914 effective.ind_first_line = direct.ind_first_line;
915 }
916 if direct.ind_hanging.is_some() {
917 effective.ind_hanging = direct.ind_hanging;
918 }
919 if direct.keep_next.is_some() {
920 effective.keep_next = direct.keep_next;
921 }
922 if direct.keep_lines.is_some() {
923 effective.keep_lines = direct.keep_lines;
924 }
925 if direct.page_break_before.is_some() {
926 effective.page_break_before = direct.page_break_before;
927 }
928 if direct.widow_control.is_some() {
929 effective.widow_control = direct.widow_control;
930 }
931 if direct.borders.is_some() {
932 effective.borders = direct.borders.clone();
933 }
934 if direct.tabs.is_some() {
935 effective.tabs = direct.tabs.clone();
936 }
937 if direct.shading.is_some() {
938 effective.shading = direct.shading.clone();
939 }
940 if direct.num_id.is_some() {
941 effective.num_id = direct.num_id;
942 }
943 if direct.num_ilvl.is_some() {
944 effective.num_ilvl = direct.num_ilvl;
945 }
946}
947
948fn sect_pr_to_geometry(sect_pr: &CT_SectPr) -> PageGeometry {
950 PageGeometry {
951 page_width: sect_pr.page_width.map(|t| t.to_pt()).unwrap_or(612.0),
952 page_height: sect_pr.page_height.map(|t| t.to_pt()).unwrap_or(792.0),
953 margin_top: sect_pr.margin_top.map(|t| t.to_pt()).unwrap_or(72.0),
954 margin_right: sect_pr.margin_right.map(|t| t.to_pt()).unwrap_or(72.0),
955 margin_bottom: sect_pr.margin_bottom.map(|t| t.to_pt()).unwrap_or(72.0),
956 margin_left: sect_pr.margin_left.map(|t| t.to_pt()).unwrap_or(72.0),
957 header_distance: sect_pr.header_distance.map(|t| t.to_pt()).unwrap_or(36.0),
958 footer_distance: sect_pr.footer_distance.map(|t| t.to_pt()).unwrap_or(36.0),
959 }
960}
961
962fn layout_header_footer(
964 sect_pr: &CT_SectPr,
965 input: &LayoutInput,
966 styles: &CT_Styles,
967 fm: &mut FontManager,
968 num_state: &mut NumberingState,
969) -> Result<Option<HeaderFooterContent>> {
970 let mut has_content = false;
971 let mut header_blocks = Vec::new();
972 let mut footer_blocks = Vec::new();
973 let mut first_header_blocks = Vec::new();
974 let mut first_footer_blocks = Vec::new();
975
976 let geometry = sect_pr_to_geometry(sect_pr);
977 let width = geometry.content_width();
978
979 for href in §_pr.header_refs {
980 let target_blocks = match href.hdr_ftr_type {
981 HdrFtrType::Default => &mut header_blocks,
982 HdrFtrType::First => &mut first_header_blocks,
983 _ => continue, };
985 if let Some(hdr) = input.headers.get(&href.rel_id) {
986 for para in &hdr.paragraphs {
987 let block = layout_paragraph(para, width, styles, input, fm, num_state)?;
988 target_blocks.push(block);
989 }
990 has_content = true;
991 }
992 }
993
994 for fref in §_pr.footer_refs {
995 let target_blocks = match fref.hdr_ftr_type {
996 HdrFtrType::Default => &mut footer_blocks,
997 HdrFtrType::First => &mut first_footer_blocks,
998 _ => continue, };
1000 if let Some(ftr) = input.footers.get(&fref.rel_id) {
1001 for para in &ftr.paragraphs {
1002 let block = layout_paragraph(para, width, styles, input, fm, num_state)?;
1003 target_blocks.push(block);
1004 }
1005 has_content = true;
1006 }
1007 }
1008
1009 if has_content {
1010 Ok(Some(HeaderFooterContent {
1011 header_blocks,
1012 footer_blocks,
1013 first_header_blocks,
1014 first_footer_blocks,
1015 }))
1016 } else {
1017 Ok(None)
1018 }
1019}
1020
1021fn resolve_font_family(
1025 rpr: &rdocx_oxml::properties::CT_RPr,
1026 theme: Option<&rdocx_oxml::theme::Theme>,
1027) -> Option<String> {
1028 if rpr.font_ascii.is_some() {
1030 return rpr.font_ascii.clone();
1031 }
1032
1033 if let (Some(theme_ref), Some(theme)) = (&rpr.font_ascii_theme, theme) {
1035 let font = match theme_ref.as_str() {
1036 "majorAscii" | "majorHAnsi" | "majorBidi" | "majorEastAsia" => {
1037 theme.major_font.as_deref()
1038 }
1039 "minorAscii" | "minorHAnsi" | "minorBidi" | "minorEastAsia" => {
1040 theme.minor_font.as_deref()
1041 }
1042 _ => None,
1043 };
1044 if let Some(f) = font {
1045 return Some(f.to_string());
1046 }
1047 }
1048
1049 None
1050}
1051
1052fn resolve_run_color(
1056 rpr: &rdocx_oxml::properties::CT_RPr,
1057 theme: Option<&rdocx_oxml::theme::Theme>,
1058) -> Color {
1059 if let Some(ref theme_name) = rpr.color_theme
1061 && let Some(theme) = theme
1062 && let Some(hex) = theme.colors.get(theme_name)
1063 {
1064 return Color::from_hex(hex);
1065 }
1066
1067 rpr.color
1069 .as_ref()
1070 .filter(|c| c.as_str() != "auto")
1071 .map(|c| Color::from_hex(c))
1072 .unwrap_or(Color::BLACK)
1073}
1074
1075fn highlight_to_color(h: ST_HighlightColor) -> Option<Color> {
1077 match h {
1078 ST_HighlightColor::None => None,
1079 ST_HighlightColor::Black => Some(Color {
1080 r: 0.0,
1081 g: 0.0,
1082 b: 0.0,
1083 a: 1.0,
1084 }),
1085 ST_HighlightColor::Blue => Some(Color {
1086 r: 0.0,
1087 g: 0.0,
1088 b: 1.0,
1089 a: 1.0,
1090 }),
1091 ST_HighlightColor::Cyan => Some(Color {
1092 r: 0.0,
1093 g: 1.0,
1094 b: 1.0,
1095 a: 1.0,
1096 }),
1097 ST_HighlightColor::DarkBlue => Some(Color {
1098 r: 0.0,
1099 g: 0.0,
1100 b: 0.545,
1101 a: 1.0,
1102 }),
1103 ST_HighlightColor::DarkCyan => Some(Color {
1104 r: 0.0,
1105 g: 0.545,
1106 b: 0.545,
1107 a: 1.0,
1108 }),
1109 ST_HighlightColor::DarkGray => Some(Color {
1110 r: 0.663,
1111 g: 0.663,
1112 b: 0.663,
1113 a: 1.0,
1114 }),
1115 ST_HighlightColor::DarkGreen => Some(Color {
1116 r: 0.0,
1117 g: 0.392,
1118 b: 0.0,
1119 a: 1.0,
1120 }),
1121 ST_HighlightColor::DarkMagenta => Some(Color {
1122 r: 0.545,
1123 g: 0.0,
1124 b: 0.545,
1125 a: 1.0,
1126 }),
1127 ST_HighlightColor::DarkRed => Some(Color {
1128 r: 0.545,
1129 g: 0.0,
1130 b: 0.0,
1131 a: 1.0,
1132 }),
1133 ST_HighlightColor::DarkYellow => Some(Color {
1134 r: 0.545,
1135 g: 0.545,
1136 b: 0.0,
1137 a: 1.0,
1138 }),
1139 ST_HighlightColor::Green => Some(Color {
1140 r: 0.0,
1141 g: 1.0,
1142 b: 0.0,
1143 a: 1.0,
1144 }),
1145 ST_HighlightColor::LightGray => Some(Color {
1146 r: 0.827,
1147 g: 0.827,
1148 b: 0.827,
1149 a: 1.0,
1150 }),
1151 ST_HighlightColor::Magenta => Some(Color {
1152 r: 1.0,
1153 g: 0.0,
1154 b: 1.0,
1155 a: 1.0,
1156 }),
1157 ST_HighlightColor::Red => Some(Color {
1158 r: 1.0,
1159 g: 0.0,
1160 b: 0.0,
1161 a: 1.0,
1162 }),
1163 ST_HighlightColor::White => Some(Color {
1164 r: 1.0,
1165 g: 1.0,
1166 b: 1.0,
1167 a: 1.0,
1168 }),
1169 ST_HighlightColor::Yellow => Some(Color {
1170 r: 1.0,
1171 g: 1.0,
1172 b: 0.0,
1173 a: 1.0,
1174 }),
1175 }
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use super::*;
1181 use std::collections::HashMap;
1182
1183 fn make_input_with_text(text: &str) -> LayoutInput {
1184 let mut doc = rdocx_oxml::document::CT_Document::new();
1185 let mut p = CT_P::new();
1186 p.add_run(text);
1187 doc.body.add_paragraph(p);
1188
1189 LayoutInput {
1190 document: doc,
1191 styles: CT_Styles::new_default(),
1192 numbering: None,
1193 headers: HashMap::new(),
1194 footers: HashMap::new(),
1195 images: HashMap::new(),
1196 core_properties: None,
1197 hyperlink_urls: HashMap::new(),
1198 footnotes: None,
1199 endnotes: None,
1200 theme: None,
1201 fonts: Vec::new(),
1202 }
1203 }
1204
1205 #[test]
1206 fn layout_simple_document() {
1207 let input = make_input_with_text("Hello World");
1208 let result = Engine::new().layout(&input);
1209 if let Ok(result) = result {
1211 assert!(!result.pages.is_empty());
1212 assert_eq!(result.pages[0].page_number, 1);
1213 assert!((result.pages[0].width - 612.0).abs() < 0.01);
1214 }
1215 }
1216
1217 #[test]
1218 fn layout_empty_document() {
1219 let mut doc = rdocx_oxml::document::CT_Document::new();
1220 doc.body.add_paragraph(CT_P::new());
1221
1222 let input = LayoutInput {
1223 document: doc,
1224 styles: CT_Styles::new_default(),
1225 numbering: None,
1226 headers: HashMap::new(),
1227 footers: HashMap::new(),
1228 images: HashMap::new(),
1229 core_properties: None,
1230 hyperlink_urls: HashMap::new(),
1231 footnotes: None,
1232 endnotes: None,
1233 theme: None,
1234 fonts: Vec::new(),
1235 };
1236
1237 let result = Engine::new().layout(&input);
1238 if let Ok(result) = result {
1239 assert_eq!(result.pages.len(), 1);
1240 }
1241 }
1242
1243 #[test]
1244 fn layout_with_heading_style() {
1245 let mut doc = rdocx_oxml::document::CT_Document::new();
1246 let mut p = CT_P::new();
1247 p.properties = Some(CT_PPr {
1248 style_id: Some("Heading1".to_string()),
1249 ..Default::default()
1250 });
1251 p.add_run("Chapter 1");
1252 doc.body.add_paragraph(p);
1253
1254 let input = LayoutInput {
1255 document: doc,
1256 styles: CT_Styles::new_default(),
1257 numbering: None,
1258 headers: HashMap::new(),
1259 footers: HashMap::new(),
1260 images: HashMap::new(),
1261 core_properties: None,
1262 hyperlink_urls: HashMap::new(),
1263 footnotes: None,
1264 endnotes: None,
1265 theme: None,
1266 fonts: Vec::new(),
1267 };
1268
1269 let result = Engine::new().layout(&input);
1270 if let Ok(result) = result {
1271 assert!(!result.pages.is_empty());
1272 assert_eq!(result.outlines.len(), 1);
1274 assert_eq!(result.outlines[0].title, "Chapter 1");
1275 assert_eq!(result.outlines[0].level, 1);
1276 assert_eq!(result.outlines[0].page_index, 0);
1277 }
1278 }
1279
1280 #[test]
1281 fn layout_nested_headings_produce_outlines() {
1282 let mut doc = rdocx_oxml::document::CT_Document::new();
1283
1284 let mut h1 = CT_P::new();
1286 h1.properties = Some(CT_PPr {
1287 style_id: Some("Heading1".to_string()),
1288 ..Default::default()
1289 });
1290 h1.add_run("Chapter 1");
1291 doc.body.add_paragraph(h1);
1292
1293 let mut h2 = CT_P::new();
1295 h2.properties = Some(CT_PPr {
1296 style_id: Some("Heading2".to_string()),
1297 ..Default::default()
1298 });
1299 h2.add_run("Section 1.1");
1300 doc.body.add_paragraph(h2);
1301
1302 let mut h1b = CT_P::new();
1304 h1b.properties = Some(CT_PPr {
1305 style_id: Some("Heading1".to_string()),
1306 ..Default::default()
1307 });
1308 h1b.add_run("Chapter 2");
1309 doc.body.add_paragraph(h1b);
1310
1311 let input = LayoutInput {
1312 document: doc,
1313 styles: CT_Styles::new_default(),
1314 numbering: None,
1315 headers: HashMap::new(),
1316 footers: HashMap::new(),
1317 images: HashMap::new(),
1318 core_properties: None,
1319 hyperlink_urls: HashMap::new(),
1320 footnotes: None,
1321 endnotes: None,
1322 theme: None,
1323 fonts: Vec::new(),
1324 };
1325
1326 let result = Engine::new().layout(&input);
1327 if let Ok(result) = result {
1328 assert_eq!(result.outlines.len(), 3);
1329 assert_eq!(result.outlines[0].level, 1);
1330 assert_eq!(result.outlines[0].title, "Chapter 1");
1331 assert_eq!(result.outlines[1].level, 2);
1332 assert_eq!(result.outlines[1].title, "Section 1.1");
1333 assert_eq!(result.outlines[2].level, 1);
1334 assert_eq!(result.outlines[2].title, "Chapter 2");
1335 }
1336 }
1337
1338 #[test]
1339 fn sect_pr_geometry_conversion() {
1340 let sect = CT_SectPr::default_letter();
1341 let geom = sect_pr_to_geometry(§);
1342 assert!((geom.page_width - 612.0).abs() < 0.01);
1343 assert!((geom.page_height - 792.0).abs() < 0.01);
1344 assert!((geom.margin_top - 72.0).abs() < 0.01);
1345 assert!((geom.content_width() - 468.0).abs() < 0.01);
1346 }
1347
1348 #[test]
1349 fn sect_pr_a4_geometry() {
1350 let sect = CT_SectPr::default_a4();
1351 let geom = sect_pr_to_geometry(§);
1352 assert!((geom.page_width - 595.3).abs() < 0.5);
1354 assert!((geom.page_height - 841.9).abs() < 0.5);
1355 }
1356}