1use crate::dml::color::ColorFormat;
4use crate::units::Emu;
5use crate::xml_util::WriteXml;
6
7#[non_exhaustive]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ShadowType {
11 Outer,
12 Inner,
13 Perspective,
14}
15
16#[derive(Debug, Clone, PartialEq)]
21pub struct ShadowFormat {
22 pub shadow_type: ShadowType,
24 pub color: Option<ColorFormat>,
26 pub blur_radius: Option<Emu>,
28 pub distance: Option<Emu>,
30 pub direction: Option<f64>,
32 pub opacity: Option<f64>,
34}
35
36impl ShadowFormat {
37 #[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 #[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 #[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 if let Some(ref color) = self.color {
97 match color {
98 ColorFormat::Rgb(rgb) => {
99 if let Some(opacity) = self.opacity {
100 #[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}