Skip to main content

ppt_rs/parts/
theme.rs

1//! Theme part
2//!
3//! Represents a theme (ppt/theme/themeN.xml).
4
5use super::base::{ContentType, Part, PartType};
6use crate::exc::PptxError;
7
8/// Theme color
9#[derive(Debug, Clone)]
10pub struct ThemeColor {
11    pub name: String,
12    pub value: String, // RGB hex value
13}
14
15impl ThemeColor {
16    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
17        ThemeColor {
18            name: name.into(),
19            value: value.into(),
20        }
21    }
22}
23
24/// Theme font
25#[derive(Debug, Clone)]
26pub struct ThemeFont {
27    pub typeface: String,
28    pub panose: Option<String>,
29}
30
31impl ThemeFont {
32    pub fn new(typeface: impl Into<String>) -> Self {
33        ThemeFont {
34            typeface: typeface.into(),
35            panose: None,
36        }
37    }
38}
39
40/// Theme part (ppt/theme/themeN.xml)
41#[derive(Debug, Clone)]
42pub struct ThemePart {
43    path: String,
44    theme_number: usize,
45    name: String,
46    major_font: ThemeFont,
47    minor_font: ThemeFont,
48    colors: Vec<ThemeColor>,
49    xml_content: Option<String>,
50}
51
52impl ThemePart {
53    /// Create a new theme part with default Office theme
54    pub fn new(theme_number: usize) -> Self {
55        ThemePart {
56            path: format!("ppt/theme/theme{}.xml", theme_number),
57            theme_number,
58            name: "Office Theme".to_string(),
59            major_font: ThemeFont::new("Calibri Light"),
60            minor_font: ThemeFont::new("Calibri"),
61            colors: Self::default_colors(),
62            xml_content: None,
63        }
64    }
65
66    fn default_colors() -> Vec<ThemeColor> {
67        vec![
68            ThemeColor::new("dk1", "000000"),
69            ThemeColor::new("lt1", "FFFFFF"),
70            ThemeColor::new("dk2", "44546A"),
71            ThemeColor::new("lt2", "E7E6E6"),
72            ThemeColor::new("accent1", "4472C4"),
73            ThemeColor::new("accent2", "ED7D31"),
74            ThemeColor::new("accent3", "A5A5A5"),
75            ThemeColor::new("accent4", "FFC000"),
76            ThemeColor::new("accent5", "5B9BD5"),
77            ThemeColor::new("accent6", "70AD47"),
78            ThemeColor::new("hlink", "0563C1"),
79            ThemeColor::new("folHlink", "954F72"),
80        ]
81    }
82
83    /// Get theme number
84    pub fn theme_number(&self) -> usize {
85        self.theme_number
86    }
87
88    /// Get theme name
89    pub fn name(&self) -> &str {
90        &self.name
91    }
92
93    /// Set theme name
94    pub fn set_name(&mut self, name: impl Into<String>) {
95        self.name = name.into();
96    }
97
98    /// Set major font (headings)
99    pub fn set_major_font(&mut self, typeface: impl Into<String>) {
100        self.major_font = ThemeFont::new(typeface);
101    }
102
103    /// Set minor font (body)
104    pub fn set_minor_font(&mut self, typeface: impl Into<String>) {
105        self.minor_font = ThemeFont::new(typeface);
106    }
107
108    /// Set a theme color
109    pub fn set_color(&mut self, name: impl Into<String>, value: impl Into<String>) {
110        let name = name.into();
111        if let Some(color) = self.colors.iter_mut().find(|c| c.name == name) {
112            color.value = value.into();
113        } else {
114            self.colors.push(ThemeColor::new(name, value));
115        }
116    }
117
118    /// Get relative path for relationships
119    pub fn rel_target(&self) -> String {
120        format!("../theme/theme{}.xml", self.theme_number)
121    }
122
123    fn generate_xml(&self) -> String {
124        let colors_xml: String = self
125            .colors
126            .iter()
127            .map(|c| {
128                format!(
129                    r#"<a:{} val="{}"><a:srgbClr val="{}"/></a:{}>"#,
130                    c.name, c.name, c.value, c.name
131                )
132            })
133            .collect::<Vec<_>>()
134            .join("\n        ");
135
136        format!(
137            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
138<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="{}">
139  <a:themeElements>
140    <a:clrScheme name="Office">
141      {}
142    </a:clrScheme>
143    <a:fontScheme name="Office">
144      <a:majorFont>
145        <a:latin typeface="{}"/>
146        <a:ea typeface=""/>
147        <a:cs typeface=""/>
148      </a:majorFont>
149      <a:minorFont>
150        <a:latin typeface="{}"/>
151        <a:ea typeface=""/>
152        <a:cs typeface=""/>
153      </a:minorFont>
154    </a:fontScheme>
155    <a:fmtScheme name="Office">
156      <a:fillStyleLst>
157        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
158        <a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="1"/></a:gradFill>
159        <a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:shade val="51000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="80000"><a:schemeClr val="phClr"><a:shade val="93000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="94000"/><a:satMod val="135000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="0"/></a:gradFill>
160      </a:fillStyleLst>
161      <a:lnStyleLst>
162        <a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
163        <a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
164        <a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
165      </a:lnStyleLst>
166      <a:effectStyleLst>
167        <a:effectStyle><a:effectLst/></a:effectStyle>
168        <a:effectStyle><a:effectLst/></a:effectStyle>
169        <a:effectStyle><a:effectLst><a:outerShdw blurRad="57150" dist="19050" dir="5400000" algn="ctr" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="63000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle>
170      </a:effectStyleLst>
171      <a:bgFillStyleLst>
172        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
173        <a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill>
174        <a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill>
175      </a:bgFillStyleLst>
176    </a:fmtScheme>
177  </a:themeElements>
178  <a:objectDefaults/>
179  <a:extraClrSchemeLst/>
180</a:theme>"#,
181            self.name, colors_xml, self.major_font.typeface, self.minor_font.typeface
182        )
183    }
184}
185
186impl Part for ThemePart {
187    fn path(&self) -> &str {
188        &self.path
189    }
190
191    fn part_type(&self) -> PartType {
192        PartType::Theme
193    }
194
195    fn content_type(&self) -> ContentType {
196        ContentType::Theme
197    }
198
199    fn to_xml(&self) -> Result<String, PptxError> {
200        if let Some(ref xml) = self.xml_content {
201            return Ok(xml.clone());
202        }
203        Ok(self.generate_xml())
204    }
205
206    fn from_xml(xml: &str) -> Result<Self, PptxError> {
207        Ok(ThemePart {
208            path: "ppt/theme/theme1.xml".to_string(),
209            theme_number: 1,
210            name: "Office Theme".to_string(),
211            major_font: ThemeFont::new("Calibri Light"),
212            minor_font: ThemeFont::new("Calibri"),
213            colors: Self::default_colors(),
214            xml_content: Some(xml.to_string()),
215        })
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_theme_new() {
225        let theme = ThemePart::new(1);
226        assert_eq!(theme.theme_number(), 1);
227        assert_eq!(theme.path(), "ppt/theme/theme1.xml");
228        assert_eq!(theme.name(), "Office Theme");
229    }
230
231    #[test]
232    fn test_theme_set_fonts() {
233        let mut theme = ThemePart::new(1);
234        theme.set_major_font("Arial");
235        theme.set_minor_font("Times New Roman");
236        let xml = theme.to_xml().unwrap();
237        assert!(xml.contains("Arial"));
238        assert!(xml.contains("Times New Roman"));
239    }
240
241    #[test]
242    fn test_theme_set_color() {
243        let mut theme = ThemePart::new(1);
244        theme.set_color("accent1", "FF0000");
245        let xml = theme.to_xml().unwrap();
246        assert!(xml.contains("FF0000"));
247    }
248
249    #[test]
250    fn test_theme_to_xml() {
251        let theme = ThemePart::new(1);
252        let xml = theme.to_xml().unwrap();
253        assert!(xml.contains("a:theme"));
254        assert!(xml.contains("a:clrScheme"));
255        assert!(xml.contains("a:fontScheme"));
256    }
257
258    #[test]
259    fn test_theme_rel_target() {
260        let theme = ThemePart::new(1);
261        assert_eq!(theme.rel_target(), "../theme/theme1.xml");
262    }
263}