diff --git a/crates/pptx/src/write/timing.rs b/crates/pptx/src/write/timing.rs index 5eb892c3..ec4bffd2 100644 --- a/crates/pptx/src/write/timing.rs +++ b/crates/pptx/src/write/timing.rs @@ -364,6 +364,25 @@ fn behaviors(w: &mut W, x: &mut Exp, ids: &mut Ids, e: &Eff) { w.close("p:to"); w.close("p:set"); } + "colorPulse" => { + // As PowerPoint writes it: text and fill go to the colour and back (autoRev). The behaviour + // duration is the one import reads as the effect duration. + let c = a.color.clone().unwrap_or(deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::rgb(0xC0, 0x00, 0x00))); + for (attr, child) in [("style.color", true), ("fillcolor", false)] { + w.open("p:animClr", A::new().a("clrSpc", "rgb").a("dir", "cw")); + w.open("p:cBhvr", A::new().o("override", child.then_some("childStyle"))); + w.empty("p:cTn", A::new().a("id", ids.next()).a("dur", dur.max(1)).a("autoRev", "1").a("fill", "remove")); + tgt(w, e.spid, e.paragraph); + w.open0("p:attrNameLst"); + w.elt("p:attrName", attr); + w.close("p:attrNameLst"); + w.close("p:cBhvr"); + w.open0("p:to"); + super::dml::color(w, &c); + w.close("p:to"); + w.close("p:animClr"); + } + } "fillColor" | "objectColor" | "fontColor" | "lineColor" | "complementaryColor" | "darken" | "lighten" => { let attr = match a.effect.as_str() { "fontColor" => "style.color", diff --git a/crates/pptx/tests/roundtrip.rs b/crates/pptx/tests/roundtrip.rs index 00b141cb..2f8d9649 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -1350,3 +1350,28 @@ fn literal_line_ends_in_run_text_read_as_breaks_and_survive_save() { assert_eq!(runs_of(&a, "one"), want); assert_eq!(runs_of(&round(&a), "one"), want, "save and reopen keep the runs"); } + +#[test] +fn color_pulse_keeps_its_effect_colour_and_duration() { + let mut p = defaults::new_presentation(None); + let s = shape(&mut p, "Pulsed", 10.0, 10.0, 200.0, 100.0); + let id = s.id; + let first = p.slides[0].id; + let sl = p.slide_mut(first).expect("slide"); + sl.shapes.push(s); + let white = ColorRef::rgb(Rgba::rgb(255, 255, 255)); + sl.animations.push(Animation { + shape: id, + class: AnimClass::Emphasis, + effect: "colorPulse".into(), + duration_ms: 1000, + color: Some(white.clone()), + ..Default::default() + }); + let q = round(&p); + let a = &q.slides[0].animations[0]; + assert_eq!((a.class, a.effect.as_str()), (AnimClass::Emphasis, "colorPulse")); + assert_eq!(a.color, Some(white)); + assert_eq!(a.amount, None, "no grow/shrink scale is invented"); + assert_eq!(a.duration_ms, 1000); +}