返回 ppt-master
formula_run_properties.py
根目录 / skills / ppt-master / scripts / svg_to_pptx / native_objects / formula_run_properties.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Native Formula Run Property Merge
4
5 Merge marker-level DrawingML defaults with compiler-owned local formula style
6 without discarding either source.
7
8 See references/native-formula.md for the owning formula-style contract.
9
10 Usage:
11 Imported by block and inline native formula builders.
12
13 Examples:
14 merge_formula_run_properties(run, marker_properties)
15
16 Dependencies:
17 None (only uses standard library)
18 """
19
20 from __future__ import annotations
21
22 from copy import deepcopy
23 from xml.etree import ElementTree as ET
24
25 from .formula_omml import validate_omml_resource_limits
26 from .formula_parser import FormulaCompileError
27
28
29 DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"
30 MATH_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math"
31
32 _DML_RUN_PROPERTIES = f"{{{DML_NS}}}rPr"
33 _MATH_RUN_PROPERTIES = f"{{{MATH_NS}}}rPr"
34 _MATH_CONTROL_PROPERTIES = f"{{{MATH_NS}}}ctrlPr"
35 _CONTROL_OWNER_TAGS = frozenset({
36 f"{{{MATH_NS}}}{name}"
37 for name in (
38 "accPr",
39 "barPr",
40 "borderBoxPr",
41 "boxPr",
42 "dPr",
43 "fPr",
44 "groupChrPr",
45 "naryPr",
46 "radPr",
47 )
48 })
49 _FILL_TAGS = frozenset({
50 f"{{{DML_NS}}}blipFill",
51 f"{{{DML_NS}}}gradFill",
52 f"{{{DML_NS}}}grpFill",
53 f"{{{DML_NS}}}noFill",
54 f"{{{DML_NS}}}pattFill",
55 f"{{{DML_NS}}}solidFill",
56 })
57 _COLOR_TAGS = frozenset({
58 f"{{{DML_NS}}}{name}"
59 for name in ("hslClr", "prstClr", "schemeClr", "scrgbClr", "srgbClr", "sysClr")
60 })
61 _ALPHA_TRANSFORM_TAGS = frozenset({
62 f"{{{DML_NS}}}{name}"
63 for name in ("alpha", "alphaMod", "alphaOff")
64 })
65 _RUN_PROPERTY_ORDER = {
66 f"{{{DML_NS}}}{name}": index
67 for index, names in enumerate((
68 ("ln",),
69 ("blipFill", "gradFill", "grpFill", "noFill", "pattFill", "solidFill"),
70 ("effectDag", "effectLst"),
71 ("highlight",),
72 ("uLnTx",),
73 ("uLn",),
74 ("uFillTx",),
75 ("uFill",),
76 ("latin",),
77 ("ea",),
78 ("cs",),
79 ("sym",),
80 ("hlinkClick",),
81 ("hlinkMouseOver",),
82 ("rtl",),
83 ("extLst",),
84 ))
85 for name in names
86 }
87
88
89 def _replace_child_group(
90 target: ET.Element,
91 local: ET.Element,
92 tags: frozenset[str],
93 ) -> None:
94 local_children = [child for child in local if child.tag in tags]
95 if not local_children:
96 return
97 positions = [index for index, child in enumerate(target) if child.tag in tags]
98 insert_at = positions[0] if positions else 0
99 for child in list(target):
100 if child.tag in tags:
101 target.remove(child)
102 for offset, child in enumerate(local_children):
103 target.insert(insert_at + offset, deepcopy(child))
104
105
106 def _inherit_default_fill_alpha(
107 default_properties: ET.Element,
108 local_properties: ET.Element,
109 ) -> None:
110 default_fill = next(
111 (child for child in default_properties if child.tag in _FILL_TAGS),
112 None,
113 )
114 local_fill = next(
115 (child for child in local_properties if child.tag in _FILL_TAGS),
116 None,
117 )
118 if default_fill is None or local_fill is None:
119 return
120 default_color = next(
121 (child for child in default_fill if child.tag in _COLOR_TAGS),
122 None,
123 )
124 local_color = next(
125 (child for child in local_fill if child.tag in _COLOR_TAGS),
126 None,
127 )
128 if default_color is None or local_color is None:
129 return
130 if any(child.tag in _ALPHA_TRANSFORM_TAGS for child in local_color):
131 return
132 for child in default_color:
133 if child.tag in _ALPHA_TRANSFORM_TAGS:
134 local_color.append(deepcopy(child))
135
136
137 def _normalize_child_order(properties: ET.Element) -> None:
138 children = list(properties)
139 ordered = sorted(
140 enumerate(children),
141 key=lambda item: (
142 _RUN_PROPERTY_ORDER.get(item[1].tag, len(_RUN_PROPERTY_ORDER)),
143 item[0],
144 ),
145 )
146 if [child for _, child in ordered] == children:
147 return
148 for child in children:
149 properties.remove(child)
150 for _, child in ordered:
151 properties.append(child)
152
153
154 def _merged_properties(
155 default_properties: ET.Element,
156 local_properties: ET.Element | None,
157 ) -> ET.Element:
158 merged = deepcopy(default_properties)
159 if local_properties is None:
160 _normalize_child_order(merged)
161 return merged
162 local = deepcopy(local_properties)
163 _inherit_default_fill_alpha(merged, local)
164 merged.attrib.update(local.attrib)
165 _replace_child_group(merged, local, _FILL_TAGS)
166 for child in local:
167 if child.tag in _FILL_TAGS:
168 continue
169 for existing in list(merged):
170 if existing.tag == child.tag:
171 merged.remove(existing)
172 merged.append(deepcopy(child))
173 _normalize_child_order(merged)
174 return merged
175
176
177 def merge_formula_run_properties(
178 run: ET.Element,
179 default_properties: ET.Element,
180 ) -> None:
181 """Merge one formula run's local a:rPr over marker-level defaults."""
182 if default_properties.tag != _DML_RUN_PROPERTIES:
183 raise RuntimeError("Formula run defaults must use one a:rPr root")
184 local_properties = next(
185 (child for child in run if child.tag == _DML_RUN_PROPERTIES),
186 None,
187 )
188 merged = _merged_properties(default_properties, local_properties)
189 if local_properties is not None:
190 run.remove(local_properties)
191 insert_at = 1 if len(run) and run[0].tag == _MATH_RUN_PROPERTIES else 0
192 run.insert(insert_at, merged)
193
194
195 def merge_formula_control_properties(
196 root: ET.Element,
197 default_properties: ET.Element,
198 ) -> None:
199 """Merge marker defaults onto visible non-selectable math controls."""
200 if default_properties.tag != _DML_RUN_PROPERTIES:
201 raise RuntimeError("Formula control defaults must use one a:rPr root")
202 for owner in root.iter():
203 if owner.tag not in _CONTROL_OWNER_TAGS:
204 continue
205 control = next(
206 (child for child in owner if child.tag == _MATH_CONTROL_PROPERTIES),
207 None,
208 )
209 if control is None:
210 control = ET.SubElement(owner, _MATH_CONTROL_PROPERTIES)
211 local_properties = next(
212 (child for child in control if child.tag == _DML_RUN_PROPERTIES),
213 None,
214 )
215 merged = _merged_properties(default_properties, local_properties)
216 if local_properties is not None:
217 control.remove(local_properties)
218 control.insert(0, merged)
219
220
221 def serialize_styled_formula_omml(root: ET.Element) -> str:
222 """Serialize styled OMML under the compiler's final size and depth limits."""
223 try:
224 xml = ET.tostring(root, encoding="unicode", short_empty_elements=True)
225 return validate_omml_resource_limits(xml)
226 except (FormulaCompileError, RecursionError) as exc:
227 raise RuntimeError(f"Invalid styled formula OMML: {exc}") from exc
228
229
230 __all__ = [
231 "merge_formula_control_properties",
232 "merge_formula_run_properties",
233 "serialize_styled_formula_omml",
234 ]
235
235 lines PYTHON