Skip to main content

oxml_drawing/
xfrm.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::units::{Angle, Emu};
7use oxml_core::xml::{get_attr, local_name, matches_local_name};
8use quick_xml::events::{BytesEnd, BytesStart, Event};
9use quick_xml::{Reader, Writer};
10
11use crate::order::OrderedRawChildren;
12
13/// Errors produced while parsing, writing, or resolving DrawingML transforms.
14#[derive(Debug)]
15pub enum TransformError {
16    Xml(OxmlError),
17    UnexpectedElement(String),
18    MissingAttribute {
19        element: String,
20        attribute: String,
21    },
22    InvalidAttribute {
23        element: String,
24        attribute: String,
25        value: String,
26    },
27    ZeroChildExtent {
28        axis: &'static str,
29    },
30    NonFiniteMatrix,
31}
32
33impl fmt::Display for TransformError {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::Xml(error) => error.fmt(formatter),
37            Self::UnexpectedElement(element) => {
38                write!(
39                    formatter,
40                    "unexpected DrawingML transform element: {element}"
41                )
42            }
43            Self::MissingAttribute { element, attribute } => {
44                write!(formatter, "DrawingML {element} requires @{attribute}")
45            }
46            Self::InvalidAttribute {
47                element,
48                attribute,
49                value,
50            } => write!(
51                formatter,
52                "DrawingML {element} has invalid @{attribute}: {value}"
53            ),
54            Self::ZeroChildExtent { axis } => {
55                write!(
56                    formatter,
57                    "DrawingML child extent is zero on the {axis} axis"
58                )
59            }
60            Self::NonFiniteMatrix => write!(formatter, "DrawingML transform matrix is not finite"),
61        }
62    }
63}
64
65impl std::error::Error for TransformError {
66    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
67        match self {
68            Self::Xml(error) => Some(error),
69            _ => None,
70        }
71    }
72}
73
74impl From<OxmlError> for TransformError {
75    fn from(error: OxmlError) -> Self {
76        Self::Xml(error)
77    }
78}
79
80pub type Result<T> = std::result::Result<T, TransformError>;
81
82/// A DrawingML two-dimensional coordinate pair.
83#[allow(non_camel_case_types)]
84#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
85pub struct CT_Point2D {
86    pub x: Emu,
87    pub y: Emu,
88}
89
90/// A DrawingML width and height pair.
91#[allow(non_camel_case_types)]
92#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
93pub struct CT_PositiveSize2D {
94    pub cx: Emu,
95    pub cy: Emu,
96}
97
98/// The offset, extent, rotation, and flips carried by `a:xfrm`.
99#[allow(non_camel_case_types)]
100#[derive(Clone, Debug, Default, Eq, PartialEq)]
101pub struct CT_Transform2D {
102    pub offset: Option<CT_Point2D>,
103    pub extent: Option<CT_PositiveSize2D>,
104    pub child_offset: Option<CT_Point2D>,
105    pub child_extent: Option<CT_PositiveSize2D>,
106    pub rotation: Angle,
107    pub flip_horizontal: bool,
108    pub flip_vertical: bool,
109    raw_children: OrderedRawChildren,
110}
111
112impl CT_Transform2D {
113    /// Parses one complete `a:xfrm` element with any namespace prefix.
114    pub fn from_xml(xml: &[u8]) -> Result<Self> {
115        let mut reader = Reader::from_reader(xml);
116        let mut buffer = Vec::new();
117        loop {
118            match reader
119                .read_event_into(&mut buffer)
120                .map_err(OxmlError::from)?
121            {
122                Event::Start(element) if matches_local_name(element.name().as_ref(), b"xfrm") => {
123                    return Self::from_element(&mut reader, &element);
124                }
125                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"xfrm") => {
126                    return Self::from_empty_element(&element);
127                }
128                Event::Start(element) | Event::Empty(element) => {
129                    return Err(TransformError::UnexpectedElement(
130                        String::from_utf8_lossy(element.name().as_ref()).into_owned(),
131                    ));
132                }
133                Event::Eof => {
134                    return Err(TransformError::Xml(OxmlError::MissingElement(
135                        "a:xfrm".to_owned(),
136                    )));
137                }
138                _ => {}
139            }
140            buffer.clear();
141        }
142    }
143
144    /// Parses an `a:xfrm` after the caller has consumed its start event.
145    pub fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
146        let mut transform = Self::from_empty_element(start)?;
147        transform.read_children(reader)?;
148        Ok(transform)
149    }
150
151    /// Parses a self-closing `a:xfrm` element.
152    pub fn from_empty_element(start: &BytesStart<'_>) -> Result<Self> {
153        if !matches_local_name(start.name().as_ref(), b"xfrm") {
154            return Err(TransformError::UnexpectedElement(
155                String::from_utf8_lossy(start.name().as_ref()).into_owned(),
156            ));
157        }
158        Ok(Self {
159            rotation: optional_i32(start, b"rot")?.map_or(Angle::default(), Angle),
160            flip_horizontal: optional_bool(start, b"flipH")?.unwrap_or(false),
161            flip_vertical: optional_bool(start, b"flipV")?.unwrap_or(false),
162            ..Self::default()
163        })
164    }
165
166    /// Writes this transform with the canonical `a:` prefix and schema order.
167    pub fn to_xml(&self) -> Result<Vec<u8>> {
168        let mut writer = Writer::new(Vec::new());
169        self.write_xml(&mut writer)?;
170        Ok(writer.into_inner())
171    }
172
173    /// Writes this transform into an existing XML writer.
174    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
175        self.write_xml_with_root(writer, "a:xfrm")
176    }
177
178    /// Writes this transform with the requested qualified root name.
179    ///
180    /// DrawingML owns the transform type, while host formats own the element
181    /// that carries it. PresentationML graphic frames therefore use
182    /// `p:xfrm`, while DrawingML shapes use `a:xfrm`.
183    pub fn write_xml_with_root<W: Write>(
184        &self,
185        writer: &mut Writer<W>,
186        root_name: &str,
187    ) -> Result<()> {
188        let mut start = BytesStart::new(root_name);
189        let rotation = (self.rotation.0 != 0).then(|| self.rotation.0.to_string());
190        if let Some(rotation) = rotation.as_deref() {
191            start.push_attribute(("rot", rotation));
192        }
193        if self.flip_horizontal {
194            start.push_attribute(("flipH", "1"));
195        }
196        if self.flip_vertical {
197            start.push_attribute(("flipV", "1"));
198        }
199
200        if self.offset.is_none()
201            && self.extent.is_none()
202            && self.child_offset.is_none()
203            && self.child_extent.is_none()
204            && self.raw_children.is_empty()
205        {
206            writer
207                .write_event(Event::Empty(start))
208                .map_err(OxmlError::from)?;
209            return Ok(());
210        }
211
212        writer
213            .write_event(Event::Start(start))
214            .map_err(OxmlError::from)?;
215        emit_raw(writer, self.raw_children.at(0))?;
216        if let Some(offset) = self.offset {
217            write_point(writer, "a:off", offset)?;
218        }
219        emit_raw(writer, self.raw_children.at(1))?;
220        if let Some(extent) = self.extent {
221            write_size(writer, "a:ext", extent)?;
222        }
223        emit_raw(writer, self.raw_children.at(2))?;
224        if let Some(offset) = self.child_offset {
225            write_point(writer, "a:chOff", offset)?;
226        }
227        emit_raw(writer, self.raw_children.at(3))?;
228        if let Some(extent) = self.child_extent {
229            write_size(writer, "a:chExt", extent)?;
230        }
231        emit_raw(writer, self.raw_children.at(4))?;
232        writer
233            .write_event(Event::End(BytesEnd::new(root_name)))
234            .map_err(OxmlError::from)?;
235        Ok(())
236    }
237
238    /// Returns the affine coefficients in PDF matrix order `a, b, c, d, e, f`.
239    pub fn matrix(&self) -> Result<[f64; 6]> {
240        let offset = self.offset.unwrap_or_default();
241        let extent = self.extent.unwrap_or_default();
242        let child_offset = self.child_offset.unwrap_or_default();
243        let scale = match self.child_extent {
244            Some(child_extent) => {
245                if child_extent.cx.0 == 0 {
246                    return Err(TransformError::ZeroChildExtent { axis: "x" });
247                }
248                if child_extent.cy.0 == 0 {
249                    return Err(TransformError::ZeroChildExtent { axis: "y" });
250                }
251                [
252                    extent.cx.0 as f64 / child_extent.cx.0 as f64,
253                    extent.cy.0 as f64 / child_extent.cy.0 as f64,
254                ]
255            }
256            None => [1.0, 1.0],
257        };
258
259        let centre_x = offset.x.0 as f64 + extent.cx.0 as f64 / 2.0;
260        let centre_y = offset.y.0 as f64 + extent.cy.0 as f64 / 2.0;
261        let mut matrix = affine_identity();
262        matrix = affine_then(
263            matrix,
264            affine_translation(-(child_offset.x.0 as f64), -(child_offset.y.0 as f64)),
265        );
266        matrix = affine_then(matrix, affine_scale(scale[0], scale[1]));
267        matrix = affine_then(
268            matrix,
269            affine_translation(offset.x.0 as f64, offset.y.0 as f64),
270        );
271        matrix = affine_then(
272            matrix,
273            affine_rotation_about(self.rotation.to_degrees(), centre_x, centre_y),
274        );
275        matrix = affine_then(
276            matrix,
277            affine_scale_about(
278                if self.flip_horizontal { -1.0 } else { 1.0 },
279                if self.flip_vertical { -1.0 } else { 1.0 },
280                centre_x,
281                centre_y,
282            ),
283        );
284
285        if matrix.into_iter().all(f64::is_finite) {
286            Ok(matrix)
287        } else {
288            Err(TransformError::NonFiniteMatrix)
289        }
290    }
291
292    /// Returns raw, not-yet-modelled children grouped by schema boundary.
293    pub fn raw_children(&self) -> &OrderedRawChildren {
294        &self.raw_children
295    }
296
297    fn read_children(&mut self, reader: &mut Reader<&[u8]>) -> Result<()> {
298        let mut boundary = 0;
299        let mut buffer = Vec::new();
300        loop {
301            match reader
302                .read_event_into(&mut buffer)
303                .map_err(OxmlError::from)?
304            {
305                Event::Start(element) => {
306                    let slot = schema_slot(element.name().as_ref());
307                    let raw = capture_element(reader, &element)?;
308                    if let Some(slot) = slot.filter(|_| is_explicit_empty_element(&raw)) {
309                        if self.set_modelled(slot, &element)? {
310                            boundary = boundary.max(slot);
311                        } else {
312                            self.raw_children.push(boundary, raw);
313                        }
314                    } else {
315                        self.raw_children.push(boundary, raw);
316                    }
317                }
318                Event::Empty(element) => {
319                    if let Some(slot) = schema_slot(element.name().as_ref()) {
320                        if self.set_modelled(slot, &element)? {
321                            boundary = boundary.max(slot);
322                        } else {
323                            self.raw_children
324                                .push(boundary, capture_empty_element(&element)?);
325                        }
326                    } else {
327                        self.raw_children
328                            .push(boundary, capture_empty_element(&element)?);
329                    }
330                }
331                Event::End(element) if matches_local_name(element.name().as_ref(), b"xfrm") => {
332                    break;
333                }
334                Event::Eof => {
335                    return Err(TransformError::Xml(OxmlError::MissingElement(
336                        "closing a:xfrm".to_owned(),
337                    )));
338                }
339                _ => {}
340            }
341            buffer.clear();
342        }
343        Ok(())
344    }
345
346    fn set_modelled(&mut self, slot: usize, element: &BytesStart<'_>) -> Result<bool> {
347        match slot {
348            1 if self.offset.is_none() => self.offset = Some(parse_point(element)?),
349            2 if self.extent.is_none() => self.extent = Some(parse_size(element)?),
350            3 if self.child_offset.is_none() => self.child_offset = Some(parse_point(element)?),
351            4 if self.child_extent.is_none() => self.child_extent = Some(parse_size(element)?),
352            1..=4 => return Ok(false),
353            _ => unreachable!("schema slots are limited to one through four"),
354        }
355        Ok(true)
356    }
357}
358
359fn schema_slot(name: &[u8]) -> Option<usize> {
360    match local_name(name) {
361        b"off" => Some(1),
362        b"ext" => Some(2),
363        b"chOff" => Some(3),
364        b"chExt" => Some(4),
365        _ => None,
366    }
367}
368
369fn parse_point(element: &BytesStart<'_>) -> Result<CT_Point2D> {
370    Ok(CT_Point2D {
371        x: Emu(required_i64(element, b"x")?),
372        y: Emu(required_i64(element, b"y")?),
373    })
374}
375
376fn parse_size(element: &BytesStart<'_>) -> Result<CT_PositiveSize2D> {
377    let cx = required_i64(element, b"cx")?;
378    let cy = required_i64(element, b"cy")?;
379    if cx < 0 {
380        return Err(invalid_attribute(element, b"cx", cx.to_string()));
381    }
382    if cy < 0 {
383        return Err(invalid_attribute(element, b"cy", cy.to_string()));
384    }
385    Ok(CT_PositiveSize2D {
386        cx: Emu(cx),
387        cy: Emu(cy),
388    })
389}
390
391fn required_i64(element: &BytesStart<'_>, attribute: &[u8]) -> Result<i64> {
392    let value = get_attr(element, attribute).ok_or_else(|| TransformError::MissingAttribute {
393        element: element_local_name(element),
394        attribute: String::from_utf8_lossy(attribute).into_owned(),
395    })?;
396    value
397        .parse()
398        .map_err(|_| invalid_attribute(element, attribute, value))
399}
400
401fn optional_i32(element: &BytesStart<'_>, attribute: &[u8]) -> Result<Option<i32>> {
402    get_attr(element, attribute)
403        .map(|value| {
404            value
405                .parse()
406                .map_err(|_| invalid_attribute(element, attribute, value))
407        })
408        .transpose()
409}
410
411fn optional_bool(element: &BytesStart<'_>, attribute: &[u8]) -> Result<Option<bool>> {
412    get_attr(element, attribute)
413        .map(|value| match value.as_str() {
414            "1" | "true" => Ok(true),
415            "0" | "false" => Ok(false),
416            _ => Err(invalid_attribute(element, attribute, value)),
417        })
418        .transpose()
419}
420
421fn invalid_attribute(element: &BytesStart<'_>, attribute: &[u8], value: String) -> TransformError {
422    TransformError::InvalidAttribute {
423        element: element_local_name(element),
424        attribute: String::from_utf8_lossy(attribute).into_owned(),
425        value,
426    }
427}
428
429fn element_local_name(element: &BytesStart<'_>) -> String {
430    String::from_utf8_lossy(local_name(element.name().as_ref())).into_owned()
431}
432
433fn write_point<W: Write>(writer: &mut Writer<W>, tag: &str, point: CT_Point2D) -> Result<()> {
434    let x = point.x.0.to_string();
435    let y = point.y.0.to_string();
436    let mut element = BytesStart::new(tag);
437    element.push_attribute(("x", x.as_str()));
438    element.push_attribute(("y", y.as_str()));
439    writer
440        .write_event(Event::Empty(element))
441        .map_err(OxmlError::from)?;
442    Ok(())
443}
444
445fn write_size<W: Write>(writer: &mut Writer<W>, tag: &str, size: CT_PositiveSize2D) -> Result<()> {
446    let cx = size.cx.0.to_string();
447    let cy = size.cy.0.to_string();
448    let mut element = BytesStart::new(tag);
449    element.push_attribute(("cx", cx.as_str()));
450    element.push_attribute(("cy", cy.as_str()));
451    writer
452        .write_event(Event::Empty(element))
453        .map_err(OxmlError::from)?;
454    Ok(())
455}
456
457fn emit_raw<'a, W: Write>(
458    writer: &mut Writer<W>,
459    children: impl Iterator<Item = &'a [u8]>,
460) -> Result<()> {
461    for child in children {
462        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
463    }
464    Ok(())
465}
466
467fn is_explicit_empty_element(xml: &[u8]) -> bool {
468    let mut reader = Reader::from_reader(xml);
469    let mut buffer = Vec::new();
470    if !matches!(reader.read_event_into(&mut buffer), Ok(Event::Start(_))) {
471        return false;
472    }
473    loop {
474        buffer.clear();
475        match reader.read_event_into(&mut buffer) {
476            Ok(Event::Text(text)) if is_xml_whitespace(text.as_ref()) => {}
477            Ok(Event::CData(text)) if is_xml_whitespace(text.as_ref()) => {}
478            Ok(Event::Comment(_) | Event::PI(_)) => {}
479            Ok(Event::End(_)) => {
480                buffer.clear();
481                return matches!(reader.read_event_into(&mut buffer), Ok(Event::Eof));
482            }
483            _ => return false,
484        }
485    }
486}
487
488fn is_xml_whitespace(bytes: &[u8]) -> bool {
489    bytes
490        .iter()
491        .all(|byte| matches!(byte, b' ' | b'\t' | b'\n' | b'\r'))
492}
493
494const fn affine_identity() -> [f64; 6] {
495    [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
496}
497
498const fn affine_translation(x: f64, y: f64) -> [f64; 6] {
499    [1.0, 0.0, 0.0, 1.0, x, y]
500}
501
502const fn affine_scale(x: f64, y: f64) -> [f64; 6] {
503    [x, 0.0, 0.0, y, 0.0, 0.0]
504}
505
506fn affine_rotation_about(degrees: f64, centre_x: f64, centre_y: f64) -> [f64; 6] {
507    let (sin, cos) = degrees.to_radians().sin_cos();
508    [
509        cos,
510        sin,
511        -sin,
512        cos,
513        centre_x - cos * centre_x + sin * centre_y,
514        centre_y - sin * centre_x - cos * centre_y,
515    ]
516}
517
518const fn affine_scale_about(x: f64, y: f64, centre_x: f64, centre_y: f64) -> [f64; 6] {
519    [x, 0.0, 0.0, y, centre_x * (1.0 - x), centre_y * (1.0 - y)]
520}
521
522const fn affine_then(first: [f64; 6], next: [f64; 6]) -> [f64; 6] {
523    [
524        next[0] * first[0] + next[2] * first[1],
525        next[1] * first[0] + next[3] * first[1],
526        next[0] * first[2] + next[2] * first[3],
527        next[1] * first[2] + next[3] * first[3],
528        next[0] * first[4] + next[2] * first[5] + next[4],
529        next[1] * first[4] + next[3] * first[5] + next[5],
530    ]
531}
532
533#[cfg(test)]
534mod tests {
535    use super::{CT_Transform2D, TransformError};
536
537    const EPSILON: f64 = 1.0e-10;
538
539    #[test]
540    fn nested_group_transform_composes_to_the_hand_computed_matrix() {
541        let transform = CT_Transform2D::from_xml(
542            br#"<a:xfrm rot="5400000" flipH="1" flipV="1"><a:off x="100" y="200"/><a:ext cx="400" cy="200"/><a:chOff x="10" y="20"/><a:chExt cx="200" cy="100"/></a:xfrm>"#,
543        )
544        .unwrap();
545
546        let actual = transform.matrix().unwrap();
547        let expected = [0.0, -2.0, 2.0, 0.0, 160.0, 520.0];
548        for (actual, expected) in actual.into_iter().zip(expected) {
549            assert!((actual - expected).abs() < EPSILON);
550        }
551    }
552
553    #[test]
554    fn transform_reads_any_prefix_and_writes_fixed_a_prefix_in_schema_order() {
555        let transform = CT_Transform2D::from_xml(
556            br#"<p:xfrm rot="-2700000" flipH="true" flipV="1"><p:chExt cx="70" cy="80"/><p:off x="-10" y="20"/><p:chOff x="50" y="60"/><p:ext cx="30" cy="40"/></p:xfrm>"#,
557        )
558        .unwrap();
559
560        assert_eq!(
561            transform.to_xml().unwrap(),
562            br#"<a:xfrm rot="-2700000" flipH="1" flipV="1"><a:off x="-10" y="20"/><a:ext cx="30" cy="40"/><a:chOff x="50" y="60"/><a:chExt cx="70" cy="80"/></a:xfrm>"#
563        );
564    }
565
566    #[test]
567    fn transform_writer_uses_the_requested_root_name() {
568        let transform = CT_Transform2D::default();
569        let mut writer = quick_xml::Writer::new(Vec::new());
570        transform
571            .write_xml_with_root(&mut writer, "p:xfrm")
572            .unwrap();
573        assert_eq!(writer.into_inner(), br#"<p:xfrm/>"#);
574    }
575
576    #[test]
577    fn unknown_transform_children_round_trip_at_their_original_boundaries() {
578        let transform = CT_Transform2D::from_xml(
579            br#"<z:xfrm><x:before x:id="1"/><z:off x="1" y="2"/><x:middle>one &amp; two</x:middle><z:ext cx="3" cy="4"/><x:after><!--kept--></x:after></z:xfrm>"#,
580        )
581        .unwrap();
582
583        assert_eq!(
584            transform.to_xml().unwrap(),
585            br#"<a:xfrm><x:before x:id="1"/><a:off x="1" y="2"/><x:middle>one &amp; two</x:middle><a:ext cx="3" cy="4"/><x:after><!--kept--></x:after></a:xfrm>"#
586        );
587    }
588
589    #[test]
590    fn zero_child_extent_returns_a_transform_error_instead_of_non_finite_coefficients() {
591        let transform = CT_Transform2D::from_xml(
592            br#"<a:xfrm><a:off x="1" y="2"/><a:ext cx="3" cy="4"/><a:chExt cx="0" cy="5"/></a:xfrm>"#,
593        )
594        .unwrap();
595
596        assert!(matches!(
597            transform.matrix(),
598            Err(TransformError::ZeroChildExtent { axis: "x" })
599        ));
600    }
601}