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