rust-pptx 0.1.6

Parse PowerPoint PPTX and legacy PPT presentations into a structured model
Documentation
use super::{
    parse_color, parse_paragraph_style, parse_run_style, text_run, CompactString, RunStyle,
    TextParseContext, TextRun, XmlNode,
};
use crate::{TextField, TextFieldParagraphProperties};
use quick_xml::{events::Event, name::ResolveResult, NsReader};
use std::borrow::Cow;

fn drawing_namespace(namespace: ResolveResult<'_>) -> bool {
    matches!(namespace, ResolveResult::Bound(ns) if matches!(ns.as_ref(),
        b"http://schemas.openxmlformats.org/drawingml/2006/main"
        | b"http://purl.oclc.org/ooxml/drawingml/main"))
}

fn normalized_lines(xml: &str) -> Cow<'_, str> {
    if xml.contains('\r') {
        Cow::Owned(xml.replace("\r\n", "\n").replace('\r', "\n"))
    } else {
        Cow::Borrowed(xml)
    }
}

fn field_tree(source: &str) -> Option<XmlNode> {
    struct Frame {
        node: Option<XmlNode>,
        children: Vec<XmlNode>,
    }
    let xml = normalized_lines(source);
    let mut reader = NsReader::from_reader(xml.as_bytes());
    let mut stack: Vec<Frame> = Vec::new();
    let mut result = None;
    loop {
        match reader.read_resolved_event().ok()? {
            (namespace, event @ (Event::Start(_) | Event::Empty(_))) => {
                let qualified = drawing_namespace(namespace);
                let empty = matches!(event, Event::Empty(_));
                let start = match event {
                    Event::Start(start) | Event::Empty(start) => start,
                    _ => unreachable!(),
                };
                let name = start.local_name();
                let selected = qualified
                    && ((stack.len() == 1 && name.as_ref() == b"fld")
                        || (stack.len() > 1 && stack.last()?.node.is_some()));
                let node = if selected {
                    let mut attributes = Vec::new();
                    for attribute in start.attributes().with_checks(false).flatten() {
                        let (namespace, local) = reader.resolve_attribute(attribute.key);
                        let accepted = match namespace {
                            ResolveResult::Unbound => !attribute.key.as_ref().contains(&b':')
                                && attribute.key.as_ref() != b"xmlns",
                            ResolveResult::Bound(ns) => name.as_ref() != b"fld" && matches!(ns.as_ref(),
                                b"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
                                | b"http://purl.oclc.org/ooxml/officeDocument/relationships"),
                            ResolveResult::Unknown(_) => false,
                        };
                        if !accepted {
                            continue;
                        }
                        let raw = std::str::from_utf8(&attribute.value).ok()?;
                        let normalized = if raw.contains(['\t', '\n']) {
                            Cow::Owned(raw.replace(['\t', '\n'], " "))
                        } else {
                            Cow::Borrowed(raw)
                        };
                        let value = quick_xml::escape::unescape(&normalized).ok()?;
                        attributes.push((
                            CompactString::from(std::str::from_utf8(local.as_ref()).ok()?),
                            CompactString::from(value),
                        ));
                    }
                    Some(XmlNode {
                        source_xml: None,
                        name: CompactString::from(std::str::from_utf8(name.as_ref()).ok()?),
                        attributes: attributes.into_boxed_slice(),
                        text: String::new(),
                        children: Box::default(),
                        subtree_names: XmlNode::name_bit(name.as_ref()),
                    })
                } else {
                    None
                };
                if empty {
                    if let Some(node) = node {
                        if stack.len() == 1 {
                            result = Some(node);
                        } else if let Some(parent) = stack.last_mut() {
                            if let Some(parent_node) = &mut parent.node {
                                parent_node.subtree_names |= node.subtree_names;
                                parent.children.push(node);
                            }
                        }
                    }
                } else {
                    stack.push(Frame {
                        node,
                        children: Vec::new(),
                    });
                }
            }
            (_, Event::Text(text)) => {
                if let Some(Frame {
                    node: Some(node), ..
                }) = stack.last_mut()
                {
                    node.text.push_str(&text.unescape().ok()?);
                }
            }
            (_, Event::CData(text)) => {
                if let Some(Frame {
                    node: Some(node), ..
                }) = stack.last_mut()
                {
                    node.text.push_str(std::str::from_utf8(&text).ok()?);
                }
            }
            (_, Event::End(_)) => {
                let Frame { node, children } = stack.pop()?;
                if let Some(mut node) = node {
                    node.children = children.into_boxed_slice();
                    if stack.len() == 1 {
                        result = Some(node);
                    } else if let Some(parent) = stack.last_mut() {
                        if let Some(parent_node) = &mut parent.node {
                            parent_node.subtree_names |= node.subtree_names;
                            parent.children.push(node);
                        }
                    }
                }
            }
            (_, Event::Eof) => return result,
            _ => {}
        }
    }
}

fn paragraph_properties(
    node: &XmlNode,
    context: &TextParseContext<'_, '_>,
) -> TextFieldParagraphProperties {
    let style = parse_paragraph_style(Some(node));
    TextFieldParagraphProperties {
        alignment: style.alignment,
        level: style.level,
        bullet: style.bullet.flatten(),
        line_spacing: style.line_spacing,
        space_before: style.space_before,
        space_after: style.space_after,
        rtl: style.rtl,
        margin_left_emu: style.margin_left_emu,
        margin_right_emu: node.attr("marR").and_then(|value| value.parse().ok()),
        indent_emu: style.indent_emu,
        default_tab_size_emu: style.default_tab_size_emu,
        tab_stops: style.tab_stops,
        east_asian_line_break: style.east_asian_line_break,
        latin_line_break: style.latin_line_break,
        hanging_punctuation: style.hanging_punctuation,
        font_alignment: node.attr("fontAlgn").map(str::to_owned),
        bullet_none: node.child("buNone").map(|_| true),
        bullet_color: node
            .child("buClr")
            .and_then(|color| parse_color(color, context.colors)),
        bullet_color_follow_text: node.child("buClrTx").map(|_| true),
        bullet_font_follow_text: node.child("buFontTx").map(|_| true),
        bullet_size_follow_text: node.child("buSzTx").map(|_| true),
        bullet_image_asset_id: node
            .child("buBlip")
            .and_then(|node| node.child("blip"))
            .and_then(|node| node.attr("embed"))
            .and_then(|id| context.rels.internal(id))
            .cloned(),
        default_run_properties: node
            .child("defRPr")
            .map(|node| parse_run_style(Some(node), context.colors, context.rels)),
    }
}

pub(super) fn push_run(
    original: &XmlNode,
    default_style: &RunStyle,
    context: &TextParseContext<'_, '_>,
    runs: &mut Vec<TextRun>,
) -> bool {
    let Some(source_xml) = original.source_xml.as_deref() else {
        return false;
    };
    let Some(node) = field_tree(source_xml) else {
        return true;
    };
    let paragraph_properties = node
        .child("pPr")
        .map(|properties| Box::new(paragraph_properties(properties, context)));
    let style = default_style
        .clone()
        .overlay(parse_run_style(
            node.child("pPr")
                .and_then(|properties| properties.child("defRPr")),
            context.colors,
            context.rels,
        ))
        .overlay(parse_run_style(
            node.child("rPr"),
            context.colors,
            context.rels,
        ));
    let value = node
        .children_named("t")
        .map(XmlNode::text_content)
        .collect();
    let mut run = text_run(value, style);
    run.field = Some(Box::new(TextField {
        id: node.attr("id").map(str::to_owned),
        field_type: node.attr("type").map(str::to_owned),
        has_cached_text: node.child("t").is_some(),
        paragraph_properties,
        source_part: context.source_part.map(str::to_owned),
        source_xml: source_xml.to_owned(),
    }));
    runs.push(run);
    true
}

pub(super) fn canonical_fragment(xml: &str) -> Cow<'_, str> {
    if xml.contains("fld") && xml.contains(['\r', '\n', '\t']) {
        Cow::Owned(
            xml.replace('\r', "&#13;")
                .replace('\n', "&#10;")
                .replace('\t', "&#9;"),
        )
    } else {
        Cow::Borrowed(xml)
    }
}