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::{Part, PartType, ContentType};
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.colors.iter()
125            .map(|c| format!(r#"<a:{} val="{}"><a:srgbClr val="{}"/></a:{}>"#, c.name, c.name, c.value, c.name))
126            .collect::<Vec<_>>()
127            .join("\n        ");
128
129        format!(
130            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
131<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="{}">
132  <a:themeElements>
133    <a:clrScheme name="Office">
134      {}
135    </a:clrScheme>
136    <a:fontScheme name="Office">
137      <a:majorFont>
138        <a:latin typeface="{}"/>
139        <a:ea typeface=""/>
140        <a:cs typeface=""/>
141      </a:majorFont>
142      <a:minorFont>
143        <a:latin typeface="{}"/>
144        <a:ea typeface=""/>
145        <a:cs typeface=""/>
146      </a:minorFont>
147    </a:fontScheme>
148    <a:fmtScheme name="Office">
149      <a:fillStyleLst>
150        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
151        <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>
152        <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>
153      </a:fillStyleLst>
154      <a:lnStyleLst>
155        <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>
156        <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>
157        <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>
158      </a:lnStyleLst>
159      <a:effectStyleLst>
160        <a:effectStyle><a:effectLst/></a:effectStyle>
161        <a:effectStyle><a:effectLst/></a:effectStyle>
162        <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>
163      </a:effectStyleLst>
164      <a:bgFillStyleLst>
165        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
166        <a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill>
167        <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>
168      </a:bgFillStyleLst>
169    </a:fmtScheme>
170  </a:themeElements>
171  <a:objectDefaults/>
172  <a:extraClrSchemeLst/>
173</a:theme>"#,
174            self.name,
175            colors_xml,
176            self.major_font.typeface,
177            self.minor_font.typeface
178        )
179    }
180}
181
182impl Part for ThemePart {
183    fn path(&self) -> &str {
184        &self.path
185    }
186
187    fn part_type(&self) -> PartType {
188        PartType::Theme
189    }
190
191    fn content_type(&self) -> ContentType {
192        ContentType::Theme
193    }
194
195    fn to_xml(&self) -> Result<String, PptxError> {
196        if let Some(ref xml) = self.xml_content {
197            return Ok(xml.clone());
198        }
199        Ok(self.generate_xml())
200    }
201
202    fn from_xml(xml: &str) -> Result<Self, PptxError> {
203        Ok(ThemePart {
204            path: "ppt/theme/theme1.xml".to_string(),
205            theme_number: 1,
206            name: "Office Theme".to_string(),
207            major_font: ThemeFont::new("Calibri Light"),
208            minor_font: ThemeFont::new("Calibri"),
209            colors: Self::default_colors(),
210            xml_content: Some(xml.to_string()),
211        })
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_theme_new() {
221        let theme = ThemePart::new(1);
222        assert_eq!(theme.theme_number(), 1);
223        assert_eq!(theme.path(), "ppt/theme/theme1.xml");
224        assert_eq!(theme.name(), "Office Theme");
225    }
226
227    #[test]
228    fn test_theme_set_fonts() {
229        let mut theme = ThemePart::new(1);
230        theme.set_major_font("Arial");
231        theme.set_minor_font("Times New Roman");
232        let xml = theme.to_xml().unwrap();
233        assert!(xml.contains("Arial"));
234        assert!(xml.contains("Times New Roman"));
235    }
236
237    #[test]
238    fn test_theme_set_color() {
239        let mut theme = ThemePart::new(1);
240        theme.set_color("accent1", "FF0000");
241        let xml = theme.to_xml().unwrap();
242        assert!(xml.contains("FF0000"));
243    }
244
245    #[test]
246    fn test_theme_to_xml() {
247        let theme = ThemePart::new(1);
248        let xml = theme.to_xml().unwrap();
249        assert!(xml.contains("a:theme"));
250        assert!(xml.contains("a:clrScheme"));
251        assert!(xml.contains("a:fontScheme"));
252    }
253
254    #[test]
255    fn test_theme_rel_target() {
256        let theme = ThemePart::new(1);
257        assert_eq!(theme.rel_target(), "../theme/theme1.xml");
258    }
259}