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