1use serde::{Deserialize, Serialize};
4
5use crate::{
6 ColorValue, ShapeEffects, ShapeFill, ShapeOutline, Theme, resolve_color_value_to_hex_with_theme,
7};
8
9const DEFAULT_STYLE_LINE_WIDTH_EMU: f64 = 9_525.0;
11
12const BACKGROUND_FILL_INDEX_BASE: u32 = 1_001;
14
15#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct ThemeFormatScheme {
19 #[serde(default, skip_serializing_if = "Vec::is_empty")]
20 pub fills: Vec<Option<ShapeFill>>,
21 #[serde(default, skip_serializing_if = "Vec::is_empty")]
22 pub lines: Vec<Option<ShapeOutline>>,
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
24 pub effects: Vec<Option<ShapeEffects>>,
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 pub background_fills: Vec<Option<ShapeFill>>,
28}
29
30impl ThemeFormatScheme {
31 pub fn is_empty(&self) -> bool {
32 self.fills.is_empty()
33 && self.lines.is_empty()
34 && self.effects.is_empty()
35 && self.background_fills.is_empty()
36 }
37
38 fn fill(&self, index: u32) -> Option<&ShapeFill> {
39 let (list, first) = if index >= BACKGROUND_FILL_INDEX_BASE {
40 (&self.background_fills, BACKGROUND_FILL_INDEX_BASE)
41 } else {
42 (&self.fills, 1)
43 };
44 list.get(index.checked_sub(first)? as usize)?.as_ref()
45 }
46
47 fn line(&self, index: u32) -> Option<&ShapeOutline> {
48 self.lines.get(index.checked_sub(1)? as usize)?.as_ref()
49 }
50}
51
52#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct StyleReference {
56 pub index: u32,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub color: Option<ColorValue>,
59}
60
61#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct ShapeStyle {
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub font_color: Option<ColorValue>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub fill: Option<StyleReference>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub line: Option<StyleReference>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub effect: Option<StyleReference>,
73 #[serde(default, skip_serializing_if = "is_false")]
74 pub fill_disabled: bool,
75 #[serde(default, skip_serializing_if = "is_false")]
76 pub line_disabled: bool,
77}
78
79fn is_false(value: &bool) -> bool {
80 !value
81}
82
83impl ShapeStyle {
84 pub fn is_empty(&self) -> bool {
85 self.font_color.is_none()
86 && self.fill.is_none()
87 && self.line.is_none()
88 && self.effect.is_none()
89 && !self.fill_disabled
90 && !self.line_disabled
91 }
92}
93
94pub fn style_fill(
96 scheme: &ThemeFormatScheme,
97 reference: &StyleReference,
98 theme: &Theme,
99) -> ShapeFill {
100 if matches!(reference.index, 0 | 1000) {
101 return ShapeFill::named("none");
102 }
103 let placeholder = placeholder_color(reference, theme);
104 let Some(fill) = scheme.fill(reference.index) else {
105 return ShapeFill {
106 fill_type: "solid".to_owned(),
107 color: placeholder,
108 gradient: None,
109 };
110 };
111 let mut resolved = fill.clone();
112 resolved.color = substitute_color(fill.color.as_ref(), placeholder.as_ref());
113 if let Some(gradient) = &mut resolved.gradient {
114 for stop in &mut gradient.stops {
115 if let Some(color) = substitute_color(Some(&stop.color), placeholder.as_ref()) {
116 stop.color = color;
117 }
118 }
119 }
120 resolved
121}
122
123pub fn style_outline(
125 scheme: &ThemeFormatScheme,
126 reference: &StyleReference,
127 theme: &Theme,
128) -> Option<ShapeOutline> {
129 if reference.index == 0 {
130 return None;
131 }
132 let placeholder = placeholder_color(reference, theme);
133 let Some(outline) = scheme.line(reference.index) else {
134 return Some(ShapeOutline {
135 width: Some(DEFAULT_STYLE_LINE_WIDTH_EMU),
136 color: placeholder,
137 ..ShapeOutline::default()
138 });
139 };
140 let mut resolved = ShapeOutline {
141 width: outline.width.or(Some(DEFAULT_STYLE_LINE_WIDTH_EMU)),
142 color: substitute_color(outline.color.as_ref(), placeholder.as_ref()),
143 ..outline.clone()
144 };
145 if let Some(gradient) = &mut resolved.gradient {
146 for stop in &mut gradient.stops {
147 if let Some(color) = substitute_color(Some(&stop.color), placeholder.as_ref()) {
148 stop.color = color;
149 }
150 }
151 }
152 Some(resolved)
153}
154
155pub fn style_effects(
157 scheme: &ThemeFormatScheme,
158 reference: &StyleReference,
159 theme: &Theme,
160) -> Option<ShapeEffects> {
161 let index = reference.index.checked_sub(1)? as usize;
162 let mut effects = scheme.effects.get(index)?.as_ref()?.clone();
163 if let Some(shadow) = &mut effects.outer_shadow {
164 shadow.color = substitute_color(
165 shadow.color.as_ref(),
166 placeholder_color(reference, theme).as_ref(),
167 );
168 }
169 Some(effects)
170}
171
172pub fn style_reference_color(
174 value: &ColorValue,
175 reference: &StyleReference,
176 theme: &Theme,
177) -> Option<ColorValue> {
178 substitute_color(Some(value), placeholder_color(reference, theme).as_ref())
179}
180
181fn placeholder_color(reference: &StyleReference, theme: &Theme) -> Option<ColorValue> {
183 let color = reference.color.as_ref()?;
184 if !color.transforms.is_empty() {
185 let base = ColorValue {
186 rgb: color.rgb.clone(),
187 theme_color: color.theme_color.clone(),
188 ..ColorValue::default()
189 };
190 let rgb = resolve_color_value_to_hex_with_theme(Some(&base), Some(theme))?;
191 return Some(ColorValue {
192 rgb: Some(rgb.trim_start_matches('#').to_owned()),
193 theme_color: None,
194 ..color.clone()
195 });
196 }
197 let rgb = resolve_color_value_to_hex_with_theme(Some(color), Some(theme))?;
198 Some(ColorValue {
199 rgb: Some(rgb.trim_start_matches('#').to_owned()),
200 alpha: color.alpha,
201 ..ColorValue::default()
202 })
203}
204
205fn substitute_color(
207 value: Option<&ColorValue>,
208 placeholder: Option<&ColorValue>,
209) -> Option<ColorValue> {
210 let value = value?;
211 if value.theme_color.as_deref() != Some("phClr") {
212 return Some(value.clone());
213 }
214 let Some(placeholder) = placeholder else {
215 return Some(value.clone());
216 };
217 let legacy = value.theme_tint.is_some()
218 || value.theme_shade.is_some()
219 || value.luminance_modulation.is_some()
220 || value.luminance_offset.is_some()
221 || value.saturation_modulation.is_some()
222 || value.alpha.is_some();
223 let placeholder = if !placeholder.transforms.is_empty() && value.transforms.is_empty() && legacy
224 {
225 ColorValue {
226 rgb: resolve_color_value_to_hex_with_theme(Some(placeholder), None)
227 .map(|hex| hex.trim_start_matches('#').to_owned()),
228 alpha: Some(crate::resolve_color_value_alpha(placeholder)),
229 ..ColorValue::default()
230 }
231 } else {
232 placeholder.clone()
233 };
234 let mut transforms: Vec<_> = placeholder.drawingml_transforms().collect();
235 if transforms.is_empty()
236 && !value.transforms.is_empty()
237 && let Some(alpha) = placeholder.alpha
238 {
239 transforms.push(crate::ColorTransform::Alpha(alpha));
240 }
241 transforms.extend(value.drawingml_transforms());
242 Some(ColorValue {
243 rgb: placeholder.rgb.clone(),
244 theme_color: None,
245 auto: None,
246 alpha: value.alpha.or(placeholder.alpha),
247 transforms,
248 ..value.clone()
249 })
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::{GradientFill, GradientStop};
256
257 fn rgb(value: &str) -> ColorValue {
258 ColorValue {
259 rgb: Some(value.to_owned()),
260 ..Default::default()
261 }
262 }
263
264 #[test]
265 fn gradient_outline_substitutes_placeholder_colour_and_preserves_modifiers() {
266 let scheme = ThemeFormatScheme {
267 lines: vec![Some(ShapeOutline {
268 gradient: Some(GradientFill {
269 gradient_type: "linear".into(),
270 angle: Some(45.0),
271 stops: vec![GradientStop {
272 position: 0.0,
273 color: ColorValue {
274 theme_color: Some("phClr".into()),
275 luminance_modulation: Some(0.5),
276 ..Default::default()
277 },
278 }],
279 }),
280 ..Default::default()
281 })],
282 ..Default::default()
283 };
284 let reference = StyleReference {
285 index: 1,
286 color: Some(ColorValue {
287 alpha: Some(0.25),
288 ..rgb("FF0000")
289 }),
290 };
291 let resolved = style_outline(&scheme, &reference, &Theme::default()).unwrap();
292 let gradient = resolved.gradient.unwrap();
293 assert_eq!(gradient.angle, Some(45.0));
294 assert_eq!(gradient.stops[0].color.rgb.as_deref(), Some("FF0000"));
295 assert_eq!(gradient.stops[0].color.theme_color, None);
296 assert_eq!(gradient.stops[0].color.luminance_modulation, Some(0.5));
297 assert_eq!(gradient.stops[0].color.alpha, Some(0.25));
298 }
299
300 fn solid(color: ColorValue) -> ShapeFill {
301 ShapeFill {
302 fill_type: "solid".to_owned(),
303 color: Some(color),
304 gradient: None,
305 }
306 }
307
308 fn reference(index: u32) -> StyleReference {
309 StyleReference {
310 index,
311 color: Some(rgb("808080")),
312 }
313 }
314
315 #[test]
316 fn fill_indices_keep_holes_and_select_background_styles() {
317 let scheme = ThemeFormatScheme {
318 fills: vec![Some(solid(rgb("112233"))), None, Some(solid(rgb("445566")))],
319 background_fills: vec![Some(solid(rgb("778899"))), Some(solid(rgb("AABBCC")))],
320 ..Default::default()
321 };
322 let theme = Theme::default();
323 for (index, expected) in [
324 (1, "112233"),
325 (2, "808080"),
326 (3, "445566"),
327 (999, "808080"),
328 (1001, "778899"),
329 (1002, "AABBCC"),
330 (1003, "808080"),
331 ] {
332 assert_eq!(
333 style_fill(&scheme, &reference(index), &theme)
334 .color
335 .unwrap()
336 .rgb
337 .as_deref(),
338 Some(expected),
339 "index {index}"
340 );
341 }
342 for index in [0, 1000] {
343 assert_eq!(
344 style_fill(&scheme, &reference(index), &theme),
345 ShapeFill::named("none")
346 );
347 }
348 }
349
350 #[test]
351 fn ordered_placeholder_transforms_compose_without_intermediate_rounding() {
352 use crate::{ColorTransform, resolve_color_value_to_rgba_hex};
353
354 let color = ColorValue {
355 theme_color: Some("phClr".to_owned()),
356 transforms: vec![ColorTransform::LumMod(0.6), ColorTransform::AlphaMod(0.5)],
357 ..Default::default()
358 };
359 let scheme = ThemeFormatScheme {
360 fills: vec![
361 Some(solid(color.clone())),
362 Some(ShapeFill {
363 fill_type: "gradient".to_owned(),
364 color: None,
365 gradient: Some(GradientFill {
366 gradient_type: "linear".to_owned(),
367 angle: None,
368 stops: vec![GradientStop {
369 position: 0.0,
370 color: color.clone(),
371 }],
372 }),
373 }),
374 ],
375 lines: vec![Some(ShapeOutline {
376 color: Some(color),
377 ..Default::default()
378 })],
379 ..Default::default()
380 };
381 let mut reference = StyleReference {
382 index: 1,
383 color: Some(ColorValue {
384 theme_color: Some("accent1".to_owned()),
385 transforms: vec![ColorTransform::Tint(0.5), ColorTransform::Alpha(0.5)],
386 ..Default::default()
387 }),
388 };
389 let theme = Theme::default();
390 let solid = style_fill(&scheme, &reference, &theme).color.unwrap();
391 let line = style_outline(&scheme, &reference, &theme)
392 .unwrap()
393 .color
394 .unwrap();
395 reference.index = 2;
396 let gradient = style_fill(&scheme, &reference, &theme).gradient.unwrap();
397 for color in [&solid, &line, &gradient.stops[0].color] {
398 assert_eq!(
399 color.transforms,
400 vec![
401 ColorTransform::Tint(0.5),
402 ColorTransform::Alpha(0.5),
403 ColorTransform::LumMod(0.6),
404 ColorTransform::AlphaMod(0.5),
405 ]
406 );
407 assert_eq!(
408 resolve_color_value_to_rgba_hex(Some(color), Some(&theme)).as_deref(),
409 Some("#4C64B040")
410 );
411 }
412 reference.index = 1;
413 let color = reference.color.as_mut().unwrap();
414 color.transforms = vec![ColorTransform::Tint(0.5)];
415 color.alpha = Some(0.5);
416 let mixed = style_fill(&scheme, &reference, &theme).color.unwrap();
417 assert_eq!(mixed.transforms, solid.transforms);
418 assert_eq!(
419 resolve_color_value_to_rgba_hex(Some(&mixed), Some(&theme)).as_deref(),
420 Some("#4C64B040")
421 );
422 }
423
424 #[test]
425 fn placeholder_modifiers_compose_in_solids_lines_and_every_gradient_stop() {
426 let first = ColorValue {
427 theme_color: Some("phClr".to_owned()),
428 luminance_modulation: Some(0.5),
429 luminance_offset: Some(0.1),
430 ..Default::default()
431 };
432 let last = ColorValue {
433 luminance_modulation: Some(0.25),
434 luminance_offset: Some(0.2),
435 ..first.clone()
436 };
437 let scheme = ThemeFormatScheme {
438 fills: vec![
439 Some(solid(first.clone())),
440 Some(ShapeFill {
441 fill_type: "gradient".to_owned(),
442 color: None,
443 gradient: Some(GradientFill {
444 gradient_type: "linear".to_owned(),
445 angle: Some(90.0),
446 stops: vec![
447 GradientStop {
448 position: 0.0,
449 color: first.clone(),
450 },
451 GradientStop {
452 position: 50000.0,
453 color: rgb("123456"),
454 },
455 GradientStop {
456 position: 100000.0,
457 color: last,
458 },
459 ],
460 }),
461 }),
462 ],
463 lines: vec![Some(ShapeOutline {
464 color: Some(first),
465 ..Default::default()
466 })],
467 ..Default::default()
468 };
469 let mut theme = Theme::default();
470 theme.color_scheme.accent1 = "808080".to_owned();
471 let reference = |index| StyleReference {
472 index,
473 color: Some(ColorValue {
474 theme_color: Some("accent1".to_owned()),
475 luminance_modulation: Some(0.5),
476 luminance_offset: Some(0.1),
477 ..Default::default()
478 }),
479 };
480 let hex = |color: &ColorValue| {
481 resolve_color_value_to_hex_with_theme(Some(color), Some(&theme)).unwrap()
482 };
483 assert_eq!(
484 hex(style_fill(&scheme, &reference(1), &theme)
485 .color
486 .as_ref()
487 .unwrap()),
488 "#474747"
489 );
490 assert_eq!(
491 hex(style_outline(&scheme, &reference(1), &theme)
492 .unwrap()
493 .color
494 .as_ref()
495 .unwrap()),
496 "#474747"
497 );
498 let gradient = style_fill(&scheme, &reference(2), &theme).gradient.unwrap();
499 assert_eq!(gradient.angle, Some(90.0));
500 assert_eq!(
501 gradient
502 .stops
503 .iter()
504 .map(|s| (s.position, hex(&s.color)))
505 .collect::<Vec<_>>(),
506 vec![
507 (0.0, "#474747".to_owned()),
508 (50000.0, "#123456".to_owned()),
509 (100000.0, "#4A4A4A".to_owned())
510 ]
511 );
512 }
513
514 #[test]
515 fn line_indices_preserve_no_fill_slots_and_explicit_style_properties() {
516 let line = ShapeOutline {
517 width: Some(28575.0),
518 color: Some(rgb("123456")),
519 style: Some("dash".to_owned()),
520 cap: Some("rnd".to_owned()),
521 ..Default::default()
522 };
523 let scheme = ThemeFormatScheme {
524 lines: vec![Some(ShapeOutline::default()), None, Some(line.clone())],
525 ..Default::default()
526 };
527 let theme = Theme::default();
528 assert_eq!(style_outline(&scheme, &reference(0), &theme), None);
529 assert!(
530 style_outline(&scheme, &reference(1), &theme)
531 .unwrap()
532 .color
533 .is_none()
534 );
535 assert_eq!(style_outline(&scheme, &reference(3), &theme), Some(line));
536 for index in [2, 9] {
537 let outline = style_outline(&scheme, &reference(index), &theme).unwrap();
538 assert_eq!(outline.width, Some(9525.0));
539 assert_eq!(outline.color.unwrap().rgb.as_deref(), Some("808080"));
540 }
541 }
542}