Skip to main content

ppt_rs/oxml/dml/
mod.rs

1//! Drawing Markup Language (DML) XML elements
2//!
3//! Core DrawingML types used across OOXML documents.
4
5use super::xmlchemy::XmlElement;
6
7/// Color types in DrawingML
8#[derive(Debug, Clone)]
9pub enum Color {
10    /// RGB color (e.g., "FF0000" for red)
11    Rgb(String),
12    /// Scheme color (e.g., "accent1", "dk1")
13    Scheme(String),
14    /// System color (e.g., "windowText")
15    System(String),
16}
17
18impl Color {
19    pub fn rgb(hex: &str) -> Self {
20        Color::Rgb(hex.trim_start_matches('#').to_uppercase())
21    }
22
23    pub fn scheme(name: &str) -> Self {
24        Color::Scheme(name.to_string())
25    }
26
27    pub fn parse(elem: &XmlElement) -> Option<Self> {
28        if let Some(srgb) = elem.find("srgbClr") {
29            return srgb.attr("val").map(|v| Color::Rgb(v.to_string()));
30        }
31        if let Some(scheme) = elem.find("schemeClr") {
32            return scheme.attr("val").map(|v| Color::Scheme(v.to_string()));
33        }
34        if let Some(sys) = elem.find("sysClr") {
35            return sys.attr("val").map(|v| Color::System(v.to_string()));
36        }
37        None
38    }
39
40    pub fn to_xml(&self) -> String {
41        match self {
42            Color::Rgb(hex) => format!(r#"<a:srgbClr val="{hex}"/>"#),
43            Color::Scheme(name) => format!(r#"<a:schemeClr val="{name}"/>"#),
44            Color::System(name) => format!(r#"<a:sysClr val="{name}"/>"#),
45        }
46    }
47}
48
49/// Effect extent (a:effectExtent)
50#[derive(Debug, Clone, Default)]
51pub struct EffectExtent {
52    pub left: i64,
53    pub top: i64,
54    pub right: i64,
55    pub bottom: i64,
56}
57
58impl EffectExtent {
59    pub fn parse(elem: &XmlElement) -> Self {
60        EffectExtent {
61            left: elem.attr("l").and_then(|v| v.parse().ok()).unwrap_or(0),
62            top: elem.attr("t").and_then(|v| v.parse().ok()).unwrap_or(0),
63            right: elem.attr("r").and_then(|v| v.parse().ok()).unwrap_or(0),
64            bottom: elem.attr("b").and_then(|v| v.parse().ok()).unwrap_or(0),
65        }
66    }
67
68    pub fn to_xml(&self) -> String {
69        let left = self.left;
70        let top = self.top;
71        let right = self.right;
72        let bottom = self.bottom;
73        format!(
74            r#"<a:effectExtent l="{left}" t="{top}" r="{right}" b="{bottom}"/>"#
75        )
76    }
77}
78
79/// Line cap style
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub enum LineCap {
82    Round,
83    Square,
84    Flat,
85}
86
87impl LineCap {
88    pub fn as_str(&self) -> &'static str {
89        match self {
90            LineCap::Round => "rnd",
91            LineCap::Square => "sq",
92            LineCap::Flat => "flat",
93        }
94    }
95}
96
97/// Line join style
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub enum LineJoin {
100    Round,
101    Bevel,
102    Miter,
103}
104
105impl LineJoin {
106    pub fn as_str(&self) -> &'static str {
107        match self {
108            LineJoin::Round => "round",
109            LineJoin::Bevel => "bevel",
110            LineJoin::Miter => "miter",
111        }
112    }
113}
114
115/// Preset dash pattern
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub enum DashPattern {
118    Solid,
119    Dash,
120    Dot,
121    DashDot,
122    DashDotDot,
123    LongDash,
124    LongDashDot,
125    LongDashDotDot,
126    SystemDash,
127    SystemDot,
128    SystemDashDot,
129    SystemDashDotDot,
130}
131
132impl DashPattern {
133    pub fn as_str(&self) -> &'static str {
134        match self {
135            DashPattern::Solid => "solid",
136            DashPattern::Dash => "dash",
137            DashPattern::Dot => "dot",
138            DashPattern::DashDot => "dashDot",
139            DashPattern::DashDotDot => "dashDotDot",
140            DashPattern::LongDash => "lgDash",
141            DashPattern::LongDashDot => "lgDashDot",
142            DashPattern::LongDashDotDot => "lgDashDotDot",
143            DashPattern::SystemDash => "sysDash",
144            DashPattern::SystemDot => "sysDot",
145            DashPattern::SystemDashDot => "sysDashDot",
146            DashPattern::SystemDashDotDot => "sysDashDotDot",
147        }
148    }
149}
150
151/// Outline (a:ln) - line/border properties
152#[derive(Debug, Clone, Default)]
153pub struct Outline {
154    pub width: Option<u32>,
155    pub cap: Option<LineCap>,
156    pub compound: Option<String>,
157    pub color: Option<Color>,
158    pub dash: Option<DashPattern>,
159    pub join: Option<LineJoin>,
160    pub miter_limit: Option<u32>,
161}
162
163impl Outline {
164    pub fn new() -> Self {
165        Outline::default()
166    }
167
168    pub fn with_width(mut self, width: u32) -> Self {
169        self.width = Some(width);
170        self
171    }
172
173    pub fn with_color(mut self, color: Color) -> Self {
174        self.color = Some(color);
175        self
176    }
177
178    pub fn with_cap(mut self, cap: LineCap) -> Self {
179        self.cap = Some(cap);
180        self
181    }
182
183    pub fn with_dash(mut self, dash: DashPattern) -> Self {
184        self.dash = Some(dash);
185        self
186    }
187
188    pub fn with_join(mut self, join: LineJoin) -> Self {
189        self.join = Some(join);
190        self
191    }
192
193    pub fn with_miter_limit(mut self, limit: u32) -> Self {
194        self.miter_limit = Some(limit);
195        self
196    }
197
198    pub fn parse(elem: &XmlElement) -> Self {
199        let mut outline = Outline::new();
200        
201        outline.width = elem.attr("w").and_then(|v| v.parse().ok());
202        if let Some(cap_str) = elem.attr("cap") {
203            outline.cap = match cap_str {
204                "rnd" => Some(LineCap::Round),
205                "sq" => Some(LineCap::Square),
206                "flat" => Some(LineCap::Flat),
207                _ => None,
208            };
209        }
210        outline.compound = elem.attr("cmpd").map(|s| s.to_string());
211
212        if let Some(solid_fill) = elem.find("solidFill") {
213            outline.color = Color::parse(solid_fill);
214        }
215
216        if let Some(prst_dash) = elem.find("prstDash") {
217            if let Some(val) = prst_dash.attr("val") {
218                outline.dash = match val {
219                    "solid" => Some(DashPattern::Solid),
220                    "dash" => Some(DashPattern::Dash),
221                    "dot" => Some(DashPattern::Dot),
222                    "dashDot" => Some(DashPattern::DashDot),
223                    "dashDotDot" => Some(DashPattern::DashDotDot),
224                    "lgDash" => Some(DashPattern::LongDash),
225                    "lgDashDot" => Some(DashPattern::LongDashDot),
226                    "lgDashDotDot" => Some(DashPattern::LongDashDotDot),
227                    "sysDash" => Some(DashPattern::SystemDash),
228                    "sysDot" => Some(DashPattern::SystemDot),
229                    "sysDashDot" => Some(DashPattern::SystemDashDot),
230                    "sysDashDotDot" => Some(DashPattern::SystemDashDotDot),
231                    _ => None,
232                };
233            }
234        }
235
236        outline
237    }
238
239    pub fn to_xml(&self) -> String {
240        let width = self.width.unwrap_or(12700);
241        let mut attrs = vec![format!(r#"w="{width}""#)];
242
243        if let Some(cap) = &self.cap {
244            attrs.push(format!(r#"cap="{}""#, cap.as_str()));
245        }
246
247        if let Some(join) = &self.join {
248            attrs.push(format!(r#"join="{}""#, join.as_str()));
249        }
250
251        if let Some(miter) = &self.miter_limit {
252            attrs.push(format!(r#"miterLim="{}""#, miter));
253        }
254
255        let attr_str = attrs.join(" ");
256        let mut inner = String::new();
257
258        if let Some(ref color) = self.color {
259            inner.push_str("<a:solidFill>");
260            inner.push_str(&color.to_xml());
261            inner.push_str("</a:solidFill>");
262        }
263
264        if let Some(dash) = &self.dash {
265            inner.push_str(&format!(r#"<a:prstDash val="{}"/>"#, dash.as_str()));
266        }
267
268        if inner.is_empty() {
269            format!(r#"<a:ln {attr_str}/>"#)
270        } else {
271            format!(r#"<a:ln {attr_str}>{inner}</a:ln>"#)
272        }
273    }
274}
275
276/// Gradient stop
277#[derive(Debug, Clone)]
278pub struct GradientStop {
279    pub position: u32, // 0-100000 (percentage * 1000)
280    pub color: Color,
281}
282
283impl GradientStop {
284    pub fn new(position: u32, color: Color) -> Self {
285        GradientStop { position, color }
286    }
287
288    pub fn to_xml(&self) -> String {
289        format!(
290            r#"<a:gs pos="{}">{}</a:gs>"#,
291            self.position,
292            self.color.to_xml()
293        )
294    }
295}
296
297/// Gradient fill
298#[derive(Debug, Clone)]
299pub struct GradientFill {
300    pub stops: Vec<GradientStop>,
301    pub angle: Option<i32>, // in 60000ths of a degree
302}
303
304impl GradientFill {
305    pub fn new() -> Self {
306        GradientFill {
307            stops: Vec::new(),
308            angle: None,
309        }
310    }
311
312    pub fn add_stop(mut self, position: u32, color: Color) -> Self {
313        self.stops.push(GradientStop::new(position, color));
314        self
315    }
316
317    pub fn with_angle(mut self, degrees: i32) -> Self {
318        self.angle = Some(degrees * 60000);
319        self
320    }
321
322    pub fn to_xml(&self) -> String {
323        let mut xml = String::from("<a:gradFill><a:gsLst>");
324        for stop in &self.stops {
325            xml.push_str(&stop.to_xml());
326        }
327        xml.push_str("</a:gsLst>");
328
329        if let Some(angle) = self.angle {
330            xml.push_str(&format!(r#"<a:lin ang="{angle}" scaled="1"/>"#));
331        }
332
333        xml.push_str("</a:gradFill>");
334        xml
335    }
336}
337
338impl Default for GradientFill {
339    fn default() -> Self {
340        Self::new()
341    }
342}
343
344/// Pattern fill type
345#[derive(Debug, Clone)]
346pub struct PatternFill {
347    pub preset: String,
348    pub foreground: Color,
349    pub background: Color,
350}
351
352impl PatternFill {
353    pub fn new(preset: &str, fg: Color, bg: Color) -> Self {
354        PatternFill {
355            preset: preset.to_string(),
356            foreground: fg,
357            background: bg,
358        }
359    }
360
361    pub fn to_xml(&self) -> String {
362        format!(
363            r#"<a:pattFill prst="{}"><a:fgClr>{}</a:fgClr><a:bgClr>{}</a:bgClr></a:pattFill>"#,
364            self.preset,
365            self.foreground.to_xml(),
366            self.background.to_xml()
367        )
368    }
369}
370
371/// Picture fill
372#[derive(Debug, Clone)]
373pub struct PictureFill {
374    pub r_embed: String, // Relationship ID to embedded image
375    pub stretch: bool,  // Stretch or tile
376}
377
378impl PictureFill {
379    pub fn new(r_embed: &str) -> Self {
380        PictureFill {
381            r_embed: r_embed.to_string(),
382            stretch: true,
383        }
384    }
385
386    pub fn with_stretch(mut self, stretch: bool) -> Self {
387        self.stretch = stretch;
388        self
389    }
390
391    pub fn to_xml(&self) -> String {
392        if self.stretch {
393            format!(
394                r#"<a:blipFill><a:blip r:embed="{}"/><a:stretch><a:fillRect/></a:stretch></a:blipFill>"#,
395                self.r_embed
396            )
397        } else {
398            format!(
399                r#"<a:blipFill><a:blip r:embed="{}"/><a:tile/></a:blipFill>"#,
400                self.r_embed
401            )
402        }
403    }
404}
405
406/// Texture fill
407#[derive(Debug, Clone)]
408pub struct TextureFill {
409    pub r_embed: String, // Relationship ID to texture image
410    pub tile: bool,      // Tile or stretch
411}
412
413impl TextureFill {
414    pub fn new(r_embed: &str) -> Self {
415        TextureFill {
416            r_embed: r_embed.to_string(),
417            tile: true,
418        }
419    }
420
421    pub fn with_tile(mut self, tile: bool) -> Self {
422        self.tile = tile;
423        self
424    }
425
426    pub fn to_xml(&self) -> String {
427        if self.tile {
428            format!(
429                r#"<a:blipFill><a:blip r:embed="{}"/><a:tile/></a:blipFill>"#,
430                self.r_embed
431            )
432        } else {
433            format!(
434                r#"<a:blipFill><a:blip r:embed="{}"/><a:stretch><a:fillRect/></a:stretch></a:blipFill>"#,
435                self.r_embed
436            )
437        }
438    }
439}
440
441/// Fill types
442#[derive(Debug, Clone)]
443pub enum Fill {
444    None,
445    Solid(Color),
446    Gradient(GradientFill),
447    Pattern(PatternFill),
448    Picture(PictureFill),
449    Texture(TextureFill),
450}
451
452impl Fill {
453    pub fn solid(color: Color) -> Self {
454        Fill::Solid(color)
455    }
456
457    pub fn picture(r_embed: &str) -> Self {
458        Fill::Picture(PictureFill::new(r_embed))
459    }
460
461    pub fn texture(r_embed: &str) -> Self {
462        Fill::Texture(TextureFill::new(r_embed))
463    }
464
465    pub fn to_xml(&self) -> String {
466        match self {
467            Fill::None => "<a:noFill/>".to_string(),
468            Fill::Solid(color) => format!("<a:solidFill>{}</a:solidFill>", color.to_xml()),
469            Fill::Gradient(grad) => grad.to_xml(),
470            Fill::Pattern(pat) => pat.to_xml(),
471            Fill::Picture(pic) => pic.to_xml(),
472            Fill::Texture(tex) => tex.to_xml(),
473        }
474    }
475}
476
477/// Point in EMUs
478#[derive(Debug, Clone, Copy, Default)]
479pub struct Point {
480    pub x: i64,
481    pub y: i64,
482}
483
484impl Point {
485    pub fn new(x: i64, y: i64) -> Self {
486        Point { x, y }
487    }
488
489    pub fn from_inches(x: f64, y: f64) -> Self {
490        Point {
491            x: (x * 914400.0) as i64,
492            y: (y * 914400.0) as i64,
493        }
494    }
495}
496
497/// Size in EMUs
498#[derive(Debug, Clone, Copy, Default)]
499pub struct Size {
500    pub width: i64,
501    pub height: i64,
502}
503
504impl Size {
505    pub fn new(width: i64, height: i64) -> Self {
506        Size { width, height }
507    }
508
509    pub fn from_inches(width: f64, height: f64) -> Self {
510        Size {
511            width: (width * 914400.0) as i64,
512            height: (height * 914400.0) as i64,
513        }
514    }
515}
516
517/// Shadow effect
518#[derive(Debug, Clone)]
519pub struct Shadow {
520    pub color: Option<Color>,
521    pub blur_radius: Option<u32>, // in EMU
522    pub distance: Option<u32>,     // in EMU
523    pub angle: Option<i32>,        // in 60000ths of a degree
524    pub offset_x: Option<i64>,     // in EMU
525    pub offset_y: Option<i64>,     // in EMU
526}
527
528impl Shadow {
529    pub fn new() -> Self {
530        Shadow {
531            color: None,
532            blur_radius: None,
533            distance: None,
534            angle: None,
535            offset_x: None,
536            offset_y: None,
537        }
538    }
539
540    pub fn with_color(mut self, color: Color) -> Self {
541        self.color = Some(color);
542        self
543    }
544
545    pub fn with_blur(mut self, radius: u32) -> Self {
546        self.blur_radius = Some(radius);
547        self
548    }
549
550    pub fn with_distance(mut self, distance: u32) -> Self {
551        self.distance = Some(distance);
552        self
553    }
554
555    pub fn with_angle(mut self, degrees: i32) -> Self {
556        self.angle = Some(degrees * 60000);
557        self
558    }
559
560    pub fn with_offset(mut self, x: i64, y: i64) -> Self {
561        self.offset_x = Some(x);
562        self.offset_y = Some(y);
563        self
564    }
565
566    pub fn to_xml(&self) -> String {
567        let mut attrs = Vec::new();
568        
569        if let Some(blur) = self.blur_radius {
570            attrs.push(format!(r#"blurRad="{blur}""#));
571        }
572        if let Some(dist) = self.distance {
573            attrs.push(format!(r#"dist="{dist}""#));
574        }
575        if let Some(angle) = self.angle {
576            attrs.push(format!(r#"dir="{angle}""#));
577        }
578
579        let attr_str = if attrs.is_empty() {
580            String::new()
581        } else {
582            format!(" {}", attrs.join(" "))
583        };
584
585        let mut inner = String::new();
586        if let Some(ref color) = self.color {
587            inner.push_str("<a:srgbClr>");
588            inner.push_str(&color.to_xml());
589            inner.push_str("</a:srgbClr>");
590        }
591
592        if let (Some(x), Some(y)) = (self.offset_x, self.offset_y) {
593            format!(
594                r#"<a:outerShdw{attr_str}><a:off x="{x}" y="{y}"/>{inner}</a:outerShdw>"#
595            )
596        } else {
597            format!(r#"<a:outerShdw{attr_str}>{inner}</a:outerShdw>"#)
598        }
599    }
600}
601
602/// Glow effect
603#[derive(Debug, Clone)]
604pub struct Glow {
605    pub color: Option<Color>,
606    pub radius: Option<u32>, // in EMU
607}
608
609impl Glow {
610    pub fn new() -> Self {
611        Glow {
612            color: None,
613            radius: None,
614        }
615    }
616
617    pub fn with_color(mut self, color: Color) -> Self {
618        self.color = Some(color);
619        self
620    }
621
622    pub fn with_radius(mut self, radius: u32) -> Self {
623        self.radius = Some(radius);
624        self
625    }
626
627    pub fn to_xml(&self) -> String {
628        let radius_attr = self.radius
629            .map(|r| format!(r#" rad="{r}""#))
630            .unwrap_or_default();
631
632        let mut inner = String::new();
633        if let Some(ref color) = self.color {
634            inner.push_str("<a:srgbClr>");
635            inner.push_str(&color.to_xml());
636            inner.push_str("</a:srgbClr>");
637        }
638
639        format!(r#"<a:glow{radius_attr}>{inner}</a:glow>"#)
640    }
641}
642
643/// Reflection effect
644#[derive(Debug, Clone)]
645pub struct Reflection {
646    pub blur_radius: Option<u32>, // in EMU
647    pub distance: Option<u32>,    // in EMU
648    pub alpha: Option<u32>,        // 0-100000 (transparency)
649}
650
651impl Reflection {
652    pub fn new() -> Self {
653        Reflection {
654            blur_radius: None,
655            distance: None,
656            alpha: None,
657        }
658    }
659
660    pub fn with_blur(mut self, radius: u32) -> Self {
661        self.blur_radius = Some(radius);
662        self
663    }
664
665    pub fn with_distance(mut self, distance: u32) -> Self {
666        self.distance = Some(distance);
667        self
668    }
669
670    pub fn with_alpha(mut self, alpha: u32) -> Self {
671        self.alpha = Some(alpha.min(100000));
672        self
673    }
674
675    pub fn to_xml(&self) -> String {
676        let mut attrs = Vec::new();
677        
678        if let Some(blur) = self.blur_radius {
679            attrs.push(format!(r#"blurRad="{blur}""#));
680        }
681        if let Some(dist) = self.distance {
682            attrs.push(format!(r#"dist="{dist}""#));
683        }
684
685        let attr_str = if attrs.is_empty() {
686            String::new()
687        } else {
688            format!(" {}", attrs.join(" "))
689        };
690
691        let mut inner = String::new();
692        if let Some(alpha) = self.alpha {
693            inner.push_str(&format!(r#"<a:alpha val="{alpha}"/>"#));
694        }
695
696        if inner.is_empty() {
697            format!(r#"<a:reflection{attr_str}/>"#)
698        } else {
699            format!(r#"<a:reflection{attr_str}>{inner}</a:reflection>"#)
700        }
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707
708    #[test]
709    fn test_color_rgb() {
710        let color = Color::rgb("FF0000");
711        let xml = color.to_xml();
712        assert!(xml.contains("srgbClr"));
713        assert!(xml.contains("FF0000"));
714    }
715
716    #[test]
717    fn test_color_scheme() {
718        let color = Color::scheme("accent1");
719        let xml = color.to_xml();
720        assert!(xml.contains("schemeClr"));
721        assert!(xml.contains("accent1"));
722    }
723
724    #[test]
725    fn test_outline_to_xml() {
726        let outline = Outline::new()
727            .with_width(25400)
728            .with_color(Color::rgb("0000FF"));
729        let xml = outline.to_xml();
730        
731        assert!(xml.contains("w=\"25400\""));
732        assert!(xml.contains("0000FF"));
733    }
734
735    #[test]
736    fn test_gradient_fill() {
737        let grad = GradientFill::new()
738            .add_stop(0, Color::rgb("FF0000"))
739            .add_stop(100000, Color::rgb("0000FF"))
740            .with_angle(90);
741        
742        let xml = grad.to_xml();
743        assert!(xml.contains("gradFill"));
744        assert!(xml.contains("FF0000"));
745        assert!(xml.contains("0000FF"));
746    }
747
748    #[test]
749    fn test_fill_solid() {
750        let fill = Fill::solid(Color::rgb("00FF00"));
751        let xml = fill.to_xml();
752        assert!(xml.contains("solidFill"));
753        assert!(xml.contains("00FF00"));
754    }
755}