From f036a8e805a2273fee34f78ad2d8ae2364a6aa24 Mon Sep 17 00:00:00 2001 From: Nikita Rakitin Date: Wed, 14 Oct 2026 06:44:59 +0300 Subject: [PATCH] PPTX: Color Pulse emphasis is written as a colour change, not a grow --- crates/pptx/src/write/timing.rs | 19 +++++++++++++++++++ crates/pptx/tests/roundtrip.rs | 25 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/crates/pptx/src/write/timing.rs b/crates/pptx/src/write/timing.rs index aa577ca7..f60b937c 100644 --- a/crates/pptx/src/write/timing.rs +++ b/crates/pptx/src/write/timing.rs @@ -354,6 +354,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 db6d0beb..79884d1f 100644 --- a/crates/pptx/tests/roundtrip.rs +++ b/crates/pptx/tests/roundtrip.rs @@ -991,3 +991,28 @@ 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 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); +}