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, tag: &str) {
1217 if self.xfrm.is_empty() {
1218 w.empty(tag);
1220 return;
1221 }
1222 w.open(tag);
1223 self.xfrm.write_xml(w);
1224 w.close(tag);
1225 }
1226
1227 pub fn effects(&self) -> Option<&EffectList> {
1236 self.effects.as_ref()
1237 }
1238
1239 pub fn effects_mut(&mut self) -> &mut EffectList {
1244 self.effects.get_or_insert_with(EffectList::default)
1245 }
1246
1247 pub fn set_outer_shadow(&mut self, shadow: ShadowEffect) {
1254 self.effects_mut().outer_shadow = Some(shadow);
1255 }
1256
1257 pub fn set_inner_shadow(&mut self, shadow: ShadowEffect) {
1259 self.effects_mut().inner_shadow = Some(shadow);
1260 }
1261
1262 pub fn set_glow(&mut self, glow: GlowEffect) {
1264 self.effects_mut().glow = Some(glow);
1265 }
1266
1267 pub fn set_soft_edge(&mut self, rad: i64) {
1269 self.effects_mut().soft_edge = Some(SoftEdgeEffect { rad });
1270 }
1271
1272 pub fn set_reflection(&mut self, reflection: ReflectionEffect) {
1274 self.effects_mut().reflection = Some(reflection);
1275 }
1276
1277 pub fn clear_outer_shadow(&mut self) {
1279 if let Some(e) = self.effects.as_mut() {
1280 e.outer_shadow = None;
1281 }
1282 }
1283
1284 pub fn clear_inner_shadow(&mut self) {
1286 if let Some(e) = self.effects.as_mut() {
1287 e.inner_shadow = None;
1288 }
1289 }
1290
1291 pub fn clear_glow(&mut self) {
1293 if let Some(e) = self.effects.as_mut() {
1294 e.glow = None;
1295 }
1296 }
1297
1298 pub fn clear_soft_edge(&mut self) {
1300 if let Some(e) = self.effects.as_mut() {
1301 e.soft_edge = None;
1302 }
1303 }
1304
1305 pub fn clear_reflection(&mut self) {
1307 if let Some(e) = self.effects.as_mut() {
1308 e.reflection = None;
1309 }
1310 }
1311
1312 pub fn clear_effects(&mut self) {
1314 self.effects = None;
1315 }
1316}
1317
1318use crate::oxml::color::ColorFormat;
1323
1324#[derive(Debug)]
1338pub struct FillFormat<'a> {
1339 fill: &'a mut Fill,
1341}
1342
1343impl<'a> FillFormat<'a> {
1344 pub fn new(fill: &'a mut Fill) -> Self {
1346 FillFormat { fill }
1347 }
1348
1349 pub fn fill(&self) -> &Fill {
1351 self.fill
1352 }
1353 pub fn fill_mut(&mut self) -> &mut Fill {
1355 self.fill
1356 }
1357
1358 pub fn fill_type(&self) -> super::simpletypes::MsoFillType {
1360 match self.fill {
1361 Fill::None => super::simpletypes::MsoFillType::Background,
1362 Fill::Solid(_) => super::simpletypes::MsoFillType::Solid,
1363 Fill::Blip { .. } => super::simpletypes::MsoFillType::Picture,
1364 Fill::Gradient(_) => super::simpletypes::MsoFillType::Gradient,
1365 Fill::Pattern(_) => super::simpletypes::MsoFillType::Pattern,
1366 Fill::Inherit => super::simpletypes::MsoFillType::Inherit,
1367 }
1368 }
1369
1370 pub fn solid(&mut self) -> ColorFormat<'_> {
1376 if !matches!(self.fill, Fill::Solid(_)) {
1378 *self.fill = Fill::Solid(Color::None);
1379 }
1380 match self.fill {
1381 Fill::Solid(c) => ColorFormat::new(c),
1382 _ => unreachable!("just set to Solid"),
1383 }
1384 }
1385
1386 pub fn set_none(&mut self) {
1390 *self.fill = Fill::None;
1391 }
1392
1393 pub fn set_picture(&mut self, rid: impl Into<String>, mode: BlipFillMode) {
1399 *self.fill = Fill::Blip {
1400 rid: rid.into(),
1401 mode,
1402 };
1403 }
1404
1405 pub fn clear(&mut self) {
1407 *self.fill = Fill::Inherit;
1408 }
1409
1410 pub fn set_solid_rgb(&mut self, c: impl Into<crate::units::RGBColor>) {
1412 *self.fill = Fill::Solid(Color::RGB(c.into()));
1413 }
1414 pub fn set_solid_theme(&mut self, t: super::simpletypes::MsoThemeColorIndex) {
1416 if let Some(s) = t.as_str() {
1417 if let Ok(sc) = s.parse::<crate::oxml::color::SchemeColor>() {
1418 *self.fill = Fill::Solid(Color::Scheme(sc));
1419 }
1420 }
1421 }
1422
1423 pub fn gradient(&mut self) -> &mut GradientFill {
1445 if !matches!(self.fill, Fill::Gradient(_)) {
1446 *self.fill = Fill::Gradient(GradientFill {
1447 stops: Vec::new(),
1448 gradient_type: GradientType::Linear(0),
1449 flip: None,
1450 rot_with_shape: None,
1451 });
1452 }
1453 match &mut self.fill {
1454 Fill::Gradient(g) => g,
1455 _ => unreachable!("just set to Gradient"),
1456 }
1457 }
1458
1459 pub fn set_gradient_linear(&mut self, stops: Vec<GradientStop>, angle: i32) {
1465 *self.fill = Fill::Gradient(GradientFill {
1466 stops,
1467 gradient_type: GradientType::Linear(angle),
1468 flip: None,
1469 rot_with_shape: None,
1470 });
1471 }
1472
1473 pub fn set_gradient_path(&mut self, stops: Vec<GradientStop>, path: GradientPath) {
1479 *self.fill = Fill::Gradient(GradientFill {
1480 stops,
1481 gradient_type: GradientType::Path(path),
1482 flip: None,
1483 rot_with_shape: None,
1484 });
1485 }
1486
1487 pub fn pattern(&mut self) -> &mut PatternFill {
1494 if !matches!(self.fill, Fill::Pattern(_)) {
1495 *self.fill = Fill::Pattern(PatternFill {
1496 prst: String::new(),
1497 fg_color: Color::None,
1498 bg_color: Color::None,
1499 });
1500 }
1501 match &mut self.fill {
1502 Fill::Pattern(p) => p,
1503 _ => unreachable!("just set to Pattern"),
1504 }
1505 }
1506
1507 pub fn set_pattern(&mut self, prst: impl Into<String>, fg: Color, bg: Color) {
1514 *self.fill = Fill::Pattern(PatternFill {
1515 prst: prst.into(),
1516 fg_color: fg,
1517 bg_color: bg,
1518 });
1519 }
1520}
1521
1522impl<'a> From<&'a mut Fill> for FillFormat<'a> {
1523 fn from(f: &'a mut Fill) -> Self {
1524 FillFormat::new(f)
1525 }
1526}
1527
1528#[derive(Debug)]
1536pub struct LineFormat<'a> {
1537 line: &'a mut Line,
1539}
1540
1541impl<'a> LineFormat<'a> {
1542 pub fn new(line: &'a mut Line) -> Self {
1544 LineFormat { line }
1545 }
1546
1547 pub fn line(&self) -> &Line {
1549 self.line
1550 }
1551 pub fn line_mut(&mut self) -> &mut Line {
1553 self.line
1554 }
1555
1556 pub fn color(&mut self) -> ColorFormat<'_> {
1558 ColorFormat::new(&mut self.line.color)
1559 }
1560
1561 pub fn width(&self) -> Option<crate::units::Emu> {
1563 self.line.width
1564 }
1565 pub fn set_width(&mut self, w: crate::units::Emu) {
1567 self.line.width = Some(w);
1568 }
1569
1570 pub fn dash_style(&self) -> Option<Dash> {
1572 self.line.dash
1573 }
1574 pub fn set_dash_style(&mut self, s: super::simpletypes::MsoLineDashStyle) {
1576 self.line.dash = Some(s.into());
1577 }
1578
1579 pub fn set_no_fill(&mut self) {
1581 self.line.no_fill = true;
1582 self.line.color = crate::oxml::color::Color::None;
1583 }
1584
1585 pub fn set_width_pt(&mut self, pt: crate::units::Pt) {
1587 self.line.width = Some(crate::units::Emu((pt.value() * 12_700.0) as i64));
1588 }
1589}
1590
1591impl<'a> From<&'a mut Line> for LineFormat<'a> {
1592 fn from(l: &'a mut Line) -> Self {
1593 LineFormat::new(l)
1594 }
1595}
1596
1597#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1627pub struct Rotation3d {
1628 pub lat: i32,
1630 pub lon: i32,
1632 pub rev: i32,
1634}
1635
1636impl Rotation3d {
1637 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1639 let lat = self.lat.to_string();
1640 let lon = self.lon.to_string();
1641 let rev = self.rev.to_string();
1642 w.empty_with(
1643 "a:rot",
1644 &[
1645 ("lat", lat.as_str()),
1646 ("lon", lon.as_str()),
1647 ("rev", rev.as_str()),
1648 ],
1649 );
1650 }
1651}
1652
1653#[derive(Clone, Debug, Default, PartialEq, Eq)]
1657pub enum CameraPreset {
1658 #[default]
1660 OrthographicFront,
1661 IsometricOffAxis1,
1663 IsometricOffAxis2,
1664 IsometricOffAxis3,
1665 IsometricOffAxis4,
1666 IsometricLeftDown,
1668 IsometricLeftUp,
1669 IsometricRightDown,
1670 IsometricRightUp,
1671 PerspectiveFront,
1673 PerspectiveLeft,
1675 PerspectiveRight,
1676 Other(String),
1678}
1679
1680impl CameraPreset {
1681 pub fn as_str(&self) -> &str {
1683 match self {
1684 CameraPreset::OrthographicFront => "orthographicFront",
1685 CameraPreset::IsometricOffAxis1 => "isometricOffAxis1",
1686 CameraPreset::IsometricOffAxis2 => "isometricOffAxis2",
1687 CameraPreset::IsometricOffAxis3 => "isometricOffAxis3",
1688 CameraPreset::IsometricOffAxis4 => "isometricOffAxis4",
1689 CameraPreset::IsometricLeftDown => "isometricLeftDown",
1690 CameraPreset::IsometricLeftUp => "isometricLeftUp",
1691 CameraPreset::IsometricRightDown => "isometricRightDown",
1692 CameraPreset::IsometricRightUp => "isometricRightUp",
1693 CameraPreset::PerspectiveFront => "perspectiveFront",
1694 CameraPreset::PerspectiveLeft => "perspectiveLeft",
1695 CameraPreset::PerspectiveRight => "perspectiveRight",
1696 CameraPreset::Other(s) => s.as_str(),
1697 }
1698 }
1699
1700 pub fn parse(s: &str) -> Self {
1704 match s {
1705 "orthographicFront" => CameraPreset::OrthographicFront,
1706 "isometricOffAxis1" => CameraPreset::IsometricOffAxis1,
1707 "isometricOffAxis2" => CameraPreset::IsometricOffAxis2,
1708 "isometricOffAxis3" => CameraPreset::IsometricOffAxis3,
1709 "isometricOffAxis4" => CameraPreset::IsometricOffAxis4,
1710 "isometricLeftDown" => CameraPreset::IsometricLeftDown,
1711 "isometricLeftUp" => CameraPreset::IsometricLeftUp,
1712 "isometricRightDown" => CameraPreset::IsometricRightDown,
1713 "isometricRightUp" => CameraPreset::IsometricRightUp,
1714 "perspectiveFront" => CameraPreset::PerspectiveFront,
1715 "perspectiveLeft" => CameraPreset::PerspectiveLeft,
1716 "perspectiveRight" => CameraPreset::PerspectiveRight,
1717 other => CameraPreset::Other(other.to_string()),
1718 }
1719 }
1720}
1721
1722#[derive(Clone, Debug, Default, PartialEq, Eq)]
1726pub struct Camera {
1727 pub preset: CameraPreset,
1729 pub fov: i32,
1731 pub zoom: i32,
1733 pub rotation: Option<Rotation3d>,
1735}
1736
1737impl Camera {
1738 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1740 let prst = self.preset.as_str();
1741 let mut attrs: Vec<(&str, &str)> = vec![("prst", prst)];
1742 let fov_s;
1743 if self.fov != 0 {
1744 fov_s = self.fov.to_string();
1745 attrs.push(("fov", fov_s.as_str()));
1746 }
1747 let zoom_s;
1748 if self.zoom != 0 && self.zoom != 100000 {
1749 zoom_s = self.zoom.to_string();
1750 attrs.push(("zoom", zoom_s.as_str()));
1751 }
1752 if let Some(rot) = &self.rotation {
1753 w.open_with("a:camera", &attrs);
1754 rot.write_xml(w);
1755 w.close("a:camera");
1756 } else {
1757 w.empty_with("a:camera", &attrs);
1758 }
1759 }
1760}
1761
1762#[derive(Clone, Debug, Default, PartialEq, Eq)]
1764pub enum LightRigType {
1765 #[default]
1767 Balanced,
1768 Bright,
1770 Chilly,
1772 Contrasting,
1774 Flat,
1776 Harsh,
1778 Morning,
1780 Soft,
1782 Sunrise,
1784 Sunset,
1786 ThreePt,
1788 TwoPt,
1790 Other(String),
1792}
1793
1794impl LightRigType {
1795 pub fn as_str(&self) -> &str {
1797 match self {
1798 LightRigType::Balanced => "balanced",
1799 LightRigType::Bright => "bright",
1800 LightRigType::Chilly => "chilly",
1801 LightRigType::Contrasting => "contrasting",
1802 LightRigType::Flat => "flat",
1803 LightRigType::Harsh => "harsh",
1804 LightRigType::Morning => "morning",
1805 LightRigType::Soft => "soft",
1806 LightRigType::Sunrise => "sunrise",
1807 LightRigType::Sunset => "sunset",
1808 LightRigType::ThreePt => "threePt",
1809 LightRigType::TwoPt => "twoPt",
1810 LightRigType::Other(s) => s.as_str(),
1811 }
1812 }
1813
1814 pub fn parse(s: &str) -> Self {
1818 match s {
1819 "balanced" => LightRigType::Balanced,
1820 "bright" => LightRigType::Bright,
1821 "chilly" => LightRigType::Chilly,
1822 "contrasting" => LightRigType::Contrasting,
1823 "flat" => LightRigType::Flat,
1824 "harsh" => LightRigType::Harsh,
1825 "morning" => LightRigType::Morning,
1826 "soft" => LightRigType::Soft,
1827 "sunrise" => LightRigType::Sunrise,
1828 "sunset" => LightRigType::Sunset,
1829 "threePt" => LightRigType::ThreePt,
1830 "twoPt" => LightRigType::TwoPt,
1831 other => LightRigType::Other(other.to_string()),
1832 }
1833 }
1834}
1835
1836#[derive(Clone, Debug, Default, PartialEq, Eq)]
1838pub enum LightRigDirection {
1839 TopLeft,
1841 #[default]
1843 Top,
1844 TopRight,
1846 Left,
1848 Right,
1850 BottomLeft,
1852 Bottom,
1854 BottomRight,
1856}
1857
1858impl LightRigDirection {
1859 pub fn as_str(&self) -> &str {
1861 match self {
1862 LightRigDirection::TopLeft => "tl",
1863 LightRigDirection::Top => "t",
1864 LightRigDirection::TopRight => "tr",
1865 LightRigDirection::Left => "l",
1866 LightRigDirection::Right => "r",
1867 LightRigDirection::BottomLeft => "bl",
1868 LightRigDirection::Bottom => "b",
1869 LightRigDirection::BottomRight => "br",
1870 }
1871 }
1872
1873 pub fn parse(s: &str) -> Self {
1877 match s {
1878 "tl" => LightRigDirection::TopLeft,
1879 "t" => LightRigDirection::Top,
1880 "tr" => LightRigDirection::TopRight,
1881 "l" => LightRigDirection::Left,
1882 "r" => LightRigDirection::Right,
1883 "bl" => LightRigDirection::BottomLeft,
1884 "b" => LightRigDirection::Bottom,
1885 "br" => LightRigDirection::BottomRight,
1886 _ => LightRigDirection::Top,
1887 }
1888 }
1889}
1890
1891#[derive(Clone, Debug, Default, PartialEq, Eq)]
1893pub struct LightRig {
1894 pub rig: LightRigType,
1896 pub dir: LightRigDirection,
1898 pub rotation: Option<Rotation3d>,
1900}
1901
1902impl LightRig {
1903 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1905 let rig = self.rig.as_str();
1906 let dir = self.dir.as_str();
1907 if let Some(rot) = &self.rotation {
1908 w.open_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
1909 rot.write_xml(w);
1910 w.close("a:lightRig");
1911 } else {
1912 w.empty_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
1913 }
1914 }
1915}
1916
1917#[derive(Clone, Debug, Default, PartialEq, Eq)]
1921pub struct Scene3d {
1922 pub camera: Camera,
1924 pub light_rig: LightRig,
1926 pub backdrop: Option<Backdrop>,
1931}
1932
1933impl Scene3d {
1934 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1946 w.open("a:scene3d");
1947 self.camera.write_xml(w);
1948 self.light_rig.write_xml(w);
1949 if let Some(b) = &self.backdrop {
1950 b.write_xml(w);
1951 }
1952 w.close("a:scene3d");
1953 }
1954}
1955
1956#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1960pub struct Point3d {
1961 pub x: i32,
1963 pub y: i32,
1965 pub z: i32,
1967}
1968
1969impl Point3d {
1970 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1972 let xs = self.x.to_string();
1973 let ys = self.y.to_string();
1974 let zs = self.z.to_string();
1975 w.empty_with(
1976 "a:anchor",
1977 &[("x", xs.as_str()), ("y", ys.as_str()), ("z", zs.as_str())],
1978 );
1979 }
1980}
1981
1982#[derive(Clone, Debug, Default, PartialEq, Eq)]
2005pub struct Backdrop {
2006 pub anchor: Option<Point3d>,
2010 pub floor: bool,
2012 pub wall: bool,
2014 pub left: bool,
2016 pub right: bool,
2018 pub top: bool,
2020 pub bottom: bool,
2022}
2023
2024impl Backdrop {
2025 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
2029 w.open("a:backdrop");
2030 if let Some(a) = &self.anchor {
2031 a.write_xml(w);
2032 }
2033 if self.floor {
2034 w.empty("a:floor");
2035 }
2036 if self.wall {
2037 w.empty("a:wall");
2038 }
2039 if self.left {
2040 w.empty("a:l");
2041 }
2042 if self.right {
2043 w.empty("a:r");
2044 }
2045 if self.top {
2046 w.empty("a:t");
2047 }
2048 if self.bottom {
2049 w.empty("a:b");
2050 }
2051 w.close("a:backdrop");
2052 }
2053}
2054
2055#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2059pub struct Bevel {
2060 pub w: i32,
2062 pub h: i32,
2064}
2065
2066impl Bevel {
2067 pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
2069 let w_s = self.w.to_string();
2070 let h_s = self.h.to_string();
2071 w.empty_with(tag, &[("w", w_s.as_str()), ("h", h_s.as_str())]);
2072 }
2073}
2074
2075#[derive(Clone, Debug, Default, PartialEq, Eq)]
2077pub enum MaterialPreset {
2078 #[default]
2080 WarmMatte,
2081 Clear,
2083 DarkEdge,
2085 Flat,
2087 LegacyMatte,
2089 LegacyMetallic,
2091 LegacyPlastic,
2093 LegacyWireframe,
2095 Matte,
2097 Metallic,
2099 Plastic,
2101 Powder,
2103 SoftEdge,
2105 SoftMetal,
2107 Other(String),
2109}
2110
2111impl MaterialPreset {
2112 pub fn as_str(&self) -> &str {
2114 match self {
2115 MaterialPreset::WarmMatte => "warmMatte",
2116 MaterialPreset::Clear => "clear",
2117 MaterialPreset::DarkEdge => "dkEdge",
2118 MaterialPreset::Flat => "flat",
2119 MaterialPreset::LegacyMatte => "legacyMatte",
2120 MaterialPreset::LegacyMetallic => "legacyMetallic",
2121 MaterialPreset::LegacyPlastic => "legacyPlastic",
2122 MaterialPreset::LegacyWireframe => "legacyWireframe",
2123 MaterialPreset::Matte => "matte",
2124 MaterialPreset::Metallic => "metallic",
2125 MaterialPreset::Plastic => "plastic",
2126 MaterialPreset::Powder => "powder",
2127 MaterialPreset::SoftEdge => "softEdge",
2128 MaterialPreset::SoftMetal => "softmetal",
2129 MaterialPreset::Other(s) => s.as_str(),
2130 }
2131 }
2132
2133 pub fn parse(s: &str) -> Self {
2137 match s {
2138 "warmMatte" => MaterialPreset::WarmMatte,
2139 "clear" => MaterialPreset::Clear,
2140 "dkEdge" => MaterialPreset::DarkEdge,
2141 "flat" => MaterialPreset::Flat,
2142 "legacyMatte" => MaterialPreset::LegacyMatte,
2143 "legacyMetallic" => MaterialPreset::LegacyMetallic,
2144 "legacyPlastic" => MaterialPreset::LegacyPlastic,
2145 "legacyWireframe" => MaterialPreset::LegacyWireframe,
2146 "matte" => MaterialPreset::Matte,
2147 "metallic" => MaterialPreset::Metallic,
2148 "plastic" => MaterialPreset::Plastic,
2149 "powder" => MaterialPreset::Powder,
2150 "softEdge" => MaterialPreset::SoftEdge,
2151 "softmetal" => MaterialPreset::SoftMetal,
2152 other => MaterialPreset::Other(other.to_string()),
2153 }
2154 }
2155}
2156
2157#[derive(Clone, Debug, Default, PartialEq, Eq)]
2161pub struct Sp3d {
2162 pub extrusion_h: i32,
2164 pub contour_w: i32,
2166 pub prst_material: MaterialPreset,
2168 pub bevel_top: Option<Bevel>,
2170 pub bevel_bottom: Option<Bevel>,
2172 pub extrusion_color: Option<Color>,
2174 pub contour_color: Option<Color>,
2176}
2177
2178impl Sp3d {
2179 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
2181 let mut attrs: Vec<(&str, &str)> = Vec::new();
2182 let eh_s;
2183 if self.extrusion_h != 0 {
2184 eh_s = self.extrusion_h.to_string();
2185 attrs.push(("extrusionH", eh_s.as_str()));
2186 }
2187 let cw_s;
2188 if self.contour_w != 0 {
2189 cw_s = self.contour_w.to_string();
2190 attrs.push(("contourW", cw_s.as_str()));
2191 }
2192 let pm_s;
2194 if !matches!(self.prst_material, MaterialPreset::WarmMatte) {
2195 pm_s = self.prst_material.as_str().to_string();
2196 attrs.push(("prstMaterial", pm_s.as_str()));
2197 }
2198 let has_children = self.bevel_top.is_some()
2199 || self.bevel_bottom.is_some()
2200 || self.extrusion_color.is_some()
2201 || self.contour_color.is_some();
2202 if !has_children && attrs.is_empty() {
2203 w.empty("a:sp3d");
2204 return;
2205 }
2206 if !has_children {
2207 w.empty_with("a:sp3d", &attrs);
2208 return;
2209 }
2210 w.open_with("a:sp3d", &attrs);
2211 if let Some(b) = &self.bevel_top {
2212 b.write_xml(w, "a:bevelT");
2213 }
2214 if let Some(b) = &self.bevel_bottom {
2215 b.write_xml(w, "a:bevelB");
2216 }
2217 if let Some(c) = &self.extrusion_color {
2218 w.open("a:extrusionClr");
2219 c.write_solid_fill(w);
2220 w.close("a:extrusionClr");
2221 }
2222 if let Some(c) = &self.contour_color {
2223 w.open("a:contourClr");
2224 c.write_solid_fill(w);
2225 w.close("a:contourClr");
2226 }
2227 w.close("a:sp3d");
2228 }
2229}
2230
2231#[cfg(test)]
2232mod tests {
2233 use super::*;
2234 use crate::oxml::color::Color;
2235 use crate::oxml::writer::XmlWriter;
2236 use crate::units::RGBColor;
2237
2238 #[test]
2242 fn adjustment_value_new() {
2243 let av = AdjustmentValue::new("adj", 16667);
2244 assert_eq!(av.name, "adj");
2245 assert_eq!(av.raw_value, 16667);
2246 }
2247
2248 #[test]
2250 fn adjustment_value_from_normalized() {
2251 let av = AdjustmentValue::from_normalized("adj", 0.5);
2252 assert_eq!(av.raw_value, 50000);
2253 let av2 = AdjustmentValue::from_normalized("adj1", 0.25);
2254 assert_eq!(av2.raw_value, 25000);
2255 }
2256
2257 #[test]
2259 fn adjustment_value_effective_value() {
2260 let av = AdjustmentValue::new("adj", 16667);
2261 assert!((av.effective_value() - 0.16667).abs() < 0.0001);
2262 }
2263
2264 #[test]
2266 fn adjustment_value_write_xml() {
2267 let av = AdjustmentValue::new("adj", 16667);
2268 let mut w = XmlWriter::new();
2269 av.write_xml(&mut w);
2270 let xml = &w.buf;
2271 assert!(xml.contains("<a:gd"), "xml: {}", xml);
2272 assert!(xml.contains(r#"name="adj""#), "xml: {}", xml);
2273 assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
2274 }
2275
2276 #[test]
2278 fn geometry_preset_with_adjustments_write_xml() {
2279 let geom = Geometry::Preset(
2280 PresetGeometry::RoundRectangle,
2281 vec![AdjustmentValue::new("adj", 16667)],
2282 );
2283 let mut w = XmlWriter::new();
2284 geom.write_xml(&mut w);
2285 let xml = &w.buf;
2286 assert!(xml.contains(r#"prst="roundRect""#), "xml: {}", xml);
2287 assert!(xml.contains("<a:avLst>"), "xml: {}", xml);
2288 assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
2289 assert!(xml.contains("</a:avLst>"), "xml: {}", xml);
2290 }
2291
2292 #[test]
2294 fn geometry_preset_no_adjustments_write_xml() {
2295 let geom = Geometry::preset(PresetGeometry::Rectangle);
2296 let mut w = XmlWriter::new();
2297 geom.write_xml(&mut w);
2298 assert!(w.buf.contains("<a:avLst/>"), "xml: {}", w.buf);
2299 }
2300
2301 #[test]
2303 fn geometry_preset_helper() {
2304 let geom = Geometry::preset(PresetGeometry::Ellipse);
2305 match geom {
2306 Geometry::Preset(p, adj) => {
2307 assert_eq!(p, PresetGeometry::Ellipse);
2308 assert!(adj.is_empty());
2309 }
2310 _ => panic!("应为 Preset 变体"),
2311 }
2312 }
2313
2314 #[test]
2318 fn fill_format_gradient_switches_mode() {
2319 let mut fill = Fill::Inherit;
2320 let mut fmt = FillFormat::new(&mut fill);
2321 let g = fmt.gradient();
2322 g.stops.push(GradientStop {
2323 pos: 0,
2324 color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
2325 });
2326 g.stops.push(GradientStop {
2327 pos: 100_000,
2328 color: Color::RGB(RGBColor(0x00, 0x00, 0xFF)),
2329 });
2330 g.gradient_type = GradientType::Linear(2_700_000);
2331 assert!(matches!(fill, Fill::Gradient(_)));
2332 if let Fill::Gradient(g) = &fill {
2333 assert_eq!(g.stops.len(), 2);
2334 assert_eq!(g.stops[0].pos, 0);
2335 assert_eq!(g.stops[1].pos, 100_000);
2336 assert!(matches!(g.gradient_type, GradientType::Linear(2_700_000)));
2337 }
2338 }
2339
2340 #[test]
2342 fn fill_format_set_gradient_linear() {
2343 let mut fill = Fill::Inherit;
2344 let mut fmt = FillFormat::new(&mut fill);
2345 let stops = vec![
2346 GradientStop {
2347 pos: 0,
2348 color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
2349 },
2350 GradientStop {
2351 pos: 100_000,
2352 color: Color::RGB(RGBColor(0x00, 0xFF, 0x00)),
2353 },
2354 ];
2355 fmt.set_gradient_linear(stops, 5_400_000);
2356 assert!(matches!(fill, Fill::Gradient(_)));
2357 if let Fill::Gradient(g) = &fill {
2358 assert_eq!(g.stops.len(), 2);
2359 assert!(matches!(g.gradient_type, GradientType::Linear(5_400_000)));
2360 }
2361 }
2362
2363 #[test]
2365 fn fill_format_set_gradient_path() {
2366 let mut fill = Fill::Inherit;
2367 let mut fmt = FillFormat::new(&mut fill);
2368 let stops = vec![GradientStop {
2369 pos: 50_000,
2370 color: Color::RGB(RGBColor(0x80, 0x80, 0x80)),
2371 }];
2372 fmt.set_gradient_path(stops, GradientPath::Circle);
2373 if let Fill::Gradient(g) = &fill {
2374 assert!(matches!(
2375 g.gradient_type,
2376 GradientType::Path(GradientPath::Circle)
2377 ));
2378 }
2379 }
2380
2381 #[test]
2383 fn fill_format_pattern_switches_mode() {
2384 let mut fill = Fill::Inherit;
2385 let mut fmt = FillFormat::new(&mut fill);
2386 let p = fmt.pattern();
2387 p.prst = "horz".to_string();
2388 p.fg_color = Color::RGB(RGBColor(0xFF, 0x00, 0x00));
2389 p.bg_color = Color::RGB(RGBColor(0xFF, 0xFF, 0xFF));
2390 assert!(matches!(fill, Fill::Pattern(_)));
2391 if let Fill::Pattern(p) = &fill {
2392 assert_eq!(p.prst, "horz");
2393 }
2394 }
2395
2396 #[test]
2398 fn fill_format_set_pattern() {
2399 let mut fill = Fill::Inherit;
2400 let mut fmt = FillFormat::new(&mut fill);
2401 fmt.set_pattern(
2402 "cross",
2403 Color::RGB(RGBColor(0x00, 0x00, 0x00)),
2404 Color::RGB(RGBColor(0xFF, 0xFF, 0xFF)),
2405 );
2406 if let Fill::Pattern(p) = &fill {
2407 assert_eq!(p.prst, "cross");
2408 assert!(matches!(p.fg_color, Color::RGB(_)));
2409 assert!(matches!(p.bg_color, Color::RGB(_)));
2410 } else {
2411 panic!("应为 Pattern 变体");
2412 }
2413 }
2414
2415 #[test]
2417 fn fill_format_fill_type_for_gradient_and_pattern() {
2418 let mut fill = Fill::Gradient(GradientFill {
2419 stops: vec![],
2420 gradient_type: GradientType::Linear(0),
2421 flip: None,
2422 rot_with_shape: None,
2423 });
2424 let fmt = FillFormat::new(&mut fill);
2425 assert_eq!(
2426 fmt.fill_type(),
2427 crate::oxml::simpletypes::MsoFillType::Gradient
2428 );
2429
2430 let mut fill = Fill::Pattern(PatternFill::default());
2431 let fmt = FillFormat::new(&mut fill);
2432 assert_eq!(
2433 fmt.fill_type(),
2434 crate::oxml::simpletypes::MsoFillType::Pattern
2435 );
2436 }
2437
2438 #[test]
2440 fn effect_list_outer_shadow_serialization() {
2441 let mut effects = EffectList::default();
2442 assert!(effects.is_empty());
2443 effects.outer_shadow = Some(ShadowEffect {
2444 dir: 2_700_000,
2445 dist: 38100,
2446 blur_rad: 40000,
2447 color: Color::RGB(RGBColor(0x00, 0x00, 0x00)),
2448 rot_with_shape: Some(false),
2449 });
2450 assert!(!effects.is_empty());
2451
2452 let mut w = XmlWriter::new();
2453 effects.write_xml(&mut w);
2454 let buf = &w.buf;
2455 assert!(buf.contains("<a:effectLst>"));
2456 assert!(buf.contains("<a:outerShdw"));
2457 assert!(buf.contains("dir=\"2700000\""));
2458 assert!(buf.contains("dist=\"38100\""));
2459 assert!(buf.contains("blurRad=\"40000\""));
2460 assert!(buf.contains("rotWithShape=\"0\""));
2461 assert!(buf.contains("<a:srgbClr val=\"000000\""));
2462 assert!(buf.contains("</a:effectLst>"));
2463 }
2464
2465 #[test]
2467 fn effect_list_glow_and_soft_edge() {
2468 let effects = EffectList {
2469 glow: Some(GlowEffect {
2470 rad: 50000,
2471 color: Color::RGB(RGBColor(0xFF, 0x00, 0xFF)),
2472 }),
2473 soft_edge: Some(SoftEdgeEffect { rad: 25000 }),
2474 ..Default::default()
2475 };
2476
2477 let mut w = XmlWriter::new();
2478 effects.write_xml(&mut w);
2479 let buf = &w.buf;
2480 assert!(buf.contains("<a:glow rad=\"50000\">"));
2481 assert!(buf.contains("<a:srgbClr val=\"FF00FF\""));
2482 assert!(buf.contains("<a:softEdge rad=\"25000\"/>"));
2483 }
2484
2485 #[test]
2487 fn effect_list_reflection_serialization() {
2488 let effects = EffectList {
2489 reflection: Some(ReflectionEffect {
2490 blur_rad: Some(50000),
2491 st_a: Some(52000),
2492 st_pos: Some(0),
2493 end_a: Some(30000),
2494 end_pos: Some(50000),
2495 dist: Some(38100),
2496 dir: Some(5_400_000),
2497 rot_with_shape: None,
2498 }),
2499 ..Default::default()
2500 };
2501
2502 let mut w = XmlWriter::new();
2503 effects.write_xml(&mut w);
2504 let buf = &w.buf;
2505 assert!(buf.contains("<a:reflection"));
2506 assert!(buf.contains("blurRad=\"50000\""));
2507 assert!(buf.contains("stA=\"52000\""));
2508 assert!(buf.contains("endPos=\"50000\""));
2509 assert!(buf.contains("dist=\"38100\""));
2510 assert!(buf.contains("/>")); }
2512
2513 #[test]
2515 fn effect_list_empty_writes_nothing() {
2516 let effects = EffectList::default();
2517 let mut w = XmlWriter::new();
2518 effects.write_xml(&mut w);
2519 assert!(w.buf.is_empty());
2520 }
2521
2522 #[test]
2526 fn blip_fill_mode_stretch_serialize() {
2527 let mode = BlipFillMode::Stretch;
2528 let mut w = XmlWriter::new();
2529 mode.write_xml(&mut w);
2530 let s = &w.buf;
2531 assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
2532 assert!(s.contains("<a:fillRect/>"), "应输出 a:fillRect,实际: {s}");
2533 assert!(s.contains("</a:stretch>"), "应关闭 a:stretch,实际: {s}");
2534 }
2535
2536 #[test]
2538 fn blip_fill_mode_tile_serialize_full() {
2539 let mode = BlipFillMode::Tile {
2540 tx: Some(914400),
2541 ty: Some(457200),
2542 sx: Some(100_000),
2543 sy: Some(50_000),
2544 flip: Some("x".to_string()),
2545 algn: Some("tl".to_string()),
2546 };
2547 let mut w = XmlWriter::new();
2548 mode.write_xml(&mut w);
2549 let s = &w.buf;
2550 assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2551 assert!(s.contains("tx=\"914400\""), "应包含 tx,实际: {s}");
2552 assert!(s.contains("ty=\"457200\""), "应包含 ty,实际: {s}");
2553 assert!(s.contains("sx=\"100000\""), "应包含 sx,实际: {s}");
2554 assert!(s.contains("sy=\"50000\""), "应包含 sy,实际: {s}");
2555 assert!(s.contains("flip=\"x\""), "应包含 flip,实际: {s}");
2556 assert!(s.contains("algn=\"tl\""), "应包含 algn,实际: {s}");
2557 assert!(s.contains("/>"), "应为自闭合标签,实际: {s}");
2558 }
2559
2560 #[test]
2562 fn blip_fill_mode_tile_serialize_partial() {
2563 let mode = BlipFillMode::Tile {
2564 tx: None,
2565 ty: None,
2566 sx: Some(200_000),
2567 sy: None,
2568 flip: None,
2569 algn: Some("ctr".to_string()),
2570 };
2571 let mut w = XmlWriter::new();
2572 mode.write_xml(&mut w);
2573 let s = &w.buf;
2574 assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2575 assert!(!s.contains("tx="), "不应包含 tx,实际: {s}");
2576 assert!(!s.contains("ty="), "不应包含 ty,实际: {s}");
2577 assert!(s.contains("sx=\"200000\""), "应包含 sx,实际: {s}");
2578 assert!(!s.contains("sy="), "不应包含 sy,实际: {s}");
2579 assert!(s.contains("algn=\"ctr\""), "应包含 algn,实际: {s}");
2580 }
2581
2582 #[test]
2584 fn blip_fill_mode_none_serialize() {
2585 let mode = BlipFillMode::None;
2586 let mut w = XmlWriter::new();
2587 mode.write_xml(&mut w);
2588 assert!(w.buf.is_empty(), "None 模式不应写出任何 XML");
2589 }
2590
2591 #[test]
2593 fn fill_blip_with_stretch_serialize() {
2594 let fill = Fill::Blip {
2595 rid: "rIdImg1".to_string(),
2596 mode: BlipFillMode::Stretch,
2597 };
2598 let mut w = XmlWriter::new();
2599 fill.write_xml(&mut w);
2600 let s = &w.buf;
2601 assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
2602 assert!(
2603 s.contains("r:embed=\"rIdImg1\""),
2604 "应包含 r:embed,实际: {s}"
2605 );
2606 assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
2607 let blip_count = s.matches("<a:blip ").count()
2610 + s.matches("<a:blip/>").count()
2611 + s.matches("<a:blip>").count();
2612 assert_eq!(
2613 blip_count, 1,
2614 "应仅输出 1 个 <a:blip 标签,实际 {} 次(可能存在双重标签 bug): {s}",
2615 blip_count
2616 );
2617 assert!(
2619 !s.contains("<a:blip><a:blip"),
2620 "检测到嵌套的双重 <a:blip> 标签(BUG-001 回归): {s}"
2621 );
2622 }
2623
2624 #[test]
2626 fn fill_blip_with_tile_serialize() {
2627 let fill = Fill::Blip {
2628 rid: "rIdImg2".to_string(),
2629 mode: BlipFillMode::Tile {
2630 tx: Some(100),
2631 ty: None,
2632 sx: None,
2633 sy: None,
2634 flip: Some("xy".to_string()),
2635 algn: None,
2636 },
2637 };
2638 let mut w = XmlWriter::new();
2639 fill.write_xml(&mut w);
2640 let s = &w.buf;
2641 assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
2642 assert!(
2643 s.contains("r:embed=\"rIdImg2\""),
2644 "应包含 r:embed,实际: {s}"
2645 );
2646 assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2647 assert!(s.contains("tx=\"100\""), "应包含 tx,实际: {s}");
2648 assert!(s.contains("flip=\"xy\""), "应包含 flip,实际: {s}");
2649 }
2650
2651 #[test]
2653 fn blip_fill_mode_default_is_stretch() {
2654 let mode = BlipFillMode::default();
2655 assert!(matches!(mode, BlipFillMode::Stretch));
2656 }
2657
2658 #[test]
2662 fn rotation_3d_serialize() {
2663 let rot = Rotation3d {
2664 lat: 0,
2665 lon: 0,
2666 rev: 1200000,
2667 };
2668 let mut w = XmlWriter::new();
2669 rot.write_xml(&mut w);
2670 let s = &w.buf;
2671 assert!(s.contains("<a:rot"), "应输出 a:rot,实际: {s}");
2672 assert!(s.contains("lat=\"0\""), "应包含 lat,实际: {s}");
2673 assert!(s.contains("lon=\"0\""), "应包含 lon,实际: {s}");
2674 assert!(s.contains("rev=\"1200000\""), "应包含 rev,实际: {s}");
2675 assert!(s.contains("/>"), "应为自闭合,实际: {s}");
2676 }
2677
2678 #[test]
2680 fn camera_default_serialize() {
2681 let camera = Camera::default();
2682 let mut w = XmlWriter::new();
2683 camera.write_xml(&mut w);
2684 let s = &w.buf;
2685 assert!(
2686 s.contains("<a:camera prst=\"orthographicFront\""),
2687 "应包含默认 prst,实际: {s}"
2688 );
2689 assert!(!s.contains("fov="), "默认 fov 不应输出,实际: {s}");
2691 assert!(!s.contains("zoom="), "默认 zoom 不应输出,实际: {s}");
2692 }
2693
2694 #[test]
2696 fn camera_with_rotation_serialize() {
2697 let camera = Camera {
2698 preset: CameraPreset::PerspectiveFront,
2699 fov: 3600000,
2700 zoom: 200000,
2703 rotation: Some(Rotation3d {
2704 lat: 30,
2705 lon: 45,
2706 rev: 0,
2707 }),
2708 };
2709 let mut w = XmlWriter::new();
2710 camera.write_xml(&mut w);
2711 let s = &w.buf;
2712 assert!(
2713 s.contains("<a:camera prst=\"perspectiveFront\""),
2714 "应包含 perspectiveFront,实际: {s}"
2715 );
2716 assert!(s.contains("fov=\"3600000\""), "应包含 fov,实际: {s}");
2717 assert!(s.contains("zoom=\"200000\""), "应包含 zoom,实际: {s}");
2718 assert!(s.contains("<a:rot"), "应包含 a:rot 子元素,实际: {s}");
2719 assert!(s.contains("</a:camera>"), "应关闭 a:camera,实际: {s}");
2720 }
2721
2722 #[test]
2724 fn scene3d_full_serialize() {
2725 let scene = Scene3d {
2726 camera: Camera {
2727 preset: CameraPreset::OrthographicFront,
2728 fov: 0,
2729 zoom: 0,
2730 rotation: Some(Rotation3d {
2731 lat: 0,
2732 lon: 0,
2733 rev: 0,
2734 }),
2735 },
2736 light_rig: LightRig {
2737 rig: LightRigType::ThreePt,
2738 dir: LightRigDirection::Top,
2739 rotation: Some(Rotation3d {
2740 lat: 0,
2741 lon: 0,
2742 rev: 1200000,
2743 }),
2744 },
2745 backdrop: None,
2746 };
2747 let mut w = XmlWriter::new();
2748 scene.write_xml(&mut w);
2749 let s = &w.buf;
2750 assert!(s.contains("<a:scene3d>"), "应输出 a:scene3d,实际: {s}");
2751 assert!(
2752 s.contains("<a:camera prst=\"orthographicFront\">"),
2753 "应包含 camera 子元素,实际: {s}"
2754 );
2755 assert!(
2756 s.contains("<a:lightRig rig=\"threePt\" dir=\"t\">"),
2757 "应包含 lightRig 子元素,实际: {s}"
2758 );
2759 assert!(s.contains("</a:scene3d>"), "应关闭 a:scene3d,实际: {s}");
2760 }
2761
2762 #[test]
2764 fn sp3d_default_serialize() {
2765 let sp3d = Sp3d::default();
2766 let mut w = XmlWriter::new();
2767 sp3d.write_xml(&mut w);
2768 let s = &w.buf;
2769 assert!(s.contains("<a:sp3d"), "应输出 a:sp3d,实际: {s}");
2770 assert!(
2772 !s.contains("prstMaterial="),
2773 "默认 warmMatte 不应输出,实际: {s}"
2774 );
2775 }
2776
2777 #[test]
2779 fn sp3d_with_bevel_and_colors_serialize() {
2780 let sp3d = Sp3d {
2781 extrusion_h: 38100,
2782 contour_w: 12700,
2783 prst_material: MaterialPreset::Metallic,
2784 bevel_top: Some(Bevel { w: 63500, h: 25400 }),
2785 bevel_bottom: None,
2786 extrusion_color: Some(Color::RGB(crate::units::RGBColor::BLACK)),
2787 contour_color: Some(Color::RGB(crate::units::RGBColor::WHITE)),
2788 };
2789 let mut w = XmlWriter::new();
2790 sp3d.write_xml(&mut w);
2791 let s = &w.buf;
2792 assert!(
2793 s.contains("extrusionH=\"38100\""),
2794 "应包含 extrusionH,实际: {s}"
2795 );
2796 assert!(
2797 s.contains("contourW=\"12700\""),
2798 "应包含 contourW,实际: {s}"
2799 );
2800 assert!(
2801 s.contains("prstMaterial=\"metallic\""),
2802 "应包含 prstMaterial,实际: {s}"
2803 );
2804 assert!(
2805 s.contains("<a:bevelT w=\"63500\" h=\"25400\"/>"),
2806 "应包含 bevelT,实际: {s}"
2807 );
2808 assert!(
2809 s.contains("<a:extrusionClr>"),
2810 "应包含 extrusionClr,实际: {s}"
2811 );
2812 assert!(s.contains("<a:contourClr>"), "应包含 contourClr,实际: {s}");
2813 assert!(s.contains("</a:sp3d>"), "应关闭 a:sp3d,实际: {s}");
2814 }
2815
2816 #[test]
2818 fn shape_properties_with_3d_serialize() {
2819 let sp = ShapeProperties {
2820 scene3d: Some(Scene3d::default()),
2821 sp3d: Some(Sp3d::default()),
2822 ..Default::default()
2823 };
2824 let mut w = XmlWriter::new();
2825 sp.write_xml(&mut w, "p:spPr");
2826 let s = &w.buf;
2827 assert!(s.contains("<a:scene3d>"), "应输出 scene3d,实际: {s}");
2829 assert!(s.contains("<a:sp3d"), "应输出 sp3d,实际: {s}");
2830 let scene_pos = s.find("<a:scene3d>").expect("scene3d 应存在");
2832 let sp3d_pos = s.find("<a:sp3d").expect("sp3d 应存在");
2833 assert!(
2834 scene_pos < sp3d_pos,
2835 "scene3d 应在 sp3d 之前,实际 scene3d@{scene_pos} sp3d@{sp3d_pos}"
2836 );
2837 }
2838
2839 #[test]
2841 fn camera_preset_from_str() {
2842 assert!(matches!(
2843 CameraPreset::parse("orthographicFront"),
2844 CameraPreset::OrthographicFront
2845 ));
2846 assert!(matches!(
2847 CameraPreset::parse("perspectiveFront"),
2848 CameraPreset::PerspectiveFront
2849 ));
2850 match CameraPreset::parse("customUnknown") {
2852 CameraPreset::Other(s) => assert_eq!(s, "customUnknown"),
2853 other => panic!("未知预设应归入 Other,实际: {other:?}"),
2854 }
2855 }
2856
2857 #[test]
2859 fn material_preset_from_str() {
2860 assert!(matches!(
2861 MaterialPreset::parse("warmMatte"),
2862 MaterialPreset::WarmMatte
2863 ));
2864 assert!(matches!(
2865 MaterialPreset::parse("metallic"),
2866 MaterialPreset::Metallic
2867 ));
2868 match MaterialPreset::parse("futureMaterial") {
2870 MaterialPreset::Other(s) => assert_eq!(s, "futureMaterial"),
2871 other => panic!("未知材质应归入 Other,实际: {other:?}"),
2872 }
2873 }
2874
2875 #[test]
2879 fn backdrop_default_serialize() {
2880 let bd = Backdrop::default();
2881 let mut w = XmlWriter::new();
2882 bd.write_xml(&mut w);
2883 let s = &w.buf;
2884 assert!(s.contains("<a:backdrop>"), "应输出 a:backdrop,实际: {s}");
2885 assert!(s.contains("</a:backdrop>"), "应关闭 a:backdrop,实际: {s}");
2886 assert!(!s.contains("<a:floor/>"), "默认不应输出 floor: {s}");
2889 assert!(!s.contains("<a:wall/>"), "默认不应输出 wall: {s}");
2890 assert!(!s.contains("<a:l/>"), "默认不应输出 l: {s}");
2891 assert!(!s.contains("<a:r/>"), "默认不应输出 r: {s}");
2892 assert!(!s.contains("<a:t/>"), "默认不应输出 t: {s}");
2893 assert!(!s.contains("<a:b/>"), "默认不应输出 b: {s}");
2894 }
2895
2896 #[test]
2898 fn backdrop_with_anchor_and_all_planes_serialize() {
2899 let bd = Backdrop {
2900 anchor: Some(Point3d {
2901 x: 100000,
2902 y: 200000,
2903 z: 300000,
2904 }),
2905 floor: true,
2906 wall: true,
2907 left: true,
2908 right: true,
2909 top: true,
2910 bottom: true,
2911 };
2912 let mut w = XmlWriter::new();
2913 bd.write_xml(&mut w);
2914 let s = &w.buf;
2915 assert!(
2917 s.contains("<a:anchor x=\"100000\" y=\"200000\" z=\"300000\"/>"),
2918 "应包含 anchor,实际: {s}"
2919 );
2920 assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2922 assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2923 assert!(s.contains("<a:l/>"), "应包含 l: {s}");
2924 assert!(s.contains("<a:r/>"), "应包含 r: {s}");
2925 assert!(s.contains("<a:t/>"), "应包含 t: {s}");
2926 assert!(s.contains("<a:b/>"), "应包含 b: {s}");
2927 let anchor_pos = s.find("<a:anchor").expect("anchor 应存在");
2930 let floor_pos = s.find("<a:floor/>").expect("floor 应存在");
2931 let wall_pos = s.find("<a:wall/>").expect("wall 应存在");
2932 let l_pos = s.find("<a:l/>").expect("l 应存在");
2933 let r_pos = s.find("<a:r/>").expect("r 应存在");
2934 let t_pos = s.find("<a:t/>").expect("t 应存在");
2935 let b_pos = s.find("<a:b/>").expect("b 应存在");
2936 assert!(anchor_pos < floor_pos, "anchor 应在 floor 之前");
2937 assert!(floor_pos < wall_pos, "floor 应在 wall 之前");
2938 assert!(wall_pos < l_pos, "wall 应在 l 之前");
2939 assert!(l_pos < r_pos, "l 应在 r 之前");
2940 assert!(r_pos < t_pos, "r 应在 t 之前");
2941 assert!(t_pos < b_pos, "t 应在 b 之前");
2942 }
2943
2944 #[test]
2946 fn backdrop_partial_planes_serialize() {
2947 let bd = Backdrop {
2948 floor: true,
2949 wall: true,
2950 ..Default::default()
2951 };
2952 let mut w = XmlWriter::new();
2953 bd.write_xml(&mut w);
2954 let s = &w.buf;
2955 assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2956 assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2957 assert!(!s.contains("<a:l/>"), "不应包含 l: {s}");
2959 assert!(!s.contains("<a:r/>"), "不应包含 r: {s}");
2960 assert!(!s.contains("<a:t/>"), "不应包含 t: {s}");
2961 assert!(!s.contains("<a:b/>"), "不应包含 b: {s}");
2962 }
2963
2964 #[test]
2966 fn scene3d_with_backdrop_serialize() {
2967 let scene = Scene3d {
2968 camera: Camera::default(),
2969 light_rig: LightRig::default(),
2970 backdrop: Some(Backdrop {
2971 floor: true,
2972 wall: true,
2973 ..Default::default()
2974 }),
2975 };
2976 let mut w = XmlWriter::new();
2977 scene.write_xml(&mut w);
2978 let s = &w.buf;
2979 assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
2980 assert!(s.contains("<a:backdrop>"), "应包含 backdrop: {s}");
2981 assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2982 assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2983 let cam_pos = s.find("<a:camera").expect("camera 应存在");
2985 let rig_pos = s.find("<a:lightRig").expect("lightRig 应存在");
2986 let bd_pos = s.find("<a:backdrop>").expect("backdrop 应存在");
2987 assert!(cam_pos < rig_pos, "camera 应在 lightRig 之前");
2988 assert!(rig_pos < bd_pos, "lightRig 应在 backdrop 之前");
2989 }
2990
2991 #[test]
2993 fn scene3d_without_backdrop_no_element() {
2994 let scene = Scene3d::default();
2995 let mut w = XmlWriter::new();
2996 scene.write_xml(&mut w);
2997 let s = &w.buf;
2998 assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
2999 assert!(!s.contains("<a:backdrop>"), "无 backdrop 时不应输出: {s}");
3000 }
3001
3002 #[test]
3004 fn point3d_serialize() {
3005 let p = Point3d {
3006 x: -100000,
3007 y: 0,
3008 z: 500000,
3009 };
3010 let mut w = XmlWriter::new();
3011 p.write_xml(&mut w);
3012 let s = &w.buf;
3013 assert!(s.contains("x=\"-100000\""), "应包含 x: {s}");
3014 assert!(s.contains("y=\"0\""), "应包含 y: {s}");
3015 assert!(s.contains("z=\"500000\""), "应包含 z: {s}");
3016 }
3017}