docxide-pdf 0.11.0

Library and CLI 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::{SmartArtDiagram, SmartArtShape};

use super::styles::ThemeFonts;
use super::textbox::{parse_shape_geometry, parse_solid_fill};
use super::{DML_NS, DSP_NS, emu_attr, find_child, read_zip_text};

const DIAGRAM_URI: &str = "http://schemas.openxmlformats.org/drawingml/2006/diagram";

fn dsp<'a>(node: roxmltree::Node<'a, 'a>, name: &str) -> Option<roxmltree::Node<'a, 'a>> {
    find_child(node, name, DSP_NS)
}

fn dml<'a>(node: roxmltree::Node<'a, 'a>, name: &str) -> Option<roxmltree::Node<'a, 'a>> {
    find_child(node, name, DML_NS)
}

fn has_dml(node: roxmltree::Node, name: &str) -> bool {
    node.children()
        .any(|n| n.tag_name().name() == name && n.tag_name().namespace() == Some(DML_NS))
}

pub(super) fn has_diagram_ref(container: roxmltree::Node) -> bool {
    container.descendants().any(|n| {
        n.tag_name().name() == "graphicData"
            && n.tag_name().namespace() == Some(DML_NS)
            && n.attribute("uri") == Some(DIAGRAM_URI)
    })
}

pub(super) fn parse_smartart_drawing<R: Read + std::io::Seek>(
    rels: &HashMap<String, String>,
    zip: &mut zip::ZipArchive<R>,
    theme: &ThemeFonts,
    display_w: f32,
    display_h: f32,
) -> SmartArtDiagram {
    let mut shapes = Vec::new();

    if let Some(target) = rels.values().find(|t| t.contains("diagrams/drawing")) {
        let zip_path = target
            .strip_prefix('/')
            .map(String::from)
            .unwrap_or_else(|| format!("word/{}", target));

        if let Some(xml) = read_zip_text(zip, &zip_path) {
            if let Ok(doc) = roxmltree::Document::parse(&xml) {
                let sp_tree = dsp(doc.root(), "drawing").and_then(|d| dsp(d, "spTree"));

                if let Some(tree) = sp_tree {
                    shapes = tree
                        .children()
                        .filter(|n| {
                            n.tag_name().name() == "sp" && n.tag_name().namespace() == Some(DSP_NS)
                        })
                        .filter_map(|sp| parse_dsp_shape(sp, theme))
                        .collect();
                }
            }
        }
    }

    SmartArtDiagram {
        display_width: display_w,
        display_height: display_h,
        shapes,
    }
}

fn parse_dsp_shape(sp: roxmltree::Node, theme: &ThemeFonts) -> Option<SmartArtShape> {
    let sp_pr = dsp(sp, "spPr")?;
    let xfrm = dml(sp_pr, "xfrm")?;
    let off = dml(xfrm, "off")?;
    let ext = dml(xfrm, "ext")?;

    let x = emu_attr(off, "x");
    let y = emu_attr(off, "y");
    let w = emu_attr(ext, "cx");
    let h = emu_attr(ext, "cy");

    let fill = if has_dml(sp_pr, "noFill") {
        None
    } else {
        parse_solid_fill(sp_pr, theme)
    };

    let (stroke_color, stroke_width) = dml(sp_pr, "ln")
        .and_then(|ln| {
            if has_dml(ln, "noFill") {
                return None;
            }
            let color = parse_solid_fill(ln, theme)?;
            let width = ln
                .attribute("w")
                .and_then(|v| v.parse::<f32>().ok())
                .map(super::emu_to_pts)
                .unwrap_or(0.75);
            Some((color, width))
        })
        .map_or((None, 0.0), |(c, w)| (Some(c), w));

    let (text, font_size, text_color) = parse_dsp_text(sp, theme);

    if fill.is_none() && text.is_empty() {
        return None;
    }

    Some(SmartArtShape {
        x,
        y,
        width: w,
        height: h,
        shape_type: parse_shape_geometry(sp_pr),
        fill,
        stroke_color,
        stroke_width,
        text,
        font_size,
        text_color,
    })
}

fn parse_dsp_text(sp: roxmltree::Node, theme: &ThemeFonts) -> (String, f32, Option<[u8; 3]>) {
    let Some(body) = dsp(sp, "txBody") else {
        return (String::new(), 0.0, None);
    };

    let mut lines = Vec::new();
    let mut font_size = 0.0_f32;
    let mut text_color: Option<[u8; 3]> = None;

    for p in body
        .children()
        .filter(|n| n.tag_name().name() == "p" && n.tag_name().namespace() == Some(DML_NS))
    {
        let mut line_text = String::new();
        for r in p
            .children()
            .filter(|n| n.tag_name().name() == "r" && n.tag_name().namespace() == Some(DML_NS))
        {
            if let Some(rpr) = dml(r, "rPr") {
                if font_size == 0.0 {
                    if let Some(sz) = rpr.attribute("sz").and_then(|v| v.parse::<f32>().ok()) {
                        font_size = sz / 100.0;
                    }
                }
                if text_color.is_none() {
                    text_color = parse_solid_fill(rpr, theme);
                }
            }
            if let Some(t) = dml(r, "t") {
                if let Some(text) = t.text() {
                    line_text.push_str(text);
                }
            }
        }
        if !line_text.is_empty() {
            lines.push(line_text);
        }
    }

    (lines.join("\n"), font_size, text_color)
}