Skip to main content

oxml_drawing/
style_ref.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::{get_attr, local_name};
7use quick_xml::events::{BytesEnd, BytesStart, Event};
8use quick_xml::{Reader, Writer};
9
10use crate::color::{ColorChoice, ColorError};
11use crate::order::OrderedRawChildren;
12
13/// Errors produced while parsing, writing, or classifying style references.
14#[derive(Debug)]
15pub enum StyleReferenceError {
16    Xml(OxmlError),
17    Color(ColorError),
18    UnexpectedElement(String),
19    MissingAttribute {
20        element: String,
21        attribute: String,
22    },
23    InvalidAttribute {
24        element: String,
25        attribute: String,
26        value: String,
27    },
28    NotFillReference,
29}
30
31impl fmt::Display for StyleReferenceError {
32    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Xml(error) => error.fmt(formatter),
35            Self::Color(error) => error.fmt(formatter),
36            Self::UnexpectedElement(element) => {
37                write!(
38                    formatter,
39                    "unexpected DrawingML style-reference element: {element}"
40                )
41            }
42            Self::MissingAttribute { element, attribute } => {
43                write!(formatter, "DrawingML {element} requires @{attribute}")
44            }
45            Self::InvalidAttribute {
46                element,
47                attribute,
48                value,
49            } => write!(
50                formatter,
51                "DrawingML {element} has invalid @{attribute}: {value}"
52            ),
53            Self::NotFillReference => write!(formatter, "style reference is not a fillRef"),
54        }
55    }
56}
57
58impl std::error::Error for StyleReferenceError {
59    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60        match self {
61            Self::Xml(error) => Some(error),
62            Self::Color(error) => Some(error),
63            _ => None,
64        }
65    }
66}
67
68impl From<OxmlError> for StyleReferenceError {
69    fn from(error: OxmlError) -> Self {
70        Self::Xml(error)
71    }
72}
73
74impl From<ColorError> for StyleReferenceError {
75    fn from(error: ColorError) -> Self {
76        Self::Color(error)
77    }
78}
79
80pub type Result<T> = std::result::Result<T, StyleReferenceError>;
81
82/// The normal or background format-scheme list selected by `fillRef@idx`.
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum FillStyleSelection {
85    FillStyle(u32),
86    BackgroundFillStyle(u32),
87}
88
89impl FillStyleSelection {
90    /// Classifies a fill reference without performing theme lookup.
91    pub const fn from_index(index: u32) -> Self {
92        if index > 1000 {
93            Self::BackgroundFillStyle(index - 1000)
94        } else {
95            Self::FillStyle(index)
96        }
97    }
98}
99
100/// The index and colour carried by `lnRef`, `fillRef`, or `effectRef`.
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct StyleMatrixReference {
103    pub index: u32,
104    pub color: Option<ColorChoice>,
105    raw_children: OrderedRawChildren,
106}
107
108impl StyleMatrixReference {
109    pub fn new(index: u32, color: ColorChoice) -> Self {
110        Self {
111            index,
112            color: Some(color),
113            raw_children: OrderedRawChildren::default(),
114        }
115    }
116
117    pub fn raw_children(&self) -> &OrderedRawChildren {
118        &self.raw_children
119    }
120}
121
122/// The theme font collection selected by `fontRef@idx`.
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum FontCollectionIndex {
125    Major,
126    Minor,
127    None,
128}
129
130impl FontCollectionIndex {
131    fn parse(value: &str) -> Option<Self> {
132        match value {
133            "major" => Some(Self::Major),
134            "minor" => Some(Self::Minor),
135            "none" => Some(Self::None),
136            _ => None,
137        }
138    }
139
140    const fn as_str(self) -> &'static str {
141        match self {
142            Self::Major => "major",
143            Self::Minor => "minor",
144            Self::None => "none",
145        }
146    }
147}
148
149/// The font collection and colour carried by `fontRef`.
150#[derive(Clone, Debug, Eq, PartialEq)]
151pub struct FontReference {
152    pub index: FontCollectionIndex,
153    pub color: Option<ColorChoice>,
154    raw_children: OrderedRawChildren,
155}
156
157impl FontReference {
158    pub fn new(index: FontCollectionIndex, color: ColorChoice) -> Self {
159        Self {
160            index,
161            color: Some(color),
162            raw_children: OrderedRawChildren::default(),
163        }
164    }
165
166    pub fn raw_children(&self) -> &OrderedRawChildren {
167        &self.raw_children
168    }
169}
170
171/// One of the four style-reference forms carried by a shape style.
172#[derive(Clone, Debug, Eq, PartialEq)]
173pub enum StyleReference {
174    Line(StyleMatrixReference),
175    Fill(StyleMatrixReference),
176    Effect(StyleMatrixReference),
177    Font(FontReference),
178}
179
180impl StyleReference {
181    /// Parses one complete style-reference element with any namespace prefix.
182    pub fn from_xml(xml: &[u8]) -> Result<Self> {
183        let mut reader = Reader::from_reader(xml);
184        let mut buffer = Vec::new();
185        loop {
186            match reader
187                .read_event_into(&mut buffer)
188                .map_err(OxmlError::from)?
189            {
190                Event::Start(element) => {
191                    let kind = ReferenceKind::parse(element.name().as_ref())?;
192                    return Self::from_element(&mut reader, &element, kind);
193                }
194                Event::Empty(element) => {
195                    let kind = ReferenceKind::parse(element.name().as_ref())?;
196                    let index = kind.parse_index(&element)?;
197                    return kind.build(index, None, OrderedRawChildren::default());
198                }
199                Event::Eof => {
200                    return Err(StyleReferenceError::Xml(OxmlError::MissingElement(
201                        "DrawingML style reference".to_owned(),
202                    )));
203                }
204                _ => {}
205            }
206            buffer.clear();
207        }
208    }
209
210    fn from_element(
211        reader: &mut Reader<&[u8]>,
212        start: &BytesStart<'_>,
213        kind: ReferenceKind,
214    ) -> Result<Self> {
215        let index = kind.parse_index(start)?;
216        let mut color = None;
217        let mut has_color = false;
218        let mut raw_children = OrderedRawChildren::default();
219        let mut boundary = 0;
220        let mut buffer = Vec::new();
221
222        loop {
223            match reader
224                .read_event_into(&mut buffer)
225                .map_err(OxmlError::from)?
226            {
227                Event::Start(element)
228                    if is_modelled_color(element.name().as_ref()) && !has_color =>
229                {
230                    color = Some(ColorChoice::from_xml(reader, &element)?);
231                    has_color = true;
232                    boundary = 1;
233                }
234                Event::Empty(element)
235                    if is_modelled_color(element.name().as_ref()) && !has_color =>
236                {
237                    color = Some(ColorChoice::from_empty_xml(&element)?);
238                    has_color = true;
239                    boundary = 1;
240                }
241                Event::Start(element) => {
242                    let occupies_color = is_any_color(element.name().as_ref()) && !has_color;
243                    raw_children.push(boundary, capture_element(reader, &element)?);
244                    if occupies_color {
245                        has_color = true;
246                        boundary = 1;
247                    }
248                }
249                Event::Empty(element) => {
250                    let occupies_color = is_any_color(element.name().as_ref()) && !has_color;
251                    raw_children.push(boundary, capture_empty_element(&element)?);
252                    if occupies_color {
253                        has_color = true;
254                        boundary = 1;
255                    }
256                }
257                Event::End(element)
258                    if local_name(element.name().as_ref()) == kind.element_name().as_bytes() =>
259                {
260                    break;
261                }
262                Event::Eof => return Err(missing_end(kind.element_name())),
263                _ => {}
264            }
265            buffer.clear();
266        }
267
268        kind.build(index, color, raw_children)
269    }
270
271    /// Returns the checked format-scheme selection for a fill reference.
272    pub fn fill_style_selection(&self) -> Result<FillStyleSelection> {
273        match self {
274            Self::Fill(reference) => Ok(FillStyleSelection::from_index(reference.index)),
275            _ => Err(StyleReferenceError::NotFillReference),
276        }
277    }
278
279    /// Writes the reference with the fixed `a:` prefix.
280    pub fn to_xml(&self) -> Result<Vec<u8>> {
281        let mut writer = Writer::new(Vec::new());
282        self.write_xml(&mut writer)?;
283        Ok(writer.into_inner())
284    }
285
286    /// Writes the reference into an existing XML writer.
287    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
288        match self {
289            Self::Line(reference) => write_matrix_reference(writer, "a:lnRef", reference),
290            Self::Fill(reference) => write_matrix_reference(writer, "a:fillRef", reference),
291            Self::Effect(reference) => write_matrix_reference(writer, "a:effectRef", reference),
292            Self::Font(reference) => write_font_reference(writer, reference),
293        }
294    }
295}
296
297#[derive(Clone, Copy)]
298enum ReferenceKind {
299    Line,
300    Fill,
301    Effect,
302    Font,
303}
304
305impl ReferenceKind {
306    fn parse(name: &[u8]) -> Result<Self> {
307        match local_name(name) {
308            b"lnRef" => Ok(Self::Line),
309            b"fillRef" => Ok(Self::Fill),
310            b"effectRef" => Ok(Self::Effect),
311            b"fontRef" => Ok(Self::Font),
312            _ => Err(StyleReferenceError::UnexpectedElement(
313                String::from_utf8_lossy(name).into_owned(),
314            )),
315        }
316    }
317
318    const fn element_name(self) -> &'static str {
319        match self {
320            Self::Line => "lnRef",
321            Self::Fill => "fillRef",
322            Self::Effect => "effectRef",
323            Self::Font => "fontRef",
324        }
325    }
326
327    fn parse_index(self, start: &BytesStart<'_>) -> Result<ReferenceIndex> {
328        let value =
329            get_attr(start, b"idx").ok_or_else(|| StyleReferenceError::MissingAttribute {
330                element: self.element_name().to_owned(),
331                attribute: "idx".to_owned(),
332            })?;
333        if matches!(self, Self::Font) {
334            return FontCollectionIndex::parse(&value)
335                .map(ReferenceIndex::Font)
336                .ok_or_else(|| invalid_attribute(self.element_name(), "idx", value));
337        }
338        let index = value
339            .parse::<u32>()
340            .map_err(|_| invalid_attribute(self.element_name(), "idx", value.clone()))?;
341        Ok(ReferenceIndex::Matrix(index))
342    }
343
344    fn build(
345        self,
346        index: ReferenceIndex,
347        color: Option<ColorChoice>,
348        raw_children: OrderedRawChildren,
349    ) -> Result<StyleReference> {
350        let reference = match (self, index) {
351            (Self::Line, ReferenceIndex::Matrix(index)) => {
352                StyleReference::Line(StyleMatrixReference {
353                    index,
354                    color,
355                    raw_children,
356                })
357            }
358            (Self::Fill, ReferenceIndex::Matrix(index)) => {
359                StyleReference::Fill(StyleMatrixReference {
360                    index,
361                    color,
362                    raw_children,
363                })
364            }
365            (Self::Effect, ReferenceIndex::Matrix(index)) => {
366                StyleReference::Effect(StyleMatrixReference {
367                    index,
368                    color,
369                    raw_children,
370                })
371            }
372            (Self::Font, ReferenceIndex::Font(index)) => StyleReference::Font(FontReference {
373                index,
374                color,
375                raw_children,
376            }),
377            _ => {
378                return Err(StyleReferenceError::InvalidAttribute {
379                    element: self.element_name().to_owned(),
380                    attribute: "idx".to_owned(),
381                    value: "index kind mismatch".to_owned(),
382                });
383            }
384        };
385        Ok(reference)
386    }
387}
388
389enum ReferenceIndex {
390    Matrix(u32),
391    Font(FontCollectionIndex),
392}
393
394fn write_matrix_reference<W: Write>(
395    writer: &mut Writer<W>,
396    tag: &str,
397    reference: &StyleMatrixReference,
398) -> Result<()> {
399    write_reference(
400        writer,
401        tag,
402        reference.index.to_string(),
403        reference.color.as_ref(),
404        &reference.raw_children,
405    )
406}
407
408fn write_font_reference<W: Write>(writer: &mut Writer<W>, reference: &FontReference) -> Result<()> {
409    write_reference(
410        writer,
411        "a:fontRef",
412        reference.index.as_str().to_owned(),
413        reference.color.as_ref(),
414        &reference.raw_children,
415    )
416}
417
418fn write_reference<W: Write>(
419    writer: &mut Writer<W>,
420    tag: &str,
421    index: String,
422    color: Option<&ColorChoice>,
423    raw_children: &OrderedRawChildren,
424) -> Result<()> {
425    let mut start = BytesStart::new(tag);
426    start.push_attribute(("idx", index.as_str()));
427    if color.is_none() && raw_children.is_empty() {
428        writer
429            .write_event(Event::Empty(start))
430            .map_err(OxmlError::from)?;
431        return Ok(());
432    }
433    writer
434        .write_event(Event::Start(start))
435        .map_err(OxmlError::from)?;
436    emit_raw(writer, raw_children.at(0))?;
437    if let Some(color) = color {
438        color.to_xml(writer)?;
439    }
440    emit_raw(writer, raw_children.at(1))?;
441    writer
442        .write_event(Event::End(BytesEnd::new(tag)))
443        .map_err(OxmlError::from)?;
444    Ok(())
445}
446
447fn is_modelled_color(name: &[u8]) -> bool {
448    matches!(
449        local_name(name),
450        b"srgbClr" | b"schemeClr" | b"sysClr" | b"prstClr"
451    )
452}
453
454fn is_any_color(name: &[u8]) -> bool {
455    matches!(
456        local_name(name),
457        b"scrgbClr" | b"srgbClr" | b"hslClr" | b"sysClr" | b"schemeClr" | b"prstClr"
458    )
459}
460
461fn emit_raw<'a, W: Write>(
462    writer: &mut Writer<W>,
463    children: impl Iterator<Item = &'a [u8]>,
464) -> Result<()> {
465    for child in children {
466        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
467    }
468    Ok(())
469}
470
471fn invalid_attribute(element: &str, attribute: &str, value: String) -> StyleReferenceError {
472    StyleReferenceError::InvalidAttribute {
473        element: element.to_owned(),
474        attribute: attribute.to_owned(),
475        value,
476    }
477}
478
479fn missing_end(element: &str) -> StyleReferenceError {
480    StyleReferenceError::Xml(OxmlError::MissingElement(format!(
481        "closing DrawingML {element}"
482    )))
483}
484
485#[cfg(test)]
486mod tests {
487    use std::panic;
488
489    use super::{FillStyleSelection, StyleReference};
490    use crate::shape_props::CT_ShapeProperties;
491
492    #[test]
493    fn fill_ref_1001_resolves_to_background_fill_style_1() {
494        let reference = StyleReference::from_xml(
495            br#"<q:fillRef idx="1001"><q:schemeClr val="phClr"/></q:fillRef>"#,
496        )
497        .unwrap();
498        assert_eq!(
499            reference.fill_style_selection().unwrap(),
500            FillStyleSelection::BackgroundFillStyle(1)
501        );
502    }
503
504    #[test]
505    fn all_four_style_reference_forms_round_trip() {
506        let cases: &[(&[u8], &[u8])] = &[
507            (
508                br#"<q:lnRef idx="2"><x:before/><q:schemeClr val="accent1"/><x:after/></q:lnRef>"#,
509                br#"<a:lnRef idx="2"><x:before/><a:schemeClr val="accent1"/><x:after/></a:lnRef>"#,
510            ),
511            (
512                br#"<q:fillRef idx="1001"><q:srgbClr val="102030"/></q:fillRef>"#,
513                br#"<a:fillRef idx="1001"><a:srgbClr val="102030"/></a:fillRef>"#,
514            ),
515            (
516                br#"<q:effectRef idx="3"><q:prstClr val="black"/></q:effectRef>"#,
517                br#"<a:effectRef idx="3"><a:prstClr val="black"/></a:effectRef>"#,
518            ),
519            (
520                br#"<q:fontRef idx="minor"><q:sysClr val="windowText" lastClr="000000"/></q:fontRef>"#,
521                br#"<a:fontRef idx="minor"><a:sysClr val="windowText" lastClr="000000"/></a:fontRef>"#,
522            ),
523        ];
524
525        for (xml, expected) in cases {
526            let parsed = StyleReference::from_xml(xml).unwrap();
527            let written = parsed.to_xml().unwrap();
528            assert_eq!(&written, expected);
529            assert_eq!(StyleReference::from_xml(&written).unwrap(), parsed);
530        }
531
532        let raw_color = br#"<q:fillRef idx="4"><q:hslClr hue="0" sat="0" lum="0"><x:kept/></q:hslClr></q:fillRef>"#;
533        assert_eq!(
534            StyleReference::from_xml(raw_color).unwrap().to_xml().unwrap(),
535            br#"<a:fillRef idx="4"><q:hslClr hue="0" sat="0" lum="0"><x:kept/></q:hslClr></a:fillRef>"#
536        );
537    }
538
539    #[test]
540    fn zero_indices_and_colourless_style_references_round_trip() {
541        let cases: &[(&[u8], &[u8])] = &[
542            (br#"<q:lnRef idx="0"/>"#, br#"<a:lnRef idx="0"/>"#),
543            (br#"<q:fillRef idx="0"/>"#, br#"<a:fillRef idx="0"/>"#),
544            (br#"<q:effectRef idx="0"/>"#, br#"<a:effectRef idx="0"/>"#),
545            (
546                br#"<q:fontRef idx="minor"/>"#,
547                br#"<a:fontRef idx="minor"/>"#,
548            ),
549        ];
550
551        for (xml, expected) in cases {
552            let parsed = StyleReference::from_xml(xml).unwrap();
553            let written = parsed.to_xml().unwrap();
554            assert_eq!(&written, expected);
555            assert_eq!(StyleReference::from_xml(&written).unwrap(), parsed);
556        }
557
558        let fill = StyleReference::from_xml(cases[1].0).unwrap();
559        assert_eq!(
560            fill.fill_style_selection().unwrap(),
561            FillStyleSelection::FillStyle(0)
562        );
563    }
564
565    #[test]
566    fn malformed_shape_and_style_references_return_errors_without_panicking() {
567        let style_cases: &[&[u8]] = &[
568            br#"<q:otherRef idx="1"><q:schemeClr val="accent1"/></q:otherRef>"#,
569            br#"<q:lnRef><q:schemeClr val="accent1"/></q:lnRef>"#,
570            br#"<q:fillRef idx="4294967296"><q:schemeClr val="accent1"/></q:fillRef>"#,
571            br#"<q:effectRef idx="wide"><q:schemeClr val="accent1"/></q:effectRef>"#,
572            br#"<q:fontRef idx="body"><q:schemeClr val="accent1"/></q:fontRef>"#,
573            br#"<q:lnRef idx="1"><q:srgbClr val="not-rgb"/></q:lnRef>"#,
574        ];
575        for xml in style_cases {
576            let result = panic::catch_unwind(|| StyleReference::from_xml(xml));
577            assert!(result.is_ok(), "style-reference parser panicked");
578            assert!(result.unwrap().is_err(), "malformed style reference parsed");
579        }
580
581        let shape_cases: &[&[u8]] = &[
582            br#"<q:notSpPr/>"#,
583            br#"<q:spPr><q:xfrm rot="bad"/></q:spPr>"#,
584            br#"<q:spPr><q:custGeom/></q:spPr>"#,
585            br#"<q:spPr><q:ln w="20116801"/></q:spPr>"#,
586            br#"<q:spPr><q:solidFill><q:srgbClr val="not-rgb"/></q:solidFill></q:spPr>"#,
587        ];
588        for xml in shape_cases {
589            let result = panic::catch_unwind(|| CT_ShapeProperties::from_xml(xml));
590            assert!(result.is_ok(), "shape-properties parser panicked");
591            assert!(
592                result.unwrap().is_err(),
593                "malformed shape properties parsed"
594            );
595        }
596    }
597}