1use std::str::FromStr;
24
25use crate::oxml::color::Color;
26use crate::oxml::simpletypes::PresetGeometry;
27use crate::units::Emu;
28
29#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
31pub struct Transform {
32 pub off_x: Option<Emu>,
33 pub off_y: Option<Emu>,
34 pub ext_cx: Option<Emu>,
35 pub ext_cy: Option<Emu>,
36 pub rot: Option<i32>,
38 pub flip_h: bool,
40 pub flip_v: bool,
42}
43
44impl Transform {
45 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
62 if self.is_empty() {
63 return;
64 }
65 let rot_s = self.rot.map(|v| v.to_string());
67 let off_x_s = self.off_x.map(|v| v.value().to_string());
68 let off_y_s = self.off_y.map(|v| v.value().to_string());
69 let ext_cx_s = self.ext_cx.map(|v| v.value().to_string());
70 let ext_cy_s = self.ext_cy.map(|v| v.value().to_string());
71
72 let mut attrs: Vec<(&str, &str)> = Vec::new();
74 if self.flip_h {
75 attrs.push(("flipH", "1"));
76 }
77 if self.flip_v {
78 attrs.push(("flipV", "1"));
79 }
80 if let Some(s) = &rot_s {
81 attrs.push(("rot", s.as_str()));
82 }
83 w.open_with("a:xfrm", &attrs);
84 if let (Some(xs), Some(ys)) = (off_x_s.as_ref(), off_y_s.as_ref()) {
85 w.empty_with("a:off", &[("x", xs.as_str()), ("y", ys.as_str())]);
86 }
87 if let (Some(xs), Some(ys)) = (ext_cx_s.as_ref(), ext_cy_s.as_ref()) {
88 w.empty_with("a:ext", &[("cx", xs.as_str()), ("cy", ys.as_str())]);
89 }
90 w.close("a:xfrm");
91 }
92
93 pub fn is_empty(&self) -> bool {
95 self.off_x.is_none()
96 && self.off_y.is_none()
97 && self.ext_cx.is_none()
98 && self.ext_cy.is_none()
99 && self.rot.is_none()
100 && !self.flip_h
101 && !self.flip_v
102 }
103}
104
105#[derive(Copy, Clone, Debug, Eq, PartialEq)]
107pub enum GradientType {
108 Linear(i32),
111 Path(GradientPath),
113}
114
115#[derive(Copy, Clone, Debug, Eq, PartialEq)]
117pub enum GradientPath {
118 Circle,
120 Rect,
122 Shape,
124}
125
126impl GradientPath {
127 pub fn as_str(self) -> &'static str {
129 match self {
130 GradientPath::Circle => "circle",
131 GradientPath::Rect => "rect",
132 GradientPath::Shape => "shape",
133 }
134 }
135}
136
137#[derive(Clone, Debug, Default, PartialEq, Eq)]
139pub struct GradientStop {
140 pub pos: u32,
142 pub color: Color,
144}
145
146#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct GradientFill {
149 pub stops: Vec<GradientStop>,
151 pub gradient_type: GradientType,
153 pub flip: Option<String>,
155 pub rot_with_shape: Option<bool>,
157}
158
159#[derive(Clone, Debug, Default, PartialEq, Eq)]
161pub struct PatternFill {
162 pub prst: String,
164 pub fg_color: Color,
166 pub bg_color: Color,
168}
169
170#[derive(Clone, Debug, Default, PartialEq, Eq)]
179pub enum BlipFillMode {
180 #[default]
184 Stretch,
185 Tile {
189 tx: Option<i64>,
191 ty: Option<i64>,
193 sx: Option<i32>,
195 sy: Option<i32>,
197 flip: Option<String>,
199 algn: Option<String>,
202 },
203 None,
207}
208
209#[derive(Clone, Debug, Default, PartialEq, Eq)]
211pub enum Fill {
212 None,
214 Solid(Color),
216 Blip {
218 rid: String,
220 mode: BlipFillMode,
222 },
223 Gradient(GradientFill),
225 Pattern(PatternFill),
227 #[default]
229 Inherit,
230}
231
232impl BlipFillMode {
233 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
238 match self {
239 BlipFillMode::Stretch => {
240 w.open("a:stretch");
241 w.empty("a:fillRect");
242 w.close("a:stretch");
243 }
244 BlipFillMode::Tile {
245 tx,
246 ty,
247 sx,
248 sy,
249 flip,
250 algn,
251 } => {
252 let tx_s = tx.map(|v| v.to_string());
254 let ty_s = ty.map(|v| v.to_string());
255 let sx_s = sx.map(|v| v.to_string());
256 let sy_s = sy.map(|v| v.to_string());
257 let mut attrs: Vec<(&str, &str)> = Vec::new();
258 if let Some(s) = &tx_s {
259 attrs.push(("tx", s));
260 }
261 if let Some(s) = &ty_s {
262 attrs.push(("ty", s));
263 }
264 if let Some(s) = &sx_s {
265 attrs.push(("sx", s));
266 }
267 if let Some(s) = &sy_s {
268 attrs.push(("sy", s));
269 }
270 if let Some(f) = flip {
271 attrs.push(("flip", f));
272 }
273 if let Some(a) = algn {
274 attrs.push(("algn", a));
275 }
276 w.empty_with("a:tile", &attrs);
277 }
278 BlipFillMode::None => {
279 }
281 }
282 }
283}
284
285impl Fill {
286 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
288 match self {
289 Fill::None => {
290 w.empty("a:noFill");
291 }
292 Fill::Solid(c) => c.write_solid_fill(w),
293 Fill::Blip { rid, mode } => {
294 w.open_with(
295 "a:blipFill",
296 &[("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS)],
297 );
298 w.empty_with("a:blip", &[("r:embed", rid.as_str())]);
302 mode.write_xml(w);
304 w.close("a:blipFill");
305 }
306 Fill::Gradient(g) => {
307 let flip_s = g.flip.as_deref();
309 let rws_s = g.rot_with_shape.map(|b| if b { "1" } else { "0" });
310 let mut attrs: Vec<(&str, &str)> = Vec::new();
311 if let Some(f) = flip_s {
312 attrs.push(("flip", f));
313 }
314 if let Some(r) = rws_s {
315 attrs.push(("rotWithShape", r));
316 }
317 if attrs.is_empty() {
318 w.open("a:gradFill");
319 } else {
320 w.open_with("a:gradFill", &attrs);
321 }
322 w.open("a:gsLst");
324 for stop in &g.stops {
325 let pos_s = stop.pos.to_string();
326 w.open_with("a:gs", &[("pos", pos_s.as_str())]);
327 stop.color.write_solid_fill(w);
328 w.close("a:gs");
329 }
330 w.close("a:gsLst");
331 match &g.gradient_type {
333 GradientType::Linear(ang) => {
334 let ang_s = ang.to_string();
335 w.empty_with("a:lin", &[("ang", ang_s.as_str()), ("scaled", "1")]);
336 }
337 GradientType::Path(p) => {
338 w.empty_with("a:path", &[("path", p.as_str())]);
339 }
340 }
341 w.close("a:gradFill");
342 }
343 Fill::Pattern(p) => {
344 w.open_with("a:pattFill", &[("prst", p.prst.as_str())]);
345 w.open("a:fgClr");
347 p.fg_color.write_solid_fill(w);
348 w.close("a:fgClr");
349 w.open("a:bgClr");
351 p.bg_color.write_solid_fill(w);
352 w.close("a:bgClr");
353 w.close("a:pattFill");
354 }
355 Fill::Inherit => { }
356 }
357 }
358}
359
360#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
362pub enum ArrowType {
363 #[default]
365 None,
366 Triangle,
368 Stealth,
370 Diamond,
372 Oval,
374 Arrow,
376}
377
378impl ArrowType {
379 pub fn as_str(self) -> &'static str {
381 match self {
382 ArrowType::None => "none",
383 ArrowType::Triangle => "triangle",
384 ArrowType::Stealth => "stealth",
385 ArrowType::Diamond => "diamond",
386 ArrowType::Oval => "oval",
387 ArrowType::Arrow => "arrow",
388 }
389 }
390}
391
392#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
396pub enum ArrowSize {
397 Small,
399 #[default]
401 Medium,
402 Large,
404}
405
406impl ArrowSize {
407 pub fn as_str(self) -> &'static str {
409 match self {
410 ArrowSize::Small => "sm",
411 ArrowSize::Medium => "med",
412 ArrowSize::Large => "lg",
413 }
414 }
415}
416
417#[derive(Copy, Clone, Debug, Default)]
419pub struct ArrowHead {
420 pub arrow_type: ArrowType,
422 pub width: ArrowSize,
424 pub length: ArrowSize,
426}
427
428#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
430pub enum LineJoin {
431 #[default]
433 Round,
434 Miter(i32),
438 Bevel,
440}
441
442#[derive(Clone, Debug, Default)]
444pub struct Line {
445 pub width: Option<Emu>, pub color: Color,
447 pub dash: Option<Dash>,
448 pub cap: Option<String>,
449 pub compound: Option<String>,
450 pub no_fill: bool,
451 pub head_end: Option<ArrowHead>,
453 pub tail_end: Option<ArrowHead>,
455 pub join: Option<LineJoin>,
457 pub fill: Fill,
462}
463
464#[derive(Copy, Clone, Debug, Eq, PartialEq)]
466pub enum Dash {
467 Solid,
468 Dash,
469 DashDot,
470 Dot,
471 LgDash,
472 LgDashDot,
473 LgDashDotDot,
474 SysDash,
475 SysDashDot,
476 SysDashDotDot,
477 SysDot,
478}
479
480impl Dash {
481 pub fn as_str(self) -> &'static str {
482 match self {
483 Dash::Solid => "solid",
484 Dash::Dash => "dash",
485 Dash::DashDot => "dashDot",
486 Dash::Dot => "dot",
487 Dash::LgDash => "lgDash",
488 Dash::LgDashDot => "lgDashDot",
489 Dash::LgDashDotDot => "lgDashDotDot",
490 Dash::SysDash => "sysDash",
491 Dash::SysDashDot => "sysDashDot",
492 Dash::SysDashDotDot => "sysDashDotDot",
493 Dash::SysDot => "sysDot",
494 }
495 }
496}
497
498impl FromStr for Dash {
499 type Err = ();
500 fn from_str(s: &str) -> Result<Self, Self::Err> {
501 Ok(match s {
502 "solid" => Dash::Solid,
503 "dash" => Dash::Dash,
504 "dashDot" => Dash::DashDot,
505 "dot" => Dash::Dot,
506 "lgDash" => Dash::LgDash,
507 "lgDashDot" => Dash::LgDashDot,
508 "lgDashDotDot" => Dash::LgDashDotDot,
509 "sysDash" => Dash::SysDash,
510 "sysDashDot" => Dash::SysDashDot,
511 "sysDashDotDot" => Dash::SysDashDotDot,
512 "sysDot" => Dash::SysDot,
513 _ => return Err(()),
514 })
515 }
516}
517
518impl Line {
519 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
536 let w_s = self.width.map(|v| v.value().to_string());
538 let mut attrs: Vec<(&str, &str)> = Vec::new();
539 if let Some(s) = &w_s {
540 attrs.push(("w", s.as_str()));
541 }
542 if let Some(c) = &self.cap {
543 attrs.push(("cap", c.as_str()));
544 }
545 if let Some(c) = &self.compound {
546 attrs.push(("cmpd", c.as_str()));
547 }
548 w.open_with("a:ln", &attrs);
549 if self.no_fill {
550 w.empty("a:noFill");
551 } else {
552 match &self.fill {
554 Fill::Gradient(_) | Fill::Pattern(_) => self.fill.write_xml(w),
555 _ => self.color.write_solid_fill(w),
556 }
557 }
558 if let Some(d) = &self.dash {
559 w.open("a:prstDash");
560 w.empty_with("a:prst", &[("val", d.as_str())]);
561 w.close("a:prstDash");
562 }
563 if let Some(h) = &self.head_end {
565 w.empty_with(
566 "a:headEnd",
567 &[
568 ("type", h.arrow_type.as_str()),
569 ("w", h.width.as_str()),
570 ("len", h.length.as_str()),
571 ],
572 );
573 }
574 if let Some(t) = &self.tail_end {
575 w.empty_with(
576 "a:tailEnd",
577 &[
578 ("type", t.arrow_type.as_str()),
579 ("w", t.width.as_str()),
580 ("len", t.length.as_str()),
581 ],
582 );
583 }
584 if let Some(j) = &self.join {
585 match j {
586 LineJoin::Round => {
587 w.empty("a:round");
588 }
589 LineJoin::Miter(lim) => {
590 let lim_s = lim.to_string();
591 w.empty_with("a:miter", &[("lim", lim_s.as_str())]);
592 }
593 LineJoin::Bevel => {
594 w.empty("a:bevel");
595 }
596 }
597 }
598 w.close("a:ln");
599 }
600}
601
602#[derive(Clone, Debug, Default, PartialEq, Eq)]
606pub struct ShadowEffect {
607 pub dir: i32,
609 pub dist: i64,
611 pub blur_rad: i64,
613 pub color: Color,
615 pub rot_with_shape: Option<bool>,
617}
618
619#[derive(Clone, Debug, Default, PartialEq, Eq)]
621pub struct GlowEffect {
622 pub rad: i64,
624 pub color: Color,
626}
627
628#[derive(Clone, Debug, Default, PartialEq, Eq)]
630pub struct SoftEdgeEffect {
631 pub rad: i64,
633}
634
635#[derive(Clone, Debug, Default, PartialEq, Eq)]
637pub struct ReflectionEffect {
638 pub blur_rad: Option<i64>,
640 pub st_a: Option<i32>,
642 pub st_pos: Option<i32>,
644 pub end_a: Option<i32>,
646 pub end_pos: Option<i32>,
648 pub dist: Option<i64>,
650 pub dir: Option<i32>,
652 pub rot_with_shape: Option<bool>,
654}
655
656#[derive(Clone, Debug, Default, PartialEq, Eq)]
661pub struct EffectList {
662 pub outer_shadow: Option<ShadowEffect>,
664 pub inner_shadow: Option<ShadowEffect>,
666 pub glow: Option<GlowEffect>,
668 pub soft_edge: Option<SoftEdgeEffect>,
670 pub reflection: Option<ReflectionEffect>,
672}
673
674impl EffectList {
675 pub fn is_empty(&self) -> bool {
677 self.outer_shadow.is_none()
678 && self.inner_shadow.is_none()
679 && self.glow.is_none()
680 && self.soft_edge.is_none()
681 && self.reflection.is_none()
682 }
683
684 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
688 if self.is_empty() {
689 return;
690 }
691 w.open("a:effectLst");
692 if let Some(g) = &self.glow {
694 let rad_s = g.rad.to_string();
695 w.open_with("a:glow", &[("rad", rad_s.as_str())]);
696 g.color.write_solid_fill(w);
697 w.close("a:glow");
698 }
699 if let Some(s) = &self.inner_shadow {
700 let dir_s = s.dir.to_string();
701 let dist_s = s.dist.to_string();
702 let blur_s = s.blur_rad.to_string();
703 w.open_with(
704 "a:innerShdw",
705 &[
706 ("blurRad", blur_s.as_str()),
707 ("dist", dist_s.as_str()),
708 ("dir", dir_s.as_str()),
709 ],
710 );
711 s.color.write_solid_fill(w);
712 w.close("a:innerShdw");
713 }
714 if let Some(s) = &self.outer_shadow {
715 let dir_s = s.dir.to_string();
716 let dist_s = s.dist.to_string();
717 let blur_s = s.blur_rad.to_string();
718 let mut attrs: Vec<(&str, &str)> = vec![
719 ("blurRad", blur_s.as_str()),
720 ("dist", dist_s.as_str()),
721 ("dir", dir_s.as_str()),
722 ];
723 let rot_s;
724 if let Some(r) = s.rot_with_shape {
725 rot_s = if r { "1".to_string() } else { "0".to_string() };
726 attrs.push(("rotWithShape", rot_s.as_str()));
727 }
728 w.open_with("a:outerShdw", &attrs);
729 s.color.write_solid_fill(w);
730 w.close("a:outerShdw");
731 }
732 if let Some(r) = &self.reflection {
733 let mut attrs: Vec<(String, String)> = Vec::new();
735 if let Some(v) = r.blur_rad {
736 attrs.push(("blurRad".to_string(), v.to_string()));
737 }
738 if let Some(v) = r.st_a {
739 attrs.push(("stA".to_string(), v.to_string()));
740 }
741 if let Some(v) = r.st_pos {
742 attrs.push(("stPos".to_string(), v.to_string()));
743 }
744 if let Some(v) = r.end_a {
745 attrs.push(("endA".to_string(), v.to_string()));
746 }
747 if let Some(v) = r.end_pos {
748 attrs.push(("endPos".to_string(), v.to_string()));
749 }
750 if let Some(v) = r.dist {
751 attrs.push(("dist".to_string(), v.to_string()));
752 }
753 if let Some(v) = r.dir {
754 attrs.push(("dir".to_string(), v.to_string()));
755 }
756 if let Some(v) = r.rot_with_shape {
757 attrs.push((
758 "rotWithShape".to_string(),
759 if v { "1".to_string() } else { "0".to_string() },
760 ));
761 }
762 let refs: Vec<(&str, &str)> = attrs
763 .iter()
764 .map(|(k, v)| (k.as_str(), v.as_str()))
765 .collect();
766 w.empty_with("a:reflection", &refs);
767 }
768 if let Some(s) = &self.soft_edge {
769 let rad_s = s.rad.to_string();
770 w.empty_with("a:softEdge", &[("rad", rad_s.as_str())]);
771 }
772 w.close("a:effectLst");
773 }
774}
775
776#[derive(Clone, Debug, Default, PartialEq, Eq)]
782pub struct GeomRect {
783 pub l: String,
785 pub t: String,
787 pub r: String,
789 pub b: String,
791}
792
793#[derive(Clone, Debug, PartialEq, Eq)]
797pub enum PathSegment {
798 MoveTo { x: i64, y: i64 },
800 LineTo { x: i64, y: i64 },
802 CubicBezTo {
804 x1: i64,
805 y1: i64,
806 x2: i64,
807 y2: i64,
808 x3: i64,
809 y3: i64,
810 },
811 QuadBezTo { x1: i64, y1: i64, x2: i64, y2: i64 },
813 ArcTo {
818 w_r: i64,
819 h_r: i64,
820 st_ang: i32,
821 sw_ang: i32,
822 },
823 Close,
825}
826
827impl PathSegment {
828 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
830 match self {
831 PathSegment::MoveTo { x, y } => {
832 let xs = x.to_string();
833 let ys = y.to_string();
834 w.open("a:moveTo");
835 w.empty_with("a:pt", &[("x", xs.as_str()), ("y", ys.as_str())]);
836 w.close("a:moveTo");
837 }
838 PathSegment::LineTo { x, y } => {
839 let xs = x.to_string();
840 let ys = y.to_string();
841 w.open("a:lnTo");
842 w.empty_with("a:pt", &[("x", xs.as_str()), ("y", ys.as_str())]);
843 w.close("a:lnTo");
844 }
845 PathSegment::CubicBezTo {
846 x1,
847 y1,
848 x2,
849 y2,
850 x3,
851 y3,
852 } => {
853 let x1s = x1.to_string();
854 let y1s = y1.to_string();
855 let x2s = x2.to_string();
856 let y2s = y2.to_string();
857 let x3s = x3.to_string();
858 let y3s = y3.to_string();
859 w.open("a:cubicBezTo");
860 w.empty_with("a:pt", &[("x", x1s.as_str()), ("y", y1s.as_str())]);
861 w.empty_with("a:pt", &[("x", x2s.as_str()), ("y", y2s.as_str())]);
862 w.empty_with("a:pt", &[("x", x3s.as_str()), ("y", y3s.as_str())]);
863 w.close("a:cubicBezTo");
864 }
865 PathSegment::QuadBezTo { x1, y1, x2, y2 } => {
866 let x1s = x1.to_string();
867 let y1s = y1.to_string();
868 let x2s = x2.to_string();
869 let y2s = y2.to_string();
870 w.open("a:quadBezTo");
871 w.empty_with("a:pt", &[("x", x1s.as_str()), ("y", y1s.as_str())]);
872 w.empty_with("a:pt", &[("x", x2s.as_str()), ("y", y2s.as_str())]);
873 w.close("a:quadBezTo");
874 }
875 PathSegment::ArcTo {
876 w_r,
877 h_r,
878 st_ang,
879 sw_ang,
880 } => {
881 let wr_s = w_r.to_string();
882 let hr_s = h_r.to_string();
883 let st_s = st_ang.to_string();
884 let sw_s = sw_ang.to_string();
885 w.empty_with(
886 "a:arcTo",
887 &[
888 ("wR", wr_s.as_str()),
889 ("hR", hr_s.as_str()),
890 ("stAng", st_s.as_str()),
891 ("swAng", sw_s.as_str()),
892 ],
893 );
894 }
895 PathSegment::Close => {
896 w.empty("a:close");
897 }
898 }
899 }
900}
901
902#[derive(Clone, Debug, Default, PartialEq, Eq)]
906pub struct Path {
907 pub width: i64,
909 pub height: i64,
911 pub fill: Option<String>,
913 pub stroke: Option<String>,
915 pub segments: Vec<PathSegment>,
917}
918
919impl Path {
920 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
922 let w_s = self.width.to_string();
923 let h_s = self.height.to_string();
924 let mut attrs: Vec<(&str, &str)> = Vec::new();
925 attrs.push(("w", w_s.as_str()));
926 attrs.push(("h", h_s.as_str()));
927 if let Some(f) = &self.fill {
928 attrs.push(("fill", f.as_str()));
929 }
930 if let Some(s) = &self.stroke {
931 attrs.push(("stroke", s.as_str()));
932 }
933 w.open_with("a:path", &attrs);
934 for seg in &self.segments {
935 seg.write_xml(w);
936 }
937 w.close("a:path");
938 }
939}
940
941#[derive(Clone, Debug, Default, PartialEq, Eq)]
945pub struct CustomGeometry {
946 pub fill: Option<String>,
948 pub stroke: Option<String>,
950 pub rect: Option<GeomRect>,
952 pub path_list: Vec<Path>,
954}
955
956impl CustomGeometry {
957 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
973 w.open("a:custGeom");
974 w.empty("a:avLst");
976 if let Some(f) = &self.fill {
978 w.leaf("a:fill", f.as_str());
979 }
980 if let Some(s) = &self.stroke {
982 w.leaf("a:stroke", s.as_str());
983 }
984 if let Some(r) = &self.rect {
986 w.empty_with(
987 "a:rect",
988 &[
989 ("l", r.l.as_str()),
990 ("t", r.t.as_str()),
991 ("r", r.r.as_str()),
992 ("b", r.b.as_str()),
993 ],
994 );
995 }
996 w.open("a:pathLst");
998 for p in &self.path_list {
999 p.write_xml(w);
1000 }
1001 w.close("a:pathLst");
1002 w.close("a:custGeom");
1003 }
1004}
1005
1006#[derive(Clone, Debug, Default, PartialEq, Eq)]
1023pub struct AdjustmentValue {
1024 pub name: String,
1026 pub raw_value: i64,
1028}
1029
1030impl AdjustmentValue {
1031 pub fn new(name: impl Into<String>, raw_value: i64) -> Self {
1037 Self {
1038 name: name.into(),
1039 raw_value,
1040 }
1041 }
1042
1043 pub fn from_normalized(name: impl Into<String>, value: f64) -> Self {
1049 Self {
1050 name: name.into(),
1051 raw_value: (value * 100000.0).round() as i64,
1052 }
1053 }
1054
1055 pub fn effective_value(&self) -> f64 {
1059 self.raw_value as f64 / 100000.0
1060 }
1061
1062 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1064 let val_s = self.raw_value.to_string();
1065 w.empty_with(
1066 "a:gd",
1067 &[
1068 ("name", self.name.as_str()),
1069 ("fmla", &format!("val {}", val_s)),
1070 ],
1071 );
1072 }
1073}
1074
1075#[derive(Clone, Debug, PartialEq, Eq)]
1079pub enum Geometry {
1080 Preset(PresetGeometry, Vec<AdjustmentValue>),
1084 Custom(CustomGeometry),
1086}
1087
1088impl Default for Geometry {
1089 fn default() -> Self {
1090 Geometry::Preset(PresetGeometry::Rectangle, Vec::new())
1091 }
1092}
1093
1094impl Geometry {
1095 pub fn preset(prst: PresetGeometry) -> Self {
1099 Geometry::Preset(prst, Vec::new())
1100 }
1101
1102 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1104 match self {
1105 Geometry::Preset(p, adjustments) => {
1106 w.open_with("a:prstGeom", &[("prst", p.as_str())]);
1107 if adjustments.is_empty() {
1109 w.empty("a:avLst");
1110 } else {
1111 w.open("a:avLst");
1112 for adj in adjustments {
1113 adj.write_xml(w);
1114 }
1115 w.close("a:avLst");
1116 }
1117 w.close("a:prstGeom");
1118 }
1119 Geometry::Custom(c) => {
1120 c.write_xml(w);
1121 }
1122 }
1123 }
1124}
1125
1126#[derive(Clone, Debug, Default)]
1128pub struct ShapeProperties {
1129 pub xfrm: Transform,
1131 pub geometry: Option<Geometry>,
1136 pub fill: Fill,
1138 pub line: Option<Line>,
1140 pub effects: Option<EffectList>,
1142 pub scene3d: Option<Scene3d>,
1146 pub sp3d: Option<Sp3d>,
1150 pub rot_deg: Option<f64>,
1152}
1153
1154impl ShapeProperties {
1155 pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
1169 w.open(tag);
1170 if !self.xfrm.is_empty() {
1172 self.xfrm.write_xml(w);
1173 }
1174 let geom = self.geometry.clone().unwrap_or_default();
1176 geom.write_xml(w);
1177 self.fill.write_xml(w);
1179 if let Some(ln) = &self.line {
1181 ln.write_xml(w);
1182 }
1183 if let Some(effects) = &self.effects {
1185 effects.write_xml(w);
1186 }
1187 if let Some(scene) = &self.scene3d {
1189 scene.write_xml(w);
1190 }
1191 if let Some(sp3d) = &self.sp3d {
1193 sp3d.write_xml(w);
1194 }
1195 w.close(tag);
1196 }
1197
1198 pub fn write_xfrm_only(&self, w: &mut super::writer::XmlWriter) {
1215 if self.xfrm.is_empty() {
1216 w.empty("p:xfrm");
1218 return;
1219 }
1220 w.open("p:xfrm");
1221 self.xfrm.write_xml(w);
1222 w.close("p:xfrm");
1223 }
1224
1225 pub fn effects(&self) -> Option<&EffectList> {
1234 self.effects.as_ref()
1235 }
1236
1237 pub fn effects_mut(&mut self) -> &mut EffectList {
1242 self.effects.get_or_insert_with(EffectList::default)
1243 }
1244
1245 pub fn set_outer_shadow(&mut self, shadow: ShadowEffect) {
1252 self.effects_mut().outer_shadow = Some(shadow);
1253 }
1254
1255 pub fn set_inner_shadow(&mut self, shadow: ShadowEffect) {
1257 self.effects_mut().inner_shadow = Some(shadow);
1258 }
1259
1260 pub fn set_glow(&mut self, glow: GlowEffect) {
1262 self.effects_mut().glow = Some(glow);
1263 }
1264
1265 pub fn set_soft_edge(&mut self, rad: i64) {
1267 self.effects_mut().soft_edge = Some(SoftEdgeEffect { rad });
1268 }
1269
1270 pub fn set_reflection(&mut self, reflection: ReflectionEffect) {
1272 self.effects_mut().reflection = Some(reflection);
1273 }
1274
1275 pub fn clear_outer_shadow(&mut self) {
1277 if let Some(e) = self.effects.as_mut() {
1278 e.outer_shadow = None;
1279 }
1280 }
1281
1282 pub fn clear_inner_shadow(&mut self) {
1284 if let Some(e) = self.effects.as_mut() {
1285 e.inner_shadow = None;
1286 }
1287 }
1288
1289 pub fn clear_glow(&mut self) {
1291 if let Some(e) = self.effects.as_mut() {
1292 e.glow = None;
1293 }
1294 }
1295
1296 pub fn clear_soft_edge(&mut self) {
1298 if let Some(e) = self.effects.as_mut() {
1299 e.soft_edge = None;
1300 }
1301 }
1302
1303 pub fn clear_reflection(&mut self) {
1305 if let Some(e) = self.effects.as_mut() {
1306 e.reflection = None;
1307 }
1308 }
1309
1310 pub fn clear_effects(&mut self) {
1312 self.effects = None;
1313 }
1314}
1315
1316use crate::oxml::color::ColorFormat;
1321
1322#[derive(Debug)]
1336pub struct FillFormat<'a> {
1337 fill: &'a mut Fill,
1339}
1340
1341impl<'a> FillFormat<'a> {
1342 pub fn new(fill: &'a mut Fill) -> Self {
1344 FillFormat { fill }
1345 }
1346
1347 pub fn fill(&self) -> &Fill {
1349 self.fill
1350 }
1351 pub fn fill_mut(&mut self) -> &mut Fill {
1353 self.fill
1354 }
1355
1356 pub fn fill_type(&self) -> super::simpletypes::MsoFillType {
1358 match self.fill {
1359 Fill::None => super::simpletypes::MsoFillType::Background,
1360 Fill::Solid(_) => super::simpletypes::MsoFillType::Solid,
1361 Fill::Blip { .. } => super::simpletypes::MsoFillType::Picture,
1362 Fill::Gradient(_) => super::simpletypes::MsoFillType::Gradient,
1363 Fill::Pattern(_) => super::simpletypes::MsoFillType::Pattern,
1364 Fill::Inherit => super::simpletypes::MsoFillType::Inherit,
1365 }
1366 }
1367
1368 pub fn solid(&mut self) -> ColorFormat<'_> {
1374 if !matches!(self.fill, Fill::Solid(_)) {
1376 *self.fill = Fill::Solid(Color::None);
1377 }
1378 match self.fill {
1379 Fill::Solid(c) => ColorFormat::new(c),
1380 _ => unreachable!("just set to Solid"),
1381 }
1382 }
1383
1384 pub fn set_none(&mut self) {
1388 *self.fill = Fill::None;
1389 }
1390
1391 pub fn set_picture(&mut self, rid: impl Into<String>, mode: BlipFillMode) {
1397 *self.fill = Fill::Blip {
1398 rid: rid.into(),
1399 mode,
1400 };
1401 }
1402
1403 pub fn clear(&mut self) {
1405 *self.fill = Fill::Inherit;
1406 }
1407
1408 pub fn set_solid_rgb(&mut self, c: impl Into<crate::units::RGBColor>) {
1410 *self.fill = Fill::Solid(Color::RGB(c.into()));
1411 }
1412 pub fn set_solid_theme(&mut self, t: super::simpletypes::MsoThemeColorIndex) {
1414 if let Some(s) = t.as_str() {
1415 if let Ok(sc) = s.parse::<crate::oxml::color::SchemeColor>() {
1416 *self.fill = Fill::Solid(Color::Scheme(sc));
1417 }
1418 }
1419 }
1420
1421 pub fn gradient(&mut self) -> &mut GradientFill {
1443 if !matches!(self.fill, Fill::Gradient(_)) {
1444 *self.fill = Fill::Gradient(GradientFill {
1445 stops: Vec::new(),
1446 gradient_type: GradientType::Linear(0),
1447 flip: None,
1448 rot_with_shape: None,
1449 });
1450 }
1451 match &mut self.fill {
1452 Fill::Gradient(g) => g,
1453 _ => unreachable!("just set to Gradient"),
1454 }
1455 }
1456
1457 pub fn set_gradient_linear(&mut self, stops: Vec<GradientStop>, angle: i32) {
1463 *self.fill = Fill::Gradient(GradientFill {
1464 stops,
1465 gradient_type: GradientType::Linear(angle),
1466 flip: None,
1467 rot_with_shape: None,
1468 });
1469 }
1470
1471 pub fn set_gradient_path(&mut self, stops: Vec<GradientStop>, path: GradientPath) {
1477 *self.fill = Fill::Gradient(GradientFill {
1478 stops,
1479 gradient_type: GradientType::Path(path),
1480 flip: None,
1481 rot_with_shape: None,
1482 });
1483 }
1484
1485 pub fn pattern(&mut self) -> &mut PatternFill {
1492 if !matches!(self.fill, Fill::Pattern(_)) {
1493 *self.fill = Fill::Pattern(PatternFill {
1494 prst: String::new(),
1495 fg_color: Color::None,
1496 bg_color: Color::None,
1497 });
1498 }
1499 match &mut self.fill {
1500 Fill::Pattern(p) => p,
1501 _ => unreachable!("just set to Pattern"),
1502 }
1503 }
1504
1505 pub fn set_pattern(&mut self, prst: impl Into<String>, fg: Color, bg: Color) {
1512 *self.fill = Fill::Pattern(PatternFill {
1513 prst: prst.into(),
1514 fg_color: fg,
1515 bg_color: bg,
1516 });
1517 }
1518}
1519
1520impl<'a> From<&'a mut Fill> for FillFormat<'a> {
1521 fn from(f: &'a mut Fill) -> Self {
1522 FillFormat::new(f)
1523 }
1524}
1525
1526#[derive(Debug)]
1534pub struct LineFormat<'a> {
1535 line: &'a mut Line,
1537}
1538
1539impl<'a> LineFormat<'a> {
1540 pub fn new(line: &'a mut Line) -> Self {
1542 LineFormat { line }
1543 }
1544
1545 pub fn line(&self) -> &Line {
1547 self.line
1548 }
1549 pub fn line_mut(&mut self) -> &mut Line {
1551 self.line
1552 }
1553
1554 pub fn color(&mut self) -> ColorFormat<'_> {
1556 ColorFormat::new(&mut self.line.color)
1557 }
1558
1559 pub fn width(&self) -> Option<crate::units::Emu> {
1561 self.line.width
1562 }
1563 pub fn set_width(&mut self, w: crate::units::Emu) {
1565 self.line.width = Some(w);
1566 }
1567
1568 pub fn dash_style(&self) -> Option<Dash> {
1570 self.line.dash
1571 }
1572 pub fn set_dash_style(&mut self, s: super::simpletypes::MsoLineDashStyle) {
1574 self.line.dash = Some(s.into());
1575 }
1576
1577 pub fn set_no_fill(&mut self) {
1579 self.line.no_fill = true;
1580 self.line.color = crate::oxml::color::Color::None;
1581 }
1582
1583 pub fn set_width_pt(&mut self, pt: crate::units::Pt) {
1585 self.line.width = Some(crate::units::Emu((pt.value() * 12_700.0) as i64));
1586 }
1587}
1588
1589impl<'a> From<&'a mut Line> for LineFormat<'a> {
1590 fn from(l: &'a mut Line) -> Self {
1591 LineFormat::new(l)
1592 }
1593}
1594
1595#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1625pub struct Rotation3d {
1626 pub lat: i32,
1628 pub lon: i32,
1630 pub rev: i32,
1632}
1633
1634impl Rotation3d {
1635 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1637 let lat = self.lat.to_string();
1638 let lon = self.lon.to_string();
1639 let rev = self.rev.to_string();
1640 w.empty_with(
1641 "a:rot",
1642 &[
1643 ("lat", lat.as_str()),
1644 ("lon", lon.as_str()),
1645 ("rev", rev.as_str()),
1646 ],
1647 );
1648 }
1649}
1650
1651#[derive(Clone, Debug, Default, PartialEq, Eq)]
1655pub enum CameraPreset {
1656 #[default]
1658 OrthographicFront,
1659 IsometricOffAxis1,
1661 IsometricOffAxis2,
1662 IsometricOffAxis3,
1663 IsometricOffAxis4,
1664 IsometricLeftDown,
1666 IsometricLeftUp,
1667 IsometricRightDown,
1668 IsometricRightUp,
1669 PerspectiveFront,
1671 PerspectiveLeft,
1673 PerspectiveRight,
1674 Other(String),
1676}
1677
1678impl CameraPreset {
1679 pub fn as_str(&self) -> &str {
1681 match self {
1682 CameraPreset::OrthographicFront => "orthographicFront",
1683 CameraPreset::IsometricOffAxis1 => "isometricOffAxis1",
1684 CameraPreset::IsometricOffAxis2 => "isometricOffAxis2",
1685 CameraPreset::IsometricOffAxis3 => "isometricOffAxis3",
1686 CameraPreset::IsometricOffAxis4 => "isometricOffAxis4",
1687 CameraPreset::IsometricLeftDown => "isometricLeftDown",
1688 CameraPreset::IsometricLeftUp => "isometricLeftUp",
1689 CameraPreset::IsometricRightDown => "isometricRightDown",
1690 CameraPreset::IsometricRightUp => "isometricRightUp",
1691 CameraPreset::PerspectiveFront => "perspectiveFront",
1692 CameraPreset::PerspectiveLeft => "perspectiveLeft",
1693 CameraPreset::PerspectiveRight => "perspectiveRight",
1694 CameraPreset::Other(s) => s.as_str(),
1695 }
1696 }
1697
1698 pub fn parse(s: &str) -> Self {
1702 match s {
1703 "orthographicFront" => CameraPreset::OrthographicFront,
1704 "isometricOffAxis1" => CameraPreset::IsometricOffAxis1,
1705 "isometricOffAxis2" => CameraPreset::IsometricOffAxis2,
1706 "isometricOffAxis3" => CameraPreset::IsometricOffAxis3,
1707 "isometricOffAxis4" => CameraPreset::IsometricOffAxis4,
1708 "isometricLeftDown" => CameraPreset::IsometricLeftDown,
1709 "isometricLeftUp" => CameraPreset::IsometricLeftUp,
1710 "isometricRightDown" => CameraPreset::IsometricRightDown,
1711 "isometricRightUp" => CameraPreset::IsometricRightUp,
1712 "perspectiveFront" => CameraPreset::PerspectiveFront,
1713 "perspectiveLeft" => CameraPreset::PerspectiveLeft,
1714 "perspectiveRight" => CameraPreset::PerspectiveRight,
1715 other => CameraPreset::Other(other.to_string()),
1716 }
1717 }
1718}
1719
1720#[derive(Clone, Debug, Default, PartialEq, Eq)]
1724pub struct Camera {
1725 pub preset: CameraPreset,
1727 pub fov: i32,
1729 pub zoom: i32,
1731 pub rotation: Option<Rotation3d>,
1733}
1734
1735impl Camera {
1736 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1738 let prst = self.preset.as_str();
1739 let mut attrs: Vec<(&str, &str)> = vec![("prst", prst)];
1740 let fov_s;
1741 if self.fov != 0 {
1742 fov_s = self.fov.to_string();
1743 attrs.push(("fov", fov_s.as_str()));
1744 }
1745 let zoom_s;
1746 if self.zoom != 0 && self.zoom != 100000 {
1747 zoom_s = self.zoom.to_string();
1748 attrs.push(("zoom", zoom_s.as_str()));
1749 }
1750 if let Some(rot) = &self.rotation {
1751 w.open_with("a:camera", &attrs);
1752 rot.write_xml(w);
1753 w.close("a:camera");
1754 } else {
1755 w.empty_with("a:camera", &attrs);
1756 }
1757 }
1758}
1759
1760#[derive(Clone, Debug, Default, PartialEq, Eq)]
1762pub enum LightRigType {
1763 #[default]
1765 Balanced,
1766 Bright,
1768 Chilly,
1770 Contrasting,
1772 Flat,
1774 Harsh,
1776 Morning,
1778 Soft,
1780 Sunrise,
1782 Sunset,
1784 ThreePt,
1786 TwoPt,
1788 Other(String),
1790}
1791
1792impl LightRigType {
1793 pub fn as_str(&self) -> &str {
1795 match self {
1796 LightRigType::Balanced => "balanced",
1797 LightRigType::Bright => "bright",
1798 LightRigType::Chilly => "chilly",
1799 LightRigType::Contrasting => "contrasting",
1800 LightRigType::Flat => "flat",
1801 LightRigType::Harsh => "harsh",
1802 LightRigType::Morning => "morning",
1803 LightRigType::Soft => "soft",
1804 LightRigType::Sunrise => "sunrise",
1805 LightRigType::Sunset => "sunset",
1806 LightRigType::ThreePt => "threePt",
1807 LightRigType::TwoPt => "twoPt",
1808 LightRigType::Other(s) => s.as_str(),
1809 }
1810 }
1811
1812 pub fn parse(s: &str) -> Self {
1816 match s {
1817 "balanced" => LightRigType::Balanced,
1818 "bright" => LightRigType::Bright,
1819 "chilly" => LightRigType::Chilly,
1820 "contrasting" => LightRigType::Contrasting,
1821 "flat" => LightRigType::Flat,
1822 "harsh" => LightRigType::Harsh,
1823 "morning" => LightRigType::Morning,
1824 "soft" => LightRigType::Soft,
1825 "sunrise" => LightRigType::Sunrise,
1826 "sunset" => LightRigType::Sunset,
1827 "threePt" => LightRigType::ThreePt,
1828 "twoPt" => LightRigType::TwoPt,
1829 other => LightRigType::Other(other.to_string()),
1830 }
1831 }
1832}
1833
1834#[derive(Clone, Debug, Default, PartialEq, Eq)]
1836pub enum LightRigDirection {
1837 TopLeft,
1839 #[default]
1841 Top,
1842 TopRight,
1844 Left,
1846 Right,
1848 BottomLeft,
1850 Bottom,
1852 BottomRight,
1854}
1855
1856impl LightRigDirection {
1857 pub fn as_str(&self) -> &str {
1859 match self {
1860 LightRigDirection::TopLeft => "tl",
1861 LightRigDirection::Top => "t",
1862 LightRigDirection::TopRight => "tr",
1863 LightRigDirection::Left => "l",
1864 LightRigDirection::Right => "r",
1865 LightRigDirection::BottomLeft => "bl",
1866 LightRigDirection::Bottom => "b",
1867 LightRigDirection::BottomRight => "br",
1868 }
1869 }
1870
1871 pub fn parse(s: &str) -> Self {
1875 match s {
1876 "tl" => LightRigDirection::TopLeft,
1877 "t" => LightRigDirection::Top,
1878 "tr" => LightRigDirection::TopRight,
1879 "l" => LightRigDirection::Left,
1880 "r" => LightRigDirection::Right,
1881 "bl" => LightRigDirection::BottomLeft,
1882 "b" => LightRigDirection::Bottom,
1883 "br" => LightRigDirection::BottomRight,
1884 _ => LightRigDirection::Top,
1885 }
1886 }
1887}
1888
1889#[derive(Clone, Debug, Default, PartialEq, Eq)]
1891pub struct LightRig {
1892 pub rig: LightRigType,
1894 pub dir: LightRigDirection,
1896 pub rotation: Option<Rotation3d>,
1898}
1899
1900impl LightRig {
1901 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1903 let rig = self.rig.as_str();
1904 let dir = self.dir.as_str();
1905 if let Some(rot) = &self.rotation {
1906 w.open_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
1907 rot.write_xml(w);
1908 w.close("a:lightRig");
1909 } else {
1910 w.empty_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
1911 }
1912 }
1913}
1914
1915#[derive(Clone, Debug, Default, PartialEq, Eq)]
1919pub struct Scene3d {
1920 pub camera: Camera,
1922 pub light_rig: LightRig,
1924 pub backdrop: Option<Backdrop>,
1929}
1930
1931impl Scene3d {
1932 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1944 w.open("a:scene3d");
1945 self.camera.write_xml(w);
1946 self.light_rig.write_xml(w);
1947 if let Some(b) = &self.backdrop {
1948 b.write_xml(w);
1949 }
1950 w.close("a:scene3d");
1951 }
1952}
1953
1954#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1958pub struct Point3d {
1959 pub x: i32,
1961 pub y: i32,
1963 pub z: i32,
1965}
1966
1967impl Point3d {
1968 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1970 let xs = self.x.to_string();
1971 let ys = self.y.to_string();
1972 let zs = self.z.to_string();
1973 w.empty_with(
1974 "a:anchor",
1975 &[("x", xs.as_str()), ("y", ys.as_str()), ("z", zs.as_str())],
1976 );
1977 }
1978}
1979
1980#[derive(Clone, Debug, Default, PartialEq, Eq)]
2003pub struct Backdrop {
2004 pub anchor: Option<Point3d>,
2008 pub floor: bool,
2010 pub wall: bool,
2012 pub left: bool,
2014 pub right: bool,
2016 pub top: bool,
2018 pub bottom: bool,
2020}
2021
2022impl Backdrop {
2023 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
2027 w.open("a:backdrop");
2028 if let Some(a) = &self.anchor {
2029 a.write_xml(w);
2030 }
2031 if self.floor {
2032 w.empty("a:floor");
2033 }
2034 if self.wall {
2035 w.empty("a:wall");
2036 }
2037 if self.left {
2038 w.empty("a:l");
2039 }
2040 if self.right {
2041 w.empty("a:r");
2042 }
2043 if self.top {
2044 w.empty("a:t");
2045 }
2046 if self.bottom {
2047 w.empty("a:b");
2048 }
2049 w.close("a:backdrop");
2050 }
2051}
2052
2053#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2057pub struct Bevel {
2058 pub w: i32,
2060 pub h: i32,
2062}
2063
2064impl Bevel {
2065 pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
2067 let w_s = self.w.to_string();
2068 let h_s = self.h.to_string();
2069 w.empty_with(tag, &[("w", w_s.as_str()), ("h", h_s.as_str())]);
2070 }
2071}
2072
2073#[derive(Clone, Debug, Default, PartialEq, Eq)]
2075pub enum MaterialPreset {
2076 #[default]
2078 WarmMatte,
2079 Clear,
2081 DarkEdge,
2083 Flat,
2085 LegacyMatte,
2087 LegacyMetallic,
2089 LegacyPlastic,
2091 LegacyWireframe,
2093 Matte,
2095 Metallic,
2097 Plastic,
2099 Powder,
2101 SoftEdge,
2103 SoftMetal,
2105 Other(String),
2107}
2108
2109impl MaterialPreset {
2110 pub fn as_str(&self) -> &str {
2112 match self {
2113 MaterialPreset::WarmMatte => "warmMatte",
2114 MaterialPreset::Clear => "clear",
2115 MaterialPreset::DarkEdge => "dkEdge",
2116 MaterialPreset::Flat => "flat",
2117 MaterialPreset::LegacyMatte => "legacyMatte",
2118 MaterialPreset::LegacyMetallic => "legacyMetallic",
2119 MaterialPreset::LegacyPlastic => "legacyPlastic",
2120 MaterialPreset::LegacyWireframe => "legacyWireframe",
2121 MaterialPreset::Matte => "matte",
2122 MaterialPreset::Metallic => "metallic",
2123 MaterialPreset::Plastic => "plastic",
2124 MaterialPreset::Powder => "powder",
2125 MaterialPreset::SoftEdge => "softEdge",
2126 MaterialPreset::SoftMetal => "softmetal",
2127 MaterialPreset::Other(s) => s.as_str(),
2128 }
2129 }
2130
2131 pub fn parse(s: &str) -> Self {
2135 match s {
2136 "warmMatte" => MaterialPreset::WarmMatte,
2137 "clear" => MaterialPreset::Clear,
2138 "dkEdge" => MaterialPreset::DarkEdge,
2139 "flat" => MaterialPreset::Flat,
2140 "legacyMatte" => MaterialPreset::LegacyMatte,
2141 "legacyMetallic" => MaterialPreset::LegacyMetallic,
2142 "legacyPlastic" => MaterialPreset::LegacyPlastic,
2143 "legacyWireframe" => MaterialPreset::LegacyWireframe,
2144 "matte" => MaterialPreset::Matte,
2145 "metallic" => MaterialPreset::Metallic,
2146 "plastic" => MaterialPreset::Plastic,
2147 "powder" => MaterialPreset::Powder,
2148 "softEdge" => MaterialPreset::SoftEdge,
2149 "softmetal" => MaterialPreset::SoftMetal,
2150 other => MaterialPreset::Other(other.to_string()),
2151 }
2152 }
2153}
2154
2155#[derive(Clone, Debug, Default, PartialEq, Eq)]
2159pub struct Sp3d {
2160 pub extrusion_h: i32,
2162 pub contour_w: i32,
2164 pub prst_material: MaterialPreset,
2166 pub bevel_top: Option<Bevel>,
2168 pub bevel_bottom: Option<Bevel>,
2170 pub extrusion_color: Option<Color>,
2172 pub contour_color: Option<Color>,
2174}
2175
2176impl Sp3d {
2177 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
2179 let mut attrs: Vec<(&str, &str)> = Vec::new();
2180 let eh_s;
2181 if self.extrusion_h != 0 {
2182 eh_s = self.extrusion_h.to_string();
2183 attrs.push(("extrusionH", eh_s.as_str()));
2184 }
2185 let cw_s;
2186 if self.contour_w != 0 {
2187 cw_s = self.contour_w.to_string();
2188 attrs.push(("contourW", cw_s.as_str()));
2189 }
2190 let pm_s;
2192 if !matches!(self.prst_material, MaterialPreset::WarmMatte) {
2193 pm_s = self.prst_material.as_str().to_string();
2194 attrs.push(("prstMaterial", pm_s.as_str()));
2195 }
2196 let has_children = self.bevel_top.is_some()
2197 || self.bevel_bottom.is_some()
2198 || self.extrusion_color.is_some()
2199 || self.contour_color.is_some();
2200 if !has_children && attrs.is_empty() {
2201 w.empty("a:sp3d");
2202 return;
2203 }
2204 if !has_children {
2205 w.empty_with("a:sp3d", &attrs);
2206 return;
2207 }
2208 w.open_with("a:sp3d", &attrs);
2209 if let Some(b) = &self.bevel_top {
2210 b.write_xml(w, "a:bevelT");
2211 }
2212 if let Some(b) = &self.bevel_bottom {
2213 b.write_xml(w, "a:bevelB");
2214 }
2215 if let Some(c) = &self.extrusion_color {
2216 w.open("a:extrusionClr");
2217 c.write_solid_fill(w);
2218 w.close("a:extrusionClr");
2219 }
2220 if let Some(c) = &self.contour_color {
2221 w.open("a:contourClr");
2222 c.write_solid_fill(w);
2223 w.close("a:contourClr");
2224 }
2225 w.close("a:sp3d");
2226 }
2227}
2228
2229#[cfg(test)]
2230mod tests {
2231 use super::*;
2232 use crate::oxml::color::Color;
2233 use crate::oxml::writer::XmlWriter;
2234 use crate::units::RGBColor;
2235
2236 #[test]
2240 fn adjustment_value_new() {
2241 let av = AdjustmentValue::new("adj", 16667);
2242 assert_eq!(av.name, "adj");
2243 assert_eq!(av.raw_value, 16667);
2244 }
2245
2246 #[test]
2248 fn adjustment_value_from_normalized() {
2249 let av = AdjustmentValue::from_normalized("adj", 0.5);
2250 assert_eq!(av.raw_value, 50000);
2251 let av2 = AdjustmentValue::from_normalized("adj1", 0.25);
2252 assert_eq!(av2.raw_value, 25000);
2253 }
2254
2255 #[test]
2257 fn adjustment_value_effective_value() {
2258 let av = AdjustmentValue::new("adj", 16667);
2259 assert!((av.effective_value() - 0.16667).abs() < 0.0001);
2260 }
2261
2262 #[test]
2264 fn adjustment_value_write_xml() {
2265 let av = AdjustmentValue::new("adj", 16667);
2266 let mut w = XmlWriter::new();
2267 av.write_xml(&mut w);
2268 let xml = &w.buf;
2269 assert!(xml.contains("<a:gd"), "xml: {}", xml);
2270 assert!(xml.contains(r#"name="adj""#), "xml: {}", xml);
2271 assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
2272 }
2273
2274 #[test]
2276 fn geometry_preset_with_adjustments_write_xml() {
2277 let geom = Geometry::Preset(
2278 PresetGeometry::RoundRectangle,
2279 vec![AdjustmentValue::new("adj", 16667)],
2280 );
2281 let mut w = XmlWriter::new();
2282 geom.write_xml(&mut w);
2283 let xml = &w.buf;
2284 assert!(xml.contains(r#"prst="roundRect""#), "xml: {}", xml);
2285 assert!(xml.contains("<a:avLst>"), "xml: {}", xml);
2286 assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
2287 assert!(xml.contains("</a:avLst>"), "xml: {}", xml);
2288 }
2289
2290 #[test]
2292 fn geometry_preset_no_adjustments_write_xml() {
2293 let geom = Geometry::preset(PresetGeometry::Rectangle);
2294 let mut w = XmlWriter::new();
2295 geom.write_xml(&mut w);
2296 assert!(w.buf.contains("<a:avLst/>"), "xml: {}", w.buf);
2297 }
2298
2299 #[test]
2301 fn geometry_preset_helper() {
2302 let geom = Geometry::preset(PresetGeometry::Ellipse);
2303 match geom {
2304 Geometry::Preset(p, adj) => {
2305 assert_eq!(p, PresetGeometry::Ellipse);
2306 assert!(adj.is_empty());
2307 }
2308 _ => panic!("应为 Preset 变体"),
2309 }
2310 }
2311
2312 #[test]
2316 fn fill_format_gradient_switches_mode() {
2317 let mut fill = Fill::Inherit;
2318 let mut fmt = FillFormat::new(&mut fill);
2319 let g = fmt.gradient();
2320 g.stops.push(GradientStop {
2321 pos: 0,
2322 color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
2323 });
2324 g.stops.push(GradientStop {
2325 pos: 100_000,
2326 color: Color::RGB(RGBColor(0x00, 0x00, 0xFF)),
2327 });
2328 g.gradient_type = GradientType::Linear(2_700_000);
2329 assert!(matches!(fill, Fill::Gradient(_)));
2330 if let Fill::Gradient(g) = &fill {
2331 assert_eq!(g.stops.len(), 2);
2332 assert_eq!(g.stops[0].pos, 0);
2333 assert_eq!(g.stops[1].pos, 100_000);
2334 assert!(matches!(g.gradient_type, GradientType::Linear(2_700_000)));
2335 }
2336 }
2337
2338 #[test]
2340 fn fill_format_set_gradient_linear() {
2341 let mut fill = Fill::Inherit;
2342 let mut fmt = FillFormat::new(&mut fill);
2343 let stops = vec![
2344 GradientStop {
2345 pos: 0,
2346 color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
2347 },
2348 GradientStop {
2349 pos: 100_000,
2350 color: Color::RGB(RGBColor(0x00, 0xFF, 0x00)),
2351 },
2352 ];
2353 fmt.set_gradient_linear(stops, 5_400_000);
2354 assert!(matches!(fill, Fill::Gradient(_)));
2355 if let Fill::Gradient(g) = &fill {
2356 assert_eq!(g.stops.len(), 2);
2357 assert!(matches!(g.gradient_type, GradientType::Linear(5_400_000)));
2358 }
2359 }
2360
2361 #[test]
2363 fn fill_format_set_gradient_path() {
2364 let mut fill = Fill::Inherit;
2365 let mut fmt = FillFormat::new(&mut fill);
2366 let stops = vec![GradientStop {
2367 pos: 50_000,
2368 color: Color::RGB(RGBColor(0x80, 0x80, 0x80)),
2369 }];
2370 fmt.set_gradient_path(stops, GradientPath::Circle);
2371 if let Fill::Gradient(g) = &fill {
2372 assert!(matches!(
2373 g.gradient_type,
2374 GradientType::Path(GradientPath::Circle)
2375 ));
2376 }
2377 }
2378
2379 #[test]
2381 fn fill_format_pattern_switches_mode() {
2382 let mut fill = Fill::Inherit;
2383 let mut fmt = FillFormat::new(&mut fill);
2384 let p = fmt.pattern();
2385 p.prst = "horz".to_string();
2386 p.fg_color = Color::RGB(RGBColor(0xFF, 0x00, 0x00));
2387 p.bg_color = Color::RGB(RGBColor(0xFF, 0xFF, 0xFF));
2388 assert!(matches!(fill, Fill::Pattern(_)));
2389 if let Fill::Pattern(p) = &fill {
2390 assert_eq!(p.prst, "horz");
2391 }
2392 }
2393
2394 #[test]
2396 fn fill_format_set_pattern() {
2397 let mut fill = Fill::Inherit;
2398 let mut fmt = FillFormat::new(&mut fill);
2399 fmt.set_pattern(
2400 "cross",
2401 Color::RGB(RGBColor(0x00, 0x00, 0x00)),
2402 Color::RGB(RGBColor(0xFF, 0xFF, 0xFF)),
2403 );
2404 if let Fill::Pattern(p) = &fill {
2405 assert_eq!(p.prst, "cross");
2406 assert!(matches!(p.fg_color, Color::RGB(_)));
2407 assert!(matches!(p.bg_color, Color::RGB(_)));
2408 } else {
2409 panic!("应为 Pattern 变体");
2410 }
2411 }
2412
2413 #[test]
2415 fn fill_format_fill_type_for_gradient_and_pattern() {
2416 let mut fill = Fill::Gradient(GradientFill {
2417 stops: vec![],
2418 gradient_type: GradientType::Linear(0),
2419 flip: None,
2420 rot_with_shape: None,
2421 });
2422 let fmt = FillFormat::new(&mut fill);
2423 assert_eq!(
2424 fmt.fill_type(),
2425 crate::oxml::simpletypes::MsoFillType::Gradient
2426 );
2427
2428 let mut fill = Fill::Pattern(PatternFill::default());
2429 let fmt = FillFormat::new(&mut fill);
2430 assert_eq!(
2431 fmt.fill_type(),
2432 crate::oxml::simpletypes::MsoFillType::Pattern
2433 );
2434 }
2435
2436 #[test]
2438 fn effect_list_outer_shadow_serialization() {
2439 let mut effects = EffectList::default();
2440 assert!(effects.is_empty());
2441 effects.outer_shadow = Some(ShadowEffect {
2442 dir: 2_700_000,
2443 dist: 38100,
2444 blur_rad: 40000,
2445 color: Color::RGB(RGBColor(0x00, 0x00, 0x00)),
2446 rot_with_shape: Some(false),
2447 });
2448 assert!(!effects.is_empty());
2449
2450 let mut w = XmlWriter::new();
2451 effects.write_xml(&mut w);
2452 let buf = &w.buf;
2453 assert!(buf.contains("<a:effectLst>"));
2454 assert!(buf.contains("<a:outerShdw"));
2455 assert!(buf.contains("dir=\"2700000\""));
2456 assert!(buf.contains("dist=\"38100\""));
2457 assert!(buf.contains("blurRad=\"40000\""));
2458 assert!(buf.contains("rotWithShape=\"0\""));
2459 assert!(buf.contains("<a:srgbClr val=\"000000\""));
2460 assert!(buf.contains("</a:effectLst>"));
2461 }
2462
2463 #[test]
2465 fn effect_list_glow_and_soft_edge() {
2466 let effects = EffectList {
2467 glow: Some(GlowEffect {
2468 rad: 50000,
2469 color: Color::RGB(RGBColor(0xFF, 0x00, 0xFF)),
2470 }),
2471 soft_edge: Some(SoftEdgeEffect { rad: 25000 }),
2472 ..Default::default()
2473 };
2474
2475 let mut w = XmlWriter::new();
2476 effects.write_xml(&mut w);
2477 let buf = &w.buf;
2478 assert!(buf.contains("<a:glow rad=\"50000\">"));
2479 assert!(buf.contains("<a:srgbClr val=\"FF00FF\""));
2480 assert!(buf.contains("<a:softEdge rad=\"25000\"/>"));
2481 }
2482
2483 #[test]
2485 fn effect_list_reflection_serialization() {
2486 let effects = EffectList {
2487 reflection: Some(ReflectionEffect {
2488 blur_rad: Some(50000),
2489 st_a: Some(52000),
2490 st_pos: Some(0),
2491 end_a: Some(30000),
2492 end_pos: Some(50000),
2493 dist: Some(38100),
2494 dir: Some(5_400_000),
2495 rot_with_shape: None,
2496 }),
2497 ..Default::default()
2498 };
2499
2500 let mut w = XmlWriter::new();
2501 effects.write_xml(&mut w);
2502 let buf = &w.buf;
2503 assert!(buf.contains("<a:reflection"));
2504 assert!(buf.contains("blurRad=\"50000\""));
2505 assert!(buf.contains("stA=\"52000\""));
2506 assert!(buf.contains("endPos=\"50000\""));
2507 assert!(buf.contains("dist=\"38100\""));
2508 assert!(buf.contains("/>")); }
2510
2511 #[test]
2513 fn effect_list_empty_writes_nothing() {
2514 let effects = EffectList::default();
2515 let mut w = XmlWriter::new();
2516 effects.write_xml(&mut w);
2517 assert!(w.buf.is_empty());
2518 }
2519
2520 #[test]
2524 fn blip_fill_mode_stretch_serialize() {
2525 let mode = BlipFillMode::Stretch;
2526 let mut w = XmlWriter::new();
2527 mode.write_xml(&mut w);
2528 let s = &w.buf;
2529 assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
2530 assert!(s.contains("<a:fillRect/>"), "应输出 a:fillRect,实际: {s}");
2531 assert!(s.contains("</a:stretch>"), "应关闭 a:stretch,实际: {s}");
2532 }
2533
2534 #[test]
2536 fn blip_fill_mode_tile_serialize_full() {
2537 let mode = BlipFillMode::Tile {
2538 tx: Some(914400),
2539 ty: Some(457200),
2540 sx: Some(100_000),
2541 sy: Some(50_000),
2542 flip: Some("x".to_string()),
2543 algn: Some("tl".to_string()),
2544 };
2545 let mut w = XmlWriter::new();
2546 mode.write_xml(&mut w);
2547 let s = &w.buf;
2548 assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2549 assert!(s.contains("tx=\"914400\""), "应包含 tx,实际: {s}");
2550 assert!(s.contains("ty=\"457200\""), "应包含 ty,实际: {s}");
2551 assert!(s.contains("sx=\"100000\""), "应包含 sx,实际: {s}");
2552 assert!(s.contains("sy=\"50000\""), "应包含 sy,实际: {s}");
2553 assert!(s.contains("flip=\"x\""), "应包含 flip,实际: {s}");
2554 assert!(s.contains("algn=\"tl\""), "应包含 algn,实际: {s}");
2555 assert!(s.contains("/>"), "应为自闭合标签,实际: {s}");
2556 }
2557
2558 #[test]
2560 fn blip_fill_mode_tile_serialize_partial() {
2561 let mode = BlipFillMode::Tile {
2562 tx: None,
2563 ty: None,
2564 sx: Some(200_000),
2565 sy: None,
2566 flip: None,
2567 algn: Some("ctr".to_string()),
2568 };
2569 let mut w = XmlWriter::new();
2570 mode.write_xml(&mut w);
2571 let s = &w.buf;
2572 assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2573 assert!(!s.contains("tx="), "不应包含 tx,实际: {s}");
2574 assert!(!s.contains("ty="), "不应包含 ty,实际: {s}");
2575 assert!(s.contains("sx=\"200000\""), "应包含 sx,实际: {s}");
2576 assert!(!s.contains("sy="), "不应包含 sy,实际: {s}");
2577 assert!(s.contains("algn=\"ctr\""), "应包含 algn,实际: {s}");
2578 }
2579
2580 #[test]
2582 fn blip_fill_mode_none_serialize() {
2583 let mode = BlipFillMode::None;
2584 let mut w = XmlWriter::new();
2585 mode.write_xml(&mut w);
2586 assert!(w.buf.is_empty(), "None 模式不应写出任何 XML");
2587 }
2588
2589 #[test]
2591 fn fill_blip_with_stretch_serialize() {
2592 let fill = Fill::Blip {
2593 rid: "rIdImg1".to_string(),
2594 mode: BlipFillMode::Stretch,
2595 };
2596 let mut w = XmlWriter::new();
2597 fill.write_xml(&mut w);
2598 let s = &w.buf;
2599 assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
2600 assert!(
2601 s.contains("r:embed=\"rIdImg1\""),
2602 "应包含 r:embed,实际: {s}"
2603 );
2604 assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
2605 let blip_count = s.matches("<a:blip ").count()
2608 + s.matches("<a:blip/>").count()
2609 + s.matches("<a:blip>").count();
2610 assert_eq!(
2611 blip_count, 1,
2612 "应仅输出 1 个 <a:blip 标签,实际 {} 次(可能存在双重标签 bug): {s}",
2613 blip_count
2614 );
2615 assert!(
2617 !s.contains("<a:blip><a:blip"),
2618 "检测到嵌套的双重 <a:blip> 标签(BUG-001 回归): {s}"
2619 );
2620 }
2621
2622 #[test]
2624 fn fill_blip_with_tile_serialize() {
2625 let fill = Fill::Blip {
2626 rid: "rIdImg2".to_string(),
2627 mode: BlipFillMode::Tile {
2628 tx: Some(100),
2629 ty: None,
2630 sx: None,
2631 sy: None,
2632 flip: Some("xy".to_string()),
2633 algn: None,
2634 },
2635 };
2636 let mut w = XmlWriter::new();
2637 fill.write_xml(&mut w);
2638 let s = &w.buf;
2639 assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
2640 assert!(
2641 s.contains("r:embed=\"rIdImg2\""),
2642 "应包含 r:embed,实际: {s}"
2643 );
2644 assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2645 assert!(s.contains("tx=\"100\""), "应包含 tx,实际: {s}");
2646 assert!(s.contains("flip=\"xy\""), "应包含 flip,实际: {s}");
2647 }
2648
2649 #[test]
2651 fn blip_fill_mode_default_is_stretch() {
2652 let mode = BlipFillMode::default();
2653 assert!(matches!(mode, BlipFillMode::Stretch));
2654 }
2655
2656 #[test]
2660 fn rotation_3d_serialize() {
2661 let rot = Rotation3d {
2662 lat: 0,
2663 lon: 0,
2664 rev: 1200000,
2665 };
2666 let mut w = XmlWriter::new();
2667 rot.write_xml(&mut w);
2668 let s = &w.buf;
2669 assert!(s.contains("<a:rot"), "应输出 a:rot,实际: {s}");
2670 assert!(s.contains("lat=\"0\""), "应包含 lat,实际: {s}");
2671 assert!(s.contains("lon=\"0\""), "应包含 lon,实际: {s}");
2672 assert!(s.contains("rev=\"1200000\""), "应包含 rev,实际: {s}");
2673 assert!(s.contains("/>"), "应为自闭合,实际: {s}");
2674 }
2675
2676 #[test]
2678 fn camera_default_serialize() {
2679 let camera = Camera::default();
2680 let mut w = XmlWriter::new();
2681 camera.write_xml(&mut w);
2682 let s = &w.buf;
2683 assert!(
2684 s.contains("<a:camera prst=\"orthographicFront\""),
2685 "应包含默认 prst,实际: {s}"
2686 );
2687 assert!(!s.contains("fov="), "默认 fov 不应输出,实际: {s}");
2689 assert!(!s.contains("zoom="), "默认 zoom 不应输出,实际: {s}");
2690 }
2691
2692 #[test]
2694 fn camera_with_rotation_serialize() {
2695 let camera = Camera {
2696 preset: CameraPreset::PerspectiveFront,
2697 fov: 3600000,
2698 zoom: 200000,
2701 rotation: Some(Rotation3d {
2702 lat: 30,
2703 lon: 45,
2704 rev: 0,
2705 }),
2706 };
2707 let mut w = XmlWriter::new();
2708 camera.write_xml(&mut w);
2709 let s = &w.buf;
2710 assert!(
2711 s.contains("<a:camera prst=\"perspectiveFront\""),
2712 "应包含 perspectiveFront,实际: {s}"
2713 );
2714 assert!(s.contains("fov=\"3600000\""), "应包含 fov,实际: {s}");
2715 assert!(s.contains("zoom=\"200000\""), "应包含 zoom,实际: {s}");
2716 assert!(s.contains("<a:rot"), "应包含 a:rot 子元素,实际: {s}");
2717 assert!(s.contains("</a:camera>"), "应关闭 a:camera,实际: {s}");
2718 }
2719
2720 #[test]
2722 fn scene3d_full_serialize() {
2723 let scene = Scene3d {
2724 camera: Camera {
2725 preset: CameraPreset::OrthographicFront,
2726 fov: 0,
2727 zoom: 0,
2728 rotation: Some(Rotation3d {
2729 lat: 0,
2730 lon: 0,
2731 rev: 0,
2732 }),
2733 },
2734 light_rig: LightRig {
2735 rig: LightRigType::ThreePt,
2736 dir: LightRigDirection::Top,
2737 rotation: Some(Rotation3d {
2738 lat: 0,
2739 lon: 0,
2740 rev: 1200000,
2741 }),
2742 },
2743 backdrop: None,
2744 };
2745 let mut w = XmlWriter::new();
2746 scene.write_xml(&mut w);
2747 let s = &w.buf;
2748 assert!(s.contains("<a:scene3d>"), "应输出 a:scene3d,实际: {s}");
2749 assert!(
2750 s.contains("<a:camera prst=\"orthographicFront\">"),
2751 "应包含 camera 子元素,实际: {s}"
2752 );
2753 assert!(
2754 s.contains("<a:lightRig rig=\"threePt\" dir=\"t\">"),
2755 "应包含 lightRig 子元素,实际: {s}"
2756 );
2757 assert!(s.contains("</a:scene3d>"), "应关闭 a:scene3d,实际: {s}");
2758 }
2759
2760 #[test]
2762 fn sp3d_default_serialize() {
2763 let sp3d = Sp3d::default();
2764 let mut w = XmlWriter::new();
2765 sp3d.write_xml(&mut w);
2766 let s = &w.buf;
2767 assert!(s.contains("<a:sp3d"), "应输出 a:sp3d,实际: {s}");
2768 assert!(
2770 !s.contains("prstMaterial="),
2771 "默认 warmMatte 不应输出,实际: {s}"
2772 );
2773 }
2774
2775 #[test]
2777 fn sp3d_with_bevel_and_colors_serialize() {
2778 let sp3d = Sp3d {
2779 extrusion_h: 38100,
2780 contour_w: 12700,
2781 prst_material: MaterialPreset::Metallic,
2782 bevel_top: Some(Bevel { w: 63500, h: 25400 }),
2783 bevel_bottom: None,
2784 extrusion_color: Some(Color::RGB(crate::units::RGBColor::BLACK)),
2785 contour_color: Some(Color::RGB(crate::units::RGBColor::WHITE)),
2786 };
2787 let mut w = XmlWriter::new();
2788 sp3d.write_xml(&mut w);
2789 let s = &w.buf;
2790 assert!(
2791 s.contains("extrusionH=\"38100\""),
2792 "应包含 extrusionH,实际: {s}"
2793 );
2794 assert!(
2795 s.contains("contourW=\"12700\""),
2796 "应包含 contourW,实际: {s}"
2797 );
2798 assert!(
2799 s.contains("prstMaterial=\"metallic\""),
2800 "应包含 prstMaterial,实际: {s}"
2801 );
2802 assert!(
2803 s.contains("<a:bevelT w=\"63500\" h=\"25400\"/>"),
2804 "应包含 bevelT,实际: {s}"
2805 );
2806 assert!(
2807 s.contains("<a:extrusionClr>"),
2808 "应包含 extrusionClr,实际: {s}"
2809 );
2810 assert!(s.contains("<a:contourClr>"), "应包含 contourClr,实际: {s}");
2811 assert!(s.contains("</a:sp3d>"), "应关闭 a:sp3d,实际: {s}");
2812 }
2813
2814 #[test]
2816 fn shape_properties_with_3d_serialize() {
2817 let sp = ShapeProperties {
2818 scene3d: Some(Scene3d::default()),
2819 sp3d: Some(Sp3d::default()),
2820 ..Default::default()
2821 };
2822 let mut w = XmlWriter::new();
2823 sp.write_xml(&mut w, "p:spPr");
2824 let s = &w.buf;
2825 assert!(s.contains("<a:scene3d>"), "应输出 scene3d,实际: {s}");
2827 assert!(s.contains("<a:sp3d"), "应输出 sp3d,实际: {s}");
2828 let scene_pos = s.find("<a:scene3d>").expect("scene3d 应存在");
2830 let sp3d_pos = s.find("<a:sp3d").expect("sp3d 应存在");
2831 assert!(
2832 scene_pos < sp3d_pos,
2833 "scene3d 应在 sp3d 之前,实际 scene3d@{scene_pos} sp3d@{sp3d_pos}"
2834 );
2835 }
2836
2837 #[test]
2839 fn camera_preset_from_str() {
2840 assert!(matches!(
2841 CameraPreset::parse("orthographicFront"),
2842 CameraPreset::OrthographicFront
2843 ));
2844 assert!(matches!(
2845 CameraPreset::parse("perspectiveFront"),
2846 CameraPreset::PerspectiveFront
2847 ));
2848 match CameraPreset::parse("customUnknown") {
2850 CameraPreset::Other(s) => assert_eq!(s, "customUnknown"),
2851 other => panic!("未知预设应归入 Other,实际: {other:?}"),
2852 }
2853 }
2854
2855 #[test]
2857 fn material_preset_from_str() {
2858 assert!(matches!(
2859 MaterialPreset::parse("warmMatte"),
2860 MaterialPreset::WarmMatte
2861 ));
2862 assert!(matches!(
2863 MaterialPreset::parse("metallic"),
2864 MaterialPreset::Metallic
2865 ));
2866 match MaterialPreset::parse("futureMaterial") {
2868 MaterialPreset::Other(s) => assert_eq!(s, "futureMaterial"),
2869 other => panic!("未知材质应归入 Other,实际: {other:?}"),
2870 }
2871 }
2872
2873 #[test]
2877 fn backdrop_default_serialize() {
2878 let bd = Backdrop::default();
2879 let mut w = XmlWriter::new();
2880 bd.write_xml(&mut w);
2881 let s = &w.buf;
2882 assert!(s.contains("<a:backdrop>"), "应输出 a:backdrop,实际: {s}");
2883 assert!(s.contains("</a:backdrop>"), "应关闭 a:backdrop,实际: {s}");
2884 assert!(!s.contains("<a:floor/>"), "默认不应输出 floor: {s}");
2887 assert!(!s.contains("<a:wall/>"), "默认不应输出 wall: {s}");
2888 assert!(!s.contains("<a:l/>"), "默认不应输出 l: {s}");
2889 assert!(!s.contains("<a:r/>"), "默认不应输出 r: {s}");
2890 assert!(!s.contains("<a:t/>"), "默认不应输出 t: {s}");
2891 assert!(!s.contains("<a:b/>"), "默认不应输出 b: {s}");
2892 }
2893
2894 #[test]
2896 fn backdrop_with_anchor_and_all_planes_serialize() {
2897 let bd = Backdrop {
2898 anchor: Some(Point3d {
2899 x: 100000,
2900 y: 200000,
2901 z: 300000,
2902 }),
2903 floor: true,
2904 wall: true,
2905 left: true,
2906 right: true,
2907 top: true,
2908 bottom: true,
2909 };
2910 let mut w = XmlWriter::new();
2911 bd.write_xml(&mut w);
2912 let s = &w.buf;
2913 assert!(
2915 s.contains("<a:anchor x=\"100000\" y=\"200000\" z=\"300000\"/>"),
2916 "应包含 anchor,实际: {s}"
2917 );
2918 assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2920 assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2921 assert!(s.contains("<a:l/>"), "应包含 l: {s}");
2922 assert!(s.contains("<a:r/>"), "应包含 r: {s}");
2923 assert!(s.contains("<a:t/>"), "应包含 t: {s}");
2924 assert!(s.contains("<a:b/>"), "应包含 b: {s}");
2925 let anchor_pos = s.find("<a:anchor").expect("anchor 应存在");
2928 let floor_pos = s.find("<a:floor/>").expect("floor 应存在");
2929 let wall_pos = s.find("<a:wall/>").expect("wall 应存在");
2930 let l_pos = s.find("<a:l/>").expect("l 应存在");
2931 let r_pos = s.find("<a:r/>").expect("r 应存在");
2932 let t_pos = s.find("<a:t/>").expect("t 应存在");
2933 let b_pos = s.find("<a:b/>").expect("b 应存在");
2934 assert!(anchor_pos < floor_pos, "anchor 应在 floor 之前");
2935 assert!(floor_pos < wall_pos, "floor 应在 wall 之前");
2936 assert!(wall_pos < l_pos, "wall 应在 l 之前");
2937 assert!(l_pos < r_pos, "l 应在 r 之前");
2938 assert!(r_pos < t_pos, "r 应在 t 之前");
2939 assert!(t_pos < b_pos, "t 应在 b 之前");
2940 }
2941
2942 #[test]
2944 fn backdrop_partial_planes_serialize() {
2945 let bd = Backdrop {
2946 floor: true,
2947 wall: true,
2948 ..Default::default()
2949 };
2950 let mut w = XmlWriter::new();
2951 bd.write_xml(&mut w);
2952 let s = &w.buf;
2953 assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2954 assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2955 assert!(!s.contains("<a:l/>"), "不应包含 l: {s}");
2957 assert!(!s.contains("<a:r/>"), "不应包含 r: {s}");
2958 assert!(!s.contains("<a:t/>"), "不应包含 t: {s}");
2959 assert!(!s.contains("<a:b/>"), "不应包含 b: {s}");
2960 }
2961
2962 #[test]
2964 fn scene3d_with_backdrop_serialize() {
2965 let scene = Scene3d {
2966 camera: Camera::default(),
2967 light_rig: LightRig::default(),
2968 backdrop: Some(Backdrop {
2969 floor: true,
2970 wall: true,
2971 ..Default::default()
2972 }),
2973 };
2974 let mut w = XmlWriter::new();
2975 scene.write_xml(&mut w);
2976 let s = &w.buf;
2977 assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
2978 assert!(s.contains("<a:backdrop>"), "应包含 backdrop: {s}");
2979 assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2980 assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2981 let cam_pos = s.find("<a:camera").expect("camera 应存在");
2983 let rig_pos = s.find("<a:lightRig").expect("lightRig 应存在");
2984 let bd_pos = s.find("<a:backdrop>").expect("backdrop 应存在");
2985 assert!(cam_pos < rig_pos, "camera 应在 lightRig 之前");
2986 assert!(rig_pos < bd_pos, "lightRig 应在 backdrop 之前");
2987 }
2988
2989 #[test]
2991 fn scene3d_without_backdrop_no_element() {
2992 let scene = Scene3d::default();
2993 let mut w = XmlWriter::new();
2994 scene.write_xml(&mut w);
2995 let s = &w.buf;
2996 assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
2997 assert!(!s.contains("<a:backdrop>"), "无 backdrop 时不应输出: {s}");
2998 }
2999
3000 #[test]
3002 fn point3d_serialize() {
3003 let p = Point3d {
3004 x: -100000,
3005 y: 0,
3006 z: 500000,
3007 };
3008 let mut w = XmlWriter::new();
3009 p.write_xml(&mut w);
3010 let s = &w.buf;
3011 assert!(s.contains("x=\"-100000\""), "应包含 x: {s}");
3012 assert!(s.contains("y=\"0\""), "应包含 y: {s}");
3013 assert!(s.contains("z=\"500000\""), "应包含 z: {s}");
3014 }
3015}