1use std::fmt;
2use std::io::Write;
3
4use oxml_core::OxmlError;
5use oxml_core::raw_xml::{capture_element, capture_empty_element};
6use oxml_core::units::{Angle, Percent1000};
7use oxml_core::xml::{get_attr, local_name, matches_local_name};
8use quick_xml::events::{BytesEnd, BytesStart, Event};
9use quick_xml::name::{Namespace, ResolveResult};
10use quick_xml::reader::NsReader;
11use quick_xml::{Reader, Writer};
12
13use crate::color::{ColorChoice, ColorError};
14use crate::namespace::A_NS;
15use crate::order::OrderedRawChildren;
16
17const MAX_POSITIVE_COORDINATE: i64 = 27_273_042_316_900;
18const FULL_CIRCLE_ANGLE: i32 = 21_600_000;
19const QUARTER_CIRCLE_ANGLE: i32 = 5_400_000;
20
21#[derive(Debug)]
23pub enum EffectError {
24 Xml(OxmlError),
25 Color(ColorError),
26 UnexpectedElement(String),
27 MissingColor,
28 InvalidAttribute {
29 element: String,
30 attribute: String,
31 value: String,
32 },
33}
34
35impl fmt::Display for EffectError {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::Xml(error) => error.fmt(formatter),
39 Self::Color(error) => error.fmt(formatter),
40 Self::UnexpectedElement(element) => {
41 write!(formatter, "unexpected DrawingML effect element: {element}")
42 }
43 Self::MissingColor => write!(formatter, "DrawingML outerShdw requires a colour child"),
44 Self::InvalidAttribute {
45 element,
46 attribute,
47 value,
48 } => write!(
49 formatter,
50 "DrawingML {element} has invalid @{attribute}: {value}"
51 ),
52 }
53 }
54}
55
56impl std::error::Error for EffectError {
57 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
58 match self {
59 Self::Xml(error) => Some(error),
60 Self::Color(error) => Some(error),
61 _ => None,
62 }
63 }
64}
65
66impl From<OxmlError> for EffectError {
67 fn from(error: OxmlError) -> Self {
68 Self::Xml(error)
69 }
70}
71
72impl From<ColorError> for EffectError {
73 fn from(error: ColorError) -> Self {
74 Self::Color(error)
75 }
76}
77
78pub type Result<T> = std::result::Result<T, EffectError>;
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub enum RectAlignment {
83 TopLeft,
84 Top,
85 TopRight,
86 Left,
87 Center,
88 Right,
89 BottomLeft,
90 Bottom,
91 BottomRight,
92}
93
94impl RectAlignment {
95 fn parse(value: &str) -> Option<Self> {
96 match value {
97 "tl" => Some(Self::TopLeft),
98 "t" => Some(Self::Top),
99 "tr" => Some(Self::TopRight),
100 "l" => Some(Self::Left),
101 "ctr" => Some(Self::Center),
102 "r" => Some(Self::Right),
103 "bl" => Some(Self::BottomLeft),
104 "b" => Some(Self::Bottom),
105 "br" => Some(Self::BottomRight),
106 _ => None,
107 }
108 }
109
110 const fn as_str(self) -> &'static str {
111 match self {
112 Self::TopLeft => "tl",
113 Self::Top => "t",
114 Self::TopRight => "tr",
115 Self::Left => "l",
116 Self::Center => "ctr",
117 Self::Right => "r",
118 Self::BottomLeft => "bl",
119 Self::Bottom => "b",
120 Self::BottomRight => "br",
121 }
122 }
123}
124
125#[allow(non_camel_case_types)]
127#[derive(Clone, Debug, Default, Eq, PartialEq)]
128pub struct CT_OuterShadowEffect {
129 pub blur_radius: Option<i64>,
130 pub distance: Option<i64>,
131 pub direction: Option<Angle>,
132 pub scale_x: Option<Percent1000>,
133 pub scale_y: Option<Percent1000>,
134 pub skew_x: Option<Angle>,
135 pub skew_y: Option<Angle>,
136 pub alignment: Option<RectAlignment>,
137 pub rotate_with_shape: Option<bool>,
138 pub color: Option<ColorChoice>,
139 raw_children: OrderedRawChildren,
140}
141
142impl CT_OuterShadowEffect {
143 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
144 let shadow = Self {
145 blur_radius: optional_parse(start, b"blurRad")?,
146 distance: optional_parse(start, b"dist")?,
147 direction: optional_parse::<i32>(start, b"dir")?.map(Angle),
148 scale_x: optional_parse::<i32>(start, b"sx")?.map(Percent1000),
149 scale_y: optional_parse::<i32>(start, b"sy")?.map(Percent1000),
150 skew_x: optional_parse::<i32>(start, b"kx")?.map(Angle),
151 skew_y: optional_parse::<i32>(start, b"ky")?.map(Angle),
152 alignment: optional_enum(start, b"algn", RectAlignment::parse)?,
153 rotate_with_shape: optional_bool(start, b"rotWithShape")?,
154 color: None,
155 raw_children: OrderedRawChildren::default(),
156 };
157 shadow.validate_attributes()?;
158 Ok(shadow)
159 }
160
161 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
162 let mut shadow = Self::from_start(start)?;
163 let mut boundary = 0;
164 let mut buffer = Vec::new();
165 loop {
166 match reader
167 .read_event_into(&mut buffer)
168 .map_err(OxmlError::from)?
169 {
170 Event::Start(element)
171 if is_color(element.name().as_ref()) && shadow.color.is_none() =>
172 {
173 shadow.color = Some(ColorChoice::from_xml(reader, &element)?);
174 boundary = 1;
175 }
176 Event::Empty(element)
177 if is_color(element.name().as_ref()) && shadow.color.is_none() =>
178 {
179 shadow.color = Some(ColorChoice::from_empty_xml(&element)?);
180 boundary = 1;
181 }
182 Event::Start(element) => shadow
183 .raw_children
184 .push(boundary, capture_element(reader, &element)?),
185 Event::Empty(element) => shadow
186 .raw_children
187 .push(boundary, capture_empty_element(&element)?),
188 Event::End(element)
189 if matches_local_name(element.name().as_ref(), b"outerShdw") =>
190 {
191 break;
192 }
193 Event::Eof => return Err(missing_end("outerShdw")),
194 _ => {}
195 }
196 buffer.clear();
197 }
198 if shadow.color.is_none() && shadow.raw_children.is_empty() {
199 return Err(EffectError::MissingColor);
200 }
201 Ok(shadow)
202 }
203
204 fn validate_attributes(&self) -> Result<()> {
205 if let Some(value) = self.blur_radius {
206 validate_coordinate("blurRad", value)?;
207 }
208 if let Some(value) = self.distance {
209 validate_coordinate("dist", value)?;
210 }
211 if let Some(value) = self.direction {
212 validate_direction(value.0)?;
213 }
214 if let Some(value) = self.skew_x {
215 validate_skew("kx", value.0)?;
216 }
217 if let Some(value) = self.skew_y {
218 validate_skew("ky", value.0)?;
219 }
220 Ok(())
221 }
222
223 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
224 self.validate_attributes()?;
225 if self.color.is_none() && self.raw_children.is_empty() {
226 return Err(EffectError::MissingColor);
227 }
228 let mut start = BytesStart::new("a:outerShdw");
229 let blur_radius = self.blur_radius.map(|value| value.to_string());
230 let distance = self.distance.map(|value| value.to_string());
231 let direction = self.direction.map(|value| value.0.to_string());
232 let scale_x = self.scale_x.map(|value| value.0.to_string());
233 let scale_y = self.scale_y.map(|value| value.0.to_string());
234 let skew_x = self.skew_x.map(|value| value.0.to_string());
235 let skew_y = self.skew_y.map(|value| value.0.to_string());
236 for (name, value) in [
237 ("blurRad", blur_radius.as_deref()),
238 ("dist", distance.as_deref()),
239 ("dir", direction.as_deref()),
240 ("sx", scale_x.as_deref()),
241 ("sy", scale_y.as_deref()),
242 ("kx", skew_x.as_deref()),
243 ("ky", skew_y.as_deref()),
244 ] {
245 if let Some(value) = value {
246 start.push_attribute((name, value));
247 }
248 }
249 if let Some(alignment) = self.alignment {
250 start.push_attribute(("algn", alignment.as_str()));
251 }
252 if let Some(rotate) = self.rotate_with_shape.map(bool_text) {
253 start.push_attribute(("rotWithShape", rotate));
254 }
255
256 write_start(writer, start)?;
257 emit_raw(writer, self.raw_children.at(0))?;
258 if let Some(color) = &self.color {
259 color.to_xml(writer)?;
260 }
261 emit_raw(writer, self.raw_children.at(1))?;
262 write_end(writer, "a:outerShdw")
263 }
264
265 pub fn raw_children(&self) -> &OrderedRawChildren {
266 &self.raw_children
267 }
268}
269
270#[allow(non_camel_case_types)]
272#[derive(Clone, Debug, Default, Eq, PartialEq)]
273pub struct CT_EffectList {
274 pub outer_shadow: Option<CT_OuterShadowEffect>,
275 raw_children: OrderedRawChildren,
276}
277
278impl CT_EffectList {
279 pub fn from_xml(xml: &[u8]) -> Result<Self> {
281 let mut reader = Reader::from_reader(xml);
282 let mut buffer = Vec::new();
283 loop {
284 match reader
285 .read_event_into(&mut buffer)
286 .map_err(OxmlError::from)?
287 {
288 Event::Start(element)
289 if matches_local_name(element.name().as_ref(), b"effectLst") =>
290 {
291 return Self::from_element(&mut reader);
292 }
293 Event::Empty(element)
294 if matches_local_name(element.name().as_ref(), b"effectLst") =>
295 {
296 return Ok(Self::default());
297 }
298 Event::Start(element) | Event::Empty(element) => {
299 return Err(unexpected(&element));
300 }
301 Event::Eof => {
302 return Err(EffectError::Xml(OxmlError::MissingElement(
303 "DrawingML effect list".to_owned(),
304 )));
305 }
306 _ => {}
307 }
308 buffer.clear();
309 }
310 }
311
312 fn from_element(reader: &mut Reader<&[u8]>) -> Result<Self> {
313 let mut effects = Self::default();
314 let mut boundary = 0;
315 let mut buffer = Vec::new();
316 loop {
317 match reader
318 .read_event_into(&mut buffer)
319 .map_err(OxmlError::from)?
320 {
321 Event::Start(element)
322 if matches_local_name(element.name().as_ref(), b"outerShdw")
323 && effects.outer_shadow.is_none() =>
324 {
325 effects.outer_shadow =
326 Some(CT_OuterShadowEffect::from_element(reader, &element)?);
327 boundary = 1;
328 }
329 Event::Empty(element)
330 if matches_local_name(element.name().as_ref(), b"outerShdw")
331 && effects.outer_shadow.is_none() =>
332 {
333 return Err(EffectError::MissingColor);
334 }
335 Event::Start(element) => effects
336 .raw_children
337 .push(boundary, capture_element(reader, &element)?),
338 Event::Empty(element) => effects
339 .raw_children
340 .push(boundary, capture_empty_element(&element)?),
341 Event::End(element)
342 if matches_local_name(element.name().as_ref(), b"effectLst") =>
343 {
344 break;
345 }
346 Event::Eof => return Err(missing_end("effectLst")),
347 _ => {}
348 }
349 buffer.clear();
350 }
351 Ok(effects)
352 }
353
354 pub fn to_xml(&self) -> Result<Vec<u8>> {
356 let mut writer = Writer::new(Vec::new());
357 self.write_xml(&mut writer)?;
358 Ok(writer.into_inner())
359 }
360
361 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
363 if self.outer_shadow.is_none() && self.raw_children.is_empty() {
364 return write_empty(writer, BytesStart::new("a:effectLst"));
365 }
366 write_start(writer, BytesStart::new("a:effectLst"))?;
367 emit_raw(writer, self.raw_children.at(0))?;
368 if let Some(shadow) = &self.outer_shadow {
369 shadow.write_xml(writer)?;
370 }
371 emit_raw(writer, self.raw_children.at(1))?;
372 write_end(writer, "a:effectLst")
373 }
374
375 pub fn raw_children(&self) -> &OrderedRawChildren {
376 &self.raw_children
377 }
378
379 pub fn has_unmodelled_placeholder_color(&self) -> bool {
381 self.raw_children
382 .at(0)
383 .chain(self.raw_children.at(1))
384 .any(raw_contains_placeholder_color)
385 || self.outer_shadow.as_ref().is_some_and(|shadow| {
386 shadow
387 .raw_children()
388 .at(0)
389 .chain(shadow.raw_children().at(1))
390 .any(raw_contains_placeholder_color)
391 })
392 }
393}
394
395pub(crate) fn raw_contains_placeholder_color(xml: &[u8]) -> bool {
396 let mut reader = NsReader::from_reader(xml);
397 let mut buffer = Vec::new();
398 loop {
399 match reader.read_resolved_event_into(&mut buffer) {
400 Ok((namespace, Event::Start(element) | Event::Empty(element)))
401 if is_drawingml_namespace(&namespace)
402 && matches_local_name(element.name().as_ref(), b"schemeClr")
403 && get_attr(&element, b"val").as_deref() == Some("phClr") =>
404 {
405 return true;
406 }
407 Ok((_, Event::Eof)) | Err(_) => return false,
408 _ => {}
409 }
410 buffer.clear();
411 }
412}
413
414pub(crate) fn raw_is_effect_dag(xml: &[u8]) -> bool {
415 let mut reader = NsReader::from_reader(xml);
416 let mut buffer = Vec::new();
417 loop {
418 match reader.read_resolved_event_into(&mut buffer) {
419 Ok((namespace, Event::Start(element) | Event::Empty(element))) => {
420 return is_drawingml_namespace(&namespace)
421 && matches_local_name(element.name().as_ref(), b"effectDag");
422 }
423 Ok((_, Event::Eof)) | Err(_) => return false,
424 _ => {}
425 }
426 buffer.clear();
427 }
428}
429
430fn is_drawingml_namespace(namespace: &ResolveResult<'_>) -> bool {
431 match namespace {
432 ResolveResult::Bound(Namespace(uri)) => *uri == A_NS.as_bytes(),
433 ResolveResult::Unknown(prefix) => prefix == b"a",
434 ResolveResult::Unbound => false,
435 }
436}
437
438fn is_color(name: &[u8]) -> bool {
439 matches!(
440 local_name(name),
441 b"srgbClr" | b"schemeClr" | b"sysClr" | b"prstClr"
442 )
443}
444
445fn validate_coordinate(attribute: &str, value: i64) -> Result<()> {
446 if (0..=MAX_POSITIVE_COORDINATE).contains(&value) {
447 Ok(())
448 } else {
449 Err(invalid_value(attribute, value))
450 }
451}
452
453fn validate_direction(value: i32) -> Result<()> {
454 if (0..FULL_CIRCLE_ANGLE).contains(&value) {
455 Ok(())
456 } else {
457 Err(invalid_value("dir", value))
458 }
459}
460
461fn validate_skew(attribute: &str, value: i32) -> Result<()> {
462 if (-QUARTER_CIRCLE_ANGLE..QUARTER_CIRCLE_ANGLE).contains(&value) {
463 Ok(())
464 } else {
465 Err(invalid_value(attribute, value))
466 }
467}
468
469fn invalid_value(attribute: &str, value: impl ToString) -> EffectError {
470 EffectError::InvalidAttribute {
471 element: "outerShdw".to_owned(),
472 attribute: attribute.to_owned(),
473 value: value.to_string(),
474 }
475}
476
477fn optional_parse<T: std::str::FromStr>(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<T>> {
478 get_attr(start, name)
479 .map(|value| {
480 value
481 .parse()
482 .map_err(|_| invalid(start, name, value.to_owned()))
483 })
484 .transpose()
485}
486
487fn optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
488 get_attr(start, name)
489 .map(|value| match value.as_str() {
490 "1" | "true" => Ok(true),
491 "0" | "false" => Ok(false),
492 _ => Err(invalid(start, name, value)),
493 })
494 .transpose()
495}
496
497fn optional_enum<T>(
498 start: &BytesStart<'_>,
499 name: &[u8],
500 parse: impl FnOnce(&str) -> Option<T>,
501) -> Result<Option<T>> {
502 get_attr(start, name)
503 .map(|value| parse(&value).ok_or_else(|| invalid(start, name, value)))
504 .transpose()
505}
506
507fn invalid(start: &BytesStart<'_>, attribute: &[u8], value: String) -> EffectError {
508 EffectError::InvalidAttribute {
509 element: String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned(),
510 attribute: String::from_utf8_lossy(attribute).into_owned(),
511 value,
512 }
513}
514
515fn unexpected(start: &BytesStart<'_>) -> EffectError {
516 EffectError::UnexpectedElement(String::from_utf8_lossy(start.name().as_ref()).into_owned())
517}
518
519fn missing_end(name: &str) -> EffectError {
520 EffectError::Xml(OxmlError::MissingElement(format!("closing a:{name}")))
521}
522
523const fn bool_text(value: bool) -> &'static str {
524 if value { "1" } else { "0" }
525}
526
527fn emit_raw<'a, W: Write>(
528 writer: &mut Writer<W>,
529 children: impl Iterator<Item = &'a [u8]>,
530) -> Result<()> {
531 for child in children {
532 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
533 }
534 Ok(())
535}
536
537fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
538 writer
539 .write_event(Event::Start(start))
540 .map_err(OxmlError::from)?;
541 Ok(())
542}
543
544fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
545 writer
546 .write_event(Event::Empty(start))
547 .map_err(OxmlError::from)?;
548 Ok(())
549}
550
551fn write_end<W: Write>(writer: &mut Writer<W>, name: &str) -> Result<()> {
552 writer
553 .write_event(Event::End(BytesEnd::new(name)))
554 .map_err(OxmlError::from)?;
555 Ok(())
556}
557
558#[cfg(test)]
559mod tests {
560 use oxml_core::units::{Angle, Percent1000};
561
562 use super::{CT_EffectList, EffectError, RectAlignment};
563 use crate::color::ColorChoice;
564
565 #[test]
566 fn a_shape_with_glow_round_trips_with_glow_intact_as_raw_xml() {
567 let xml = br#"<z:effectLst><a:glow rad="63500"><a:srgbClr val="FF0000"><a:alpha val="50000"/></a:srgbClr><!--kept--></a:glow></z:effectLst>"#;
568
569 let written = CT_EffectList::from_xml(xml).unwrap().to_xml().unwrap();
570
571 assert_eq!(written, br#"<a:effectLst><a:glow rad="63500"><a:srgbClr val="FF0000"><a:alpha val="50000"/></a:srgbClr><!--kept--></a:glow></a:effectLst>"#);
572 }
573
574 #[test]
575 fn outer_shadow_properties_and_colour_round_trip_structurally() {
576 let xml = br#"<z:effectLst><z:outerShdw blurRad="50800" dist="38100" dir="2700000" sx="120000" sy="80000" kx="1200000" ky="-600000" algn="br" rotWithShape="1"><z:schemeClr val="accent2"><z:alpha val="60000"/></z:schemeClr></z:outerShdw></z:effectLst>"#;
577 let parsed = CT_EffectList::from_xml(xml).unwrap();
578 let shadow = parsed.outer_shadow.as_ref().unwrap();
579
580 assert_eq!(shadow.blur_radius, Some(50_800));
581 assert_eq!(shadow.distance, Some(38_100));
582 assert_eq!(shadow.direction, Some(Angle(2_700_000)));
583 assert_eq!(shadow.scale_x, Some(Percent1000(120_000)));
584 assert_eq!(shadow.scale_y, Some(Percent1000(80_000)));
585 assert_eq!(shadow.skew_x, Some(Angle(1_200_000)));
586 assert_eq!(shadow.skew_y, Some(Angle(-600_000)));
587 assert_eq!(shadow.alignment, Some(RectAlignment::BottomRight));
588 assert_eq!(shadow.rotate_with_shape, Some(true));
589 assert!(matches!(shadow.color, Some(ColorChoice::Scheme { .. })));
590
591 let written = parsed.to_xml().unwrap();
592 assert_eq!(CT_EffectList::from_xml(&written).unwrap(), parsed);
593 }
594
595 #[test]
596 fn effect_list_writes_schema_order_and_keeps_raw_effect_positions() {
597 let xml = br#"<z:effectLst><x:before/><z:blur rad="10"/><z:glow rad="20"><x:item>one & two</x:item></z:glow><z:innerShdw><z:srgbClr val="010203"/></z:innerShdw><z:outerShdw dist="30"><x:shadowBefore/><z:srgbClr val="AABBCC"/><x:shadowAfter/></z:outerShdw><z:prstShdw prst="shdw1"><z:srgbClr val="040506"/></z:prstShdw><z:reflection blurRad="40"/><x:after/></z:effectLst>"#;
598
599 let written = CT_EffectList::from_xml(xml).unwrap().to_xml().unwrap();
600
601 assert_eq!(written, br#"<a:effectLst><x:before/><z:blur rad="10"/><z:glow rad="20"><x:item>one & two</x:item></z:glow><z:innerShdw><z:srgbClr val="010203"/></z:innerShdw><a:outerShdw dist="30"><x:shadowBefore/><a:srgbClr val="AABBCC"/><x:shadowAfter/></a:outerShdw><z:prstShdw prst="shdw1"><z:srgbClr val="040506"/></z:prstShdw><z:reflection blurRad="40"/><x:after/></a:effectLst>"#);
602 }
603
604 #[test]
605 fn malformed_outer_shadow_values_return_errors_without_panicking() {
606 let cases: &[&[u8]] = &[
607 br#"<a:effectLst><a:outerShdw blurRad="-1"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
608 br#"<a:effectLst><a:outerShdw dist="27273042316901"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
609 br#"<a:effectLst><a:outerShdw dir="21600000"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
610 br#"<a:effectLst><a:outerShdw kx="5400000"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
611 br#"<a:effectLst><a:outerShdw sx="wide"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
612 br#"<a:effectLst><a:outerShdw algn="middle"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
613 br#"<a:effectLst><a:outerShdw rotWithShape="maybe"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
614 br#"<a:effectLst><a:outerShdw><a:srgbClr val="XYZXYZ"/></a:outerShdw></a:effectLst>"#,
615 br#"<a:effectLst><a:outerShdw/></a:effectLst>"#,
616 ];
617 for xml in cases {
618 let result = std::panic::catch_unwind(|| CT_EffectList::from_xml(xml));
619 assert!(
620 result.is_ok(),
621 "effect parser panicked for {}",
622 String::from_utf8_lossy(xml)
623 );
624 assert!(
625 result.unwrap().is_err(),
626 "malformed outer shadow parsed successfully"
627 );
628 }
629 assert!(matches!(
630 CT_EffectList::from_xml(cases[8]),
631 Err(EffectError::MissingColor)
632 ));
633 }
634}