pptx 0.1.0

A Rust library for creating and manipulating PowerPoint (.pptx) files
Documentation
//! Basic theme support for reading the color scheme from theme1.xml.
//!
//! In OOXML, each slide master links to a theme part (`ppt/theme/theme1.xml`)
//! that defines the color scheme used by the presentation.  The color scheme
//! has 12 named slots:
//!
//! - dk1, dk2 (dark colors)
//! - lt1, lt2 (light colors)
//! - accent1 through accent6
//! - hlink (hyperlink)
//! - folHlink (followed hyperlink)

mod parser;

#[cfg(test)]
mod tests;

use std::fmt::Write;

use crate::error::{PptxError, PptxResult};
use crate::text::font::RgbColor;

pub use parser::parse_theme_color_scheme;

/// The 12 theme color slots from `<a:clrScheme>`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThemeColorScheme {
    pub dk1: RgbColor,
    pub dk2: RgbColor,
    pub lt1: RgbColor,
    pub lt2: RgbColor,
    pub accent1: RgbColor,
    pub accent2: RgbColor,
    pub accent3: RgbColor,
    pub accent4: RgbColor,
    pub accent5: RgbColor,
    pub accent6: RgbColor,
    pub hlink: RgbColor,
    pub fol_hlink: RgbColor,
}

/// Creates a color scheme matching `PowerPoint`'s built-in "Office" theme defaults.
impl Default for ThemeColorScheme {
    fn default() -> Self {
        Self {
            dk1: RgbColor::new(0, 0, 0),
            dk2: RgbColor::new(31, 73, 125),
            lt1: RgbColor::new(255, 255, 255),
            lt2: RgbColor::new(238, 236, 225),
            accent1: RgbColor::new(79, 129, 189),
            accent2: RgbColor::new(192, 80, 77),
            accent3: RgbColor::new(155, 187, 89),
            accent4: RgbColor::new(128, 100, 162),
            accent5: RgbColor::new(75, 172, 198),
            accent6: RgbColor::new(247, 150, 70),
            hlink: RgbColor::new(0, 0, 255),
            fol_hlink: RgbColor::new(128, 0, 128),
        }
    }
}

impl ThemeColorScheme {
    /// Get the RGB color for a given theme color slot name.
    ///
    /// Accepts XML attribute values like "dk1", "lt1", "accent1", etc.
    /// Also accepts the mapped scheme names like "tx1" (maps to dk1),
    /// "tx2" (maps to dk2), "bg1" (maps to lt1), "bg2" (maps to lt2).
    #[must_use]
    pub fn by_name(&self, name: &str) -> Option<RgbColor> {
        match name {
            "dk1" | "tx1" => Some(self.dk1),
            "dk2" | "tx2" => Some(self.dk2),
            "lt1" | "bg1" => Some(self.lt1),
            "lt2" | "bg2" => Some(self.lt2),
            "accent1" => Some(self.accent1),
            "accent2" => Some(self.accent2),
            "accent3" => Some(self.accent3),
            "accent4" => Some(self.accent4),
            "accent5" => Some(self.accent5),
            "accent6" => Some(self.accent6),
            "hlink" => Some(self.hlink),
            "folHlink" => Some(self.fol_hlink),
            _ => None,
        }
    }

    /// Generate an `<a:clrScheme>` XML fragment from this color scheme.
    ///
    /// The generated XML uses `<a:srgbClr>` for all color slots.
    #[must_use]
    pub fn to_xml_string(&self) -> String {
        fn write_slot(xml: &mut String, tag: &str, color: RgbColor) {
            write!(
                xml,
                r#"<a:{tag}><a:srgbClr val="{hex}"/></a:{tag}>"#,
                tag = tag,
                hex = color.to_hex(),
            )
            .unwrap_or_else(|_| unreachable!("fmt::Write for String is infallible"));
        }

        let mut xml = String::from(r#"<a:clrScheme name="Office">"#);

        write_slot(&mut xml, "dk1", self.dk1);
        write_slot(&mut xml, "lt1", self.lt1);
        write_slot(&mut xml, "dk2", self.dk2);
        write_slot(&mut xml, "lt2", self.lt2);
        write_slot(&mut xml, "accent1", self.accent1);
        write_slot(&mut xml, "accent2", self.accent2);
        write_slot(&mut xml, "accent3", self.accent3);
        write_slot(&mut xml, "accent4", self.accent4);
        write_slot(&mut xml, "accent5", self.accent5);
        write_slot(&mut xml, "accent6", self.accent6);
        write_slot(&mut xml, "hlink", self.hlink);
        write_slot(&mut xml, "folHlink", self.fol_hlink);

        xml.push_str("</a:clrScheme>");
        xml
    }
}

/// Replace the `<a:clrScheme>` in existing theme XML with the given color scheme.
///
/// This finds the `<a:clrScheme ...>...</a:clrScheme>` section in the raw theme XML
/// and replaces it with the XML generated by `scheme.to_xml_string()`.
///
/// Returns the updated theme XML bytes.
///
/// # Errors
///
/// Returns an error if the theme XML is not valid UTF-8 or does not contain
/// a `<a:clrScheme>` element.
pub fn update_theme_color_scheme(
    theme_xml: &[u8],
    scheme: &ThemeColorScheme,
) -> PptxResult<Vec<u8>> {
    let xml_str = std::str::from_utf8(theme_xml)?;

    // Find the start of <a:clrScheme
    let start = xml_str
        .find("<a:clrScheme")
        .ok_or_else(|| PptxError::InvalidXml("No <a:clrScheme> found in theme XML".to_string()))?;

    // Find the end of </a:clrScheme>
    let end_tag = "</a:clrScheme>";
    let end = xml_str[start..]
        .find(end_tag)
        .ok_or_else(|| PptxError::InvalidXml("No </a:clrScheme> found in theme XML".to_string()))?;
    let end_pos = start + end + end_tag.len();

    let mut result = String::with_capacity(xml_str.len());
    result.push_str(&xml_str[..start]);
    result.push_str(&scheme.to_xml_string());
    result.push_str(&xml_str[end_pos..]);

    Ok(result.into_bytes())
}