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#[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#[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 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.raw_children.push(boundary, raw);
172 boundary = boundary.max(raw_boundary_after(&name));
173 }
174 }
175 Event::Empty(element) => {
176 let name = local_name(element.name().as_ref()).to_vec();
177 let raw = capture_empty_element(&element)?;
178 if properties.parse_modelled(&name, &raw)? {
179 boundary = boundary.max(modelled_boundary(&name));
180 } else {
181 properties.raw_children.push(boundary, raw);
182 boundary = boundary.max(raw_boundary_after(&name));
183 }
184 }
185 Event::End(element) if matches_local_name(element.name().as_ref(), b"spPr") => {
186 break;
187 }
188 Event::Eof => return Err(missing_end()),
189 _ => {}
190 }
191 buffer.clear();
192 }
193 Ok(properties)
194 }
195
196 fn parse_modelled(&mut self, name: &[u8], raw: &[u8]) -> Result<bool> {
197 match name {
198 b"xfrm" if self.transform.is_none() => {
199 self.transform = Some(CT_Transform2D::from_xml(raw)?);
200 }
201 b"custGeom" if self.custom_geometry.is_none() => {
202 self.custom_geometry = Some(CT_CustomGeometry2D::from_xml(raw)?);
203 }
204 b"prstGeom" if self.preset_geometry.is_none() => {
205 self.preset_geometry = Some(CT_PresetGeometry2D::from_xml(raw)?);
206 }
207 name if is_fill(name) && self.fill.is_none() => {
208 self.fill = Some(Fill::from_xml(raw)?);
209 }
210 b"ln" if self.line.is_none() => {
211 self.line = Some(CT_LineProperties::from_xml(raw)?);
212 }
213 b"effectLst" if self.effects.is_none() => {
214 self.effects = Some(CT_EffectList::from_xml(raw)?);
215 }
216 _ => return Ok(false),
217 }
218 Ok(true)
219 }
220
221 pub fn to_xml(&self) -> Result<Vec<u8>> {
223 let mut writer = Writer::new(Vec::new());
224 self.write_xml(&mut writer)?;
225 Ok(writer.into_inner())
226 }
227
228 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
230 self.write_xml_as(writer, "a:spPr")
231 }
232
233 pub fn write_xml_as<W: Write>(&self, writer: &mut Writer<W>, name: &str) -> Result<()> {
235 let mut start = BytesStart::new(name);
236 push_raw_attributes(&mut start, &self.raw_attributes);
237 if self.transform.is_none()
238 && self.custom_geometry.is_none()
239 && self.preset_geometry.is_none()
240 && self.fill.is_none()
241 && self.line.is_none()
242 && self.effects.is_none()
243 && self.raw_children.is_empty()
244 {
245 writer
246 .write_event(Event::Empty(start))
247 .map_err(OxmlError::from)?;
248 return Ok(());
249 }
250
251 writer
252 .write_event(Event::Start(start))
253 .map_err(OxmlError::from)?;
254 emit_raw(writer, self.raw_children.at(0))?;
255 if let Some(transform) = &self.transform {
256 transform.write_xml(writer)?;
257 }
258 emit_raw(writer, self.raw_children.at(1))?;
259 if let Some(geometry) = &self.custom_geometry {
260 writer
261 .get_mut()
262 .write_all(&geometry.to_xml()?)
263 .map_err(OxmlError::from)?;
264 } else if let Some(geometry) = &self.preset_geometry {
265 writer
266 .get_mut()
267 .write_all(&geometry.to_xml()?)
268 .map_err(OxmlError::from)?;
269 }
270 emit_raw(writer, self.raw_children.at(2))?;
271 if let Some(fill) = &self.fill {
272 fill.write_xml(writer)?;
273 }
274 emit_raw(writer, self.raw_children.at(3))?;
275 if let Some(line) = &self.line {
276 line.write_xml(writer)?;
277 }
278 emit_raw(writer, self.raw_children.at(4))?;
279 if let Some(effects) = &self.effects {
280 effects.write_xml(writer)?;
281 }
282 for boundary in 5..=8 {
283 emit_raw(writer, self.raw_children.at(boundary))?;
284 }
285 writer
286 .write_event(Event::End(BytesEnd::new(name)))
287 .map_err(OxmlError::from)?;
288 Ok(())
289 }
290
291 pub fn raw_children(&self) -> &OrderedRawChildren {
292 &self.raw_children
293 }
294
295 pub fn has_unmodelled_effect(&self) -> bool {
297 (0..=8).any(|boundary| self.raw_children.at(boundary).any(raw_is_effect_dag))
298 }
299
300 pub fn has_unmodelled_effect_placeholder_color(&self) -> bool {
302 (0..=8).any(|boundary| {
303 self.raw_children
304 .at(boundary)
305 .any(|xml| raw_is_effect_dag(xml) && raw_contains_placeholder_color(xml))
306 })
307 }
308}
309
310fn is_fill(name: &[u8]) -> bool {
311 matches!(
312 name,
313 b"noFill" | b"solidFill" | b"gradFill" | b"pattFill" | b"blipFill"
314 )
315}
316
317fn modelled_boundary(name: &[u8]) -> usize {
318 match name {
319 b"xfrm" => 1,
320 b"custGeom" | b"prstGeom" => 2,
321 name if is_fill(name) => 3,
322 b"ln" => 4,
323 b"effectLst" => 5,
324 _ => 0,
325 }
326}
327
328fn raw_boundary_after(name: &[u8]) -> usize {
329 match name {
330 b"xfrm" => 1,
331 b"custGeom" | b"prstGeom" => 2,
332 b"noFill" | b"solidFill" | b"gradFill" | b"blipFill" | b"pattFill" | b"grpFill" => 3,
333 b"ln" => 4,
334 b"effectLst" | b"effectDag" => 5,
335 b"scene3d" => 6,
336 b"sp3d" => 7,
337 b"extLst" => 8,
338 _ => 0,
339 }
340}
341
342fn emit_raw<'a, W: Write>(
343 writer: &mut Writer<W>,
344 children: impl Iterator<Item = &'a [u8]>,
345) -> Result<()> {
346 for child in children {
347 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
348 }
349 Ok(())
350}
351
352fn capture_raw_attributes(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
353 let mut raw = Vec::new();
354 for attribute in start.attributes() {
355 let attribute = attribute.map_err(OxmlError::from)?;
356 let name = std::str::from_utf8(attribute.key.as_ref())
357 .map_err(OxmlError::from)?
358 .to_owned();
359 let value = attribute
360 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
361 .map_err(OxmlError::from)?
362 .into_owned();
363 raw.push((name, value));
364 }
365 Ok(raw)
366}
367
368fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
369 for (name, value) in attributes {
370 start.push_attribute((name.as_str(), value.as_str()));
371 }
372}
373
374fn element_name(element: &BytesStart<'_>) -> String {
375 String::from_utf8_lossy(element.name().as_ref()).into_owned()
376}
377
378fn missing_end() -> ShapePropertiesError {
379 ShapePropertiesError::Xml(OxmlError::MissingElement(
380 "closing DrawingML shape properties".to_owned(),
381 ))
382}
383
384#[cfg(test)]
385mod tests {
386 use super::CT_ShapeProperties;
387
388 #[test]
389 fn shape_properties_round_trip_in_schema_order() {
390 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>"#;
391 let properties = CT_ShapeProperties::from_xml(xml).unwrap();
392 assert!(properties.transform.is_some());
393 assert!(properties.custom_geometry.is_some());
394 assert!(properties.fill.is_some());
395 assert!(properties.line.is_some());
396 assert!(properties.effects.is_some());
397
398 let written = properties.to_xml().unwrap();
399 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>"#);
400 assert_eq!(CT_ShapeProperties::from_xml(&written).unwrap(), properties);
401 }
402
403 #[test]
404 fn shape_property_root_attributes_round_trip_without_loss() {
405 let properties =
406 CT_ShapeProperties::from_xml(br#"<q:spPr bwMode="gray" x:future="keep & stay"/>"#)
407 .unwrap();
408 assert_eq!(
409 properties.to_xml().unwrap(),
410 br#"<a:spPr bwMode="gray" x:future="keep & stay"/>"#
411 );
412 }
413
414 #[test]
415 fn preset_geometry_round_trips_with_unknown_children_verbatim() {
416 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 & stay</u:last></q:prstGeom><u:after/></p:spPr>"#;
417 let properties = CT_ShapeProperties::from_xml(xml).unwrap();
418 let preset = properties.preset_geometry.as_ref().unwrap();
419 assert_eq!(preset.preset, "trapezoid");
420 assert_eq!(preset.adjust_values().len(), 1);
421
422 let written = String::from_utf8(properties.to_xml().unwrap()).unwrap();
423 assert!(written.starts_with("<a:spPr>"));
424 assert!(written.contains("<a:prstGeom prst=\"trapezoid\">"));
425 for raw in [
426 "<u:before/>",
427 "<u:first id=\"1\"><u:nested/></u:first>",
428 "<u:inside/>",
429 "<u:last>keep & stay</u:last>",
430 "<u:after/>",
431 ] {
432 assert!(written.contains(raw), "missing raw subtree {raw}");
433 }
434 let before = written.find("<u:before/>").unwrap();
435 let preset_start = written.find("<a:prstGeom").unwrap();
436 let first = written.find("<u:first").unwrap();
437 let adjustments = written.find("<a:avLst").unwrap();
438 let inside = written.find("<u:inside/>").unwrap();
439 let guide = written.find("<a:gd ").unwrap();
440 let last = written.find("<u:last>").unwrap();
441 let preset_end = written.find("</a:prstGeom>").unwrap();
442 let after = written.find("<u:after/>").unwrap();
443 assert!(before < preset_start);
444 assert!(preset_start < first && first < adjustments);
445 assert!(adjustments < inside && inside < guide);
446 assert!(guide < last && last < preset_end && preset_end < after);
447 assert_eq!(
448 CT_ShapeProperties::from_xml(written.as_bytes()).unwrap(),
449 properties
450 );
451 }
452}