collective

Browse / github.com/storytold/deckcraft / patch

PPTX: media-bookmark triggers and Show media controls survive a save and reopen (part of #362)

PPTX: media-bookmark triggers and Show media controls survive a save and reopen (part of #362) Two round-trip gaps in the .pptx reader and writer, found by agent runs that saved a deck with media and reopened it. - **#362** (first point): an interactive sequence started by `onMediaBookmark` was imported as a click trigger on the media shape and saved back as a click sequence, so the bookmark trigger was lost and a click took its place. The reader now leaves such sequences out (logged), so nothing is turned into a click. The other points of #362 (a video pause shortcut, pen colours on dark slides) are not touched here. - `show.setup {showMediaControls: false}` is stored as PowerPoint's

Status
open
Score
0.00
Adopters
0 other operators
Author
zberry (imported from pull request #1320, open)
Base
38d2ade2b77de39c0e06278ddfd44d1082ff80a8
Size
+61 −0 in 5 files
Review
pending
Shared
2026-10-28
Id
ccbc54bbf5d32a9e9c8f148b

How it went

No other operator has reported using it yet.

Diff

raw · crates/pptx/src/read/mod.rs crates/pptx/src/read/timing.rs crates/pptx/src/write/mod.rs crates/pptx/tests/it/roundtrip.rs crates/pptx/tests/it/trigger_steps.rs

diff --git a/crates/pptx/src/read/mod.rs b/crates/pptx/src/read/mod.rs--- a/crates/pptx/src/read/mod.rs+++ b/crates/pptx/src/read/mod.rs@@ -1210,6 +1210,7 @@ fn read_show(sp: &El, shows: &HashMap<String, String>) -> ShowSettings {         without_narration: sp.bool("showNarration") == Some(false),         without_animation: sp.bool("showAnimation") == Some(false),         use_timings: sp.bool("useTimings").unwrap_or(true),+        show_media_controls: sp.find("showMediaCtrls").and_then(|e| e.bool("val")).unwrap_or(true),         ..Default::default()     };     if sp.child("browse").is_some() {diff --git a/crates/pptx/src/read/timing.rs b/crates/pptx/src/read/timing.rs--- a/crates/pptx/src/read/timing.rs+++ b/crates/pptx/src/read/timing.rs@@ -69,6 +69,15 @@ fn collect<'a>(e: &'a El, trigger: Option<u32>, out: &mut Vec<Found<'a>>, depth:             && let Some(ctn) = c.child("cTn")             && ctn.attr("nodeType") == Some("interactiveSeq")         {+            // Only a click starts a button sequence we can write back. A media bookmark+            // (`onMediaBookmark`) or other start has no model: leave its effects out rather+            // than save them as clicks on the media shape (#362).+            let other_start =+                ctn.child("stCondLst").is_some_and(|l| l.children_named("cond").any(|c| c.attr("evt").is_some_and(|evt| evt != "onClick")));+            if other_start {+                log::warn!("pptx: an animation sequence with a start other than a click was left out");+                continue;+            }             trig = ctn.path(&["stCondLst", "cond", "tgtEl", "spTgt"]).and_then(|s| s.u32("spid"));         }         collect(c, trig, out, depth + 1);diff --git a/crates/pptx/src/write/mod.rs b/crates/pptx/src/write/mod.rs--- a/crates/pptx/src/write/mod.rs+++ b/crates/pptx/src/write/mod.rs@@ -1181,6 +1181,14 @@ fn pres_props_xml(p: &Presentation) -> Vec<u8> {     w.open0("p:penClr");     w.val("a:srgbClr", s.pen_color.hex());     w.close("p:penClr");+    if !s.show_media_controls {+        // "Show media controls" is PowerPoint's p14 extension; absent means shown.+        w.open0("p:extLst");+        w.open("p:ext", A::new().a("uri", "{2FDB2607-1784-4EEB-B798-7EB5836EED8A}"));+        w.empty("p14:showMediaCtrls", A::new().a("xmlns:p14", NS_P14).a("val", 0));+        w.close("p:ext");+        w.close("p:extLst");+    }     w.close("p:showPr");     w.close("p:presentationPr");     w.finish()diff --git a/crates/pptx/tests/it/roundtrip.rs b/crates/pptx/tests/it/roundtrip.rs--- a/crates/pptx/tests/it/roundtrip.rs+++ b/crates/pptx/tests/it/roundtrip.rs@@ -1925,3 +1925,12 @@ fn zoom_inserts_linked_slide_pictures() {     assert!(s.execute("insert.zoom", &json!({"slides": [42]})).is_err());     assert!(s.execute("insert.zoom", &json!({"kind": "slide"})).is_err()); }++/// Show media controls is PowerPoint's `p14:showMediaCtrls` extension inside `p:showPr`.+#[test]+fn hidden_media_controls_survive_save_and_reopen() {+    let mut p = Presentation::default();+    p.show.show_media_controls = false;+    assert!(!round(&p).show.show_media_controls);+    assert!(round(&Presentation::default()).show.show_media_controls);+}diff --git a/crates/pptx/tests/it/trigger_steps.rs b/crates/pptx/tests/it/trigger_steps.rs--- a/crates/pptx/tests/it/trigger_steps.rs+++ b/crates/pptx/tests/it/trigger_steps.rs@@ -93,3 +93,37 @@ fn triggered_paragraph_build_keeps_a_click_for_each_paragraph() {         assert_eq!(back.slides[0].animations[0].trigger, Some(ids[0]));     } }++/// A sequence that starts at a media bookmark (`onMediaBookmark`, #362) is not a click on the+/// media shape. Reading it as one made a save turn the bookmark effects into click effects, so+/// they are left out instead of being given the wrong trigger.+#[test]+fn a_media_bookmark_sequence_is_not_read_as_a_click_trigger() {+    use std::io::Write;+    let mut p = deck();+    let ids: Vec<_> = p.slides[0].shapes.iter().map(|s| s.id).collect();+    Arc::make_mut(&mut p.slides[0]).animations.push(Animation {+        shape: ids[2],+        trigger: Some(ids[0]),+        effect: "appear".into(),+        text_build: TextBuild::AsOne,+        ..Default::default()+    });+    assert_eq!(round(&p).slides[0].animations.len(), 1, "a click trigger still round-trips");+    let bytes = deckcraft_pptx::export(&p).expect("export");+    let mut z = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");+    let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));+    for i in 0..z.len() {+        let mut f = z.by_index(i).expect("entry");+        let mut data = Vec::new();+        f.read_to_end(&mut data).expect("read");+        if f.name() == "ppt/slides/slide1.xml" {+            let xml = String::from_utf8(data).expect("utf8").replace(r#"evt="onClick""#, r#"evt="onMediaBookmark""#);+            data = xml.into_bytes();+        }+        out.start_file(f.name(), zip::write::SimpleFileOptions::default()).expect("start");+        out.write_all(&data).expect("write");+    }+    let q = deckcraft_pptx::import(&out.finish().expect("finish").into_inner()).expect("import");+    assert!(q.slides[0].animations.is_empty(), "{:?}", q.slides[0].animations);+}