Skip to main content

sheetkit_core/
theme.rs

1//! Theme color resolution.
2
3use sheetkit_xml::theme::ThemeColors;
4
5/// Resolve a theme color index to an ARGB hex string.
6/// Applies tint modification if specified.
7pub fn resolve_theme_color(theme: &ThemeColors, index: u32, tint: Option<f64>) -> Option<String> {
8    let base = theme.get(index as usize)?;
9    if base.is_empty() {
10        return None;
11    }
12    match tint {
13        Some(t) if t != 0.0 => Some(apply_tint(base, t)),
14        _ => Some(base.to_string()),
15    }
16}
17
18/// Apply a tint value to an ARGB hex color.
19/// Tint > 0 lightens toward white, tint < 0 darkens toward black.
20fn apply_tint(argb: &str, tint: f64) -> String {
21    if argb.len() < 8 {
22        return argb.to_string();
23    }
24    let r = u8::from_str_radix(&argb[2..4], 16).unwrap_or(0);
25    let g = u8::from_str_radix(&argb[4..6], 16).unwrap_or(0);
26    let b = u8::from_str_radix(&argb[6..8], 16).unwrap_or(0);
27
28    let (r, g, b) = if tint < 0.0 {
29        let factor = 1.0 + tint;
30        (
31            (r as f64 * factor) as u8,
32            (g as f64 * factor) as u8,
33            (b as f64 * factor) as u8,
34        )
35    } else {
36        (
37            (r as f64 + (255.0 - r as f64) * tint) as u8,
38            (g as f64 + (255.0 - g as f64) * tint) as u8,
39            (b as f64 + (255.0 - b as f64) * tint) as u8,
40        )
41    };
42
43    format!("FF{:02X}{:02X}{:02X}", r, g, b)
44}
45
46/// Get the default Office theme colors.
47pub fn default_theme_colors() -> ThemeColors {
48    ThemeColors {
49        colors: [
50            "FF000000".to_string(),
51            "FFFFFFFF".to_string(),
52            "FF44546A".to_string(),
53            "FFE7E6E6".to_string(),
54            "FF4472C4".to_string(),
55            "FFED7D31".to_string(),
56            "FFA5A5A5".to_string(),
57            "FFFFC000".to_string(),
58            "FF5B9BD5".to_string(),
59            "FF70AD47".to_string(),
60            "FF0563C1".to_string(),
61            "FF954F72".to_string(),
62        ],
63    }
64}
65
66/// Generate default theme1.xml content as raw bytes.
67pub fn default_theme_xml() -> Vec<u8> {
68    let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
69<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
70  <a:themeElements>
71    <a:clrScheme name="Office">
72      <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
73      <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
74      <a:dk2><a:srgbClr val="44546A"/></a:dk2>
75      <a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
76      <a:accent1><a:srgbClr val="4472C4"/></a:accent1>
77      <a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
78      <a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
79      <a:accent4><a:srgbClr val="FFC000"/></a:accent4>
80      <a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>
81      <a:accent6><a:srgbClr val="70AD47"/></a:accent6>
82      <a:hlink><a:srgbClr val="0563C1"/></a:hlink>
83      <a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
84    </a:clrScheme>
85    <a:fontScheme name="Office">
86      <a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
87      <a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
88    </a:fontScheme>
89    <a:fmtScheme name="Office">
90      <a:fillStyleLst>
91        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
92        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
93        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
94      </a:fillStyleLst>
95      <a:lnStyleLst>
96        <a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>
97        <a:ln w="12700"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>
98        <a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>
99      </a:lnStyleLst>
100      <a:effectStyleLst>
101        <a:effectStyle><a:effectLst/></a:effectStyle>
102        <a:effectStyle><a:effectLst/></a:effectStyle>
103        <a:effectStyle><a:effectLst/></a:effectStyle>
104      </a:effectStyleLst>
105      <a:bgFillStyleLst>
106        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
107        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
108        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
109      </a:bgFillStyleLst>
110    </a:fmtScheme>
111  </a:themeElements>
112</a:theme>"#;
113    xml.as_bytes().to_vec()
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_resolve_theme_color_no_tint() {
122        let theme = default_theme_colors();
123        let color = resolve_theme_color(&theme, 0, None);
124        assert_eq!(color, Some("FF000000".to_string()));
125    }
126
127    #[test]
128    fn test_resolve_theme_color_with_positive_tint() {
129        let theme = default_theme_colors();
130        let color = resolve_theme_color(&theme, 0, Some(0.5));
131        assert!(color.is_some());
132        let c = color.unwrap();
133        assert_eq!(&c[0..2], "FF");
134    }
135
136    #[test]
137    fn test_resolve_invalid_index() {
138        let theme = default_theme_colors();
139        assert!(resolve_theme_color(&theme, 99, None).is_none());
140    }
141
142    #[test]
143    fn test_apply_tint_lighten() {
144        let result = apply_tint("FF000000", 0.5);
145        assert_eq!(result, "FF7F7F7F");
146    }
147
148    #[test]
149    fn test_apply_tint_darken() {
150        let result = apply_tint("FFFFFFFF", -0.5);
151        assert_eq!(result, "FF7F7F7F");
152    }
153
154    #[test]
155    fn test_apply_tint_zero() {
156        let theme = default_theme_colors();
157        let color = resolve_theme_color(&theme, 4, Some(0.0));
158        assert_eq!(color, Some("FF4472C4".to_string()));
159    }
160
161    #[test]
162    fn test_default_theme_has_all_colors() {
163        let theme = default_theme_colors();
164        for i in 0..12 {
165            assert!(!theme.colors[i].is_empty());
166        }
167    }
168
169    #[test]
170    fn test_default_theme_xml_parseable() {
171        let xml_bytes = default_theme_xml();
172        let colors = sheetkit_xml::theme::parse_theme_colors(&xml_bytes);
173        assert_eq!(colors.colors[0], "FF000000");
174        assert_eq!(colors.colors[1], "FFFFFFFF");
175        assert_eq!(colors.colors[4], "FF4472C4");
176    }
177}