diff --git a/crates/pptx/src/read/shapes.rs b/crates/pptx/src/read/shapes.rs index 3954cee8..45f819e3 100644 --- a/crates/pptx/src/read/shapes.rs +++ b/crates/pptx/src/read/shapes.rs @@ -346,7 +346,8 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S let mut pics = vec![]; e.find_all("blip", &mut pics); pics.into_iter().find_map(|b| dml::media_ref(imp, part, b)) - }; + } + .or_else(|| vml_preview(imp, part, gd?.find("oleObj")?.attr("spid")?)); let label = if let Some(o) = gd.and_then(|g| g.find("oleObj")) { o.attr("progId").or(o.attr("name")).unwrap_or("Object").to_string() } else if uri.ends_with("/diagram") { @@ -375,6 +376,18 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S s } +/// A legacy OLE object's preview: the picture of its shape (`spid`) in the slide's VML drawing. +fn vml_preview(imp: &mut Imp, part: &Part, spid: &str) -> Option { + let found = part.rels.all_of("vmlDrawing").find_map(|r| { + let doc = imp.pkg.xml(&r.target)?; + let mut shapes = vec![]; + doc.root.find_all("shape", &mut shapes); + let relid = shapes.into_iter().find(|s| s.attr("id") == Some(spid))?.find("imagedata")?.attr("o:relid")?.to_string(); + Some(imp.pkg.rels(&r.target).get(&relid).filter(|i| !i.external)?.target.clone()) + })?; + imp.media_for(&found) +} + /// A diagram's data part, without the link to its cached drawing: that link is a slide /// relationship id, which a save renumbers. Only diagrams whose drawing is missing or empty get /// here, and those are drawn from their data. diff --git a/crates/pptx/src/write/shapes.rs b/crates/pptx/src/write/shapes.rs index 3df45c6c..09c3af2c 100644 --- a/crates/pptx/src/write/shapes.rs +++ b/crates/pptx/src/write/shapes.rs @@ -637,6 +637,7 @@ fn opaque( { let mut root = doc.root; set_rel_ids(&mut root, &ids, 0); + ole_fallback(&mut root, x, o, preview); let id = cx.id_for(s); set_cnvpr_id(&mut root, id); w.raw(&root.to_xml()); @@ -686,6 +687,36 @@ fn shape_with_id(w: &mut W, x: &mut Exp, o: &mut Out, cx: &mut Ctx, s: &Shape, i } } +/// A legacy OLE object finds its preview in the slide's VML drawing through `spid`, and PowerPoint +/// won't open the slide without it. We don't write that drawing, so write PowerPoint's own +/// fallback form instead: no `spid`, and the preview as a `p:pic`. +fn ole_fallback(root: &mut crate::xml::El, x: &mut Exp, o: &mut Out, preview: Option) { + let xfrm = root.child("xfrm").map(|t| { + let at = |e: &str, a: &str| t.child(e).and_then(|c| c.attr(a)).unwrap_or("0").to_string(); + (at("off", "x"), at("off", "y"), at("ext", "cx"), at("ext", "cy")) + }); + let Some(ole) = root + .elements_mut() + .find(|e| e.is("graphic")) + .and_then(|g| g.elements_mut().find(|e| e.is("graphicData"))) + .and_then(|d| d.elements_mut().find(|e| e.is("oleObj"))) + else { + return; + }; + if ole.child("pic").is_some() || ole.attr("spid").is_none() { + return; + } + ole.attrs.retain(|(k, _)| k != "spid"); + let rel = preview.and_then(|m| x.media_rel(o, m, "image")).unwrap_or_else(|| x.placeholder_rel(o)); + let (ox, oy, cx, cy) = xfrm.unwrap_or_default(); + let pic = format!( + r#""# + ); + if let Ok(d) = crate::xml::parse(pic.as_bytes()) { + ole.children.push(crate::xml::Node::El(d.root)); + } +} + fn set_rel_ids(e: &mut crate::xml::El, ids: &std::collections::HashMap, depth: usize) { if depth > crate::xml::MAX_DEPTH { return; diff --git a/crates/pptx/tests/roundtrip.rs b/crates/pptx/tests/roundtrip.rs index ddd62e17..7e4745fb 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -762,3 +762,25 @@ fn smartart_without_a_drawing_keeps_its_parts() { assert!(data.contains("Manager"), "{data}"); assert!(!data.contains("dataModelExt"), "the link to a drawing that isn't written is dropped: {data}"); } + +/// #203: a legacy OLE object (preview in the VML drawing we don't write) is saved in PowerPoint's +/// fallback form, without `spid` and with a `p:pic` preview, or PowerPoint won't open the file. +#[test] +fn legacy_ole_object_is_written_in_the_fallback_form() { + let mut p = defaults::new_presentation(None); + let bin = p.add_media("oleObject1.bin", "application/vnd.openxmlformats-officedocument.oleObject", b"\xD0\xCF\x11\xE0not-a-real-ole".to_vec()); + let prev = p.add_media("image1.png", "image/png", png(4, 3, [10, 20, 30])); + let xml = r#""#; + let parts = vec![("rId4".to_string(), "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject".to_string(), bin)]; + let mut s = new_slide(&mut p, "OLE"); + let mut frame = shape(&mut p, "Object 3", 0.0, 0.0, 72.0, 72.0); + frame.kind = ShapeKind::Opaque { xml: xml.into(), preview: Some(prev), label: "Slide".into(), parts }; + s.shapes.push(frame); + p.slides.push(Arc::new(s)); + + let q = round(&round(&p)); + let ShapeKind::Opaque { xml, parts, preview, .. } = &by_name(&q.slides[1], "Object 3").kind else { panic!("not kept as an object") }; + assert!(xml.contains("") && !xml.contains("spid"), "{xml}"); + assert!(preview.is_some(), "the preview picture is kept"); + assert!(parts.iter().any(|(_, k, _)| k.ends_with("/oleObject")), "the embedding is kept: {parts:?}"); +}