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_fonts.py

135lines · modecode

1# Copyright (c) Microsoft Corporation.
2# SPDX-License-Identifier: MIT
3"""Font normalization, matching, and extraction utilities.
4
5Centralizes font-related constants and functions used by
6build_deck.py, extract_content.py, and validate_deck.py.
7"""
8
9from pptx.enum.text import PP_ALIGN
10from pptx_colors import rgb_to_hex
11
12FONT_WEIGHT_SUFFIXES = (
13 " Semibold",
14 " SemiBold",
15 " Bold",
16 " Light",
17 " Thin",
18 " Black",
19 " Medium",
20 " ExtraBold",
21 " ExtraLight",
22)
23
24ALIGNMENT_MAP = {
25 "left": PP_ALIGN.LEFT,
26 "center": PP_ALIGN.CENTER,
27 "right": PP_ALIGN.RIGHT,
28 "justify": PP_ALIGN.JUSTIFY,
29}
30
31ALIGNMENT_REVERSE_MAP = {1: "left", 2: "center", 3: "right", 4: "justify"}
32
33
34def normalize_font_family(name: str) -> str:
35 """Strip weight suffixes from a font name to get the base family."""
36 for suffix in FONT_WEIGHT_SUFFIXES:
37 if name.endswith(suffix):
38 return name[: -len(suffix)]
39 return name
40
41
42def font_family_matches(font_name: str, expected_fonts: set[str]) -> bool:
43 """Check if a font matches expected fonts.
44
45 Weight variants (e.g. Segoe UI Semibold) are treated as
46 compatible with the base family.
47 """
48 if font_name in expected_fonts:
49 return True
50 base = font_name
51 for suffix in FONT_WEIGHT_SUFFIXES:
52 if font_name.endswith(suffix):
53 base = font_name[: -len(suffix)]
54 break
55 for expected in expected_fonts:
56 exp_base = expected
57 for suffix in FONT_WEIGHT_SUFFIXES:
58 if expected.endswith(suffix):
59 exp_base = expected[: -len(suffix)]
60 break
61 if base == exp_base:
62 return True
63 return False
64
65
66def extract_font_info(font) -> dict:
67 """Extract font information from a python-pptx font object."""
68 info = {}
69 if font.name:
70 info["font"] = font.name
71 if font.size:
72 info["size"] = int(font.size.pt)
73 try:
74 if font.color and font.color.rgb:
75 info["color"] = rgb_to_hex(font.color.rgb)
76 except (AttributeError, TypeError):
77 pass
78 if font.bold:
79 info["bold"] = True
80 if font.italic:
81 info["italic"] = True
82 if font.underline:
83 info["underline"] = True
84 # Character spacing (spc attribute in hundredths of a point)
85 spc = _extract_char_spacing(font)
86 if spc is not None:
87 info["char_spacing"] = spc
88 return info
89
90
91def _extract_char_spacing(font) -> float | None:
92 """Extract character spacing from font's underlying XML (a:rPr spc attribute).
93
94 Returns spacing in points (spc is stored in hundredths of a point).
95 """
96 try:
97 rpr = font._element
98 spc_val = rpr.get("spc")
99 if spc_val is not None:
100 return int(spc_val) / 100.0
101 except (AttributeError, TypeError):
102 pass
103 return None
104
105
106def extract_paragraph_font(paragraph) -> dict:
107 """Extract font properties from a paragraph's default run properties.
108
109 python-pptx exposes paragraph-level defaults via ``paragraph.font``.
110 Many PPTX files store styling here rather than on individual runs.
111 """
112 info = {}
113 font = paragraph.font
114 if font.name:
115 info["font"] = font.name
116 if font.size:
117 info["size"] = int(font.size.pt)
118 try:
119 if font.color and font.color.rgb:
120 info["color"] = rgb_to_hex(font.color.rgb)
121 except (AttributeError, TypeError):
122 pass
123 if font.bold is True:
124 info["bold"] = True
125 if font.italic is True:
126 info["italic"] = True
127 return info
128
129
130def extract_alignment(paragraph) -> str | None:
131 """Map a paragraph alignment enum to a string."""
132 al = paragraph.alignment
133 if al is None:
134 return None
135 return ALIGNMENT_REVERSE_MAP.get(int(al))
136