Merge PR #728 from midasmatterhorn: pptx: write linked pictures with r:link so PowerPoint resolves external images

This commit is contained in:
Brandon Thomas
2026-10-14 12:53:34 -04:00
2 changed files with 38 additions and 2 deletions
+10 -2
View File
@@ -95,6 +95,9 @@ pub fn fill(w: &mut W, x: &mut Exp, o: &mut Out, f: &Fill) {
/// `a:blipFill` / `p:blipFill`.
pub fn blip_fill(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, pf: &PictureFill) {
// An image that lives outside the package is a linked picture: `a:blip` names it through
// `r:link`; `r:embed` may only reference a part inside the package.
let linked = x.p.media(pf.media).is_some_and(|m| m.data.is_empty() && m.link.is_some());
let rid = x.media_rel(o, pf.media, "image").unwrap_or_else(|| x.placeholder_rel(o));
w.open(tag, A::new().a("rotWithShape", if pf.upright { "0" } else { "1" }));
let adj = &pf.adjust;
@@ -106,7 +109,7 @@ pub fn blip_fill(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, pf: &PictureFil
|| adj.clear_color.is_some()
|| adj.bilevel.is_some()
|| adj.alpha_inv;
let a = A::new().a("r:embed", &rid);
let a = if linked { A::new().a("r:link", &rid) } else { A::new().a("r:embed", &rid) };
if has_kids {
w.open("a:blip", a);
if let Some(c) = adj.clear_color {
@@ -521,7 +524,12 @@ pub fn ppr(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, level: Option<u8>, p:
Some(Bullet::Picture { media }) => match x.media_rel(o, *media, "image") {
Some(rid) => {
w.open0("a:buBlip");
w.empty("a:blip", A::new().a("r:embed", rid));
let a = if x.p.media(*media).is_some_and(|m| m.data.is_empty() && m.link.is_some()) {
A::new().a("r:link", &rid)
} else {
A::new().a("r:embed", &rid)
};
w.empty("a:blip", a);
w.close("a:buBlip");
}
None => w.empty("a:buChar", A::new().a("char", "•")),
+28
View File
@@ -991,3 +991,31 @@ fn transition_defaults_read_like_powerpoint() {
assert_eq!((t.kind.as_str(), t.option.as_str(), t.duration_ms), (*kind, *option, 2000), "{el}");
}
}
#[test]
fn linked_pictures_are_written_with_r_link() {
// A picture whose bytes live outside the package must be referenced with `r:link`; `r:embed`
// may only name a part inside the package (PowerPoint won't load the linked image otherwise).
let mut p = defaults::new_presentation(None);
let media = deckcraft_model::MediaId(5000);
p.media.push(deckcraft_model::MediaItem {
id: media,
name: "linked.png".into(),
content_type: "image/png".into(),
data: Arc::new(Vec::new()),
link: Some("file:///tmp/linked.png".into()),
});
let mut pic = shape(&mut p, "Linked", 100.0, 100.0, 200.0, 150.0);
pic.kind = ShapeKind::Picture { fill: PictureFill { media, ..Default::default() } };
Arc::make_mut(&mut p.slides[0]).shapes.push(pic);
let bytes = deckcraft_pptx::export(&p).expect("export");
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("zip");
let mut xml = String::new();
std::io::Read::read_to_string(&mut zip.by_name("ppt/slides/slide1.xml").expect("slide"), &mut xml).expect("read");
assert!(xml.contains(r#"r:link="rId"#), "{xml}");
assert!(!xml.contains(r#"r:embed="rId"#), "{xml}");
let mut rels = String::new();
std::io::Read::read_to_string(&mut zip.by_name("ppt/slides/_rels/slide1.xml.rels").expect("rels"), &mut rels).expect("read");
assert!(rels.contains(r#"Target="file:///tmp/linked.png" TargetMode="External""#), "{rels}");
}