1#[derive(Debug, Clone, Default)]
8pub struct ThemeColors {
9 pub colors: [String; 12],
12}
13
14impl ThemeColors {
15 pub const SLOT_NAMES: [&str; 12] = [
17 "dk1", "lt1", "dk2", "lt2", "accent1", "accent2", "accent3", "accent4", "accent5",
18 "accent6", "hlink", "folHlink",
19 ];
20
21 pub fn get(&self, index: usize) -> Option<&str> {
23 self.colors.get(index).map(|s| s.as_str())
24 }
25}
26
27pub 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}