docxide-pdf 0.9.0

CLI and Library for converting DOCX files to PDF, matching Microsoft Word's output as closely as possible
Documentation
use std::collections::HashMap;
use std::io::Read;

use crate::model::{Alignment, Block, Footnote, HeaderFooter, LineSpacing, Paragraph};

use super::numbering::NumberingInfo;
use super::parse_table_node;
use super::runs::parse_runs;
use super::styles::{StylesInfo, ThemeFonts, parse_alignment, parse_line_spacing};
use super::{WML_NS, parse_paragraph_borders, parse_tab_stops, twips_attr, wml, wml_attr};

pub(super) fn parse_header_footer_xml<R: Read + std::io::Seek>(
    xml_content: &str,
    styles: &StylesInfo,
    theme: &ThemeFonts,
    rels: &HashMap<String, String>,
    zip: &mut zip::ZipArchive<R>,
) -> Option<HeaderFooter> {
    let xml = roxmltree::Document::parse(xml_content).ok()?;
    let root = xml.root_element();
    let mut blocks = Vec::new();

    let mut top_nodes: Vec<roxmltree::Node> = Vec::new();
    for child in root.children() {
        if child.tag_name().namespace() != Some(WML_NS) {
            continue;
        }
        if child.tag_name().name() == "sdt" {
            if let Some(content) = wml(child, "sdtContent") {
                for inner in content.children() {
                    if inner.tag_name().namespace() == Some(WML_NS)
                        && (inner.tag_name().name() == "p" || inner.tag_name().name() == "tbl")
                    {
                        top_nodes.push(inner);
                    }
                }
            }
        } else if child.tag_name().name() == "p" || child.tag_name().name() == "tbl" {
            top_nodes.push(child);
        }
    }

    let numbering = NumberingInfo::default();
    let mut counters = HashMap::new();
    let mut last_seen_level = HashMap::new();

    for node in top_nodes {
        match node.tag_name().name() {
            "tbl" => {
                let table = parse_table_node(
                    node,
                    styles,
                    theme,
                    rels,
                    zip,
                    &numbering,
                    &mut counters,
                    &mut last_seen_level,
                );
                blocks.push(Block::Table(table));
            }
            "p" => {
                let ppr = wml(node, "pPr");
                let para_style_id = ppr
                    .and_then(|ppr| wml_attr(ppr, "pStyle"))
                    .unwrap_or(&styles.default_paragraph_style_id);
                let para_style = styles.paragraph_styles.get(para_style_id);

                let alignment = ppr
                    .and_then(|ppr| wml_attr(ppr, "jc"))
                    .map(parse_alignment)
                    .or_else(|| para_style.and_then(|s| s.alignment))
                    .unwrap_or(Alignment::Left);

                let inline_spacing = ppr.and_then(|ppr| wml(ppr, "spacing"));
                let line_spacing = inline_spacing
                    .and_then(|n| {
                        n.attribute((WML_NS, "line"))
                            .and_then(|v| v.parse::<f32>().ok())
                            .map(|line_val| parse_line_spacing(n, line_val))
                    })
                    .or_else(|| para_style.and_then(|s| s.line_spacing));

                let space_before = inline_spacing
                    .and_then(|n| twips_attr(n, "before"))
                    .or_else(|| para_style.and_then(|s| s.space_before))
                    .unwrap_or(0.0);
                let space_after = inline_spacing
                    .and_then(|n| twips_attr(n, "after"))
                    .or_else(|| para_style.and_then(|s| s.space_after))
                    .unwrap_or(0.0);

                let parsed = parse_runs(node, styles, theme, rels, zip, &NumberingInfo::default());

                let borders = ppr.map(parse_paragraph_borders).unwrap_or_default();
                let tab_stops = ppr.map(parse_tab_stops).unwrap_or_default();

                blocks.push(Block::Paragraph(Paragraph {
                    runs: parsed.runs,
                    alignment,
                    line_spacing,
                    space_before,
                    space_after,
                    borders,
                    tab_stops,
                    extra_line_breaks: parsed.line_break_count,
                    floating_images: parsed.floating_images,
                    textboxes: parsed.textboxes,
                    ..Paragraph::default()
                }));
            }
            _ => {}
        }
    }

    if blocks.is_empty() {
        None
    } else {
        Some(HeaderFooter { blocks })
    }
}

pub(super) fn parse_footnotes<R: Read + std::io::Seek>(
    zip: &mut zip::ZipArchive<R>,
    styles: &StylesInfo,
    theme: &ThemeFonts,
) -> HashMap<u32, Footnote> {
    let mut footnotes = HashMap::new();
    let Some(xml_text) = super::read_zip_text(zip, "word/footnotes.xml") else {
        return footnotes;
    };
    let Ok(xml) = roxmltree::Document::parse(&xml_text) else {
        return footnotes;
    };
    let root = xml.root_element();

    for node in root.children() {
        if node.tag_name().namespace() != Some(WML_NS) || node.tag_name().name() != "footnote" {
            continue;
        }
        // Skip separator/continuationSeparator footnotes (type attribute, IDs 0 and 1)
        if node.attribute((WML_NS, "type")).is_some() {
            continue;
        }
        let Some(id) = node
            .attribute((WML_NS, "id"))
            .and_then(|v| v.parse::<u32>().ok())
        else {
            continue;
        };

        let mut paragraphs = Vec::new();
        let empty_rels = HashMap::new();
        for p in node.children() {
            if p.tag_name().namespace() != Some(WML_NS) || p.tag_name().name() != "p" {
                continue;
            }
            let ppr = wml(p, "pPr");
            let para_style_id = ppr
                .and_then(|ppr| wml_attr(ppr, "pStyle"))
                .unwrap_or("FootnoteText");
            let para_style = styles.paragraph_styles.get(para_style_id);

            let alignment = ppr
                .and_then(|ppr| wml_attr(ppr, "jc"))
                .map(parse_alignment)
                .or_else(|| para_style.and_then(|s| s.alignment))
                .unwrap_or(Alignment::Left);

            let parsed = parse_runs(
                p,
                styles,
                theme,
                &empty_rels,
                zip,
                &NumberingInfo::default(),
            );

            let inline_spacing = ppr.and_then(|ppr| wml(ppr, "spacing"));
            let space_before = inline_spacing
                .and_then(|n| twips_attr(n, "before"))
                .or_else(|| para_style.and_then(|s| s.space_before))
                .unwrap_or(0.0);
            let space_after = inline_spacing
                .and_then(|n| twips_attr(n, "after"))
                .or_else(|| para_style.and_then(|s| s.space_after))
                .unwrap_or(0.0);
            let line_spacing = inline_spacing
                .and_then(|n| {
                    n.attribute((WML_NS, "line"))
                        .and_then(|v| v.parse::<f32>().ok())
                        .map(|line_val| parse_line_spacing(n, line_val))
                })
                .or_else(|| para_style.and_then(|s| s.line_spacing))
                .or(Some(LineSpacing::Auto(1.0)));

            paragraphs.push(Paragraph {
                runs: parsed.runs,
                space_before,
                space_after,
                alignment,
                line_spacing,
                extra_line_breaks: parsed.line_break_count,
                ..Paragraph::default()
            });
        }

        if !paragraphs.is_empty() {
            footnotes.insert(id, Footnote { paragraphs });
        }
    }

    footnotes
}