Skip to main content

sheetkit_xml/
theme.rs

1//! Theme XML schema structures.
2//!
3//! Represents `xl/theme/theme1.xml` in the OOXML package.
4//! Only the color scheme is parsed; other theme elements are preserved as raw XML.
5
6/// Simplified theme representation focusing on the color scheme.
7#[derive(Debug, Clone, Default)]
8pub struct ThemeColors {
9    /// 12 theme color slots: dk1, lt1, dk2, lt2, accent1-6, hlink, folHlink.
10    /// Each stored as ARGB hex string (e.g., "FF000000").
11    pub colors: [String; 12],
12}
13
14impl ThemeColors {
15    /// Standard Excel theme color slot names.
16    pub const SLOT_NAMES: [&str; 12] = [
17        "dk1", "lt1", "dk2", "lt2", "accent1", "accent2", "accent3", "accent4", "accent5",
18        "accent6", "hlink", "folHlink",
19    ];
20
21    /// Get color by theme index (0-11).
22    pub fn get(&self, index: usize) -> Option<&str> {
23        self.colors.get(index).map(|s| s.as_str())
24    }
25}
26
27/// Parse theme colors from theme1.xml raw bytes.
28/// Uses quick-xml Reader API directly since the theme namespace is complex.
29pub fn parse_theme_colors(xml_bytes: &[u8]) -> ThemeColors {
30    use quick_xml::events::Event;
31    use quick_xml::Reader;
32
33    let mut reader = Reader::from_reader(xml_bytes);
34    reader.config_mut().trim_text(true);
35    let mut buf = Vec::new();
36    let mut colors = ThemeColors::default();
37    let mut current_slot: Option<usize> = None;
38    let mut in_color_scheme = false;
39
40    loop {
41        match reader.read_event_into(&mut buf) {
42            Ok(Event::Start(ref e)) => {
43                let local_name = e.local_name();
44                let name = std::str::from_utf8(local_name.as_ref()).unwrap_or("");
45                if name == "clrScheme" {
46                    in_color_scheme = true;
47                }
48                if in_color_scheme {
49                    if let Some(idx) = ThemeColors::SLOT_NAMES.iter().position(|&s| s == name) {
50                        current_slot = Some(idx);
51                    }
52                    if let Some(slot) = current_slot {
53                        extract_color_from_element(e, &mut colors, slot);
54                    }
55                }
56            }
57            Ok(Event::Empty(ref e)) => {
58                if in_color_scheme {
59                    let local_name = e.local_name();
60                    let name = std::str::from_utf8(local_name.as_ref()).unwrap_or("");
61                    if let Some(idx) = ThemeColors::SLOT_NAMES.iter().position(|&s| s == name) {
62                        current_slot = Some(idx);
63                    }
64                    if let Some(slot) = current_slot {
65                        extract_color_from_element(e, &mut colors, slot);
66                    }
67                    if ThemeColors::SLOT_NAMES.contains(&name) {
68                        current_slot = None;
69                    }
70                }
71            }
72            Ok(Event::End(ref e)) => {
73                let local = e.local_name();
74                let name = std::str::from_utf8(local.as_ref()).unwrap_or("");
75                if name == "clrScheme" {
76                    in_color_scheme = false;
77                }
78                if in_color_scheme && ThemeColors::SLOT_NAMES.contains(&name) {
79                    current_slot = None;
80                }
81            }
82            Ok(Event::Eof) => break,
83            Err(_) => break,
84            _ => {}
85        }
86        buf.clear();
87    }
88    colors
89}
90
91fn extract_color_from_element(
92    e: &quick_xml::events::BytesStart<'_>,
93    colors: &mut ThemeColors,
94    slot_idx: usize,
95) {
96    let local_name = e.local_name();
97    let name = std::str::from_utf8(local_name.as_ref()).unwrap_or("");
98    if name == "srgbClr" {
99        for attr in e.attributes().flatten() {
100            if attr.key.as_ref() == b"val" {
101                if let Ok(val) = std::str::from_utf8(&attr.value) {
102                    colors.colors[slot_idx] = format!("FF{}", val);
103                }
104            }
105        }
106    } else if name == "sysClr" {
107        for attr in e.attributes().flatten() {
108            if attr.key.as_ref() == b"lastClr" {
109                if let Ok(val) = std::str::from_utf8(&attr.value) {
110                    colors.colors[slot_idx] = format!("FF{}", val);
111                }
112            }
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_parse_theme_colors() {
123        let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
124<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
125  <a:themeElements>
126    <a:clrScheme name="Office">
127      <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
128      <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
129      <a:dk2><a:srgbClr val="44546A"/></a:dk2>
130      <a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
131      <a:accent1><a:srgbClr val="4472C4"/></a:accent1>
132      <a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
133      <a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
134      <a:accent4><a:srgbClr val="FFC000"/></a:accent4>
135      <a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>
136      <a:accent6><a:srgbClr val="70AD47"/></a:accent6>
137      <a:hlink><a:srgbClr val="0563C1"/></a:hlink>
138      <a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
139    </a:clrScheme>
140  </a:themeElements>
141</a:theme>"#;
142        let colors = parse_theme_colors(xml);
143        assert_eq!(colors.colors[0], "FF000000");
144        assert_eq!(colors.colors[1], "FFFFFFFF");
145        assert_eq!(colors.colors[4], "FF4472C4");
146        assert_eq!(colors.colors[11], "FF954F72");
147    }
148
149    #[test]
150    fn test_empty_theme() {
151        let colors = parse_theme_colors(b"<a:theme></a:theme>");
152        assert_eq!(colors.colors[0], "");
153    }
154
155    #[test]
156    fn test_theme_color_get() {
157        let mut colors = ThemeColors::default();
158        colors.colors[0] = "FF000000".to_string();
159        assert_eq!(colors.get(0), Some("FF000000"));
160        assert_eq!(colors.get(12), None);
161    }
162}