diff --git a/crates/pptx/src/read/shapes.rs b/crates/pptx/src/read/shapes.rs index 05a7438..7cb6635 100644 --- a/crates/pptx/src/read/shapes.rs +++ b/crates/pptx/src/read/shapes.rs @@ -356,7 +356,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(|| gd.and_then(|g| g.find("oleObj")).and_then(|o| vml_preview(imp, part, o.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") { @@ -370,6 +371,21 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S s } +/// The preview picture of an old-style embedded object (`p:oleObj spid` with no `p:pic`): the +/// `v:imagedata` of the legacy VML drawing shape with that id, as PowerPoint 2007 wrote them. +fn vml_preview(imp: &mut Imp, part: &Part, spid: &str) -> Option { + for rel in part.rels.items.iter().filter(|r| r.kind == "vmlDrawing" && !r.external) { + let Some(doc) = imp.pkg.xml(&rel.target) else { continue }; + let mut shapes = vec![]; + doc.root.find_all("shape", &mut shapes); + let Some(img) = shapes.into_iter().find(|s| s.attr("id") == Some(spid)).and_then(|s| s.find("imagedata")) else { continue }; + let Some(id) = img.attr("o:relid").or(img.attr("r:id")) else { continue }; + let target = imp.pkg.rels(&rel.target).get(id).filter(|r| !r.external).map(|r| r.target.clone())?; + return imp.media_for(&target); + } + None +} + /// SmartArt: read the pre-drawn shapes of its drawing part as a group. fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) -> Option { let rel_ids = gd.child("relIds")?; diff --git a/crates/pptx/tests/ole.rs b/crates/pptx/tests/ole.rs new file mode 100644 index 0000000..5c0bc1d --- /dev/null +++ b/crates/pptx/tests/ole.rs @@ -0,0 +1,98 @@ +//! Embedded (OLE) objects and their preview pictures, as other versions of PowerPoint write them. + +use std::io::{Cursor, Read, Write}; + +use deckcraft_model::{ShapeKind, defaults}; + +fn png() -> Vec { + let mut out = Cursor::new(vec![]); + image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(4, 4, image::Rgb([10, 200, 30]))) + .write_to(&mut out, image::ImageFormat::Png) + .expect("png"); + out.into_inner() +} + +/// An exported deck with `parts` added (or replaced) and `edit` applied to every part's text. +fn with_parts(parts: &[(&str, Vec)], edit: impl Fn(&str, String) -> String) -> Vec { + let base = deckcraft_pptx::export(&defaults::new_presentation(None)).expect("export"); + let mut z = zip::ZipArchive::new(Cursor::new(base)).expect("zip"); + let mut out = zip::ZipWriter::new(Cursor::new(Vec::new())); + let o = zip::write::SimpleFileOptions::default(); + for i in 0..z.len() { + let mut f = z.by_index(i).expect("entry"); + let name = f.name().to_string(); + if parts.iter().any(|(n, _)| *n == name) { + continue; + } + let mut d = vec![]; + f.read_to_end(&mut d).expect("read"); + if name.ends_with(".xml") || name.ends_with(".rels") { + d = edit(&name, String::from_utf8(d).expect("utf8")).into_bytes(); + } + out.start_file(name, o).expect("start"); + out.write_all(&d).expect("write"); + } + for (name, data) in parts { + out.start_file(*name, o).expect("start"); + out.write_all(data).expect("write"); + } + out.finish().expect("finish").into_inner() +} + +/// PowerPoint 2007 style: `p:oleObj spid` with no `p:pic`; the preview is the `v:imagedata` of the +/// VML shape with that id in the slide's legacy drawing part. +fn ole_deck(spid_in_vml: &str) -> Vec { + let vml = format!( + r#" + + +"# + ); + let vml_rels = r#" +"#; + let frame = r#""#; + with_parts( + &[ + ("ppt/drawings/vmlDrawing1.vml", vml.into_bytes()), + ("ppt/drawings/_rels/vmlDrawing1.vml.rels", vml_rels.as_bytes().to_vec()), + ("ppt/media/preview.png", png()), + ("ppt/media/other.png", vec![1, 2, 3]), + ("ppt/embeddings/oleObject1.bin", vec![0; 16]), + ], + |name, s| { + if name == "ppt/slides/slide1.xml" { + s.replacen("", &format!("{frame}"), 1) + } else if name == "ppt/slides/_rels/slide1.xml.rels" { + s.replacen( + "", + r#""#, + 1, + ) + } else { + s + } + }, + ) +} + +fn object_preview(bytes: &[u8]) -> Option> { + let p = deckcraft_pptx::import(bytes).expect("import"); + let s = p.slides[0].shapes.iter().find(|s| s.name == "Object 3").expect("object"); + match &s.kind { + ShapeKind::Opaque { preview, label, .. } => { + assert_eq!(label, "PowerPoint.Slide.8"); + preview.and_then(|m| p.media(m)).map(|m| m.data.to_vec()) + } + k => panic!("{k:?}"), + } +} + +#[test] +fn an_old_ole_object_shows_its_vml_preview() { + assert_eq!(object_preview(&ole_deck("_x0000_s681987")), Some(png()), "the VML shape with the object's spid"); +} + +#[test] +fn an_ole_object_without_a_matching_vml_shape_has_no_preview() { + assert_eq!(object_preview(&ole_deck("_x0000_s1")), None); +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index bc457b6..aae393f 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -717,6 +717,13 @@ impl Renderer { paint::picture(ctx, f.pres, &paint::resolved(rctx, pf, None), bounds, m, t); ctx.pop_layer(); } + // An object DeckCraft doesn't model (an embedded OLE object, an unknown graphic): its + // preview picture, stretched over the frame, as PowerPoint draws it. + ShapeKind::Opaque { preview: Some(media), .. } => { + ctx.set_transform(m); + let pf = deckcraft_model::style::PictureFill { media: *media, ..Default::default() }; + paint::picture(ctx, f.pres, &pf, Rect::new(0.0, 0.0, w, h), m, None); + } ShapeKind::Media(mc) => { ctx.set_transform(m); let bounds = Rect::new(0.0, 0.0, w, h); diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 63bbddb..ee3b704 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1146,3 +1146,34 @@ fn broken_metafiles_draw_nothing_and_never_panic() { let _ = render_slide(&p, 0, &RenderOpts { scale: 0.25, ..Default::default() }); } } + +#[test] +fn an_object_deckcraft_doesnt_model_shows_its_preview_picture() { + // An embedded OLE object (or an unknown graphic) draws its preview stretched over its frame, + // as PowerPoint does; with no preview it draws nothing. + let png = { + let mut out = std::io::Cursor::new(vec![]); + image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(4, 2, image::Rgb([200, 20, 20]))) + .write_to(&mut out, image::ImageFormat::Png) + .unwrap(); + out.into_inner() + }; + for with_preview in [true, false] { + let mut p = Presentation::default(); + let media = p.add_media("image1.png", "image/png", png.clone()); + let sh = Shape { + id: ShapeId(600), + xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 200.0)), + kind: ShapeKind::Opaque { xml: String::new(), preview: with_preview.then_some(media), label: "Package".into() }, + ..Default::default() + }; + std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; + let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }); + for (x, y) in [(110, 110), (300, 200), (490, 290)] { + let px = img.pixel(x, y); + let red = px[0] > 180 && px[1] < 60 && px[2] < 60; + assert_eq!(red, with_preview, "({x}, {y}) {px:?}"); + } + assert!(img.pixel(520, 200)[1] > 200, "nothing outside the frame"); + } +}