ppt_rs/generator/
gradients.rs

1//! Gradient fill support for PPTX shapes
2//!
3//! Provides gradient types and XML generation for shape fills.
4
5/// Gradient types
6#[derive(Clone, Debug, Copy, PartialEq, Eq)]
7pub enum GradientType {
8    /// Linear gradient
9    Linear,
10    /// Radial gradient
11    Radial,
12    /// Rectangular gradient
13    Rectangular,
14    /// Path gradient
15    Path,
16}
17
18impl GradientType {
19    /// Get OOXML gradient type value
20    pub fn xml_value(&self) -> &'static str {
21        match self {
22            GradientType::Linear => "lin",
23            GradientType::Radial => "circle",
24            GradientType::Rectangular => "rect",
25            GradientType::Path => "path",
26        }
27    }
28}
29
30/// Gradient direction for linear gradients (in degrees)
31#[derive(Clone, Debug, Copy, PartialEq, Eq)]
32pub enum GradientDirection {
33    /// Left to right (0°)
34    Horizontal,
35    /// Top to bottom (90°)
36    Vertical,
37    /// Top-left to bottom-right (45°)
38    DiagonalDown,
39    /// Bottom-left to top-right (315°)
40    DiagonalUp,
41    /// Custom angle in degrees
42    Custom(u32),
43}
44
45impl GradientDirection {
46    /// Get angle in 60000ths of a degree (OOXML format)
47    pub fn angle(&self) -> u32 {
48        match self {
49            GradientDirection::Horizontal => 0,
50            GradientDirection::Vertical => 5400000,
51            GradientDirection::DiagonalDown => 2700000,
52            GradientDirection::DiagonalUp => 18900000,
53            GradientDirection::Custom(deg) => deg * 60000,
54        }
55    }
56}
57
58/// A color stop in a gradient
59#[derive(Clone, Debug)]
60pub struct GradientStop {
61    /// Position (0-100000, where 100000 = 100%)
62    pub position: u32,
63    /// Color (RGB hex)
64    pub color: String,
65    /// Transparency (0-100000, where 100000 = fully transparent)
66    pub transparency: Option<u32>,
67}
68
69impl GradientStop {
70    /// Create a new gradient stop
71    pub fn new(position: u32, color: &str) -> Self {
72        GradientStop {
73            position: position.min(100000),
74            color: color.trim_start_matches('#').to_uppercase(),
75            transparency: None,
76        }
77    }
78
79    /// Create stop at start (0%)
80    pub fn start(color: &str) -> Self {
81        Self::new(0, color)
82    }
83
84    /// Create stop at middle (50%)
85    pub fn middle(color: &str) -> Self {
86        Self::new(50000, color)
87    }
88
89    /// Create stop at end (100%)
90    pub fn end(color: &str) -> Self {
91        Self::new(100000, color)
92    }
93
94    /// Set transparency (0-100 percent)
95    pub fn with_transparency(mut self, percent: u32) -> Self {
96        self.transparency = Some((percent.min(100) * 1000) as u32);
97        self
98    }
99}
100
101/// Gradient fill definition
102#[derive(Clone, Debug)]
103pub struct GradientFill {
104    /// Gradient type
105    pub gradient_type: GradientType,
106    /// Direction (for linear gradients)
107    pub direction: GradientDirection,
108    /// Color stops
109    pub stops: Vec<GradientStop>,
110    /// Rotate with shape
111    pub rotate_with_shape: bool,
112}
113
114impl GradientFill {
115    /// Create a new gradient fill
116    pub fn new(gradient_type: GradientType) -> Self {
117        GradientFill {
118            gradient_type,
119            direction: GradientDirection::Vertical,
120            stops: Vec::new(),
121            rotate_with_shape: true,
122        }
123    }
124
125    /// Create a linear gradient
126    pub fn linear(direction: GradientDirection) -> Self {
127        let mut fill = Self::new(GradientType::Linear);
128        fill.direction = direction;
129        fill
130    }
131
132    /// Create a radial gradient
133    pub fn radial() -> Self {
134        Self::new(GradientType::Radial)
135    }
136
137    /// Create a simple two-color gradient
138    pub fn two_color(start_color: &str, end_color: &str) -> Self {
139        Self::linear(GradientDirection::Vertical)
140            .add_stop(GradientStop::start(start_color))
141            .add_stop(GradientStop::end(end_color))
142    }
143
144    /// Create a three-color gradient
145    pub fn three_color(start_color: &str, middle_color: &str, end_color: &str) -> Self {
146        Self::linear(GradientDirection::Vertical)
147            .add_stop(GradientStop::start(start_color))
148            .add_stop(GradientStop::middle(middle_color))
149            .add_stop(GradientStop::end(end_color))
150    }
151
152    /// Add a gradient stop
153    pub fn add_stop(mut self, stop: GradientStop) -> Self {
154        self.stops.push(stop);
155        self
156    }
157
158    /// Set direction
159    pub fn with_direction(mut self, direction: GradientDirection) -> Self {
160        self.direction = direction;
161        self
162    }
163
164    /// Set rotate with shape
165    pub fn with_rotate(mut self, rotate: bool) -> Self {
166        self.rotate_with_shape = rotate;
167        self
168    }
169
170    /// Sort stops by position
171    pub fn sorted(mut self) -> Self {
172        self.stops.sort_by_key(|s| s.position);
173        self
174    }
175}
176
177/// Preset gradient definitions
178pub struct PresetGradients;
179
180impl PresetGradients {
181    /// Blue gradient
182    pub fn blue() -> GradientFill {
183        GradientFill::two_color("0066CC", "003366")
184    }
185
186    /// Green gradient
187    pub fn green() -> GradientFill {
188        GradientFill::two_color("00CC66", "006633")
189    }
190
191    /// Red gradient
192    pub fn red() -> GradientFill {
193        GradientFill::two_color("CC0000", "660000")
194    }
195
196    /// Orange gradient
197    pub fn orange() -> GradientFill {
198        GradientFill::two_color("FF9900", "CC6600")
199    }
200
201    /// Purple gradient
202    pub fn purple() -> GradientFill {
203        GradientFill::two_color("9933CC", "660099")
204    }
205
206    /// Gray gradient
207    pub fn gray() -> GradientFill {
208        GradientFill::two_color("999999", "333333")
209    }
210
211    /// Sunrise gradient (orange to yellow)
212    pub fn sunrise() -> GradientFill {
213        GradientFill::three_color("FF6600", "FFCC00", "FFFF66")
214    }
215
216    /// Ocean gradient (dark blue to light blue)
217    pub fn ocean() -> GradientFill {
218        GradientFill::three_color("003366", "0066CC", "66CCFF")
219    }
220
221    /// Forest gradient (dark green to light green)
222    pub fn forest() -> GradientFill {
223        GradientFill::three_color("003300", "006600", "66CC66")
224    }
225
226    /// Rainbow gradient
227    pub fn rainbow() -> GradientFill {
228        GradientFill::linear(GradientDirection::Horizontal)
229            .add_stop(GradientStop::new(0, "FF0000"))
230            .add_stop(GradientStop::new(17000, "FF9900"))
231            .add_stop(GradientStop::new(33000, "FFFF00"))
232            .add_stop(GradientStop::new(50000, "00FF00"))
233            .add_stop(GradientStop::new(67000, "0000FF"))
234            .add_stop(GradientStop::new(83000, "9900FF"))
235            .add_stop(GradientStop::new(100000, "FF00FF"))
236    }
237}
238
239/// Generate gradient fill XML
240pub fn generate_gradient_fill_xml(gradient: &GradientFill) -> String {
241    let mut xml = String::from(r#"<a:gradFill rotWithShape=""#);
242    xml.push_str(if gradient.rotate_with_shape { "1" } else { "0" });
243    xml.push_str(r#"">"#);
244
245    // Generate gradient stop list
246    xml.push_str("<a:gsLst>");
247    for stop in &gradient.stops {
248        xml.push_str(&format!(
249            r#"<a:gs pos="{}"><a:srgbClr val="{}""#,
250            stop.position, stop.color
251        ));
252
253        if let Some(alpha) = stop.transparency {
254            xml.push_str(&format!(r#"><a:alpha val="{}"/></a:srgbClr>"#, 100000 - alpha));
255        } else {
256            xml.push_str("/>");
257        }
258
259        xml.push_str("</a:gs>");
260    }
261    xml.push_str("</a:gsLst>");
262
263    // Generate gradient type-specific elements
264    match gradient.gradient_type {
265        GradientType::Linear => {
266            xml.push_str(&format!(
267                r#"<a:lin ang="{}" scaled="1"/>"#,
268                gradient.direction.angle()
269            ));
270        }
271        GradientType::Radial | GradientType::Rectangular | GradientType::Path => {
272            xml.push_str(&format!(
273                r#"<a:path path="{}"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>"#,
274                gradient.gradient_type.xml_value()
275            ));
276        }
277    }
278
279    xml.push_str("</a:gradFill>");
280    xml
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_gradient_type_xml() {
289        assert_eq!(GradientType::Linear.xml_value(), "lin");
290        assert_eq!(GradientType::Radial.xml_value(), "circle");
291    }
292
293    #[test]
294    fn test_gradient_direction_angle() {
295        assert_eq!(GradientDirection::Horizontal.angle(), 0);
296        assert_eq!(GradientDirection::Vertical.angle(), 5400000);
297        assert_eq!(GradientDirection::Custom(45).angle(), 2700000);
298    }
299
300    #[test]
301    fn test_gradient_stop() {
302        let stop = GradientStop::new(50000, "#FF0000");
303        assert_eq!(stop.position, 50000);
304        assert_eq!(stop.color, "FF0000");
305    }
306
307    #[test]
308    fn test_gradient_stop_with_transparency() {
309        let stop = GradientStop::new(0, "000000").with_transparency(50);
310        assert_eq!(stop.transparency, Some(50000));
311    }
312
313    #[test]
314    fn test_two_color_gradient() {
315        let gradient = GradientFill::two_color("FF0000", "0000FF");
316        assert_eq!(gradient.stops.len(), 2);
317        assert_eq!(gradient.stops[0].color, "FF0000");
318        assert_eq!(gradient.stops[1].color, "0000FF");
319    }
320
321    #[test]
322    fn test_three_color_gradient() {
323        let gradient = GradientFill::three_color("FF0000", "00FF00", "0000FF");
324        assert_eq!(gradient.stops.len(), 3);
325    }
326
327    #[test]
328    fn test_preset_gradients() {
329        let blue = PresetGradients::blue();
330        assert_eq!(blue.stops.len(), 2);
331
332        let rainbow = PresetGradients::rainbow();
333        assert_eq!(rainbow.stops.len(), 7);
334    }
335
336    #[test]
337    fn test_generate_gradient_xml() {
338        let gradient = GradientFill::two_color("FF0000", "0000FF");
339        let xml = generate_gradient_fill_xml(&gradient);
340        assert!(xml.contains("gradFill"));
341        assert!(xml.contains("gsLst"));
342        assert!(xml.contains("FF0000"));
343        assert!(xml.contains("0000FF"));
344    }
345
346    #[test]
347    fn test_radial_gradient_xml() {
348        let gradient = GradientFill::radial()
349            .add_stop(GradientStop::start("FFFFFF"))
350            .add_stop(GradientStop::end("000000"));
351        let xml = generate_gradient_fill_xml(&gradient);
352        assert!(xml.contains("path"));
353        assert!(xml.contains("circle"));
354    }
355}