ppt-rs 0.2.12

Create, read, and update PowerPoint 2007+ (.pptx) files with rich formatting, bullet styles, themes, and templates.
Documentation
//! Theme part
//!
//! Represents a theme (ppt/theme/themeN.xml).

use super::base::{ContentType, Part, PartType};
use crate::exc::PptxError;

/// Theme color
#[derive(Debug, Clone)]
pub struct ThemeColor {
    pub name: String,
    pub value: String, // RGB hex value
}

impl ThemeColor {
    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
        ThemeColor {
            name: name.into(),
            value: value.into(),
        }
    }
}

/// Theme font
#[derive(Debug, Clone)]
pub struct ThemeFont {
    pub typeface: String,
    pub panose: Option<String>,
}

impl ThemeFont {
    pub fn new(typeface: impl Into<String>) -> Self {
        ThemeFont {
            typeface: typeface.into(),
            panose: None,
        }
    }
}

/// Theme part (ppt/theme/themeN.xml)
#[derive(Debug, Clone)]
pub struct ThemePart {
    path: String,
    theme_number: usize,
    name: String,
    major_font: ThemeFont,
    minor_font: ThemeFont,
    colors: Vec<ThemeColor>,
    xml_content: Option<String>,
}

impl ThemePart {
    /// Create a new theme part with default Office theme
    pub fn new(theme_number: usize) -> Self {
        ThemePart {
            path: format!("ppt/theme/theme{}.xml", theme_number),
            theme_number,
            name: "Office Theme".to_string(),
            major_font: ThemeFont::new("Calibri Light"),
            minor_font: ThemeFont::new("Calibri"),
            colors: Self::default_colors(),
            xml_content: None,
        }
    }

    fn default_colors() -> Vec<ThemeColor> {
        vec![
            ThemeColor::new("dk1", "000000"),
            ThemeColor::new("lt1", "FFFFFF"),
            ThemeColor::new("dk2", "44546A"),
            ThemeColor::new("lt2", "E7E6E6"),
            ThemeColor::new("accent1", "4472C4"),
            ThemeColor::new("accent2", "ED7D31"),
            ThemeColor::new("accent3", "A5A5A5"),
            ThemeColor::new("accent4", "FFC000"),
            ThemeColor::new("accent5", "5B9BD5"),
            ThemeColor::new("accent6", "70AD47"),
            ThemeColor::new("hlink", "0563C1"),
            ThemeColor::new("folHlink", "954F72"),
        ]
    }

    /// Get theme number
    pub fn theme_number(&self) -> usize {
        self.theme_number
    }

    /// Get theme name
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Set theme name
    pub fn set_name(&mut self, name: impl Into<String>) {
        self.name = name.into();
    }

    /// Set major font (headings)
    pub fn set_major_font(&mut self, typeface: impl Into<String>) {
        self.major_font = ThemeFont::new(typeface);
    }

    /// Set minor font (body)
    pub fn set_minor_font(&mut self, typeface: impl Into<String>) {
        self.minor_font = ThemeFont::new(typeface);
    }

    /// Set a theme color
    pub fn set_color(&mut self, name: impl Into<String>, value: impl Into<String>) {
        let name = name.into();
        if let Some(color) = self.colors.iter_mut().find(|c| c.name == name) {
            color.value = value.into();
        } else {
            self.colors.push(ThemeColor::new(name, value));
        }
    }

    /// Get relative path for relationships
    pub fn rel_target(&self) -> String {
        format!("../theme/theme{}.xml", self.theme_number)
    }

    fn generate_xml(&self) -> String {
        let colors_xml: String = self
            .colors
            .iter()
            .map(|c| {
                format!(
                    r#"<a:{} val="{}"><a:srgbClr val="{}"/></a:{}>"#,
                    c.name, c.name, c.value, c.name
                )
            })
            .collect::<Vec<_>>()
            .join("\n        ");

        format!(
            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="{}">
  <a:themeElements>
    <a:clrScheme name="Office">
      {}
    </a:clrScheme>
    <a:fontScheme name="Office">
      <a:majorFont>
        <a:latin typeface="{}"/>
        <a:ea typeface=""/>
        <a:cs typeface=""/>
      </a:majorFont>
      <a:minorFont>
        <a:latin typeface="{}"/>
        <a:ea typeface=""/>
        <a:cs typeface=""/>
      </a:minorFont>
    </a:fontScheme>
    <a:fmtScheme name="Office">
      <a:fillStyleLst>
        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
        <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>
        <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>
      </a:fillStyleLst>
      <a:lnStyleLst>
        <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>
        <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>
        <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>
      </a:lnStyleLst>
      <a:effectStyleLst>
        <a:effectStyle><a:effectLst/></a:effectStyle>
        <a:effectStyle><a:effectLst/></a:effectStyle>
        <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>
      </a:effectStyleLst>
      <a:bgFillStyleLst>
        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
        <a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill>
        <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>
      </a:bgFillStyleLst>
    </a:fmtScheme>
  </a:themeElements>
  <a:objectDefaults/>
  <a:extraClrSchemeLst/>
</a:theme>"#,
            self.name, colors_xml, self.major_font.typeface, self.minor_font.typeface
        )
    }
}

impl Part for ThemePart {
    fn path(&self) -> &str {
        &self.path
    }

    fn part_type(&self) -> PartType {
        PartType::Theme
    }

    fn content_type(&self) -> ContentType {
        ContentType::Theme
    }

    fn to_xml(&self) -> Result<String, PptxError> {
        if let Some(ref xml) = self.xml_content {
            return Ok(xml.clone());
        }
        Ok(self.generate_xml())
    }

    fn from_xml(xml: &str) -> Result<Self, PptxError> {
        Ok(ThemePart {
            path: "ppt/theme/theme1.xml".to_string(),
            theme_number: 1,
            name: "Office Theme".to_string(),
            major_font: ThemeFont::new("Calibri Light"),
            minor_font: ThemeFont::new("Calibri"),
            colors: Self::default_colors(),
            xml_content: Some(xml.to_string()),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_theme_new() {
        let theme = ThemePart::new(1);
        assert_eq!(theme.theme_number(), 1);
        assert_eq!(theme.path(), "ppt/theme/theme1.xml");
        assert_eq!(theme.name(), "Office Theme");
    }

    #[test]
    fn test_theme_set_fonts() {
        let mut theme = ThemePart::new(1);
        theme.set_major_font("Arial");
        theme.set_minor_font("Times New Roman");
        let xml = theme.to_xml().unwrap();
        assert!(xml.contains("Arial"));
        assert!(xml.contains("Times New Roman"));
    }

    #[test]
    fn test_theme_set_color() {
        let mut theme = ThemePart::new(1);
        theme.set_color("accent1", "FF0000");
        let xml = theme.to_xml().unwrap();
        assert!(xml.contains("FF0000"));
    }

    #[test]
    fn test_theme_to_xml() {
        let theme = ThemePart::new(1);
        let xml = theme.to_xml().unwrap();
        assert!(xml.contains("a:theme"));
        assert!(xml.contains("a:clrScheme"));
        assert!(xml.contains("a:fontScheme"));
    }

    #[test]
    fn test_theme_rel_target() {
        let theme = ThemePart::new(1);
        assert_eq!(theme.rel_target(), "../theme/theme1.xml");
    }
}