Skip to main content

pptx/dml/
effect.rs

1//! `DrawingML` effect types (shadow, glow, etc.).
2
3use crate::dml::color::ColorFormat;
4use crate::units::Emu;
5use crate::xml_util::WriteXml;
6
7/// The type of shadow effect.
8#[non_exhaustive]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ShadowType {
11    Outer,
12    Inner,
13    Perspective,
14}
15
16/// Shadow effect formatting.
17///
18/// Corresponds to `<a:effectLst>` containing `<a:outerShdw>`, `<a:innerShdw>`,
19/// or perspective shadow elements.
20#[derive(Debug, Clone, PartialEq)]
21pub struct ShadowFormat {
22    /// The type of shadow (outer, inner, or perspective).
23    pub shadow_type: ShadowType,
24    /// Shadow color.
25    pub color: Option<ColorFormat>,
26    /// Blur radius in EMU.
27    pub blur_radius: Option<Emu>,
28    /// Distance from the shape in EMU.
29    pub distance: Option<Emu>,
30    /// Direction angle in degrees (0-360).
31    pub direction: Option<f64>,
32    /// Opacity from 0.0 (fully transparent) to 1.0 (fully opaque).
33    pub opacity: Option<f64>,
34}
35
36impl ShadowFormat {
37    /// Create an outer shadow with the given parameters.
38    #[must_use]
39    pub const fn outer(color: ColorFormat, blur: Emu, distance: Emu, angle: f64) -> Self {
40        Self {
41            shadow_type: ShadowType::Outer,
42            color: Some(color),
43            blur_radius: Some(blur),
44            distance: Some(distance),
45            direction: Some(angle),
46            opacity: None,
47        }
48    }
49
50    /// Create an inner shadow with the given parameters.
51    #[must_use]
52    pub const fn inner(color: ColorFormat, blur: Emu, distance: Emu, angle: f64) -> Self {
53        Self {
54            shadow_type: ShadowType::Inner,
55            color: Some(color),
56            blur_radius: Some(blur),
57            distance: Some(distance),
58            direction: Some(angle),
59            opacity: None,
60        }
61    }
62}
63
64impl WriteXml for ShadowFormat {
65    fn write_xml<W: std::fmt::Write>(&self, w: &mut W) -> std::fmt::Result {
66        w.write_str("<a:effectLst>")?;
67
68        let tag = match self.shadow_type {
69            ShadowType::Outer | ShadowType::Perspective => "a:outerShdw",
70            ShadowType::Inner => "a:innerShdw",
71        };
72
73        w.write_char('<')?;
74        w.write_str(tag)?;
75
76        if let Some(blur) = self.blur_radius {
77            write!(w, r#" blurRad="{blur}""#)?;
78        }
79        if let Some(dist) = self.distance {
80            write!(w, r#" dist="{dist}""#)?;
81        }
82        if let Some(dir) = self.direction {
83            // EMU values fit in i64 range
84            #[allow(clippy::cast_possible_truncation)]
85            let dir_val = (dir * 60_000.0) as i64;
86            write!(w, r#" dir="{dir_val}""#)?;
87        }
88
89        if self.shadow_type == ShadowType::Perspective {
90            w.write_str(r#" sx="100000" sy="23000" kx="1200000" algn="bl" rotWithShape="0""#)?;
91        }
92
93        w.write_char('>')?;
94
95        // Color with optional opacity
96        if let Some(ref color) = self.color {
97            match color {
98                ColorFormat::Rgb(rgb) => {
99                    if let Some(opacity) = self.opacity {
100                        // EMU values fit in i64 range
101                        #[allow(clippy::cast_possible_truncation)]
102                        let alpha = (opacity * 100_000.0) as i64;
103                        write!(
104                            w,
105                            r#"<a:srgbClr val="{}"><a:alpha val="{}"/></a:srgbClr>"#,
106                            rgb.to_hex(),
107                            alpha
108                        )?;
109                    } else {
110                        color.write_xml(w)?;
111                    }
112                }
113                _ => {
114                    color.write_xml(w)?;
115                }
116            }
117        }
118
119        w.write_str("</")?;
120        w.write_str(tag)?;
121        w.write_char('>')?;
122
123        w.write_str("</a:effectLst>")
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_outer_shadow_xml() {
133        let shadow =
134            ShadowFormat::outer(ColorFormat::rgb(0, 0, 0), Emu(50_800), Emu(38_100), 270.0);
135        let xml = shadow.to_xml_string();
136        assert!(xml.starts_with("<a:effectLst>"));
137        assert!(xml.contains("<a:outerShdw"));
138        assert!(xml.contains(r#"blurRad="50800""#));
139        assert!(xml.contains(r#"dist="38100""#));
140        assert!(xml.contains("dir="));
141        assert!(xml.contains("000000"));
142        assert!(xml.ends_with("</a:effectLst>"));
143    }
144
145    #[test]
146    fn test_inner_shadow_xml() {
147        let shadow = ShadowFormat::inner(
148            ColorFormat::rgb(128, 128, 128),
149            Emu(25_400),
150            Emu(12_700),
151            90.0,
152        );
153        let xml = shadow.to_xml_string();
154        assert!(xml.contains("<a:innerShdw"));
155        assert!(xml.contains("808080"));
156    }
157
158    #[test]
159    fn test_shadow_with_opacity() {
160        let mut shadow =
161            ShadowFormat::outer(ColorFormat::rgb(0, 0, 0), Emu(50_800), Emu(38_100), 270.0);
162        shadow.opacity = Some(0.5);
163        let xml = shadow.to_xml_string();
164        assert!(xml.contains(r#"<a:alpha val="50000"/>"#));
165    }
166}