From 872cdbf33136545699bdfa280dbbebbb6cbd9557 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 20:04:49 -0500 Subject: [PATCH] pptx: take nested math text once and check an object's part for relationships once (#1762) --- crates/pptx/src/read.rs | 5 +++++ crates/pptx/src/read/dml.rs | 31 ++++++++++++++++++++++++++----- crates/pptx/src/read/shapes.rs | 3 ++- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/crates/pptx/src/read.rs b/crates/pptx/src/read.rs index 288e497f8..2056d53d7 100644 --- a/crates/pptx/src/read.rs +++ b/crates/pptx/src/read.rs @@ -46,6 +46,9 @@ pub struct Imp<'a> { pub tag_bytes: usize, /// The tag budget ran out and the warning was given. pub tags_capped: bool, + /// Whether a part has relationships of its own, by normalized part name: looked up once + /// however many embedded objects point at the part. + pub has_rels: HashMap, } impl Imp<'_> { @@ -446,6 +449,7 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec< tags_by_part: HashMap::new(), tag_bytes: 0, tags_capped: false, + has_rels: HashMap::new(), }; let part = Part::new(&pkg, &theme_path, &doc); let theme = theme::read_theme(&mut imp, &part, &doc.root); @@ -487,6 +491,7 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec), PptxE tags_by_part: HashMap::new(), tag_bytes: 0, tags_capped: false, + has_rels: HashMap::new(), }; // Slide ids first so hyperlinks anywhere can point at slides. let slide_ids = register_slides(&mut imp, &docs, &slide_entries, &mut lost_slides); diff --git a/crates/pptx/src/read/dml.rs b/crates/pptx/src/read/dml.rs index 2d82fa36a..c8902e4c7 100644 --- a/crates/pptx/src/read/dml.rs +++ b/crates/pptx/src/read/dml.rs @@ -664,12 +664,22 @@ fn push_text_runs(para: &mut Paragraph, props: RunProps, text: &str) { /// Appends an Office Math (a14:m) run: its plain text and its OMML. fn push_math_run(para: &mut Paragraph, part: &Part, e: &El) { if para.runs.len() < MAX_PARAGRAPH_RUNS { - let mut text = String::new(); - let mut ts = vec![]; - e.find_all("t", &mut ts); - for t in ts { - text.push_str(&t.text()); + // The text of the outermost `t` elements: a `t` nested in another is already part of + // the outer one's text, and taking it again would copy it once per nesting level. + fn outer_t(e: &El, depth: usize, text: &mut String) { + if depth > crate::xml::MAX_DEPTH { + return; + } + for c in e.elements() { + if c.is("t") { + text.push_str(&c.text()); + } else { + outer_t(c, depth + 1, text); + } + } } + let mut text = String::new(); + outer_t(e, 0, &mut text); let omml = e.elements().map(|c| part.keep_inner(c)).collect::(); para.runs.push(Run { text, props: RunProps::default(), kind: RunKind::Math { omml } }); } @@ -714,6 +724,17 @@ mod tests { use super::*; use crate::xml::parse; + #[test] + fn nested_math_text_is_taken_once() { + // 200 `m:t` nested in each other around one word: the word, not 200 copies of it. + let x = format!(r#"{}word{}"#, "".repeat(200), "".repeat(200)); + let d = parse(x.as_bytes()).unwrap(); + let part = Part { name: "ppt/slides/slide1.xml".into(), rels: Default::default(), ns: vec![], inner_ns: vec![] }; + let mut para = Paragraph::default(); + push_math_run(&mut para, &part, &d.root); + assert_eq!(para.runs[0].text, "word"); + } + #[test] fn colors_and_transforms() { let d = parse( diff --git a/crates/pptx/src/read/shapes.rs b/crates/pptx/src/read/shapes.rs index b3e3d48e6..3230db100 100644 --- a/crates/pptx/src/read/shapes.rs +++ b/crates/pptx/src/read/shapes.rs @@ -475,7 +475,8 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S .into_iter() .map(|id| { let rel = part.rels.get(&id).filter(|r| !r.external)?; - if !imp.pkg.rels(&rel.target).items.is_empty() { + let pkg = imp.pkg; + if *imp.has_rels.entry(crate::opc::norm(&rel.target)).or_insert_with(|| !pkg.rels(&rel.target).items.is_empty()) { return None; } let (kind, target) = (rel.ty.clone(), rel.target.clone());