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::namespace::{
17    property_is_drawing, property_namespace_scope, property_reject_writer_conflicts,
18};
19use crate::order::OrderedRawChildren;
20use crate::xfrm::{CT_Transform2D, TransformError};
21
22/// Errors produced while parsing or writing DrawingML shape properties.
23#[derive(Debug)]
24pub enum ShapePropertiesError {
25    Xml(OxmlError),
26    Transform(TransformError),
27    Geometry(GeometryError),
28    Fill(FillError),
29    Line(LineError),
30    Effect(EffectError),
31    UnexpectedElement(String),
32}
33
34impl fmt::Display for ShapePropertiesError {
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            Self::Xml(error) => error.fmt(formatter),
38            Self::Transform(error) => error.fmt(formatter),
39            Self::Geometry(error) => error.fmt(formatter),
40            Self::Fill(error) => error.fmt(formatter),
41            Self::Line(error) => error.fmt(formatter),
42            Self::Effect(error) => error.fmt(formatter),
43            Self::UnexpectedElement(element) => {
44                write!(
45                    formatter,
46                    "unexpected DrawingML shape-properties element: {element}"
47                )
48            }
49        }
50    }
51}
52
53impl std::error::Error for ShapePropertiesError {
54    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
55        match self {
56            Self::Xml(error) => Some(error),
57            Self::Transform(error) => Some(error),
58            Self::Geometry(error) => Some(error),
59            Self::Fill(error) => Some(error),
60            Self::Line(error) => Some(error),
61            Self::Effect(error) => Some(error),
62            Self::UnexpectedElement(_) => None,
63        }
64    }
65}
66
67impl From<OxmlError> for ShapePropertiesError {
68    fn from(error: OxmlError) -> Self {
69        Self::Xml(error)
70    }
71}
72
73impl From<TransformError> for ShapePropertiesError {
74    fn from(error: TransformError) -> Self {
75        Self::Transform(error)
76    }
77}
78
79impl From<GeometryError> for ShapePropertiesError {
80    fn from(error: GeometryError) -> Self {
81        Self::Geometry(error)
82    }
83}
84
85impl From<FillError> for ShapePropertiesError {
86    fn from(error: FillError) -> Self {
87        Self::Fill(error)
88    }
89}
90
91impl From<LineError> for ShapePropertiesError {
92    fn from(error: LineError) -> Self {
93        Self::Line(error)
94    }
95}
96
97impl From<EffectError> for ShapePropertiesError {
98    fn from(error: EffectError) -> Self {
99        Self::Effect(error)
100    }
101}
102
103pub type Result<T> = std::result::Result<T, ShapePropertiesError>;
104
105/// The modelled children of one DrawingML `a:spPr` element.
106#[allow(non_camel_case_types)]
107#[derive(Clone, Debug, Default)]
108pub struct CT_ShapeProperties {
109    pub transform: Option<CT_Transform2D>,
110    pub custom_geometry: Option<CT_CustomGeometry2D>,
111    pub preset_geometry: Option<CT_PresetGeometry2D>,
112    pub fill: Option<Fill>,
113    pub line: Option<CT_LineProperties>,
114    pub effects: Option<CT_EffectList>,
115    raw_attributes: Vec<(String, String)>,
116    raw_children: OrderedRawChildren,
117    source_namespaces: Vec<(String, String)>,
118    source_xml: Option<Vec<u8>>,
119    source_projection: Vec<u8>,
120}
121
122impl PartialEq for CT_ShapeProperties {
123    fn eq(&self, other: &Self) -> bool {
124        self.transform == other.transform
125            && self.custom_geometry == other.custom_geometry
126            && self.preset_geometry == other.preset_geometry
127            && self.fill == other.fill
128            && self.line == other.line
129            && self.effects == other.effects
130            && self.raw_attributes == other.raw_attributes
131            && self.raw_children == other.raw_children
132    }
133}
134
135impl CT_ShapeProperties {
136    /// Returns staged local properties with absent source-master defaults filled.
137    pub fn with_inherited_defaults(&self, defaults: &Self) -> Result<Self> {
138        let scoped_defaults = defaults.scoped_inheritance_defaults()?;
139        let defaults = &scoped_defaults;
140        let mut result = self.scoped_inheritance_defaults()?;
141        for (name, value) in &defaults.raw_attributes {
142            if name == "xmlns" || name.starts_with("xmlns:") {
143                continue;
144            }
145            let identity = shape_property_attribute_identity(name, &defaults.source_namespaces)?;
146            let mut existing = None;
147            for (key, local) in &self.raw_attributes {
148                if key == "xmlns" || key.starts_with("xmlns:") {
149                    continue;
150                }
151                if shape_property_attribute_identity(key, &self.source_namespaces)? == identity {
152                    existing = Some(local);
153                    break;
154                }
155            }
156            if let Some(local) = existing {
157                if name != "bwMode"
158                    && (local != value
159                        || !shape_property_context_equal(
160                            &self.source_namespaces,
161                            &defaults.source_namespaces,
162                        ))
163                {
164                    return Err(OxmlError::InvalidValue(
165                        "conflicting inherited opaque shape-property attribute".into(),
166                    )
167                    .into());
168                }
169            } else {
170                if name != "bwMode"
171                    && !shape_property_context_equal(
172                        &self.source_namespaces,
173                        &defaults.source_namespaces,
174                    )
175                {
176                    return Err(OxmlError::InvalidValue(
177                        "unrepresentable inherited shape-property namespace authority".into(),
178                    )
179                    .into());
180                }
181                result.raw_attributes.push((name.clone(), value.clone()));
182            }
183        }
184
185        match (&mut result.transform, &defaults.transform) {
186            (Some(local), Some(parent)) => {
187                if local.offset.is_none() {
188                    local.offset = parent.offset;
189                }
190                if local.extent.is_none() {
191                    local.extent = parent.extent;
192                }
193                if local.child_offset.is_none() {
194                    local.child_offset = parent.child_offset;
195                }
196                if local.child_extent.is_none() {
197                    local.child_extent = parent.child_extent;
198                }
199            }
200            (local @ None, Some(parent)) => *local = Some(parent.clone()),
201            _ => {}
202        }
203        if result.custom_geometry.is_none() && result.preset_geometry.is_none() {
204            result.custom_geometry = defaults.custom_geometry.clone();
205            result.preset_geometry = defaults.preset_geometry.clone();
206        }
207        if result.line.is_none() {
208            result.line = defaults.line.clone();
209        }
210        let mut occupied = Vec::new();
211        for boundary in 0..=8 {
212            for raw in self.raw_children.at(boundary) {
213                if let Some(slot) = inherited_shape_property_slot(raw, &self.source_namespaces)? {
214                    occupied.push(slot);
215                }
216            }
217        }
218        if result.fill.is_none() && !occupied.contains(&2) {
219            result.fill = defaults.fill.clone();
220        }
221        if result.effects.is_none() && !occupied.contains(&4) {
222            result.effects = defaults.effects.clone();
223        }
224        if self.transform.is_some() {
225            occupied.push(0);
226        }
227        if self.custom_geometry.is_some() || self.preset_geometry.is_some() {
228            occupied.push(1);
229        }
230        if self.fill.is_some() {
231            occupied.push(2);
232        }
233        if self.line.is_some() {
234            occupied.push(3);
235        }
236        if self.effects.is_some() {
237            occupied.push(4);
238        }
239        for boundary in 0..=8 {
240            for raw in defaults.raw_children.at(boundary) {
241                if let Some(slot) = inherited_shape_property_slot(raw, &defaults.source_namespaces)?
242                {
243                    if occupied.contains(&slot) {
244                        continue;
245                    }
246                    occupied.push(slot);
247                } else if shape_property_context_equal(
248                    &self.source_namespaces,
249                    &defaults.source_namespaces,
250                ) && (0..=8).any(|at| self.raw_children.at(at).any(|local| local == raw))
251                {
252                    continue;
253                }
254                result.raw_children.push(
255                    boundary,
256                    close_inherited_shape_property(raw, &defaults.source_namespaces)?,
257                );
258            }
259        }
260        let xml = result.to_xml()?;
261        let reparsed = Self::from_xml_with_inherited_namespaces(&xml, &self.source_namespaces)?;
262        if reparsed.transform != result.transform
263            || reparsed.fill != result.fill
264            || reparsed.line != result.line
265        {
266            return Err(OxmlError::InvalidValue(
267                "inherited shape properties changed during reparse".into(),
268            )
269            .into());
270        }
271        // The staged serialization is the new physical owner. Retain its fresh
272        // projection so a subsequent inheritance operation does not treat our
273        // own completed merge as an ambiguous edit of the predecessor scope.
274        Ok(reparsed)
275    }
276
277    // Only inheritance transfers typed descendants out of their physical owner.
278    // Ordinary serialization and equality keep their existing model semantics.
279    fn scoped_inheritance_defaults(&self) -> Result<Self> {
280        let projection = self.to_xml()?;
281        let original = self.source_xml.as_deref().unwrap_or(&projection);
282        let unchanged = projection == self.source_projection;
283        if !unchanged && self.source_xml.is_some() {
284            let mut reader = Reader::from_reader(original);
285            let mut depth = 0;
286            loop {
287                match reader.read_event().map_err(OxmlError::from)? {
288                    event @ (Event::Start(_) | Event::Empty(_)) => {
289                        let empty = matches!(&event, Event::Empty(_));
290                        let start = match &event {
291                            Event::Start(start) | Event::Empty(start) => start,
292                            _ => unreachable!(),
293                        };
294                        if depth > 0
295                            && capture_raw_attributes(start)?
296                                .iter()
297                                .any(|(name, _)| name == "xmlns" || name.starts_with("xmlns:"))
298                        {
299                            return Err(OxmlError::InvalidValue(
300                                "edited inherited shape properties have nested namespace authority"
301                                    .into(),
302                            )
303                            .into());
304                        }
305                        if !empty {
306                            depth += 1;
307                        }
308                    }
309                    Event::End(_) => depth -= 1,
310                    Event::Eof => break,
311                    _ => {}
312                }
313            }
314        }
315        let input = if unchanged { original } else { &projection };
316        let closed = close_inherited_typed_shape_groups(input, &self.source_namespaces)?;
317        let represented =
318            Self::from_xml_with_inherited_namespaces(&closed, &self.source_namespaces)?;
319        let mut writer_scope = self.source_namespaces.clone();
320        if !writer_scope.iter().any(|(prefix, _)| prefix == "a") {
321            writer_scope.push((
322                "a".into(),
323                "http://schemas.openxmlformats.org/drawingml/2006/main".into(),
324            ));
325        }
326        let source_metadata = inherited_shape_metadata(original, &self.source_namespaces)?;
327        let represented_metadata = inherited_shape_metadata(&represented.to_xml()?, &writer_scope)?;
328        if let Some((key, _)) = source_metadata
329            .iter()
330            .find(|(key, count)| represented_metadata.get(*key).copied().unwrap_or(0) < **count)
331        {
332            return Err(OxmlError::InvalidValue(format!(
333                "unrepresentable opaque metadata in inherited shape properties at {key}"
334            ))
335            .into());
336        }
337        Ok(represented)
338    }
339
340    /// Parses one complete `a:spPr` element with any namespace prefix.
341    pub fn from_xml(xml: &[u8]) -> Result<Self> {
342        Self::from_xml_with_inherited_namespaces(xml, &[])
343    }
344
345    /// Parses a property fragment in its actual enclosing namespace scope.
346    pub fn from_xml_with_inherited_namespaces(
347        xml: &[u8],
348        inherited: &[(String, String)],
349    ) -> Result<Self> {
350        let mut reader = Reader::from_reader(xml);
351        let mut buffer = Vec::new();
352        loop {
353            match reader
354                .read_event_into(&mut buffer)
355                .map_err(OxmlError::from)?
356            {
357                Event::Start(element) if matches_local_name(element.name().as_ref(), b"spPr") => {
358                    let scope = property_namespace_scope(&element, inherited)?;
359                    property_reject_writer_conflicts(&scope)?;
360                    let mut properties = Self::from_element(&mut reader, &element, &scope)?;
361                    properties.source_xml = Some(xml.to_vec());
362                    properties.source_projection = properties.to_xml()?;
363                    return Ok(properties);
364                }
365                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spPr") => {
366                    property_reject_writer_conflicts(&property_namespace_scope(
367                        &element, inherited,
368                    )?)?;
369                    let mut properties = Self::from_start(&element)?;
370                    properties.source_namespaces = property_namespace_scope(&element, inherited)?;
371                    properties.source_xml = Some(xml.to_vec());
372                    properties.source_projection = properties.to_xml()?;
373                    return Ok(properties);
374                }
375                Event::Start(element) | Event::Empty(element) => {
376                    return Err(ShapePropertiesError::UnexpectedElement(element_name(
377                        &element,
378                    )));
379                }
380                Event::Eof => {
381                    return Err(ShapePropertiesError::Xml(OxmlError::MissingElement(
382                        "DrawingML shape properties".to_owned(),
383                    )));
384                }
385                _ => {}
386            }
387            buffer.clear();
388        }
389    }
390
391    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
392        Ok(Self {
393            raw_attributes: capture_raw_attributes(start)?,
394            ..Self::default()
395        })
396    }
397
398    fn from_element(
399        reader: &mut Reader<&[u8]>,
400        start: &BytesStart<'_>,
401        namespaces: &[(String, String)],
402    ) -> Result<Self> {
403        let mut properties = Self::from_start(start)?;
404        properties.source_namespaces = namespaces.to_vec();
405        let mut boundary = 0;
406        let mut buffer = Vec::new();
407
408        loop {
409            match reader
410                .read_event_into(&mut buffer)
411                .map_err(OxmlError::from)?
412            {
413                Event::Start(element) => {
414                    let name = local_name(element.name().as_ref()).to_vec();
415                    let owned = property_is_drawing(&element, namespaces)?;
416                    let child_scope = property_namespace_scope(&element, namespaces)?;
417                    let raw = capture_element(reader, &element)?;
418                    if owned && properties.parse_modelled(&name, &raw, &child_scope)? {
419                        boundary = boundary.max(modelled_boundary(&name));
420                    } else {
421                        properties.raw_children.push(
422                            if owned {
423                                boundary.max(raw_boundary_before(&name))
424                            } else {
425                                boundary
426                            },
427                            raw,
428                        );
429                        if owned {
430                            boundary = boundary.max(raw_boundary_after(&name));
431                        }
432                    }
433                }
434                Event::Empty(element) => {
435                    let name = local_name(element.name().as_ref()).to_vec();
436                    let owned = property_is_drawing(&element, namespaces)?;
437                    let child_scope = property_namespace_scope(&element, namespaces)?;
438                    let raw = capture_empty_element(&element)?;
439                    if owned && properties.parse_modelled(&name, &raw, &child_scope)? {
440                        boundary = boundary.max(modelled_boundary(&name));
441                    } else {
442                        properties.raw_children.push(
443                            if owned {
444                                boundary.max(raw_boundary_before(&name))
445                            } else {
446                                boundary
447                            },
448                            raw,
449                        );
450                        if owned {
451                            boundary = boundary.max(raw_boundary_after(&name));
452                        }
453                    }
454                }
455                Event::End(element) if matches_local_name(element.name().as_ref(), b"spPr") => {
456                    break;
457                }
458                Event::Eof => return Err(missing_end()),
459                _ => {}
460            }
461            buffer.clear();
462        }
463        Ok(properties)
464    }
465
466    fn parse_modelled(
467        &mut self,
468        name: &[u8],
469        raw: &[u8],
470        namespaces: &[(String, String)],
471    ) -> Result<bool> {
472        if matches!(
473            name,
474            b"xfrm" | b"custGeom" | b"prstGeom" | b"ln" | b"effectLst"
475        ) || is_fill(name)
476        {
477            property_reject_writer_conflicts(namespaces)?;
478        }
479        match name {
480            b"xfrm" if self.transform.is_none() => {
481                self.transform = Some(CT_Transform2D::from_xml(raw)?);
482            }
483            b"custGeom" if self.custom_geometry.is_none() => {
484                self.custom_geometry = Some(CT_CustomGeometry2D::from_xml(raw)?);
485            }
486            b"prstGeom" if self.preset_geometry.is_none() => {
487                self.preset_geometry = Some(CT_PresetGeometry2D::from_xml(raw)?);
488            }
489            name if is_fill(name) && self.fill.is_none() => {
490                self.fill = Some(Fill::from_xml(raw)?);
491            }
492            b"ln" if self.line.is_none() => {
493                self.line = Some(CT_LineProperties::from_xml_with_inherited_namespaces(
494                    raw, namespaces,
495                )?);
496            }
497            b"effectLst" if self.effects.is_none() => {
498                self.effects = Some(CT_EffectList::from_xml(raw)?);
499            }
500            _ => return Ok(false),
501        }
502        Ok(true)
503    }
504
505    /// Writes shape properties with the fixed `a:` prefix and schema order.
506    pub fn to_xml(&self) -> Result<Vec<u8>> {
507        let mut writer = Writer::new(Vec::new());
508        self.write_xml(&mut writer)?;
509        Ok(writer.into_inner())
510    }
511
512    /// Writes shape properties into an existing XML writer.
513    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
514        self.write_xml_as(writer, "a:spPr")
515    }
516
517    /// Writes shape properties under the caller's required root name.
518    pub fn write_xml_as<W: Write>(&self, writer: &mut Writer<W>, name: &str) -> Result<()> {
519        let mut start = BytesStart::new(name);
520        push_raw_attributes(&mut start, &self.raw_attributes);
521        if self.transform.is_none()
522            && self.custom_geometry.is_none()
523            && self.preset_geometry.is_none()
524            && self.fill.is_none()
525            && self.line.is_none()
526            && self.effects.is_none()
527            && self.raw_children.is_empty()
528        {
529            writer
530                .write_event(Event::Empty(start))
531                .map_err(OxmlError::from)?;
532            return Ok(());
533        }
534
535        writer
536            .write_event(Event::Start(start))
537            .map_err(OxmlError::from)?;
538        emit_raw(writer, self.raw_children.at(0))?;
539        if let Some(transform) = &self.transform {
540            transform.write_xml(writer)?;
541        }
542        emit_raw(writer, self.raw_children.at(1))?;
543        if let Some(geometry) = &self.custom_geometry {
544            writer
545                .get_mut()
546                .write_all(&geometry.to_xml()?)
547                .map_err(OxmlError::from)?;
548        } else if let Some(geometry) = &self.preset_geometry {
549            writer
550                .get_mut()
551                .write_all(&geometry.to_xml()?)
552                .map_err(OxmlError::from)?;
553        }
554        emit_raw(writer, self.raw_children.at(2))?;
555        if let Some(fill) = &self.fill {
556            fill.write_xml(writer)?;
557        }
558        emit_raw(writer, self.raw_children.at(3))?;
559        if let Some(line) = &self.line {
560            line.write_xml(writer)?;
561        }
562        emit_raw(writer, self.raw_children.at(4))?;
563        if let Some(effects) = &self.effects {
564            effects.write_xml(writer)?;
565        }
566        for boundary in 5..=8 {
567            emit_raw(writer, self.raw_children.at(boundary))?;
568        }
569        writer
570            .write_event(Event::End(BytesEnd::new(name)))
571            .map_err(OxmlError::from)?;
572        Ok(())
573    }
574
575    pub fn raw_children(&self) -> &OrderedRawChildren {
576        &self.raw_children
577    }
578
579    /// Returns whether an opaque effect DAG is present instead of a typed list.
580    pub fn has_unmodelled_effect(&self) -> bool {
581        (0..=8).any(|boundary| self.raw_children.at(boundary).any(raw_is_effect_dag))
582    }
583
584    /// Reports a placeholder colour inside an opaque effect DAG.
585    pub fn has_unmodelled_effect_placeholder_color(&self) -> bool {
586        (0..=8).any(|boundary| {
587            self.raw_children
588                .at(boundary)
589                .any(|xml| raw_is_effect_dag(xml) && raw_contains_placeholder_color(xml))
590        })
591    }
592}
593
594fn is_fill(name: &[u8]) -> bool {
595    matches!(
596        name,
597        b"noFill" | b"solidFill" | b"gradFill" | b"pattFill" | b"blipFill"
598    )
599}
600
601fn modelled_boundary(name: &[u8]) -> usize {
602    match name {
603        b"xfrm" => 1,
604        b"custGeom" | b"prstGeom" => 2,
605        name if is_fill(name) => 3,
606        b"ln" => 4,
607        b"effectLst" => 5,
608        _ => 0,
609    }
610}
611
612fn raw_boundary_after(name: &[u8]) -> usize {
613    match name {
614        b"xfrm" => 1,
615        b"custGeom" | b"prstGeom" => 2,
616        b"noFill" | b"solidFill" | b"gradFill" | b"blipFill" | b"pattFill" | b"grpFill" => 3,
617        b"ln" => 4,
618        b"effectLst" | b"effectDag" => 5,
619        b"scene3d" => 6,
620        b"sp3d" => 7,
621        b"extLst" => 8,
622        _ => 0,
623    }
624}
625
626/// The earliest boundary an unmodelled child may be written at. A child such
627/// as `a:scene3d` read where no `a:effectLst` stood stays behind the effect
628/// slot, so an effect list added later is written before it.
629fn raw_boundary_before(name: &[u8]) -> usize {
630    raw_boundary_after(name).saturating_sub(1)
631}
632
633fn shape_property_attribute_identity(
634    name: &str,
635    scope: &[(String, String)],
636) -> Result<(String, String)> {
637    if let Some((prefix, local)) = name.split_once(':') {
638        let uri = if prefix == "xml" {
639            "http://www.w3.org/XML/1998/namespace"
640        } else {
641            scope
642                .iter()
643                .rev()
644                .find(|(key, _)| key == prefix)
645                .map(|(_, uri)| uri.as_str())
646                .ok_or_else(|| {
647                    OxmlError::InvalidValue(
648                        "unbound inherited shape-property attribute prefix".into(),
649                    )
650                })?
651        };
652        Ok((uri.to_owned(), local.to_owned()))
653    } else {
654        Ok((String::new(), name.to_owned()))
655    }
656}
657
658fn shape_property_context_equal(left: &[(String, String)], right: &[(String, String)]) -> bool {
659    let mut left: std::collections::BTreeMap<_, _> = left
660        .iter()
661        .map(|(prefix, uri)| (prefix.as_str(), uri.as_str()))
662        .collect();
663    let mut right: std::collections::BTreeMap<_, _> = right
664        .iter()
665        .map(|(prefix, uri)| (prefix.as_str(), uri.as_str()))
666        .collect();
667    left.entry("").or_insert("");
668    right.entry("").or_insert("");
669    left == right
670}
671
672fn inherited_shape_property_slot(
673    raw: &[u8],
674    namespaces: &[(String, String)],
675) -> Result<Option<usize>> {
676    let mut reader = Reader::from_reader(raw);
677    match reader.read_event().map_err(OxmlError::from)? {
678        Event::Start(start) | Event::Empty(start) if property_is_drawing(&start, namespaces)? => {
679            let name = local_name(start.name().as_ref()).to_vec();
680            Ok(match name.as_slice() {
681                b"xfrm" => Some(0),
682                b"custGeom" | b"prstGeom" => Some(1),
683                b"grpFill" => Some(2),
684                name if is_fill(name) => Some(2),
685                b"ln" => Some(3),
686                b"effectLst" | b"effectDag" => Some(4),
687                b"scene3d" => Some(5),
688                b"sp3d" => Some(6),
689                b"extLst" => Some(7),
690                _ => None,
691            })
692        }
693        _ => Ok(None),
694    }
695}
696
697// Admission checks metadata occupancy at the same expanded physical path.
698// Values of modeled attributes can legitimately change through public fields.
699// Namespace declarations are authority, not opaque attribute payload, and were
700// already closed independently before the owning projection was reparsed.
701fn inherited_shape_metadata(
702    xml: &[u8],
703    namespaces: &[(String, String)],
704) -> Result<std::collections::BTreeMap<String, usize>> {
705    let mut reader = Reader::from_reader(xml);
706    let mut scopes = vec![namespaces.to_vec()];
707    let mut paths = vec![String::new()];
708    let mut metadata = std::collections::BTreeMap::new();
709    loop {
710        match reader.read_event().map_err(OxmlError::from)? {
711            event @ (Event::Start(_) | Event::Empty(_)) => {
712                let empty = matches!(&event, Event::Empty(_));
713                let start = match &event {
714                    Event::Start(start) | Event::Empty(start) => start,
715                    _ => unreachable!(),
716                };
717                let scope = property_namespace_scope(start, scopes.last().unwrap())?;
718                let name = start.name();
719                let spelling = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
720                let (prefix, local) = spelling.split_once(':').unwrap_or(("", spelling));
721                let uri = scope
722                    .iter()
723                    .find(|(key, _)| key == prefix)
724                    .map_or("", |(_, uri)| uri.as_str());
725                if !prefix.is_empty() && uri.is_empty() {
726                    return Err(OxmlError::InvalidValue(
727                        "unbound shape-property metadata owner".into(),
728                    )
729                    .into());
730                }
731                let path = if paths.len() == 1 {
732                    String::new()
733                } else {
734                    format!(
735                        "{}/{}:{uri}{}:{local}",
736                        paths.last().unwrap(),
737                        uri.len(),
738                        local.len()
739                    )
740                };
741                for (name, _) in capture_raw_attributes(start)? {
742                    if name == "xmlns" || name.starts_with("xmlns:") {
743                        continue;
744                    }
745                    let (attribute_uri, attribute_local) =
746                        shape_property_attribute_identity(&name, &scope)?;
747                    if uri == "http://schemas.openxmlformats.org/drawingml/2006/main"
748                        && local == "xfrm"
749                        && attribute_uri.is_empty()
750                        && matches!(attribute_local.as_str(), "rot" | "flipH" | "flipV")
751                    {
752                        // The existing transform writer canonicalizes schema-default
753                        // rotation and flip values. They are modeled scalars, not
754                        // unsupported opaque metadata.
755                        continue;
756                    }
757                    let key = format!(
758                        "{path}@{}:{attribute_uri}{}:{attribute_local}",
759                        attribute_uri.len(),
760                        attribute_local.len()
761                    );
762                    *metadata.entry(key).or_insert(0) += 1;
763                }
764                if !empty {
765                    scopes.push(scope);
766                    paths.push(path);
767                }
768            }
769            Event::End(_) => {
770                scopes.pop();
771                paths.pop();
772            }
773            Event::Comment(text) => {
774                let key = format!("{}!comment:{:?}", paths.last().unwrap(), &*text);
775                *metadata.entry(key).or_insert(0) += 1;
776            }
777            Event::PI(text) => {
778                let key = format!("{}!pi:{:?}", paths.last().unwrap(), &*text);
779                *metadata.entry(key).or_insert(0) += 1;
780            }
781            Event::Text(text) if text.iter().any(|byte| !byte.is_ascii_whitespace()) => {
782                let key = format!("{}!text:{:?}", paths.last().unwrap(), &*text);
783                *metadata.entry(key).or_insert(0) += 1;
784            }
785            Event::CData(text) => {
786                let key = format!("{}!cdata:{:?}", paths.last().unwrap(), &*text);
787                *metadata.entry(key).or_insert(0) += 1;
788            }
789            Event::Eof => break,
790            _ => {}
791        }
792    }
793    Ok(metadata)
794}
795
796// Close every physical descendant of a transferred typed property. Lower-level
797// typed writers may omit their root declarations, so opaque descendant scopes
798// must already be self-contained when those writers receive the parsed model.
799fn close_inherited_typed_shape_groups(
800    xml: &[u8],
801    namespaces: &[(String, String)],
802) -> Result<Vec<u8>> {
803    let mut reader = Reader::from_reader(xml);
804    let mut writer = Writer::new(Vec::new());
805    let mut scopes = vec![namespaces.to_vec()];
806    let mut transferring = Vec::new();
807    loop {
808        match reader.read_event().map_err(OxmlError::from)? {
809            event @ (Event::Start(_) | Event::Empty(_)) => {
810                let empty = matches!(&event, Event::Empty(_));
811                let start = match &event {
812                    Event::Start(start) | Event::Empty(start) => start,
813                    _ => unreachable!(),
814                };
815                let scope = property_namespace_scope(start, scopes.last().unwrap())?;
816                let transfer = transferring.last().copied().unwrap_or(false)
817                    || (scopes.len() == 2
818                        && property_is_drawing(start, &scope)?
819                        && modelled_boundary(local_name(start.name().as_ref())) > 0);
820                if transfer {
821                    let mut content = std::str::from_utf8(start.as_ref())
822                        .map_err(OxmlError::from)?
823                        .to_owned();
824                    let attributes = capture_raw_attributes(start)?;
825                    let mut closed_scope = scope.clone();
826                    if !closed_scope.iter().any(|(prefix, _)| prefix.is_empty()) {
827                        closed_scope.push((String::new(), String::new()));
828                    }
829                    for (prefix, uri) in closed_scope {
830                        let name = if prefix.is_empty() {
831                            "xmlns".to_owned()
832                        } else {
833                            format!("xmlns:{prefix}")
834                        };
835                        if !attributes.iter().any(|(key, _)| key == &name) {
836                            content.push_str(&format!(
837                                " {name}=\"{}\"",
838                                quick_xml::escape::escape(&uri)
839                            ));
840                        }
841                    }
842                    let closed = BytesStart::from_content(content, start.name().as_ref().len());
843                    writer
844                        .write_event(if empty {
845                            Event::Empty(closed)
846                        } else {
847                            Event::Start(closed)
848                        })
849                        .map_err(OxmlError::from)?;
850                } else {
851                    writer.write_event(event).map_err(OxmlError::from)?;
852                }
853                if !empty {
854                    scopes.push(scope);
855                    transferring.push(transfer);
856                }
857            }
858            Event::End(end) => {
859                writer
860                    .write_event(Event::End(end))
861                    .map_err(OxmlError::from)?;
862                scopes.pop();
863                transferring.pop();
864            }
865            Event::Eof => break,
866            event => writer.write_event(event).map_err(OxmlError::from)?,
867        }
868    }
869    Ok(writer.into_inner())
870}
871
872fn close_inherited_shape_property(raw: &[u8], namespaces: &[(String, String)]) -> Result<Vec<u8>> {
873    let mut reader = Reader::from_reader(raw);
874    let (start, empty) = match reader.read_event().map_err(OxmlError::from)? {
875        Event::Start(start) => (start, false),
876        Event::Empty(start) => (start, true),
877        _ => return Ok(raw.to_vec()),
878    };
879    let mut content = std::str::from_utf8(start.as_ref())
880        .map_err(OxmlError::from)?
881        .to_owned();
882    let attributes = capture_raw_attributes(&start)?;
883    let mut scope = namespaces.to_vec();
884    if !scope.iter().any(|(prefix, _)| prefix.is_empty()) {
885        scope.push((String::new(), String::new()));
886    }
887    for (prefix, uri) in &scope {
888        let name = if prefix.is_empty() {
889            "xmlns".to_owned()
890        } else {
891            format!("xmlns:{prefix}")
892        };
893        if attributes.iter().any(|(key, _)| key == &name) {
894            continue;
895        }
896        content.push_str(&format!(" {name}=\"{}\"", quick_xml::escape::escape(uri)));
897    }
898    let name_len = start.name().as_ref().len();
899    let mut writer = Writer::new(Vec::new());
900    let closed = BytesStart::from_content(content, name_len);
901    if empty {
902        writer
903            .write_event(Event::Empty(closed))
904            .map_err(OxmlError::from)?;
905    } else {
906        writer
907            .write_event(Event::Start(closed))
908            .map_err(OxmlError::from)?;
909        writer
910            .get_mut()
911            .extend_from_slice(&raw[reader.buffer_position() as usize..]);
912    }
913    Ok(writer.into_inner())
914}
915
916fn emit_raw<'a, W: Write>(
917    writer: &mut Writer<W>,
918    children: impl Iterator<Item = &'a [u8]>,
919) -> Result<()> {
920    for child in children {
921        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
922    }
923    Ok(())
924}
925
926fn capture_raw_attributes(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
927    let mut raw = Vec::new();
928    for attribute in start.attributes() {
929        let attribute = attribute.map_err(OxmlError::from)?;
930        let name = std::str::from_utf8(attribute.key.as_ref())
931            .map_err(OxmlError::from)?
932            .to_owned();
933        let value = attribute
934            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
935            .map_err(OxmlError::from)?
936            .into_owned();
937        raw.push((name, value));
938    }
939    Ok(raw)
940}
941
942fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
943    for (name, value) in attributes {
944        start.push_attribute((name.as_str(), value.as_str()));
945    }
946}
947
948fn element_name(element: &BytesStart<'_>) -> String {
949    String::from_utf8_lossy(element.name().as_ref()).into_owned()
950}
951
952fn missing_end() -> ShapePropertiesError {
953    ShapePropertiesError::Xml(OxmlError::MissingElement(
954        "closing DrawingML shape properties".to_owned(),
955    ))
956}
957
958#[cfg(test)]
959mod tests {
960    use super::CT_ShapeProperties;
961
962    #[test]
963    fn shape_properties_round_trip_in_schema_order() {
964        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>"#;
965        let properties = CT_ShapeProperties::from_xml(xml).unwrap();
966        assert!(properties.transform.is_some());
967        assert!(properties.custom_geometry.is_some());
968        assert!(properties.fill.is_some());
969        assert!(properties.line.is_some());
970        assert!(properties.effects.is_some());
971
972        let written = properties.to_xml().unwrap();
973        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>"#);
974        assert_eq!(CT_ShapeProperties::from_xml(&written).unwrap(), properties);
975    }
976
977    #[test]
978    fn modelled_children_added_later_precede_unmodelled_3d_and_extensions() {
979        let xml = br#"<a:spPr><a:prstGeom prst="rect"/><a:scene3d/><x:after/><a:sp3d/><a:extLst/></a:spPr>"#;
980        let mut properties = CT_ShapeProperties::from_xml(xml).unwrap();
981        assert_eq!(properties.to_xml().unwrap(), xml);
982
983        properties.fill = Some(crate::fill::Fill::from_xml(br#"<a:noFill/>"#).unwrap());
984        properties.line = Some(crate::line::CT_LineProperties::default());
985        properties.effects = Some(crate::effect::CT_EffectList::default());
986        assert_eq!(
987            properties.to_xml().unwrap(),
988            br#"<a:spPr><a:prstGeom prst="rect"/><a:noFill/><a:ln/><a:effectLst/><a:scene3d/><x:after/><a:sp3d/><a:extLst/></a:spPr>"#
989        );
990    }
991
992    #[test]
993    fn shape_property_root_attributes_round_trip_without_loss() {
994        let properties =
995            CT_ShapeProperties::from_xml(br#"<q:spPr bwMode="gray" x:future="keep &amp; stay"/>"#)
996                .unwrap();
997        assert_eq!(
998            properties.to_xml().unwrap(),
999            br#"<a:spPr bwMode="gray" x:future="keep &amp; stay"/>"#
1000        );
1001    }
1002
1003    #[test]
1004    fn preset_geometry_round_trips_with_unknown_children_verbatim() {
1005        let xml = br#"<p:spPr><u:before/><q:prstGeom xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" 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>"#;
1006        for declaration in ["", " xmlns:q=\"urn:foreign\""] {
1007            let raw = format!("<q:prstGeom{declaration} prst=\"trapezoid\"/>");
1008            let source = format!("<p:spPr>{raw}</p:spPr>");
1009            let opaque = CT_ShapeProperties::from_xml(source.as_bytes()).unwrap();
1010            assert!(opaque.preset_geometry.is_none());
1011            assert!(
1012                String::from_utf8(opaque.to_xml().unwrap())
1013                    .unwrap()
1014                    .contains(&raw)
1015            );
1016        }
1017        let properties = CT_ShapeProperties::from_xml(xml).unwrap();
1018        let preset = properties.preset_geometry.as_ref().unwrap();
1019        assert_eq!(preset.preset, "trapezoid");
1020        assert_eq!(preset.adjust_values().len(), 1);
1021
1022        let written = String::from_utf8(properties.to_xml().unwrap()).unwrap();
1023        assert!(written.starts_with("<a:spPr>"));
1024        assert!(written.contains("<a:prstGeom prst=\"trapezoid\">"));
1025        for raw in [
1026            "<u:before/>",
1027            "<u:first id=\"1\"><u:nested/></u:first>",
1028            "<u:inside/>",
1029            "<u:last>keep &amp; stay</u:last>",
1030            "<u:after/>",
1031        ] {
1032            assert!(written.contains(raw), "missing raw subtree {raw}");
1033        }
1034        let before = written.find("<u:before/>").unwrap();
1035        let preset_start = written.find("<a:prstGeom").unwrap();
1036        let first = written.find("<u:first").unwrap();
1037        let adjustments = written.find("<a:avLst").unwrap();
1038        let inside = written.find("<u:inside/>").unwrap();
1039        let guide = written.find("<a:gd ").unwrap();
1040        let last = written.find("<u:last>").unwrap();
1041        let preset_end = written.find("</a:prstGeom>").unwrap();
1042        let after = written.find("<u:after/>").unwrap();
1043        assert!(before < preset_start);
1044        assert!(preset_start < first && first < adjustments);
1045        assert!(adjustments < inside && inside < guide);
1046        assert!(guide < last && last < preset_end && preset_end < after);
1047        assert_eq!(
1048            CT_ShapeProperties::from_xml(written.as_bytes()).unwrap(),
1049            properties
1050        );
1051    }
1052}
1053
1054#[cfg(test)]
1055#[test]
1056fn fx216_local_group_fill_blocks_inherited_fill_choice() {
1057    let group = CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:grpFill/></a:spPr>"#).unwrap();
1058    let solid = CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></a:spPr>"#).unwrap();
1059    let before_group = group.to_xml().unwrap();
1060    let before_solid = solid.to_xml().unwrap();
1061    let result = group.with_inherited_defaults(&solid).unwrap();
1062    assert!(
1063        result.fill.is_none(),
1064        "raw local group fill must remain the only fill choice"
1065    );
1066    let xml = result.to_xml().unwrap();
1067    assert!(String::from_utf8_lossy(&xml).contains("<a:grpFill/>"));
1068    assert!(!String::from_utf8_lossy(&xml).contains("solidFill"));
1069    assert_eq!(
1070        CT_ShapeProperties::from_xml(&xml)
1071            .unwrap()
1072            .to_xml()
1073            .unwrap(),
1074        xml
1075    );
1076    let converse = solid
1077        .with_inherited_defaults(&group)
1078        .unwrap()
1079        .to_xml()
1080        .unwrap();
1081    assert!(!String::from_utf8_lossy(&converse).contains("grpFill"));
1082    assert!(String::from_utf8_lossy(&converse).contains("solidFill"));
1083    let foreign=CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:f="urn:foreign"><f:grpFill keep='&#65;'/></a:spPr>"#).unwrap();
1084    let result = foreign.with_inherited_defaults(&solid).unwrap();
1085    assert!(result.fill.is_some());
1086    assert!(
1087        String::from_utf8_lossy(&result.to_xml().unwrap()).contains("<f:grpFill keep='&#65;'/>")
1088    );
1089    assert_eq!(group.to_xml().unwrap(), before_group);
1090    assert_eq!(solid.to_xml().unwrap(), before_solid);
1091}
1092
1093#[cfg(test)]
1094#[test]
1095fn fx216_opaque_attribute_aliases_reuse_or_refuse_expanded_identity() {
1096    for inherited_value in ["shared", "different"] {
1097        let local=CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer" xmlns:y="urn:producer" y:keep="shared"/>"#).unwrap();
1098        let parent_xml = format!(
1099            r#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer" xmlns:y="urn:producer" x:keep="{inherited_value}"/>"#
1100        );
1101        let defaults = CT_ShapeProperties::from_xml(parent_xml.as_bytes()).unwrap();
1102        let before_local = local.to_xml().unwrap();
1103        let before_defaults = defaults.to_xml().unwrap();
1104        if inherited_value == "different" {
1105            assert!(local.with_inherited_defaults(&defaults).is_err());
1106        } else {
1107            let xml = local
1108                .with_inherited_defaults(&defaults)
1109                .unwrap()
1110                .to_xml()
1111                .unwrap();
1112            let mut reader = quick_xml::NsReader::from_reader(xml.as_slice());
1113            let mut count = 0;
1114            if let Event::Empty(element) = reader.read_event().unwrap() {
1115                for attribute in element.attributes() {
1116                    let attribute = attribute.unwrap();
1117                    let (namespace, name) = reader.resolver().resolve_attribute(attribute.key);
1118                    if name.as_ref() == b"keep"
1119                        && matches!(namespace,quick_xml::name::ResolveResult::Bound(uri) if uri.as_ref()==b"urn:producer")
1120                    {
1121                        count += 1;
1122                    }
1123                }
1124            } else {
1125                panic!("expected unchanged empty property owner");
1126            }
1127            assert_eq!(
1128                count, 1,
1129                "one expanded opaque attribute must not become two alias spellings"
1130            );
1131            assert_eq!(
1132                CT_ShapeProperties::from_xml(&xml)
1133                    .unwrap()
1134                    .to_xml()
1135                    .unwrap(),
1136                xml
1137            );
1138        }
1139        assert_eq!(local.to_xml().unwrap(), before_local);
1140        assert_eq!(defaults.to_xml().unwrap(), before_defaults);
1141    }
1142}
1143
1144#[cfg(test)]
1145#[test]
1146fn fx216_opaque_children_with_equal_bytes_keep_distinct_namespace_authority() {
1147    let local=CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:z="urn:local"><z:payload meaning="z:item"/></a:spPr>"#).unwrap();
1148    let defaults=CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:z="urn:source"><z:payload meaning="z:item"/></a:spPr>"#).unwrap();
1149    let before_local = local.to_xml().unwrap();
1150    let before_defaults = defaults.to_xml().unwrap();
1151    let xml = local
1152        .with_inherited_defaults(&defaults)
1153        .unwrap()
1154        .to_xml()
1155        .unwrap();
1156    let mut reader = quick_xml::NsReader::from_reader(xml.as_slice());
1157    let mut seen = std::collections::HashSet::new();
1158    loop {
1159        let (namespace, event) = reader.read_resolved_event().unwrap();
1160        match event {
1161            Event::Empty(element) if element.local_name().as_ref() == b"payload" => {
1162                if let quick_xml::name::ResolveResult::Bound(namespace) = namespace {
1163                    seen.insert(String::from_utf8_lossy(namespace.as_ref()).into_owned());
1164                }
1165            }
1166            Event::Eof => break,
1167            _ => {}
1168        }
1169    }
1170    assert_eq!(
1171        seen,
1172        std::collections::HashSet::from(["urn:local".to_owned(), "urn:source".to_owned()])
1173    );
1174    assert_eq!(local.to_xml().unwrap(), before_local);
1175    assert_eq!(defaults.to_xml().unwrap(), before_defaults);
1176    let same = local
1177        .with_inherited_defaults(&local)
1178        .unwrap()
1179        .to_xml()
1180        .unwrap();
1181    assert_eq!(same, before_local);
1182    assert_eq!(
1183        CT_ShapeProperties::from_xml(&xml)
1184            .unwrap()
1185            .to_xml()
1186            .unwrap(),
1187        xml
1188    );
1189}
1190
1191#[cfg(test)]
1192#[test]
1193fn fx216_typed_inherited_fill_preserves_source_opaque_namespace() {
1194    let local=CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:z="urn:local"/>"#).unwrap();
1195    let defaults=CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:z="urn:source"><a:solidFill><a:srgbClr val="FF0000"/><z:payload meaning="z:item"/></a:solidFill></a:spPr>"#).unwrap();
1196    let before_local = local.to_xml().unwrap();
1197    let before_defaults = defaults.to_xml().unwrap();
1198    let result = local.with_inherited_defaults(&defaults).unwrap();
1199    let xml = result.to_xml().unwrap();
1200    let mut reader = quick_xml::NsReader::from_reader(xml.as_slice());
1201    let mut seen = false;
1202    loop {
1203        let (namespace, event) = reader.read_resolved_event().unwrap();
1204        match event {
1205            Event::Empty(element) if element.local_name().as_ref() == b"payload" => {
1206                assert!(
1207                    matches!(namespace,quick_xml::name::ResolveResult::Bound(uri) if uri.as_ref()==b"urn:source")
1208                );
1209                seen = true;
1210            }
1211            Event::Eof => break,
1212            _ => {}
1213        }
1214    }
1215    assert!(seen);
1216    assert_eq!(local.to_xml().unwrap(), before_local);
1217    assert_eq!(defaults.to_xml().unwrap(), before_defaults);
1218}
1219
1220#[cfg(test)]
1221#[test]
1222fn fx216_typed_groups_close_actual_ancestor_scopes_and_preserve_edits() {
1223    let local = CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:z="urn:local" xmlns="urn:local"><opaque/></a:spPr>"#).unwrap();
1224    let groups = [
1225        r#"<a:xfrm><a:off x="10" y="20"/><z:payload meaning="z:item"/></a:xfrm>"#,
1226        r#"<a:prstGeom prst="rect"><a:avLst/><z:payload meaning="z:item"/></a:prstGeom>"#,
1227        r#"<a:custGeom><a:avLst/><a:gdLst/><a:ahLst/><a:cxnLst/><a:pathLst><z:payload meaning="z:item"/></a:pathLst></a:custGeom>"#,
1228        r#"<a:solidFill><a:srgbClr val="FF0000"/><z:payload meaning="z:item"/></a:solidFill>"#,
1229        r#"<a:solidFill><a:srgbClr val="FF0000"/><payload meaning="item"/></a:solidFill>"#,
1230        r#"<a:ln w="10" z:keep="z:item"><z:payload meaning="z:item"/></a:ln>"#,
1231        r#"<a:effectLst><z:payload meaning="z:item"/></a:effectLst>"#,
1232        r#"<a:solidFill><a:srgbClr val="FF0000"/><z:payload xmlns:z="urn:source" meaning="z:item"/></a:solidFill>"#,
1233    ];
1234    for (index, group) in groups.iter().enumerate() {
1235        // The namespace authority comes from the physical shape ancestor, not
1236        // from either the property fragment or the destination owner.
1237        let outer = format!(
1238            r#"<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:z="urn:source" xmlns="urn:source"><p:spPr>{group}</p:spPr></p:sp>"#
1239        );
1240        let mut reader = Reader::from_reader(outer.as_bytes());
1241        let Event::Start(owner) = reader.read_event().unwrap() else {
1242            panic!("shape owner")
1243        };
1244        let scope = property_namespace_scope(&owner, &[]).unwrap();
1245        let Event::Start(properties) = reader.read_event().unwrap() else {
1246            panic!("property owner")
1247        };
1248        let raw = capture_element(&mut reader, &properties).unwrap();
1249        let mut defaults =
1250            CT_ShapeProperties::from_xml_with_inherited_namespaces(&raw, &scope).unwrap();
1251        if index == 0 {
1252            defaults
1253                .transform
1254                .as_mut()
1255                .unwrap()
1256                .offset
1257                .as_mut()
1258                .unwrap()
1259                .x = oxml_core::units::Emu(123);
1260        }
1261        if index == 1 {
1262            defaults.preset_geometry.as_mut().unwrap().preset = "ellipse".into();
1263        }
1264        if index == 5 {
1265            defaults.line.as_mut().unwrap().width = Some(456);
1266        }
1267        let before = defaults.to_xml().unwrap();
1268        let result = local.with_inherited_defaults(&defaults).unwrap();
1269        let xml = result.to_xml().unwrap();
1270        let mut scan = quick_xml::NsReader::from_reader(xml.as_slice());
1271        let mut payloads = 0;
1272        let mut locals = 0;
1273        loop {
1274            let (namespace, event) = scan.read_resolved_event().unwrap();
1275            match event {
1276                Event::Empty(start) if start.local_name().as_ref() == b"payload" => {
1277                    assert!(
1278                        matches!(namespace, quick_xml::name::ResolveResult::Bound(uri) if uri.as_ref() == b"urn:source"),
1279                        "{index}: {}",
1280                        String::from_utf8_lossy(&xml)
1281                    );
1282                    payloads += 1;
1283                }
1284                Event::Empty(start) if start.local_name().as_ref() == b"opaque" => {
1285                    assert!(
1286                        matches!(namespace, quick_xml::name::ResolveResult::Bound(uri) if uri.as_ref() == b"urn:local")
1287                    );
1288                    locals += 1;
1289                }
1290                Event::Eof => break,
1291                _ => {}
1292            }
1293        }
1294        assert_eq!(payloads, 1, "group {index}");
1295        assert_eq!(locals, 1);
1296        let reparsed = CT_ShapeProperties::from_xml(&xml).unwrap();
1297        if index == 0 {
1298            assert_eq!(reparsed.transform.unwrap().offset.unwrap().x.0, 123);
1299        }
1300        if index == 1 {
1301            assert_eq!(reparsed.preset_geometry.unwrap().preset, "ellipse");
1302        }
1303        if index == 5 {
1304            assert_eq!(reparsed.line.unwrap().width, Some(456));
1305        }
1306        assert_eq!(defaults.to_xml().unwrap(), before);
1307    }
1308    let mut nested = CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:ln w="10" xmlns:z="urn:nested"><z:payload/></a:ln></a:spPr>"#).unwrap();
1309    nested.line.as_mut().unwrap().width = Some(99);
1310    let before = nested.to_xml().unwrap();
1311    assert!(local.with_inherited_defaults(&nested).is_err());
1312    assert_eq!(nested.to_xml().unwrap(), before);
1313}
1314
1315#[cfg(test)]
1316#[test]
1317fn fx216_unrepresentable_typed_metadata_refuses_local_and_parent_without_mutation() {
1318    let empty = CT_ShapeProperties::from_xml(br#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:z="urn:source"/>"#).unwrap();
1319    for group in [
1320        r#"<a:solidFill z:meaning="z:item"><a:srgbClr val="FF0000"/></a:solidFill>"#,
1321        r#"<a:ln><a:solidFill z:meaning="z:item"><a:srgbClr val="FF0000"/></a:solidFill></a:ln>"#,
1322        r#"<a:prstGeom prst="rect"><a:avLst><a:gd name="adjust" fmla="val 100" z:meaning="z:item"/></a:avLst></a:prstGeom>"#,
1323    ] {
1324        let xml = format!(
1325            r#"<a:spPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:z="urn:source">{group}</a:spPr>"#
1326        );
1327        let properties = CT_ShapeProperties::from_xml(xml.as_bytes()).unwrap();
1328        let before = properties.to_xml().unwrap();
1329        if !String::from_utf8_lossy(&before).contains("z:meaning") {
1330            assert!(
1331                empty.with_inherited_defaults(&properties).is_err(),
1332                "unrepresentable inherited {group}"
1333            );
1334            assert!(
1335                properties.with_inherited_defaults(&empty).is_err(),
1336                "unrepresentable local {group}"
1337            );
1338        } else {
1339            let inherited = empty
1340                .with_inherited_defaults(&properties)
1341                .unwrap()
1342                .to_xml()
1343                .unwrap();
1344            assert!(String::from_utf8_lossy(&inherited).contains("z:meaning"));
1345        }
1346        assert_eq!(properties.to_xml().unwrap(), before);
1347    }
1348}