1use super::context::ThemeColorMap;
2
3pub fn default_drawing_scheme_colors() -> ThemeColorMap {
5 ThemeColorMap::from([
6 ("bg1".to_string(), "#ffffff".to_string()),
7 ("bg2".to_string(), "#f3f4f6".to_string()),
8 ("tx1".to_string(), "#000000".to_string()),
9 ("tx2".to_string(), "#1f2937".to_string()),
10 ("dk1".to_string(), "#000000".to_string()),
11 ("dk2".to_string(), "#1f2937".to_string()),
12 ("lt1".to_string(), "#ffffff".to_string()),
13 ("lt2".to_string(), "#f3f4f6".to_string()),
14 ("accent1".to_string(), "#4472c4".to_string()),
15 ("accent2".to_string(), "#ed7d31".to_string()),
16 ("accent3".to_string(), "#70ad47".to_string()),
17 ("accent4".to_string(), "#5b9bd5".to_string()),
18 ("accent5".to_string(), "#7030a0".to_string()),
19 ("accent6".to_string(), "#ffc000".to_string()),
20 ("hlink".to_string(), "#0563c1".to_string()),
21 ("folhlink".to_string(), "#954f72".to_string()),
22 ("followedhyperlink".to_string(), "#954f72".to_string()),
23 ])
24}
25
26pub fn normalize_hex_color(value: Option<&str>) -> Option<String> {
28 let normalized = value?.trim();
29 if normalized.is_empty() || normalized.eq_ignore_ascii_case("auto") {
30 return None;
31 }
32
33 if normalized.len() == 6 && normalized.chars().all(|ch| ch.is_ascii_hexdigit()) {
34 return Some(format!("#{normalized}"));
35 }
36
37 if normalized.len() == 3 && normalized.chars().all(|ch| ch.is_ascii_hexdigit()) {
38 let expanded: String = normalized.chars().flat_map(|ch| [ch, ch]).collect();
39 return Some(format!("#{expanded}"));
40 }
41
42 if normalized.starts_with('#')
43 && normalized.len() == 7
44 && normalized[1..].chars().all(|ch| ch.is_ascii_hexdigit())
45 {
46 return Some(normalized.to_string());
47 }
48
49 None
50}
51
52#[derive(Clone, Debug, PartialEq)]
54pub struct ResolvedDrawingColor {
55 pub color: String,
56 pub opacity: Option<f64>,
57}
58
59pub fn resolve_drawing_color_from_xml(
60 color_xml: Option<&str>,
61 theme_colors: &ThemeColorMap,
62) -> Option<ResolvedDrawingColor> {
63 let color_xml = color_xml?;
64 let defaults = default_drawing_scheme_colors();
65
66 let srgb = find_drawing_attribute_value(color_xml, "a:srgbClr", "val")
67 .as_deref()
68 .and_then(|value| normalize_hex_color(Some(value)));
69 let sys = find_drawing_attribute_value(color_xml, "a:sysClr", "lastClr")
70 .as_deref()
71 .and_then(|value| normalize_hex_color(Some(value)));
72 let scheme_token = find_drawing_attribute_value(color_xml, "a:schemeClr", "val")
73 .map(|token| token.trim().to_ascii_lowercase());
74 let scheme = scheme_token.as_ref().and_then(|token| {
75 theme_colors
76 .get(token)
77 .cloned()
78 .or_else(|| defaults.get(token).cloned())
79 });
80 let preset = find_drawing_attribute_value(color_xml, "a:prstClr", "val").and_then(|value| {
81 match value.as_str() {
82 "black" => Some("#000000".to_string()),
83 "white" => Some("#ffffff".to_string()),
84 _ => None,
85 }
86 });
87 let color = apply_drawing_color_transforms(&srgb.or(sys).or(scheme).or(preset)?, color_xml)?;
88
89 let alpha_raw = find_drawing_attribute_value(color_xml, "a:alpha", "val");
90 let alpha = alpha_raw.and_then(|raw| raw.parse::<f64>().ok());
91 let opacity = alpha
92 .filter(|value| value.is_finite() && *value >= 0.0)
93 .map(|value| (value / 100_000.0).clamp(0.0, 1.0));
94
95 Some(ResolvedDrawingColor { color, opacity })
96}
97
98fn apply_drawing_color_transforms(color: &str, xml: &str) -> Option<String> {
99 let tags = [
100 "a:tint", "a:shade", "a:sat", "a:satMod", "a:satOff", "a:lum", "a:lumMod", "a:lumOff",
101 ];
102 let transforms = crate::xml::extract_balanced_tag_blocks_in_order(xml, &tags);
103 if transforms.is_empty() {
104 return Some(color.to_string());
105 }
106 let values = transforms.into_iter().filter_map(|tag| {
107 let value = crate::xml::get_attribute(&xml[tag.start..tag.end], "val")?
108 .parse::<f64>()
109 .ok()?;
110 if !value.is_finite() {
111 return None;
112 }
113 Some((tag.tag_name, value / 100_000.0))
114 });
115 transform_rgb(color, values, false)
116}
117
118fn linear(c: f64) -> f64 {
119 if c <= 0.04045 {
120 c / 12.92
121 } else {
122 ((c + 0.055) / 1.055).powf(2.4)
123 }
124}
125
126pub(super) fn srgb(c: f64) -> f64 {
127 if c <= 0.0031308 {
128 c * 12.92
129 } else {
130 1.055 * c.powf(1. / 2.4) - 0.055
131 }
132}
133
134pub(super) fn hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> [f64; 3] {
135 let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
136 let x = chroma * (1.0 - (hue % 2.0 - 1.0).abs());
137 let offset = lightness - chroma / 2.0;
138 match hue as u32 {
139 0 => [chroma, x, 0.0],
140 1 => [x, chroma, 0.0],
141 2 => [0.0, chroma, x],
142 3 => [0.0, x, chroma],
143 4 => [x, 0.0, chroma],
144 _ => [chroma, 0.0, x],
145 }
146 .map(|channel| channel + offset)
147}
148
149pub(super) fn transform_rgb(
150 color: &str,
151 transforms: impl IntoIterator<Item = (impl AsRef<str>, f64)>,
152 linear_tints: bool,
153) -> Option<String> {
154 let mut rgb = [0.0; 3];
155 for (index, channel) in rgb.iter_mut().enumerate() {
156 *channel =
157 u8::from_str_radix(color.get(1 + index * 2..3 + index * 2)?, 16).ok()? as f64 / 255.0;
158 }
159 for (name, amount) in transforms {
160 let name = name.as_ref().trim_start_matches("a:");
161 match name {
162 "tint" | "shade" if linear_tints => {
163 rgb = rgb.map(|c| {
164 srgb(linear(c) * amount + if name == "tint" { 1. - amount } else { 0. })
165 });
166 }
167 "gamma" => rgb = rgb.map(srgb),
168 "invGamma" => rgb = rgb.map(linear),
169 "inv" => rgb = rgb.map(|c| srgb(1. - linear(c))),
170 "gray" => rgb = [0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; 3],
171 "red" | "green" | "blue" | "redMod" | "greenMod" | "blueMod" | "redOff"
172 | "greenOff" | "blueOff" => {
173 let i = if name.starts_with("red") {
174 0
175 } else if name.starts_with("green") {
176 1
177 } else {
178 2
179 };
180 rgb[i] = srgb(if name.ends_with("Mod") {
181 linear(rgb[i]) * amount
182 } else if name.ends_with("Off") {
183 linear(rgb[i]) + amount
184 } else {
185 amount
186 });
187 }
188 "tint" => rgb = rgb.map(|channel| channel * amount + 1.0 - amount),
189 "shade" => rgb = rgb.map(|channel| channel * amount),
190 "comp" | "hue" | "hueMod" | "hueOff" | "sat" | "satMod" | "satOff" | "lum"
191 | "lumMod" | "lumOff" => {
192 let max = rgb.into_iter().fold(0.0_f64, f64::max);
193 let min = rgb.into_iter().fold(1.0_f64, f64::min);
194 let delta = max - min;
195 let mut lightness = (max + min) / 2.0;
196 let mut saturation = if delta == 0.0 {
197 0.0
198 } else {
199 delta / (1.0 - (2.0 * lightness - 1.0).abs())
200 };
201 let hue = if delta == 0.0 {
202 0.0
203 } else if max == rgb[0] {
204 ((rgb[1] - rgb[2]) / delta).rem_euclid(6.0)
205 } else if max == rgb[1] {
206 (rgb[2] - rgb[0]) / delta + 2.0
207 } else {
208 (rgb[0] - rgb[1]) / delta + 4.0
209 };
210 let hue = if name == "comp" || name.starts_with("hue") {
211 match name {
212 "comp" => hue + 3.,
213 "hue" => amount * 6.,
214 "hueMod" => hue * amount,
215 _ => hue + amount * 6.,
216 }
217 .rem_euclid(6.)
218 } else {
219 let channel = if name.starts_with("sat") {
220 &mut saturation
221 } else {
222 &mut lightness
223 };
224 *channel = if name.ends_with("Mod") {
225 *channel * amount
226 } else if name.ends_with("Off") {
227 *channel + amount
228 } else {
229 amount
230 }
231 .clamp(0., 1.);
232 hue
233 };
234 rgb = hsl_to_rgb(hue, saturation, lightness);
235 }
236 _ => continue,
237 }
238 rgb = rgb.map(|channel| channel.clamp(0.0, 1.0));
239 }
240 Some(format!(
241 "#{:02x}{:02x}{:02x}",
242 (rgb[0] * 255.0).round() as u8,
243 (rgb[1] * 255.0).round() as u8,
244 (rgb[2] * 255.0).round() as u8
245 ))
246}
247
248fn find_drawing_attribute_value(xml: &str, tag_name: &str, attribute: &str) -> Option<String> {
249 let tag_token = super::scan::find_tag_token(xml, tag_name)?;
250 crate::xml::get_attribute(&tag_token, attribute)
251}