diff --git a/crates/pptx/src/read/timing.rs b/crates/pptx/src/read/timing.rs index 524f9ea2..f84869f5 100644 --- a/crates/pptx/src/read/timing.rs +++ b/crates/pptx/src/read/timing.rs @@ -94,6 +94,13 @@ fn max_dur(e: &El, depth: usize) -> Option { best } +/// Does any behaviour time node (`p:cBhvr/p:cTn`) under the effect node `c` carry `autoRev`? +fn behaviour_auto_reverses(c: &El) -> bool { + let mut behaviours = vec![]; + c.find_all("cBhvr", &mut behaviours); + behaviours.iter().any(|b| b.child("cTn").and_then(|t| t.bool("autoRev")).unwrap_or(false)) +} + /// Flatten the timing tree into the Animation Pane list. pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, shapes: &[Shape]) -> Vec { let _ = (imp, part); @@ -144,7 +151,9 @@ pub fn animations(imp: &mut Imp, part: &Part, ctx: &IdCtx, timing: &El, shapes: trigger: f.trigger.and_then(|t| ctx.lookup(t)), preset_id: Some(preset), preset_subtype: Some(sub), - auto_reverse: c.bool("autoRev").unwrap_or(false), + // PowerPoint's "Auto-reverse" is `autoRev` on the behaviour nodes (`p:cBhvr/p:cTn`); Color Pulse + // always writes it there and plays back and forth on its own, so it isn't read as the option. + auto_reverse: c.bool("autoRev").unwrap_or(false) || (effect != "colorPulse" && behaviour_auto_reverses(c)), // PowerPoint's "Rewind when done playing" is `fill="remove"` on the effect node. rewind: c.attr("fill") == Some("remove"), smooth_start: c.f64("accel").map(|v| v / 100_000.0).unwrap_or(0.0), diff --git a/crates/pptx/tests/it/anim_auto_reverse.rs b/crates/pptx/tests/it/anim_auto_reverse.rs new file mode 100644 index 00000000..36741074 --- /dev/null +++ b/crates/pptx/tests/it/anim_auto_reverse.rs @@ -0,0 +1,44 @@ +//! "Auto-reverse" is written by PowerPoint on the behaviour time nodes, not on the effect node. + +use std::io::{Cursor, Read, Write}; + +use deckcraft_geom::Xfrm; +use deckcraft_model::anim::{AnimClass, Animation}; +use deckcraft_model::{Shape, ShapeId, defaults}; + +fn deck_with(effect: &str, behaviour: &str) -> Vec { + let mut p = defaults::new_presentation(None); + let id = ShapeId(p.alloc_id().expect("id")); + let shape = Shape { id, name: "Box".into(), xfrm: Some(Xfrm::new(10.0, 10.0, 200.0, 100.0)), ..Default::default() }; + let sl = std::sync::Arc::make_mut(&mut p.slides[0]); + sl.shapes = vec![shape]; + sl.animations.push(Animation { shape: id, class: AnimClass::Emphasis, effect: effect.into(), duration_ms: 1000, ..Default::default() }); + let mut z = zip::ZipArchive::new(Cursor::new(deckcraft_pptx::export(&p).expect("export"))).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 name = f.name().to_string(); + let mut d = vec![]; + f.read_to_end(&mut d).expect("read"); + if name == "ppt/slides/slide1.xml" { + let xml = String::from_utf8(d).expect("utf8"); + let at = xml.find(behaviour).expect("behaviour written"); + let cb = at + xml[at..].find(""), [true]); +} + +#[test] +fn color_pulse_behaviour_auto_reverse_is_not_the_option() { + assert_eq!(deck_with("colorPulse", "