microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/1124-exclude-python-env-dirs-from-skill-validation

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/skills/experimental/powerpoint/scripts/pptx_fills.py

358lines · modecode

1# Copyright (c) Microsoft Corporation.
2# SPDX-License-Identifier: MIT
3"""Fill and line application/extraction utilities for PowerPoint skill scripts.
4
5Handles solid, gradient, and pattern fills plus line/border properties.
6"""
7
8from lxml import etree
9from pptx.enum.dml import MSO_FILL, MSO_LINE_DASH_STYLE, MSO_PATTERN_TYPE
10from pptx.oxml.ns import qn
11from pptx.util import Pt
12from pptx_colors import (
13 apply_color_spec,
14 apply_color_to_fill,
15 extract_color,
16 resolve_color,
17 rgb_to_hex,
18)
19
20DASH_STYLE_MAP = {
21 "solid": MSO_LINE_DASH_STYLE.SOLID,
22 "dash": MSO_LINE_DASH_STYLE.DASH,
23 "dash_dot": MSO_LINE_DASH_STYLE.DASH_DOT,
24 "dash_dot_dot": MSO_LINE_DASH_STYLE.DASH_DOT_DOT,
25 "long_dash": MSO_LINE_DASH_STYLE.LONG_DASH,
26 "long_dash_dot": MSO_LINE_DASH_STYLE.LONG_DASH_DOT,
27 "round_dot": MSO_LINE_DASH_STYLE.ROUND_DOT,
28 "square_dot": MSO_LINE_DASH_STYLE.SQUARE_DOT,
29}
30
31DASH_STYLE_REVERSE = {v: k for k, v in DASH_STYLE_MAP.items()}
32
33
34def apply_fill(shape, fill_spec, colors: dict):
35 """Apply fill specification to a shape or background.
36
37 Supports:
38 str — solid fill via resolve_color()
39 dict with type: gradient — gradient fill with angle and stops
40 dict with type: pattern — pattern fill with fore/back colors
41 dict with type: solid — explicit solid fill
42 None — no fill (background)
43 """
44 if fill_spec is None:
45 shape.fill.background()
46 return
47
48 if isinstance(fill_spec, str):
49 shape.fill.solid()
50 color_spec = resolve_color(fill_spec, colors)
51 apply_color_to_fill(shape.fill, color_spec)
52 return
53
54 if not isinstance(fill_spec, dict):
55 return
56
57 fill_type = fill_spec.get("type", "solid")
58
59 if fill_type == "solid":
60 _apply_solid_fill(shape, fill_spec, colors)
61 return
62
63 if fill_type == "gradient":
64 _apply_gradient_fill(shape, fill_spec, colors)
65 return
66
67 if fill_type == "pattern":
68 _apply_pattern_fill(shape, fill_spec, colors)
69
70
71def _set_alpha_on_color_element(color_el, alpha_val: str):
72 """Set or update an alpha child element on a color XML element."""
73 existing = color_el.find(qn("a:alpha"))
74 if existing is not None:
75 existing.set("val", alpha_val)
76 else:
77 etree.SubElement(color_el, qn("a:alpha")).set("val", alpha_val)
78
79
80def _apply_solid_fill(shape, fill_spec: dict, colors: dict):
81 """Apply a solid fill with optional alpha."""
82 shape.fill.solid()
83 color_spec = resolve_color(fill_spec.get("color", "#000000"), colors)
84 apply_color_to_fill(shape.fill, color_spec)
85 if "alpha" not in fill_spec:
86 return
87 alpha_val = str(int(fill_spec["alpha"] * 1000))
88 solid_el = shape.fill._fill._solidFill
89 if solid_el is not None and len(solid_el) > 0:
90 _set_alpha_on_color_element(solid_el[0], alpha_val)
91
92
93def _apply_gradient_fill(shape, fill_spec: dict, colors: dict):
94 """Apply a gradient fill with stops and optional per-stop alpha."""
95 shape.fill.gradient()
96 shape.fill.gradient_angle = fill_spec.get("angle", 90)
97 stops_data = fill_spec.get("stops", [])
98
99 existing_count = len(shape.fill.gradient_stops)
100 if len(stops_data) > existing_count:
101 gs_lst = shape.fill._fill._element.find(qn("a:gsLst"))
102 if gs_lst is not None:
103 for _ in range(len(stops_data) - existing_count):
104 new_gs = etree.SubElement(gs_lst, qn("a:gs"))
105 new_gs.set("pos", "0")
106 etree.SubElement(new_gs, qn("a:srgbClr")).set("val", "000000")
107
108 for i, stop in enumerate(stops_data):
109 if i >= len(shape.fill.gradient_stops):
110 break
111 gs = shape.fill.gradient_stops[i]
112 color_spec = resolve_color(stop["color"], colors)
113 apply_color_spec(gs.color, color_spec)
114 gs.position = stop["position"]
115 if "alpha" in stop:
116 alpha_val = str(int(stop["alpha"] * 1000))
117 gs_el = gs._element
118 color_el = gs_el[0] if len(gs_el) > 0 else None
119 if color_el is not None:
120 _set_alpha_on_color_element(color_el, alpha_val)
121
122
123def _apply_pattern_fill(shape, fill_spec: dict, colors: dict):
124 """Apply a pattern fill with fore/back colors and optional alpha."""
125 shape.fill.patterned()
126 pattern_name = fill_spec.get("pattern", "CROSS").upper()
127 shape.fill.pattern = getattr(MSO_PATTERN_TYPE, pattern_name, MSO_PATTERN_TYPE.CROSS)
128 fore_spec = resolve_color(fill_spec.get("fore_color", "#000000"), colors)
129 back_spec = resolve_color(fill_spec.get("back_color", "#FFFFFF"), colors)
130 apply_color_spec(shape.fill.fore_color, fore_spec)
131 apply_color_spec(shape.fill.back_color, back_spec)
132
133 patt_el = shape.fill._fill._pattFill
134 if patt_el is None:
135 return
136 if "fore_alpha" in fill_spec:
137 fg = patt_el.find(qn("a:fgClr"))
138 if fg is not None and len(fg) > 0:
139 _set_alpha_on_color_element(fg[0], str(int(fill_spec["fore_alpha"] * 1000)))
140 if "back_alpha" in fill_spec:
141 bg = patt_el.find(qn("a:bgClr"))
142 if bg is not None and len(bg) > 0:
143 _set_alpha_on_color_element(bg[0], str(int(fill_spec["back_alpha"] * 1000)))
144
145
146def extract_fill(fill) -> dict | str | None:
147 """Extract fill information from a shape's fill object.
148
149 Returns:
150 str — hex color string for solid fills
151 dict — structured fill spec for gradient or pattern fills
152 None — no fill or background fill
153 """
154 try:
155 fill_type = fill.type
156 if fill_type is None or fill_type == MSO_FILL.BACKGROUND:
157 return None
158
159 if fill_type == MSO_FILL.SOLID:
160 color = extract_color(fill.fore_color) or rgb_to_hex(fill.fore_color.rgb)
161 # Check for alpha on the color element at XML level
162 try:
163 solid_el = fill._fill._solidFill
164 if solid_el is not None and len(solid_el) > 0:
165 alpha_el = solid_el[0].find(qn("a:alpha"))
166 if alpha_el is not None:
167 alpha_val = int(alpha_el.get("val", "100000"))
168 return {
169 "type": "solid",
170 "color": color,
171 "alpha": round(alpha_val / 1000, 1),
172 }
173 except (AttributeError, TypeError):
174 pass
175 return color
176
177 if fill_type == MSO_FILL.GRADIENT:
178 stops = []
179 for gs in fill.gradient_stops:
180 color = extract_color(gs.color)
181 if color is not None:
182 stop_data = {
183 "position": gs.position,
184 "color": color,
185 }
186 # Extract alpha from the gradient stop's color element
187 alpha_el = gs._element.find(".//" + qn("a:alpha"))
188 if alpha_el is not None:
189 alpha_val = int(alpha_el.get("val", "100000"))
190 stop_data["alpha"] = round(alpha_val / 1000, 1)
191 stops.append(stop_data)
192 result = {"type": "gradient", "stops": stops}
193 try:
194 result["angle"] = fill.gradient_angle
195 except ValueError:
196 pass
197 return result
198
199 if fill_type == MSO_FILL.PATTERNED:
200 pattern_val = fill.pattern
201 pattern_name = "cross"
202 for attr in dir(MSO_PATTERN_TYPE):
203 if attr.startswith("_"):
204 continue
205 try:
206 if getattr(MSO_PATTERN_TYPE, attr) == pattern_val:
207 pattern_name = attr.lower()
208 break
209 except (AttributeError, TypeError):
210 pass
211 result = {
212 "type": "pattern",
213 "pattern": pattern_name,
214 "fore_color": extract_color(fill.fore_color)
215 or rgb_to_hex(fill.fore_color.rgb),
216 "back_color": extract_color(fill.back_color)
217 or rgb_to_hex(fill.back_color.rgb),
218 }
219 # Extract alpha from pattern fore/back color elements
220 try:
221 patt_el = fill._fill._pattFill
222 if patt_el is not None:
223 fg = patt_el.find(qn("a:fgClr"))
224 if fg is not None and len(fg) > 0:
225 alpha_el = fg[0].find(qn("a:alpha"))
226 if alpha_el is not None:
227 result["fore_alpha"] = round(
228 int(alpha_el.get("val", "100000")) / 1000, 1
229 )
230 bg = patt_el.find(qn("a:bgClr"))
231 if bg is not None and len(bg) > 0:
232 alpha_el = bg[0].find(qn("a:alpha"))
233 if alpha_el is not None:
234 result["back_alpha"] = round(
235 int(alpha_el.get("val", "100000")) / 1000, 1
236 )
237 except (AttributeError, TypeError):
238 pass
239 return result
240 except (AttributeError, TypeError):
241 pass
242
243 return None
244
245
246def apply_line(shape, elem: dict, colors: dict):
247 """Apply line/border properties from element definition.
248
249 Reads line_color, line_width, and dash_style from elem dict.
250 """
251 if "line_color" in elem:
252 color_spec = resolve_color(elem["line_color"], colors)
253 apply_color_spec(shape.line.color, color_spec)
254 shape.line.width = Pt(elem.get("line_width", 1))
255 if "dash_style" in elem:
256 shape.line.dash_style = DASH_STYLE_MAP.get(
257 elem["dash_style"], MSO_LINE_DASH_STYLE.SOLID
258 )
259 else:
260 shape.line.fill.background()
261
262
263def extract_line(shape) -> dict:
264 """Extract line/border properties from a shape."""
265 result = {}
266 try:
267 line = shape.line
268 if line.color and line.color.type is not None:
269 result["line_color"] = extract_color(line.color) or rgb_to_hex(
270 line.color.rgb
271 )
272 if line.width:
273 result["line_width"] = round(line.width.pt, 1)
274 if line.dash_style and line.dash_style != MSO_LINE_DASH_STYLE.SOLID:
275 result["dash_style"] = DASH_STYLE_REVERSE.get(line.dash_style, "solid")
276 except (AttributeError, TypeError):
277 pass
278 return result
279
280
281def extract_effect_list(shape) -> dict | None:
282 """Extract outer shadow effect from a shape's effectLst.
283
284 Returns dict with shadow properties when present, None otherwise.
285 """
286 try:
287 sp = shape._element
288 effect_lst = sp.find(".//" + qn("a:effectLst"))
289 if effect_lst is None or len(effect_lst) == 0:
290 return None
291 shadow = effect_lst.find(qn("a:outerShdw"))
292 if shadow is None:
293 return None
294 return parse_shadow_xml(shadow)
295 except (AttributeError, TypeError, IndexError):
296 return None
297
298
299def apply_effect_list(shape, effect: dict):
300 """Apply outer shadow effect to a shape's spPr element."""
301 if not effect or effect.get("type") != "outer_shadow":
302 return
303 sp_pr = shape._element.find(qn("p:spPr"))
304 if sp_pr is None:
305 sp_pr = shape._element.spPr
306
307 existing = sp_pr.find(qn("a:effectLst"))
308 if existing is not None:
309 sp_pr.remove(existing)
310
311 effect_lst = etree.SubElement(sp_pr, qn("a:effectLst"))
312 build_shadow_xml(effect_lst, effect)
313
314
315def parse_shadow_xml(shadow) -> dict:
316 """Parse an outerShdw XML element into a shadow effect dict."""
317 result = {"type": "outer_shadow"}
318 for attr in ("blurRad", "dist", "dir", "algn", "rotWithShape"):
319 val = shadow.get(attr)
320 if val is not None:
321 result[attr] = val
322 color_el = shadow[0] if len(shadow) > 0 else None
323 if color_el is not None:
324 tag = color_el.tag.split("}")[-1]
325 if tag == "prstClr":
326 result["color"] = color_el.get("val", "black")
327 result["color_type"] = "preset"
328 elif tag == "srgbClr":
329 result["color"] = "#" + color_el.get("val", "000000")
330 result["color_type"] = "rgb"
331 alpha_el = color_el.find(qn("a:alpha"))
332 if alpha_el is not None:
333 result["alpha"] = round(int(alpha_el.get("val", "100000")) / 1000, 1)
334 return result
335
336
337def build_shadow_xml(parent, effect: dict):
338 """Build an outerShdw XML element under the given parent element."""
339 shadow = etree.SubElement(parent, qn("a:outerShdw"))
340 for attr in ("blurRad", "dist", "dir", "algn", "rotWithShape"):
341 if attr in effect:
342 shadow.set(attr, str(effect[attr]))
343
344 color_type = effect.get("color_type", "preset")
345 color_val = effect.get("color", "black")
346 if color_type == "preset":
347 color_el = etree.SubElement(shadow, qn("a:prstClr"))
348 color_el.set("val", color_val)
349 elif color_type == "rgb":
350 color_el = etree.SubElement(shadow, qn("a:srgbClr"))
351 color_el.set("val", color_val.lstrip("#"))
352 else:
353 color_el = etree.SubElement(shadow, qn("a:prstClr"))
354 color_el.set("val", "black")
355
356 if "alpha" in effect:
357 alpha_sub = etree.SubElement(color_el, qn("a:alpha"))
358 alpha_sub.set("val", str(int(effect["alpha"] * 1000)))
359