Skip to main content

oxml_drawing/
shape_props.rs

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::xml::{local_name, matches_local_name};
7use quick_xml::events::{BytesEnd, BytesStart, Event};
8use quick_xml::{Reader, Writer, XmlVersion};
9
10use crate::effect::{
11    CT_EffectList, EffectError, raw_contains_placeholder_color, raw_is_effect_dag,
12};
13use crate::fill::{Fill, FillError};
14use crate::geometry::{CT_CustomGeometry2D, CT_PresetGeometry2D, GeometryError};
15use crate::line::{CT_LineProperties, LineError};
16use crate::order::OrderedRawChildren;
17use crate::xfrm::{CT_Transform2D, TransformError};
18
19/// Errors produced while parsing or writing DrawingML shape properties.
20#[derive(Debug)]
21pub enum ShapePropertiesError {
22    Xml(OxmlError),
23    Transform(TransformError),
24    Geometry(GeometryError),
25    Fill(FillError),
26    Line(LineError),
27    Effect(EffectError),
28    UnexpectedElement(String),
29}
30
31impl fmt::Display for ShapePropertiesError {
32    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Xml(error) => error.fmt(formatter),
35            Self::Transform(error) => error.fmt(formatter),
36            Self::Geometry(error) => error.fmt(formatter),
37            Self::Fill(error) => error.fmt(formatter),
38            Self::Line(error) => error.fmt(formatter),
39            Self::Effect(error) => error.fmt(formatter),
40            Self::UnexpectedElement(element) => {
41                write!(
42                    formatter,
43                    "unexpected DrawingML shape-properties element: {element}"
44                )
45            }
46        }
47    }
48}
49
50impl std::error::Error for ShapePropertiesError {
51    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
52        match self {
53            Self::Xml(error) => Some(error),
54            Self::Transform(error) => Some(error),
55            Self::Geometry(error) => Some(error),
56            Self::Fill(error) => Some(error),
57            Self::Line(error) => Some(error),
58            Self::Effect(error) => Some(error),
59            Self::UnexpectedElement(_) => None,
60        }
61    }
62}
63
64impl From<OxmlError> for ShapePropertiesError {
65    fn from(error: OxmlError) -> Self {
66        Self::Xml(error)
67    }
68}
69
70impl From<TransformError> for ShapePropertiesError {
71    fn from(error: TransformError) -> Self {
72        Self::Transform(error)
73    }
74}
75
76impl From<GeometryError> for ShapePropertiesError {
77    fn from(error: GeometryError) -> Self {
78        Self::Geometry(error)
79    }
80}
81
82impl From<FillError> for ShapePropertiesError {
83    fn from(error: FillError) -> Self {
84        Self::Fill(error)
85    }
86}
87
88impl From<LineError> for ShapePropertiesError {
89    fn from(error: LineError) -> Self {
90        Self::Line(error)
91    }
92}
93
94impl From<EffectError> for ShapePropertiesError {
95    fn from(error: EffectError) -> Self {
96        Self::Effect(error)
97    }
98}
99
100pub type Result<T> = std::result::Result<T, ShapePropertiesError>;
101
102/// The modelled children of one DrawingML `a:spPr` element.
103#[allow(non_camel_case_types)]
104#[derive(Clone, Debug, Default, PartialEq)]
105pub struct CT_ShapeProperties {
106    pub transform: Option<CT_Transform2D>,
107    pub custom_geometry: Option<CT_CustomGeometry2D>,
108    pub preset_geometry: Option<CT_PresetGeometry2D>,
109    pub fill: Option<Fill>,
110    pub line: Option<CT_LineProperties>,
111    pub effects: Option<CT_EffectList>,
112    raw_attributes: Vec<(String, String)>,
113    raw_children: OrderedRawChildren,
114}
115
116impl CT_ShapeProperties {
117    /// Parses one complete `a:spPr` element with any namespace prefix.
118    pub fn from_xml(xml: &[u8]) -> Result<Self> {
119        let mut reader = Reader::from_reader(xml);
120        let mut buffer = Vec::new();
121        loop {
122            match reader
123                .read_event_into(&mut buffer)
124                .map_err(OxmlError::from)?
125            {
126                Event::Start(element) if matches_local_name(element.name().as_ref(), b"spPr") => {
127                    return Self::from_element(&mut reader, &element);
128                }
129                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spPr") => {
130                    return Self::from_start(&element);
131                }
132                Event::Start(element) | Event::Empty(element) => {
133                    return Err(ShapePropertiesError::UnexpectedElement(element_name(
134                        &element,
135                    )));
136                }
137                Event::Eof => {
138                    return Err(ShapePropertiesError::Xml(OxmlError::MissingElement(
139                        "DrawingML shape properties".to_owned(),
140                    )));
141                }
142                _ => {}
143            }
144            buffer.clear();
145        }
146    }
147
148    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
149        Ok(Self {
150            raw_attributes: capture_raw_attributes(start)?,
151            ..Self::default()
152        })
153    }
154
155    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
156        let mut properties = Self::from_start(start)?;
157        let mut boundary = 0;
158        let mut buffer = Vec::new();
159
160        loop {
161            match reader
162                .read_event_into(&mut buffer)
163                .map_err(OxmlError::from)?
164            {
165                Event::Start(element) => {
166                    let name = local_name(element.name().as_ref()).to_vec();
167                    let raw = capture_element(reader, &element)?;
168                    if properties.parse_modelled(&name, &raw)? {
169                        boundary = boundary.max(modelled_boundary(&name));
170                    } else {
171                        properties
172                            .raw_children
173                            .push(boundary.max(raw_boundary_before(&name)), raw);
174                        boundary = boundary.max(raw_boundary_after(&name));
175                    }
176                }
177                Event::Empty(element) => {
178                    let name = local_name(element.name().as_ref()).to_vec();
179                    let raw = capture_empty_element(&element)?;
180                    if properties.parse_modelled(&name, &raw)? {
181                        boundary = boundary.max(modelled_boundary(&name));
182                    } else {
183                        properties
184                            .raw_children
185                            .push(boundary.max(raw_boundary_before(&name)), raw);
186                        boundary = boundary.max(raw_boundary_after(&name));
187                    }
188                }
189                Event::End(element) if matches_local_name(element.name().as_ref(), b"spPr") => {
190                    break;
191                }
192                Event::Eof => return Err(missing_end()),
193                _ => {}
194            }
195            buffer.clear();
196        }
197        Ok(properties)
198    }
199
200    fn parse_modelled(&mut self, name: &[u8], raw: &[u8]) -> Result<bool> {
201        match name {
202            b"xfrm" if self.transform.is_none() => {
203                self.transform = Some(CT_Transform2D::from_xml(raw)?);
204            }
205            b"custGeom" if self.custom_geometry.is_none() => {
206                self.custom_geometry = Some(CT_CustomGeometry2D::from_xml(raw)?);
207            }
208            b"prstGeom" if self.preset_geometry.is_none() => {
209                self.preset_geometry = Some(CT_PresetGeometry2D::from_xml(raw)?);
210            }
211            name if is_fill(name) && self.fill.is_none() => {
212                self.fill = Some(Fill::from_xml(raw)?);
213            }
214            b"ln" if self.line.is_none() => {
215                self.line = Some(CT_LineProperties::from_xml(raw)?);
216            }
217            b"effectLst" if self.effects.is_none() => {
218                self.effects = Some(CT_EffectList::from_xml(raw)?);
219            }
220            _ => return Ok(false),
221        }
222        Ok(true)
223    }
224
225    /// Writes shape properties with the fixed `a:` prefix and schema order.
226    pub fn to_xml(&self) -> Result<Vec<u8>> {
227        let mut writer = Writer::new(Vec::new());
228        self.write_xml(&mut writer)?;
229        Ok(writer.into_inner())
230    }
231
232    /// Writes shape properties into an existing XML writer.
233    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
234        self.write_xml_as(writer, "a:spPr")
235    }
236
237    /// Writes shape properties under the caller's required root name.
238    pub fn write_xml_as<W: Write>(&self, writer: &mut Writer<W>, name: &str) -> Result<()> {
239        let mut start = BytesStart::new(name);
240        push_raw_attributes(&mut start, &self.raw_attributes);
241        if self.transform.is_none()
242            && self.custom_geometry.is_none()
243            && self.preset_geometry.is_none()
244            && self.fill.is_none()
245            && self.line.is_none()
246            && self.effects.is_none()
247            && self.raw_children.is_empty()
248        {
249            writer
250                .write_event(Event::Empty(start))
251                .map_err(OxmlError::from)?;
252            return Ok(());
253        }
254
255        writer
256            .write_event(Event::Start(start))
257            .map_err(OxmlError::from)?;
258        emit_raw(writer, self.raw_children.at(0))?;
259        if let Some(transform) = &self.transform {
260            transform.write_xml(writer)?;
261        }
262        emit_raw(writer, self.raw_children.at(1))?;
263        if let Some(geometry) = &self.custom_geometry {
264            writer
265                .get_mut()
266                .write_all(&geometry.to_xml()?)
267                .map_err(OxmlError::from)?;
268        } else if let Some(geometry) = &self.preset_geometry {
269            writer
270                .get_mut()
271                .write_all(&geometry.to_xml()?)
272                .map_err(OxmlError::from)?;
273        }
274        emit_raw(writer, self.raw_children.at(2))?;
275        if let Some(fill) = &self.fill {
276            fill.write_xml(writer)?;
277        }
278        emit_raw(writer, self.raw_children.at(3))?;
279        if let Some(line) = &self.line {
280            line.write_xml(writer)?;
281        }
282        emit_raw(writer, self.raw_children.at(4))?;
283        if let Some(effects) = &self.effects {
284            effects.write_xml(writer)?;
285        }
286        for boundary in 5..=8 {
287            emit_raw(writer, self.raw_children.at(boundary))?;
288        }
289        writer
290            .write_event(Event::End(BytesEnd::new(name)))
291            .map_err(OxmlError::from)?;
292        Ok(())
293    }
294
295    pub fn raw_children(&self) -> &OrderedRawChildren {
296        &self.raw_children
297    }
298
299    /// Returns whether an opaque effect DAG is present instead of a typed list.
300    pub fn has_unmodelled_effect(&self) -> bool {
301        (0..=8).any(|boundary| self.raw_children.at(boundary).any(raw_is_effect_dag))
302    }
303
304    /// Reports a placeholder colour inside an opaque effect DAG.
305    pub fn has_unmodelled_effect_placeholder_color(&self) -> bool {
306        (0..=8).any(|boundary| {
307            self.raw_children
308                .at(boundary)
309                .any(|xml| raw_is_effect_dag(xml) && raw_contains_placeholder_color(xml))
310        })
311    }
312}
313
314fn is_fill(name: &[u8]) -> bool {
315    matches!(
316        name,
317        b"noFill" | b"solidFill" | b"gradFill" | b"pattFill" | b"blipFill"
318    )
319}
320
321fn modelled_boundary(name: &[u8]) -> usize {
322    match name {
323        b"xfrm" => 1,
324        b"custGeom" | b"prstGeom" => 2,
325        name if is_fill(name) => 3,
326        b"ln" => 4,
327        b"effectLst" => 5,
328        _ => 0,
329    }
330}
331
332fn raw_boundary_after(name: &[u8]) -> usize {
333    match name {
334        b"xfrm" => 1,
335        b"custGeom" | b"prstGeom" => 2,
336        b"noFill" | b"solidFill" | b"gradFill" | b"blipFill" | b"pattFill" | b"grpFill" => 3,
337        b"ln" => 4,
338        b"effectLst" | b"effectDag" => 5,
339        b"scene3d" => 6,
340        b"sp3d" => 7,
341        b"extLst" => 8,
342        _ => 0,
343    }
344}
345
346/// The earliest boundary an unmodelled child may be written at. A child such
347/// as `a:scene3d` read where no `a:effectLst` stood stays behind the effect
348/// slot, so an effect list added later is written before it.
349fn raw_boundary_before(name: &[u8]) -> usize {
350    raw_boundary_after(name).saturating_sub(1)
351}
352
353fn emit_raw<'a, W: Write>(
354    writer: &mut Writer<W>,
355    children: impl Iterator<Item = &'a [u8]>,
356) -> Result<()> {
357    for child in children {
358        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
359    }
360    Ok(())
361}
362
363fn capture_raw_attributes(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
364    let mut raw = Vec::new();
365    for attribute in start.attributes() {
366        let attribute = attribute.map_err(OxmlError::from)?;
367        let name = std::str::from_utf8(attribute.key.as_ref())
368            .map_err(OxmlError::from)?
369            .to_owned();
370        let value = attribute
371            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
372            .map_err(OxmlError::from)?
373            .into_owned();
374        raw.push((name, value));
375    }
376    Ok(raw)
377}
378
379fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
380    for (name, value) in attributes {
381        start.push_attribute((name.as_str(), value.as_str()));
382    }
383}
384
385fn element_name(element: &BytesStart<'_>) -> String {
386    String::from_utf8_lossy(element.name().as_ref()).into_owned()
387}
388
389fn missing_end() -> ShapePropertiesError {
390    ShapePropertiesError::Xml(OxmlError::MissingElement(
391        "closing DrawingML shape properties".to_owned(),
392    ))
393}
394
395#[cfg(test)]
396mod tests {
397    use super::CT_ShapeProperties;
398
399    #[test]
400    fn shape_properties_round_trip_in_schema_order() {
401        let xml = br#"<z:spPr><x:before/><z:xfrm rot="60000"><z:off x="1" y="2"/></z:xfrm><x:afterXfrm/><z:custGeom><z:pathLst><z:path/></z:pathLst></z:custGeom><x:afterGeom/><z:solidFill><z:srgbClr val="112233"/></z:solidFill><x:afterFill/><z:ln w="12700"><z:noFill/></z:ln><x:afterLine/><z:effectLst><z:glow rad="40000"><z:srgbClr val="445566"/></z:glow></z:effectLst><x:afterEffects/></z:spPr>"#;
402        let properties = CT_ShapeProperties::from_xml(xml).unwrap();
403        assert!(properties.transform.is_some());
404        assert!(properties.custom_geometry.is_some());
405        assert!(properties.fill.is_some());
406        assert!(properties.line.is_some());
407        assert!(properties.effects.is_some());
408
409        let written = properties.to_xml().unwrap();
410        assert_eq!(written, br#"<a:spPr><x:before/><a:xfrm rot="60000"><a:off x="1" y="2"/></a:xfrm><x:afterXfrm/><a:custGeom><a:pathLst><a:path/></a:pathLst></a:custGeom><x:afterGeom/><a:solidFill><a:srgbClr val="112233"/></a:solidFill><x:afterFill/><a:ln w="12700"><a:noFill/></a:ln><x:afterLine/><a:effectLst><z:glow rad="40000"><z:srgbClr val="445566"/></z:glow></a:effectLst><x:afterEffects/></a:spPr>"#);
411        assert_eq!(CT_ShapeProperties::from_xml(&written).unwrap(), properties);
412    }
413
414    #[test]
415    fn modelled_children_added_later_precede_unmodelled_3d_and_extensions() {
416        let xml = br#"<a:spPr><a:prstGeom prst="rect"/><a:scene3d/><x:after/><a:sp3d/><a:extLst/></a:spPr>"#;
417        let mut properties = CT_ShapeProperties::from_xml(xml).unwrap();
418        assert_eq!(properties.to_xml().unwrap(), xml);
419
420        properties.fill = Some(crate::fill::Fill::from_xml(br#"<a:noFill/>"#).unwrap());
421        properties.line = Some(crate::line::CT_LineProperties::default());
422        properties.effects = Some(crate::effect::CT_EffectList::default());
423        assert_eq!(
424            properties.to_xml().unwrap(),
425            br#"<a:spPr><a:prstGeom prst="rect"/><a:noFill/><a:ln/><a:effectLst/><a:scene3d/><x:after/><a:sp3d/><a:extLst/></a:spPr>"#
426        );
427    }
428
429    #[test]
430    fn shape_property_root_attributes_round_trip_without_loss() {
431        let properties =
432            CT_ShapeProperties::from_xml(br#"<q:spPr bwMode="gray" x:future="keep &amp; stay"/>"#)
433                .unwrap();
434        assert_eq!(
435            properties.to_xml().unwrap(),
436            br#"<a:spPr bwMode="gray" x:future="keep &amp; stay"/>"#
437        );
438    }
439
440    #[test]
441    fn preset_geometry_round_trips_with_unknown_children_verbatim() {
442        let xml = br#"<p:spPr><u:before/><q:prstGeom prst="trapezoid"><u:first id="1"><u:nested/></u:first><q:avLst><u:inside/><q:gd name="adj" fmla="val 50000"/></q:avLst><u:last>keep &amp; stay</u:last></q:prstGeom><u:after/></p:spPr>"#;
443        let properties = CT_ShapeProperties::from_xml(xml).unwrap();
444        let preset = properties.preset_geometry.as_ref().unwrap();
445        assert_eq!(preset.preset, "trapezoid");
446        assert_eq!(preset.adjust_values().len(), 1);
447
448        let written = String::from_utf8(properties.to_xml().unwrap()).unwrap();
449        assert!(written.starts_with("<a:spPr>"));
450        assert!(written.contains("<a:prstGeom prst=\"trapezoid\">"));
451        for raw in [
452            "<u:before/>",
453            "<u:first id=\"1\"><u:nested/></u:first>",
454            "<u:inside/>",
455            "<u:last>keep &amp; stay</u:last>",
456            "<u:after/>",
457        ] {
458            assert!(written.contains(raw), "missing raw subtree {raw}");
459        }
460        let before = written.find("<u:before/>").unwrap();
461        let preset_start = written.find("<a:prstGeom").unwrap();
462        let first = written.find("<u:first").unwrap();
463        let adjustments = written.find("<a:avLst").unwrap();
464        let inside = written.find("<u:inside/>").unwrap();
465        let guide = written.find("<a:gd ").unwrap();
466        let last = written.find("<u:last>").unwrap();
467        let preset_end = written.find("</a:prstGeom>").unwrap();
468        let after = written.find("<u:after/>").unwrap();
469        assert!(before < preset_start);
470        assert!(preset_start < first && first < adjustments);
471        assert!(adjustments < inside && inside < guide);
472        assert!(guide < last && last < preset_end && preset_end < after);
473        assert_eq!(
474            CT_ShapeProperties::from_xml(written.as_bytes()).unwrap(),
475            properties
476        );
477    }
478}