mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
PPTX: legacy OLE objects are saved in PowerPoint's fallback form (#203)
A legacy OLE object finds its preview in the slide's VML drawing through spid; DeckCraft doesn't write that drawing, and PowerPoint refused to open a file whose oleObj has a spid but no VML shape. The reader now takes the preview picture from the VML drawing (so DeckCraft shows it too), and the writer drops spid and adds that preview as a p:pic, the form PowerPoint itself writes as the fallback. Checked in PowerPoint: the issue's 45545_Comment.pptx saved by DeckCraft opens with its three embedded objects and draws slide 8's table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c462859f6f
commit
f58f4f425f
@@ -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<deckcraft_model::MediaId> {
|
||||
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.
|
||||
|
||||
@@ -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<deckcraft_model::MediaId>) {
|
||||
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#"<p:pic><p:nvPicPr><p:cNvPr id="0" name=""/><p:cNvPicPr/><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="{rel}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="{ox}" y="{oy}"/><a:ext cx="{cx}" cy="{cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>"#
|
||||
);
|
||||
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<String, String>, depth: usize) {
|
||||
if depth > crate::xml::MAX_DEPTH {
|
||||
return;
|
||||
|
||||
@@ -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#"<p:graphicFrame xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:nvGraphicFramePr><p:cNvPr id="4" name="Object 3"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></p:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/presentationml/2006/ole"><p:oleObj spid="_x0000_s1026" name="Slide" r:id="rId4" progId="PowerPoint.Slide.8"><p:embed/></p:oleObj></a:graphicData></a:graphic></p:graphicFrame>"#;
|
||||
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("<p:pic>") && !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:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user