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