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, XmlVersion};
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 pub fn parse(value: &str) -> Option<Self> {
97 match value {
98 "tl" => Some(Self::TopLeft),
99 "t" => Some(Self::Top),
100 "tr" => Some(Self::TopRight),
101 "l" => Some(Self::Left),
102 "ctr" => Some(Self::Center),
103 "r" => Some(Self::Right),
104 "bl" => Some(Self::BottomLeft),
105 "b" => Some(Self::Bottom),
106 "br" => Some(Self::BottomRight),
107 _ => None,
108 }
109 }
110
111 pub const fn as_str(self) -> &'static str {
113 match self {
114 Self::TopLeft => "tl",
115 Self::Top => "t",
116 Self::TopRight => "tr",
117 Self::Left => "l",
118 Self::Center => "ctr",
119 Self::Right => "r",
120 Self::BottomLeft => "bl",
121 Self::Bottom => "b",
122 Self::BottomRight => "br",
123 }
124 }
125}
126
127#[allow(non_camel_case_types)]
129#[derive(Clone, Debug, Default, Eq, PartialEq)]
130pub struct CT_OuterShadowEffect {
131 pub blur_radius: Option<i64>,
132 pub distance: Option<i64>,
133 pub direction: Option<Angle>,
134 pub scale_x: Option<Percent1000>,
135 pub scale_y: Option<Percent1000>,
136 pub skew_x: Option<Angle>,
137 pub skew_y: Option<Angle>,
138 pub alignment: Option<RectAlignment>,
139 pub rotate_with_shape: Option<bool>,
140 pub color: Option<ColorChoice>,
141 raw_children: OrderedRawChildren,
142}
143
144impl CT_OuterShadowEffect {
145 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
146 let shadow = Self {
147 blur_radius: optional_parse(start, b"blurRad")?,
148 distance: optional_parse(start, b"dist")?,
149 direction: optional_parse::<i32>(start, b"dir")?.map(Angle),
150 scale_x: optional_parse::<i32>(start, b"sx")?.map(Percent1000),
151 scale_y: optional_parse::<i32>(start, b"sy")?.map(Percent1000),
152 skew_x: optional_parse::<i32>(start, b"kx")?.map(Angle),
153 skew_y: optional_parse::<i32>(start, b"ky")?.map(Angle),
154 alignment: optional_enum(start, b"algn", RectAlignment::parse)?,
155 rotate_with_shape: optional_bool(start, b"rotWithShape")?,
156 color: None,
157 raw_children: OrderedRawChildren::default(),
158 };
159 shadow.validate_attributes()?;
160 Ok(shadow)
161 }
162
163 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
164 let mut shadow = Self::from_start(start)?;
165 let mut boundary = 0;
166 let mut buffer = Vec::new();
167 loop {
168 match reader
169 .read_event_into(&mut buffer)
170 .map_err(OxmlError::from)?
171 {
172 Event::Start(element)
173 if is_color(element.name().as_ref()) && shadow.color.is_none() =>
174 {
175 shadow.color = Some(ColorChoice::from_xml(reader, &element)?);
176 boundary = 1;
177 }
178 Event::Empty(element)
179 if is_color(element.name().as_ref()) && shadow.color.is_none() =>
180 {
181 shadow.color = Some(ColorChoice::from_empty_xml(&element)?);
182 boundary = 1;
183 }
184 Event::Start(element) => shadow
185 .raw_children
186 .push(boundary, capture_element(reader, &element)?),
187 Event::Empty(element) => shadow
188 .raw_children
189 .push(boundary, capture_empty_element(&element)?),
190 Event::End(element)
191 if matches_local_name(element.name().as_ref(), b"outerShdw") =>
192 {
193 break;
194 }
195 Event::Eof => return Err(missing_end("outerShdw")),
196 _ => {}
197 }
198 buffer.clear();
199 }
200 if shadow.color.is_none() && shadow.raw_children.is_empty() {
201 return Err(EffectError::MissingColor);
202 }
203 Ok(shadow)
204 }
205
206 fn validate_attributes(&self) -> Result<()> {
207 if let Some(value) = self.blur_radius {
208 validate_coordinate("blurRad", value)?;
209 }
210 if let Some(value) = self.distance {
211 validate_coordinate("dist", value)?;
212 }
213 if let Some(value) = self.direction {
214 validate_direction(value.0)?;
215 }
216 if let Some(value) = self.skew_x {
217 validate_skew("kx", value.0)?;
218 }
219 if let Some(value) = self.skew_y {
220 validate_skew("ky", value.0)?;
221 }
222 Ok(())
223 }
224
225 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
226 self.validate_attributes()?;
227 if self.color.is_none() && self.raw_children.is_empty() {
228 return Err(EffectError::MissingColor);
229 }
230 let mut start = BytesStart::new("a:outerShdw");
231 let blur_radius = self.blur_radius.map(|value| value.to_string());
232 let distance = self.distance.map(|value| value.to_string());
233 let direction = self.direction.map(|value| value.0.to_string());
234 let scale_x = self.scale_x.map(|value| value.0.to_string());
235 let scale_y = self.scale_y.map(|value| value.0.to_string());
236 let skew_x = self.skew_x.map(|value| value.0.to_string());
237 let skew_y = self.skew_y.map(|value| value.0.to_string());
238 for (name, value) in [
239 ("blurRad", blur_radius.as_deref()),
240 ("dist", distance.as_deref()),
241 ("dir", direction.as_deref()),
242 ("sx", scale_x.as_deref()),
243 ("sy", scale_y.as_deref()),
244 ("kx", skew_x.as_deref()),
245 ("ky", skew_y.as_deref()),
246 ] {
247 if let Some(value) = value {
248 start.push_attribute((name, value));
249 }
250 }
251 if let Some(alignment) = self.alignment {
252 start.push_attribute(("algn", alignment.as_str()));
253 }
254 if let Some(rotate) = self.rotate_with_shape.map(bool_text) {
255 start.push_attribute(("rotWithShape", rotate));
256 }
257
258 write_start(writer, start)?;
259 emit_raw(writer, self.raw_children.at(0))?;
260 if let Some(color) = &self.color {
261 color.to_xml(writer)?;
262 }
263 emit_raw(writer, self.raw_children.at(1))?;
264 write_end(writer, "a:outerShdw")
265 }
266
267 pub fn raw_children(&self) -> &OrderedRawChildren {
268 &self.raw_children
269 }
270
271 pub fn has_unmodelled_color(&self) -> bool {
274 (0..=1).any(|boundary| self.raw_children.at(boundary).any(raw_is_color))
275 }
276
277 pub fn replace_color(&mut self, color: ColorChoice) {
281 let mut raw_children = OrderedRawChildren::default();
282 let mut boundary = 0;
283 for child in self.raw_children.at(0) {
284 if raw_is_color(child) {
285 boundary = 1;
286 } else {
287 raw_children.push(boundary, child.to_vec());
288 }
289 }
290 for child in self.raw_children.at(1).filter(|child| !raw_is_color(child)) {
291 raw_children.push(1, child.to_vec());
292 }
293 self.raw_children = raw_children;
294 self.color = Some(color);
295 }
296}
297
298#[allow(non_camel_case_types)]
300#[derive(Clone, Debug, Default, Eq, PartialEq)]
301pub struct CT_EffectList {
302 pub outer_shadow: Option<CT_OuterShadowEffect>,
303 raw_attributes: Vec<(String, String)>,
304 raw_children: OrderedRawChildren,
305}
306
307impl CT_EffectList {
308 pub fn from_xml(xml: &[u8]) -> Result<Self> {
310 let mut reader = Reader::from_reader(xml);
311 let mut buffer = Vec::new();
312 loop {
313 match reader
314 .read_event_into(&mut buffer)
315 .map_err(OxmlError::from)?
316 {
317 Event::Start(element)
318 if matches_local_name(element.name().as_ref(), b"effectLst") =>
319 {
320 let mut effects = Self::from_element(&mut reader)?;
321 effects.raw_attributes = capture_raw_attributes(&element)?;
322 return Ok(effects);
323 }
324 Event::Empty(element)
325 if matches_local_name(element.name().as_ref(), b"effectLst") =>
326 {
327 return Ok(Self {
328 raw_attributes: capture_raw_attributes(&element)?,
329 ..Self::default()
330 });
331 }
332 Event::Start(element) | Event::Empty(element) => {
333 return Err(unexpected(&element));
334 }
335 Event::Eof => {
336 return Err(EffectError::Xml(OxmlError::MissingElement(
337 "DrawingML effect list".to_owned(),
338 )));
339 }
340 _ => {}
341 }
342 buffer.clear();
343 }
344 }
345
346 fn from_element(reader: &mut Reader<&[u8]>) -> Result<Self> {
347 let mut effects = Self::default();
348 let mut boundary = 0;
349 let mut buffer = Vec::new();
350 loop {
351 match reader
352 .read_event_into(&mut buffer)
353 .map_err(OxmlError::from)?
354 {
355 Event::Start(element)
356 if matches_local_name(element.name().as_ref(), b"outerShdw")
357 && effects.outer_shadow.is_none() =>
358 {
359 effects.outer_shadow =
360 Some(CT_OuterShadowEffect::from_element(reader, &element)?);
361 boundary = 1;
362 }
363 Event::Empty(element)
364 if matches_local_name(element.name().as_ref(), b"outerShdw")
365 && effects.outer_shadow.is_none() =>
366 {
367 return Err(EffectError::MissingColor);
368 }
369 Event::Start(element) => {
370 boundary = boundary.max(raw_effect_boundary(element.name().as_ref()));
371 effects
372 .raw_children
373 .push(boundary, capture_element(reader, &element)?);
374 }
375 Event::Empty(element) => {
376 boundary = boundary.max(raw_effect_boundary(element.name().as_ref()));
377 effects
378 .raw_children
379 .push(boundary, capture_empty_element(&element)?);
380 }
381 Event::End(element)
382 if matches_local_name(element.name().as_ref(), b"effectLst") =>
383 {
384 break;
385 }
386 Event::Eof => return Err(missing_end("effectLst")),
387 _ => {}
388 }
389 buffer.clear();
390 }
391 Ok(effects)
392 }
393
394 pub fn to_xml(&self) -> Result<Vec<u8>> {
396 let mut writer = Writer::new(Vec::new());
397 self.write_xml(&mut writer)?;
398 Ok(writer.into_inner())
399 }
400
401 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
403 let mut start = BytesStart::new("a:effectLst");
404 for (name, value) in &self.raw_attributes {
405 start.push_attribute((name.as_str(), value.as_str()));
406 }
407 if self.outer_shadow.is_none() && self.raw_children.is_empty() {
408 return write_empty(writer, start);
409 }
410 write_start(writer, start)?;
411 emit_raw(writer, self.raw_children.at(0))?;
412 if let Some(shadow) = &self.outer_shadow {
413 shadow.write_xml(writer)?;
414 }
415 emit_raw(writer, self.raw_children.at(1))?;
416 write_end(writer, "a:effectLst")
417 }
418
419 pub fn raw_children(&self) -> &OrderedRawChildren {
420 &self.raw_children
421 }
422
423 pub fn has_unmodelled_placeholder_color(&self) -> bool {
425 self.raw_children
426 .at(0)
427 .chain(self.raw_children.at(1))
428 .any(raw_contains_placeholder_color)
429 || self.outer_shadow.as_ref().is_some_and(|shadow| {
430 shadow
431 .raw_children()
432 .at(0)
433 .chain(shadow.raw_children().at(1))
434 .any(raw_contains_placeholder_color)
435 })
436 }
437}
438
439fn raw_effect_boundary(name: &[u8]) -> usize {
442 match local_name(name) {
443 b"prstShdw" | b"reflection" | b"softEdge" => 1,
444 _ => 0,
445 }
446}
447
448pub(crate) fn raw_contains_placeholder_color(xml: &[u8]) -> bool {
449 let mut reader = NsReader::from_reader(xml);
450 let mut buffer = Vec::new();
451 loop {
452 match reader.read_resolved_event_into(&mut buffer) {
453 Ok((namespace, Event::Start(element) | Event::Empty(element)))
454 if is_drawingml_namespace(&namespace)
455 && matches_local_name(element.name().as_ref(), b"schemeClr")
456 && get_attr(&element, b"val").as_deref() == Some("phClr") =>
457 {
458 return true;
459 }
460 Ok((_, Event::Eof)) | Err(_) => return false,
461 _ => {}
462 }
463 buffer.clear();
464 }
465}
466
467pub fn raw_is_effect_dag(xml: &[u8]) -> bool {
469 let mut reader = NsReader::from_reader(xml);
470 let mut buffer = Vec::new();
471 loop {
472 match reader.read_resolved_event_into(&mut buffer) {
473 Ok((namespace, Event::Start(element) | Event::Empty(element))) => {
474 return is_drawingml_namespace(&namespace)
475 && matches_local_name(element.name().as_ref(), b"effectDag");
476 }
477 Ok((_, Event::Eof)) | Err(_) => return false,
478 _ => {}
479 }
480 buffer.clear();
481 }
482}
483
484fn is_drawingml_namespace(namespace: &ResolveResult<'_>) -> bool {
485 match namespace {
486 ResolveResult::Bound(Namespace(uri)) => *uri == A_NS.as_bytes(),
487 ResolveResult::Unknown(prefix) => prefix == b"a",
488 ResolveResult::Unbound => false,
489 }
490}
491
492fn raw_is_color(xml: &[u8]) -> bool {
493 let name_end = xml
494 .iter()
495 .position(|byte| byte.is_ascii_whitespace() || matches!(byte, b'/' | b'>'))
496 .unwrap_or(xml.len());
497 matches!(
498 local_name(xml.get(1..name_end).unwrap_or_default()),
499 b"scrgbClr" | b"srgbClr" | b"hslClr" | b"sysClr" | b"schemeClr" | b"prstClr"
500 )
501}
502
503fn capture_raw_attributes(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
504 let mut raw = Vec::new();
505 for attribute in start.attributes() {
506 let attribute = attribute.map_err(OxmlError::from)?;
507 let name = std::str::from_utf8(attribute.key.as_ref())
508 .map_err(OxmlError::from)?
509 .to_owned();
510 let value = attribute
511 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
512 .map_err(OxmlError::from)?
513 .into_owned();
514 raw.push((name, value));
515 }
516 Ok(raw)
517}
518
519fn is_color(name: &[u8]) -> bool {
520 matches!(
521 local_name(name),
522 b"srgbClr" | b"schemeClr" | b"sysClr" | b"prstClr"
523 )
524}
525
526fn validate_coordinate(attribute: &str, value: i64) -> Result<()> {
527 if (0..=MAX_POSITIVE_COORDINATE).contains(&value) {
528 Ok(())
529 } else {
530 Err(invalid_value(attribute, value))
531 }
532}
533
534fn validate_direction(value: i32) -> Result<()> {
535 if (0..FULL_CIRCLE_ANGLE).contains(&value) {
536 Ok(())
537 } else {
538 Err(invalid_value("dir", value))
539 }
540}
541
542fn validate_skew(attribute: &str, value: i32) -> Result<()> {
543 if (-QUARTER_CIRCLE_ANGLE..QUARTER_CIRCLE_ANGLE).contains(&value) {
544 Ok(())
545 } else {
546 Err(invalid_value(attribute, value))
547 }
548}
549
550fn invalid_value(attribute: &str, value: impl ToString) -> EffectError {
551 EffectError::InvalidAttribute {
552 element: "outerShdw".to_owned(),
553 attribute: attribute.to_owned(),
554 value: value.to_string(),
555 }
556}
557
558fn optional_parse<T: std::str::FromStr>(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<T>> {
559 get_attr(start, name)
560 .map(|value| {
561 value
562 .parse()
563 .map_err(|_| invalid(start, name, value.to_owned()))
564 })
565 .transpose()
566}
567
568fn optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
569 get_attr(start, name)
570 .map(|value| match value.as_str() {
571 "1" | "true" => Ok(true),
572 "0" | "false" => Ok(false),
573 _ => Err(invalid(start, name, value)),
574 })
575 .transpose()
576}
577
578fn optional_enum<T>(
579 start: &BytesStart<'_>,
580 name: &[u8],
581 parse: impl FnOnce(&str) -> Option<T>,
582) -> Result<Option<T>> {
583 get_attr(start, name)
584 .map(|value| parse(&value).ok_or_else(|| invalid(start, name, value)))
585 .transpose()
586}
587
588fn invalid(start: &BytesStart<'_>, attribute: &[u8], value: String) -> EffectError {
589 EffectError::InvalidAttribute {
590 element: String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned(),
591 attribute: String::from_utf8_lossy(attribute).into_owned(),
592 value,
593 }
594}
595
596fn unexpected(start: &BytesStart<'_>) -> EffectError {
597 EffectError::UnexpectedElement(String::from_utf8_lossy(start.name().as_ref()).into_owned())
598}
599
600fn missing_end(name: &str) -> EffectError {
601 EffectError::Xml(OxmlError::MissingElement(format!("closing a:{name}")))
602}
603
604const fn bool_text(value: bool) -> &'static str {
605 if value { "1" } else { "0" }
606}
607
608fn emit_raw<'a, W: Write>(
609 writer: &mut Writer<W>,
610 children: impl Iterator<Item = &'a [u8]>,
611) -> Result<()> {
612 for child in children {
613 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
614 }
615 Ok(())
616}
617
618fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
619 writer
620 .write_event(Event::Start(start))
621 .map_err(OxmlError::from)?;
622 Ok(())
623}
624
625fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
626 writer
627 .write_event(Event::Empty(start))
628 .map_err(OxmlError::from)?;
629 Ok(())
630}
631
632fn write_end<W: Write>(writer: &mut Writer<W>, name: &str) -> Result<()> {
633 writer
634 .write_event(Event::End(BytesEnd::new(name)))
635 .map_err(OxmlError::from)?;
636 Ok(())
637}
638
639#[cfg(test)]
640mod tests {
641 use oxml_core::units::{Angle, Percent1000};
642
643 use super::{CT_EffectList, EffectError, RectAlignment};
644 use crate::color::ColorChoice;
645
646 #[test]
647 fn a_shape_with_glow_round_trips_with_glow_intact_as_raw_xml() {
648 let xml = br#"<z:effectLst><a:glow rad="63500"><a:srgbClr val="FF0000"><a:alpha val="50000"/></a:srgbClr><!--kept--></a:glow></z:effectLst>"#;
649
650 let written = CT_EffectList::from_xml(xml).unwrap().to_xml().unwrap();
651
652 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>"#);
653 }
654
655 #[test]
656 fn outer_shadow_properties_and_colour_round_trip_structurally() {
657 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>"#;
658 let parsed = CT_EffectList::from_xml(xml).unwrap();
659 let shadow = parsed.outer_shadow.as_ref().unwrap();
660
661 assert_eq!(shadow.blur_radius, Some(50_800));
662 assert_eq!(shadow.distance, Some(38_100));
663 assert_eq!(shadow.direction, Some(Angle(2_700_000)));
664 assert_eq!(shadow.scale_x, Some(Percent1000(120_000)));
665 assert_eq!(shadow.scale_y, Some(Percent1000(80_000)));
666 assert_eq!(shadow.skew_x, Some(Angle(1_200_000)));
667 assert_eq!(shadow.skew_y, Some(Angle(-600_000)));
668 assert_eq!(shadow.alignment, Some(RectAlignment::BottomRight));
669 assert_eq!(shadow.rotate_with_shape, Some(true));
670 assert!(matches!(shadow.color, Some(ColorChoice::Scheme { .. })));
671
672 let written = parsed.to_xml().unwrap();
673 assert_eq!(CT_EffectList::from_xml(&written).unwrap(), parsed);
674 }
675
676 #[test]
677 fn effect_list_writes_schema_order_and_keeps_raw_effect_positions() {
678 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>"#;
679
680 let written = CT_EffectList::from_xml(xml).unwrap().to_xml().unwrap();
681
682 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>"#);
683 }
684
685 #[test]
686 fn an_outer_shadow_added_later_precedes_the_effects_the_schema_puts_after_it() {
687 let xml = br#"<a:effectLst><a:glow rad="1"><a:srgbClr val="FF0000"/></a:glow><x:middle/><a:reflection/><x:after/><a:softEdge rad="2"/></a:effectLst>"#;
688 let mut effects = CT_EffectList::from_xml(xml).unwrap();
689 assert_eq!(effects.to_xml().unwrap(), xml);
690
691 effects.outer_shadow = Some(super::CT_OuterShadowEffect {
692 color: Some(ColorChoice::srgb(crate::color::RgbColor::new(0, 0, 0))),
693 ..Default::default()
694 });
695 assert_eq!(
696 effects.to_xml().unwrap(),
697 br#"<a:effectLst><a:glow rad="1"><a:srgbClr val="FF0000"/></a:glow><x:middle/><a:outerShdw><a:srgbClr val="000000"/></a:outerShdw><a:reflection/><x:after/><a:softEdge rad="2"/></a:effectLst>"#
698 );
699 }
700
701 #[test]
702 fn root_attributes_and_an_unmodelled_shadow_colour_are_kept_until_replaced() {
703 let xml = br#"<z:effectLst xmlns:x="urn:x" x:keep="1"><z:outerShdw dist="1"><z:scrgbClr r="0" g="0" b="0"><z:alpha val="50000"/></z:scrgbClr><x:after/></z:outerShdw></z:effectLst>"#;
704 let mut effects = CT_EffectList::from_xml(xml).unwrap();
705 assert_eq!(
706 effects.to_xml().unwrap(),
707 br#"<a:effectLst xmlns:x="urn:x" x:keep="1"><a:outerShdw dist="1"><z:scrgbClr r="0" g="0" b="0"><z:alpha val="50000"/></z:scrgbClr><x:after/></a:outerShdw></a:effectLst>"#
708 );
709 let shadow = effects.outer_shadow.as_mut().unwrap();
710 assert!(shadow.color.is_none());
711 assert!(shadow.has_unmodelled_color());
712
713 shadow.replace_color(ColorChoice::srgb(crate::color::RgbColor::new(1, 2, 3)));
714 assert!(!shadow.has_unmodelled_color());
715 assert_eq!(
716 effects.to_xml().unwrap(),
717 br#"<a:effectLst xmlns:x="urn:x" x:keep="1"><a:outerShdw dist="1"><a:srgbClr val="010203"/><x:after/></a:outerShdw></a:effectLst>"#
718 );
719 }
720
721 #[test]
722 fn malformed_outer_shadow_values_return_errors_without_panicking() {
723 let cases: &[&[u8]] = &[
724 br#"<a:effectLst><a:outerShdw blurRad="-1"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
725 br#"<a:effectLst><a:outerShdw dist="27273042316901"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
726 br#"<a:effectLst><a:outerShdw dir="21600000"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
727 br#"<a:effectLst><a:outerShdw kx="5400000"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
728 br#"<a:effectLst><a:outerShdw sx="wide"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
729 br#"<a:effectLst><a:outerShdw algn="middle"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
730 br#"<a:effectLst><a:outerShdw rotWithShape="maybe"><a:srgbClr val="000000"/></a:outerShdw></a:effectLst>"#,
731 br#"<a:effectLst><a:outerShdw><a:srgbClr val="XYZXYZ"/></a:outerShdw></a:effectLst>"#,
732 br#"<a:effectLst><a:outerShdw/></a:effectLst>"#,
733 ];
734 for xml in cases {
735 let result = std::panic::catch_unwind(|| CT_EffectList::from_xml(xml));
736 assert!(
737 result.is_ok(),
738 "effect parser panicked for {}",
739 String::from_utf8_lossy(xml)
740 );
741 assert!(
742 result.unwrap().is_err(),
743 "malformed outer shadow parsed successfully"
744 );
745 }
746 assert!(matches!(
747 CT_EffectList::from_xml(cases[8]),
748 Err(EffectError::MissingColor)
749 ));
750 }
751}