1use std::fmt;
4
5use oxml_drawing::color::ColorChoice;
6use oxml_drawing::effect::CT_EffectList;
7use oxml_drawing::fill::Fill;
8use oxml_drawing::line::CT_LineProperties;
9use oxml_drawing::shape_props::CT_ShapeProperties;
10use oxml_drawing::style_ref::FontCollectionIndex;
11use oxml_drawing::theme::CT_EffectStyle;
12use rpptx_oxml::connector::CT_ConnectionShape;
13use rpptx_oxml::shape_tree::{CT_Shape, CT_ShapeStyle};
14
15use crate::ResolveCtx;
16
17#[derive(Clone, Debug, Default, Eq, PartialEq)]
19pub struct EffectiveShapeStyle {
20 pub fill: Option<Fill>,
21 pub line: Option<CT_LineProperties>,
22 pub effects: Option<CT_EffectList>,
23 pub font_collection: Option<FontCollectionIndex>,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum ResolveError {
29 StyleIndexOutOfRange {
30 reference: &'static str,
31 index: u32,
32 available: usize,
33 },
34 UnresolvedPlaceholderColor {
35 reference: &'static str,
36 },
37 ConcreteValue {
38 kind: &'static str,
39 detail: String,
40 },
41}
42
43impl fmt::Display for ResolveError {
44 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::StyleIndexOutOfRange {
47 reference,
48 index,
49 available,
50 } => write!(
51 formatter,
52 "{reference} style index {index} is outside the {available} available entries"
53 ),
54 Self::UnresolvedPlaceholderColor { reference } => write!(
55 formatter,
56 "{reference} style retains an unresolved phClr placeholder colour"
57 ),
58 Self::ConcreteValue { kind, detail } => {
59 write!(
60 formatter,
61 "cannot resolve {kind} to a concrete value: {detail}"
62 )
63 }
64 }
65 }
66}
67
68impl std::error::Error for ResolveError {}
69
70impl ResolveCtx<'_> {
71 pub fn effective_shape_style(
73 &self,
74 shape: &CT_Shape,
75 ) -> Result<EffectiveShapeStyle, ResolveError> {
76 let matrix = &self.theme.theme_elements.format_scheme;
77 let referenced_effect_style = self.referenced_effect_style(shape.style())?;
78 let mut effective = if let Some(style) = shape.style() {
79 EffectiveShapeStyle {
80 fill: referenced_fill(
81 style.fill_reference.index,
82 &matrix.fill_styles,
83 &matrix.background_fill_styles,
84 )?,
85 line: matrix_entry("line", style.line_reference.index, &matrix.line_styles)?,
86 effects: None,
87 font_collection: Some(style.font_reference.index),
88 }
89 } else {
90 EffectiveShapeStyle::default()
91 };
92
93 if let Some(explicit) = &shape.shape_properties.fill {
94 effective.fill = Some(explicit.clone());
95 }
96 if let Some(explicit) = &shape.shape_properties.line {
97 if let Some(line) = effective.line.as_mut() {
98 overlay_line(line, explicit);
99 } else {
100 effective.line = Some(explicit.clone());
101 }
102 }
103
104 let fill_reference = shape
105 .style()
106 .and_then(|style| style.fill_reference.color.as_ref());
107 let line_reference = shape
108 .style()
109 .and_then(|style| style.line_reference.color.as_ref());
110 if let Some(fill) = effective.fill.as_mut() {
111 substitute_fill(fill, fill_reference, "fill")?;
112 }
113 if let Some(line) = effective.line.as_mut()
114 && let Some(fill) = line.fill.as_mut()
115 {
116 substitute_fill(fill, line_reference, "line")?;
117 }
118 effective.effects = resolve_effects(
119 referenced_effect_style,
120 shape.style(),
121 &shape.shape_properties,
122 )?;
123 Ok(effective)
124 }
125
126 pub(crate) fn effective_effects(
131 &self,
132 style: Option<&CT_ShapeStyle>,
133 properties: &CT_ShapeProperties,
134 ) -> Result<Option<CT_EffectList>, ResolveError> {
135 let referenced_effect_style = self.referenced_effect_style(style)?;
136 resolve_effects(referenced_effect_style, style, properties)
137 }
138
139 fn referenced_effect_style(
140 &self,
141 style: Option<&CT_ShapeStyle>,
142 ) -> Result<Option<CT_EffectStyle>, ResolveError> {
143 match style {
144 Some(style) => matrix_entry(
145 "effect",
146 style.effect_reference.index,
147 &self.theme.theme_elements.format_scheme.effect_styles,
148 ),
149 None => Ok(None),
150 }
151 }
152
153 pub(crate) fn effective_connector_line(
156 &self,
157 connector: &CT_ConnectionShape,
158 ) -> Result<Option<CT_LineProperties>, ResolveError> {
159 let style = connector.style();
160 let mut line = match style {
161 Some(style) => matrix_entry(
162 "line",
163 style.line_reference.index,
164 &self.theme.theme_elements.format_scheme.line_styles,
165 )?,
166 None => None,
167 };
168 if let Some(explicit) = &connector.shape_properties.line {
169 if let Some(line) = line.as_mut() {
170 overlay_line(line, explicit);
171 } else {
172 line = Some(explicit.clone());
173 }
174 }
175 if let Some(fill) = line.as_mut().and_then(|line| line.fill.as_mut()) {
176 let reference = style.and_then(|style| style.line_reference.color.as_ref());
177 substitute_fill(fill, reference, "line")?;
178 }
179 Ok(line)
180 }
181}
182
183fn resolve_effects(
186 referenced_effect_style: Option<CT_EffectStyle>,
187 style: Option<&CT_ShapeStyle>,
188 properties: &CT_ShapeProperties,
189) -> Result<Option<CT_EffectList>, ResolveError> {
190 let has_explicit_unmodelled_effect = properties.has_unmodelled_effect();
191 let unmodelled_effect_has_placeholder = if has_explicit_unmodelled_effect {
192 properties.has_unmodelled_effect_placeholder_color()
193 } else {
194 referenced_effect_style
195 .as_ref()
196 .is_some_and(|style| style.has_unmodelled_effect_placeholder_color())
197 };
198 if unmodelled_effect_has_placeholder {
199 return Err(ResolveError::UnresolvedPlaceholderColor {
200 reference: "effect",
201 });
202 }
203 let mut effects = if has_explicit_unmodelled_effect {
204 None
205 } else if let Some(explicit) = &properties.effects {
206 Some(explicit.clone())
207 } else {
208 referenced_effect_style
209 .filter(|style| !style.has_unmodelled_effect())
210 .and_then(|style| style.effect_list)
211 };
212 if let Some(effects) = effects.as_mut() {
213 let effect_reference = style.and_then(|style| style.effect_reference.color.as_ref());
214 if let Some(shadow) = effects.outer_shadow.as_mut()
215 && let Some(color) = shadow.color.as_mut()
216 {
217 substitute_color(color, effect_reference, "effect")?;
218 }
219 if effects.has_unmodelled_placeholder_color() {
220 return Err(ResolveError::UnresolvedPlaceholderColor {
221 reference: "effect",
222 });
223 }
224 }
225 Ok(effects)
226}
227
228fn matrix_entry<T: Clone>(
229 reference: &'static str,
230 index: u32,
231 entries: &[T],
232) -> Result<Option<T>, ResolveError> {
233 if index == 0 {
234 return Ok(None);
235 }
236 let offset = usize::try_from(index - 1).ok();
237 offset
238 .and_then(|offset| entries.get(offset))
239 .cloned()
240 .map(Some)
241 .ok_or(ResolveError::StyleIndexOutOfRange {
242 reference,
243 index,
244 available: entries.len(),
245 })
246}
247
248pub(crate) fn referenced_fill(
249 index: u32,
250 normal: &[Fill],
251 background: &[Fill],
252) -> Result<Option<Fill>, ResolveError> {
253 if index > 1000 {
254 matrix_entry("background fill", index - 1000, background)
255 } else {
256 matrix_entry("fill", index, normal)
257 }
258}
259
260fn overlay_line(target: &mut CT_LineProperties, source: &CT_LineProperties) {
261 if source.width.is_some() {
262 target.width = source.width;
263 }
264 if source.cap.is_some() {
265 target.cap = source.cap;
266 }
267 if source.fill.is_some() {
268 target.fill.clone_from(&source.fill);
269 }
270 if source.dash.is_some() {
271 target.dash.clone_from(&source.dash);
272 }
273 if source.join.is_some() {
274 target.join.clone_from(&source.join);
275 }
276 if source.head_end.is_some() {
277 target.head_end.clone_from(&source.head_end);
278 }
279 if source.tail_end.is_some() {
280 target.tail_end.clone_from(&source.tail_end);
281 }
282}
283
284pub(crate) fn substitute_fill(
285 fill: &mut Fill,
286 reference: Option<&ColorChoice>,
287 kind: &'static str,
288) -> Result<(), ResolveError> {
289 match fill {
290 Fill::Solid(fill) => substitute_optional_color(&mut fill.color, reference, kind),
291 Fill::Gradient(fill) => {
292 for stop in &mut fill.stops {
293 substitute_optional_color(&mut stop.color, reference, kind)?;
294 }
295 Ok(())
296 }
297 Fill::Pattern(fill) => {
298 substitute_optional_color(&mut fill.foreground, reference, kind)?;
299 substitute_optional_color(&mut fill.background, reference, kind)
300 }
301 Fill::NoFill(_) | Fill::Blip(_) => Ok(()),
302 }
303}
304
305fn substitute_optional_color(
306 color: &mut Option<ColorChoice>,
307 reference: Option<&ColorChoice>,
308 kind: &'static str,
309) -> Result<(), ResolveError> {
310 if let Some(color) = color {
311 substitute_color(color, reference, kind)?;
312 }
313 Ok(())
314}
315
316fn substitute_color(
317 color: &mut ColorChoice,
318 reference: Option<&ColorChoice>,
319 kind: &'static str,
320) -> Result<(), ResolveError> {
321 if !matches!(color, ColorChoice::Scheme { value, .. } if value == "phClr") {
322 return Ok(());
323 }
324 let placeholder_transforms = color.transforms().to_vec();
325 let Some(mut replacement) = reference.cloned() else {
326 return Err(ResolveError::UnresolvedPlaceholderColor { reference: kind });
327 };
328 if matches!(&replacement, ColorChoice::Scheme { value, .. } if value == "phClr") {
329 return Err(ResolveError::UnresolvedPlaceholderColor { reference: kind });
330 }
331 transforms_mut(&mut replacement).extend(placeholder_transforms);
332 *color = replacement;
333 Ok(())
334}
335
336fn transforms_mut(color: &mut ColorChoice) -> &mut Vec<oxml_drawing::color::ColorTransform> {
337 match color {
338 ColorChoice::Srgb { transforms, .. }
339 | ColorChoice::Scheme { transforms, .. }
340 | ColorChoice::System { transforms, .. }
341 | ColorChoice::Preset { transforms, .. } => transforms,
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use oxml_drawing::color::{ColorChoice, ColorMap, ColorTransform};
348 use oxml_drawing::fill::Fill;
349 use oxml_drawing::line::{LineCap, LineDash, ST_PresetLineDashVal};
350 use oxml_drawing::style_ref::FontCollectionIndex;
351 use oxml_drawing::text::CT_TextListStyle;
352 use oxml_drawing::theme::CT_OfficeStyleSheet;
353 use rpptx_oxml::shape_tree::{CT_Shape, ShapeTreeChild};
354 use rpptx_oxml::slide_parts::{CT_Slide, CT_SlideLayout, CT_SlideMaster};
355
356 use super::{ResolveCtx, ResolveError};
357
358 const P_NS: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
359 const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
360
361 #[test]
362 fn shape_with_style_resolves_theme_fill_with_reference_colour_substituted() {
363 let fixture = Fixture::new(&shape("", &style_refs(0, 1, 0, "minor", "AABBCC")));
364 let resolved = fixture
365 .context()
366 .effective_shape_style(fixture.shape(0))
367 .unwrap();
368
369 let Some(Fill::Solid(fill)) = resolved.fill else {
370 panic!("expected a resolved solid fill");
371 };
372 let Some(ColorChoice::Srgb {
373 value, transforms, ..
374 }) = fill.color
375 else {
376 panic!("expected the reference sRGB colour");
377 };
378 assert_eq!(value.to_string(), "AABBCC");
379 assert!(transforms.is_empty());
380 assert_eq!(resolved.font_collection, Some(FontCollectionIndex::Minor));
381 }
382
383 #[test]
384 fn fill_ref_1001_selects_first_background_fill() {
385 let fixture = Fixture::new(&shape("", &style_refs(0, 1001, 0, "major", "102030")));
386 let resolved = fixture
387 .context()
388 .effective_shape_style(fixture.shape(0))
389 .unwrap();
390
391 let Some(Fill::Solid(fill)) = resolved.fill else {
392 panic!("expected the first background solid fill");
393 };
394 assert!(matches!(
395 fill.color,
396 Some(ColorChoice::Srgb { value, .. }) if value.to_string() == "102030"
397 ));
398 }
399
400 #[test]
401 fn style_matrix_zero_is_none_and_out_of_range_is_error() {
402 let shapes = [
403 shape("", &style_refs(0, 0, 0, "none", "112233")),
404 shape("", &style_refs(0, 4, 0, "none", "112233")),
405 ]
406 .join("");
407 let fixture = Fixture::new(&shapes);
408
409 let zero = fixture
410 .context()
411 .effective_shape_style(fixture.shape(0))
412 .unwrap();
413 assert!(zero.fill.is_none());
414 assert!(zero.line.is_none());
415 assert!(zero.effects.is_none());
416 assert_eq!(zero.font_collection, Some(FontCollectionIndex::None));
417
418 assert_eq!(
419 fixture
420 .context()
421 .effective_shape_style(fixture.shape(1))
422 .unwrap_err(),
423 ResolveError::StyleIndexOutOfRange {
424 reference: "fill",
425 index: 4,
426 available: 3,
427 }
428 );
429 }
430
431 #[test]
432 fn placeholder_colour_substitution_keeps_transform_order() {
433 let mut fixture = Fixture::new(&shape(
434 "",
435 &style_refs_with_fill_colour(
436 1,
437 r#"<a:schemeClr val="accent2"><a:shade val="80000"/></a:schemeClr>"#,
438 ),
439 ));
440 fixture.theme.theme_elements.format_scheme.fill_styles[0] = Fill::from_xml(
441 br#"<a:solidFill><a:schemeClr val="phClr"><a:tint val="30000"/></a:schemeClr></a:solidFill>"#,
442 )
443 .unwrap();
444
445 let resolved = fixture
446 .context()
447 .effective_shape_style(fixture.shape(0))
448 .unwrap();
449 let Some(Fill::Solid(fill)) = resolved.fill else {
450 panic!("expected a solid fill");
451 };
452 let Some(ColorChoice::Scheme {
453 value, transforms, ..
454 }) = fill.color
455 else {
456 panic!("expected a substituted scheme colour");
457 };
458 assert_eq!(value, "accent2");
459 assert!(matches!(
460 transforms.as_slice(),
461 [ColorTransform::Shade(shade), ColorTransform::Tint(tint)]
462 if shade.0 == 80_000 && tint.0 == 30_000
463 ));
464 }
465
466 #[test]
467 fn explicit_shape_properties_overlay_the_theme_style() {
468 let fixture = Fixture::new(&shape(
469 r#"<a:noFill/><a:ln w="25400"><a:prstDash val="dot"/></a:ln><a:effectLst/>"#,
470 &style_refs(2, 1, 3, "minor", "445566"),
471 ));
472 let resolved = fixture
473 .context()
474 .effective_shape_style(fixture.shape(0))
475 .unwrap();
476
477 assert!(matches!(resolved.fill, Some(Fill::NoFill(_))));
478 let line = resolved.line.unwrap();
479 assert_eq!(line.width, Some(25_400));
480 assert_eq!(line.cap, Some(LineCap::Flat));
481 assert!(matches!(
482 line.dash,
483 Some(LineDash::Preset(ref dash)) if dash.value == ST_PresetLineDashVal::Dot
484 ));
485 assert!(matches!(
486 line.fill,
487 Some(Fill::Solid(fill))
488 if matches!(fill.color, Some(ColorChoice::Srgb { value, .. }) if value.to_string() == "445566")
489 ));
490 assert!(resolved.effects.unwrap().outer_shadow.is_none());
491 assert_eq!(resolved.font_collection, Some(FontCollectionIndex::Minor));
492 }
493
494 #[test]
495 fn modelled_effect_placeholder_colour_is_substituted() {
496 let mut fixture = Fixture::new(&shape("", &style_refs(0, 0, 3, "none", "667788")));
497 let Fill::Solid(fill) = Fill::from_xml(
498 br#"<a:solidFill><a:schemeClr val="phClr"><a:tint val="25000"/></a:schemeClr></a:solidFill>"#,
499 )
500 .unwrap()
501 else {
502 unreachable!();
503 };
504 fixture.theme.theme_elements.format_scheme.effect_styles[2]
505 .effect_list
506 .as_mut()
507 .unwrap()
508 .outer_shadow
509 .as_mut()
510 .unwrap()
511 .color = fill.color;
512
513 let resolved = fixture
514 .context()
515 .effective_shape_style(fixture.shape(0))
516 .unwrap();
517 let color = resolved
518 .effects
519 .unwrap()
520 .outer_shadow
521 .unwrap()
522 .color
523 .unwrap();
524 assert!(matches!(
525 color,
526 ColorChoice::Srgb { value, transforms, .. }
527 if value.to_string() == "667788"
528 && matches!(transforms.as_slice(), [ColorTransform::Tint(tint)] if tint.0 == 25_000)
529 ));
530 }
531
532 #[test]
533 fn unmodelled_effect_with_placeholder_colour_is_rejected() {
534 let fixture = Fixture::new(&shape(
535 r#"<a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst>"#,
536 &style_refs(0, 0, 0, "none", "778899"),
537 ));
538
539 assert_eq!(
540 fixture
541 .context()
542 .effective_shape_style(fixture.shape(0))
543 .unwrap_err(),
544 ResolveError::UnresolvedPlaceholderColor {
545 reference: "effect",
546 }
547 );
548 }
549
550 #[test]
551 fn explicit_opaque_effect_dag_replaces_theme_effect() {
552 let fixture = Fixture::new(&shape(
553 r#"<a:effectDag><a:cont/></a:effectDag>"#,
554 &style_refs(0, 0, 3, "none", "778899"),
555 ));
556
557 let resolved = fixture
558 .context()
559 .effective_shape_style(fixture.shape(0))
560 .unwrap();
561 assert!(resolved.effects.is_none());
562 }
563
564 #[test]
565 fn explicit_opaque_effect_dag_with_placeholder_colour_is_rejected() {
566 let fixture = Fixture::new(&shape(
567 r#"<a:effectDag><a:cont><a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst></a:cont></a:effectDag>"#,
568 &style_refs(0, 0, 3, "none", "778899"),
569 ));
570
571 assert_eq!(
572 fixture
573 .context()
574 .effective_shape_style(fixture.shape(0))
575 .unwrap_err(),
576 ResolveError::UnresolvedPlaceholderColor {
577 reference: "effect",
578 }
579 );
580 }
581
582 #[test]
583 fn theme_opaque_effect_dag_with_placeholder_colour_is_rejected() {
584 let mut fixture = Fixture::new(&shape("", &style_refs(0, 0, 1, "none", "778899")));
585 let xml = String::from_utf8(fixture.theme.to_xml().unwrap()).unwrap();
586 let xml = xml.replacen(
587 "<a:effectLst/>",
588 r#"<a:effectDag><a:cont><a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst></a:cont></a:effectDag>"#,
589 1,
590 );
591 fixture.theme = CT_OfficeStyleSheet::from_xml(xml.as_bytes()).unwrap();
592
593 assert_eq!(
594 fixture
595 .context()
596 .effective_shape_style(fixture.shape(0))
597 .unwrap_err(),
598 ResolveError::UnresolvedPlaceholderColor {
599 reference: "effect",
600 }
601 );
602 }
603
604 #[test]
605 fn foreign_namespace_effect_dag_does_not_replace_theme_effect() {
606 let fixture = Fixture::new(&shape(
607 r#"<x:effectDag xmlns:x="urn:extension"><x:schemeClr val="phClr"/></x:effectDag>"#,
608 &style_refs(0, 0, 3, "none", "778899"),
609 ));
610
611 let resolved = fixture
612 .context()
613 .effective_shape_style(fixture.shape(0))
614 .unwrap();
615 assert!(resolved.effects.is_some());
616 }
617
618 #[test]
619 fn foreign_namespace_scheme_color_is_not_a_placeholder_color() {
620 let fixture = Fixture::new(&shape(
621 r#"<a:effectDag><a:cont><x:schemeClr xmlns:x="urn:extension" val="phClr"/></a:cont></a:effectDag>"#,
622 &style_refs(0, 0, 3, "none", "778899"),
623 ));
624
625 let resolved = fixture
626 .context()
627 .effective_shape_style(fixture.shape(0))
628 .unwrap();
629 assert!(resolved.effects.is_none());
630 }
631
632 struct Fixture {
633 theme: CT_OfficeStyleSheet,
634 master: CT_SlideMaster,
635 layout: CT_SlideLayout,
636 slide: CT_Slide,
637 default_text_style: CT_TextListStyle,
638 }
639
640 impl Fixture {
641 fn new(shapes: &str) -> Self {
642 Self {
643 theme: CT_OfficeStyleSheet::office_default(),
644 master: CT_SlideMaster::from_xml(master_xml().as_bytes()).unwrap(),
645 layout: CT_SlideLayout::from_xml(layout_xml().as_bytes()).unwrap(),
646 slide: CT_Slide::from_xml(slide_xml(shapes).as_bytes()).unwrap(),
647 default_text_style: CT_TextListStyle::default(),
648 }
649 }
650
651 fn context(&self) -> ResolveCtx<'_> {
652 ResolveCtx::new(
653 &self.theme,
654 ColorMap::default(),
655 &self.master,
656 &self.layout,
657 &self.slide,
658 &self.default_text_style,
659 )
660 }
661
662 fn shape(&self, index: usize) -> &CT_Shape {
663 let ShapeTreeChild::Shape(shape) =
664 &self.slide.common_slide_data.shape_tree.children[index]
665 else {
666 panic!("expected an ordinary shape");
667 };
668 shape
669 }
670 }
671
672 fn style_refs(line: u32, fill: u32, effect: u32, font: &str, color: &str) -> String {
673 format!(
674 r#"<p:style><a:lnRef idx="{line}"><a:srgbClr val="{color}"/></a:lnRef><a:fillRef idx="{fill}"><a:srgbClr val="{color}"/></a:fillRef><a:effectRef idx="{effect}"><a:srgbClr val="{color}"/></a:effectRef><a:fontRef idx="{font}"><a:srgbClr val="{color}"/></a:fontRef></p:style>"#
675 )
676 }
677
678 fn style_refs_with_fill_colour(fill: u32, fill_colour: &str) -> String {
679 format!(
680 r#"<p:style><a:lnRef idx="0"><a:srgbClr val="000000"/></a:lnRef><a:fillRef idx="{fill}">{fill_colour}</a:fillRef><a:effectRef idx="0"><a:srgbClr val="000000"/></a:effectRef><a:fontRef idx="minor"><a:srgbClr val="000000"/></a:fontRef></p:style>"#
681 )
682 }
683
684 fn shape(properties: &str, style: &str) -> String {
685 format!(
686 "<p:sp><p:nvSpPr><p:cNvPr/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr>{properties}</p:spPr>{style}</p:sp>"
687 )
688 }
689
690 fn slide_xml(shapes: &str) -> String {
691 format!(
692 "<p:sld xmlns:p=\"{P_NS}\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld></p:sld>",
693 shape_tree(shapes)
694 )
695 }
696
697 fn layout_xml() -> String {
698 format!(
699 "<p:sldLayout xmlns:p=\"{P_NS}\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld></p:sldLayout>",
700 shape_tree("")
701 )
702 }
703
704 fn master_xml() -> String {
705 format!(
706 "<p:sldMaster xmlns:p=\"{P_NS}\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld><p:clrMap bg1=\"lt1\" tx1=\"dk1\" bg2=\"lt2\" tx2=\"dk2\" accent1=\"accent1\" accent2=\"accent2\" accent3=\"accent3\" accent4=\"accent4\" accent5=\"accent5\" accent6=\"accent6\" hlink=\"hlink\" folHlink=\"folHlink\"/></p:sldMaster>",
707 shape_tree("")
708 )
709 }
710
711 fn shape_tree(shapes: &str) -> String {
712 format!("<p:spTree><p:nvGrpSpPr/><p:grpSpPr/>{shapes}</p:spTree>")
713 }
714}