1use super::xmlchemy::XmlElement;
6
7#[derive(Debug, Clone)]
9pub enum Color {
10 Rgb(String),
12 Scheme(String),
14 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#[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#[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#[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#[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#[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#[derive(Debug, Clone)]
278pub struct GradientStop {
279 pub position: u32, 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#[derive(Debug, Clone)]
299pub struct GradientFill {
300 pub stops: Vec<GradientStop>,
301 pub angle: Option<i32>, }
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#[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#[derive(Debug, Clone)]
373pub struct PictureFill {
374 pub r_embed: String, pub stretch: bool, }
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#[derive(Debug, Clone)]
408pub struct TextureFill {
409 pub r_embed: String, pub tile: bool, }
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#[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#[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#[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#[derive(Debug, Clone)]
519pub struct Shadow {
520 pub color: Option<Color>,
521 pub blur_radius: Option<u32>, pub distance: Option<u32>, pub angle: Option<i32>, pub offset_x: Option<i64>, pub offset_y: Option<i64>, }
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#[derive(Debug, Clone)]
604pub struct Glow {
605 pub color: Option<Color>,
606 pub radius: Option<u32>, }
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#[derive(Debug, Clone)]
645pub struct Reflection {
646 pub blur_radius: Option<u32>, pub distance: Option<u32>, pub alpha: Option<u32>, }
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}