返回 ppt-master
custgeom_to_svg.py
根目录 / skills / ppt-master / scripts / pptx_to_svg / custgeom_to_svg.py
1 """DrawingML <a:custGeom> -> SVG <path d="..."> conversion.
2
3 Reverse of svg_to_pptx/drawingml/paths.py path_commands_to_drawingml.
4
5 Path command mapping:
6 <a:moveTo> -> M
7 <a:lnTo> -> L
8 <a:cubicBezTo> -> C
9 <a:quadBezTo> -> Q
10 <a:arcTo> -> A (DrawingML uses center + sweep angles; we convert
11 to SVG endpoint parameterization)
12 <a:close/> -> Z
13
14 DrawingML <a:path w="..." h="..."> defines a shape-local coordinate system.
15 Each path resolves guide formulas against that local coordinate system, then
16 its coordinates are projected into the shape's absolute SVG frame.
17 """
18
19 from __future__ import annotations
20
21 from dataclasses import dataclass
22 from xml.etree import ElementTree as ET
23
24 from pptx_shapes import FormulaEvaluationError, FormulaEvaluator
25
26 from .emu_units import NS, Xfrm
27 from .preset_registry_to_svg import render_evaluated_path
28
29
30 @dataclass(frozen=True)
31 class _EvaluatedCommand:
32 name: str
33 parameters: tuple[float, ...]
34
35
36 def convert_custom_geom(
37 cust_geom: ET.Element,
38 xfrm: Xfrm,
39 ) -> str | None:
40 """Evaluate a complete custom geometry and return absolute SVG path data."""
41 path_lst = cust_geom.find("a:pathLst", NS)
42 if path_lst is None:
43 return None
44 shape_evaluator = _custom_geometry_evaluator(cust_geom, xfrm.w, xfrm.h)
45 paths = [
46 _convert_one_path(path_elem, cust_geom, xfrm, shape_evaluator)
47 for path_elem in path_lst.findall("a:path", NS)
48 ]
49 rendered = [path for path in paths if path]
50 return " ".join(rendered) if rendered else None
51
52
53 def _custom_geometry_evaluator(
54 cust_geom: ET.Element,
55 width: float,
56 height: float,
57 ) -> FormulaEvaluator:
58 evaluator = FormulaEvaluator(width, height)
59 for list_name in ("avLst", "gdLst"):
60 guide_list = cust_geom.find(f"a:{list_name}", NS)
61 if guide_list is None:
62 continue
63 for guide in guide_list.findall("a:gd", NS):
64 name = guide.attrib.get("name", "").strip()
65 formula = guide.attrib.get("fmla", "").strip()
66 if not name or not formula:
67 raise FormulaEvaluationError(
68 f"Custom geometry {list_name} contains an incomplete guide"
69 )
70 evaluator.bind(name, evaluator.evaluate(formula))
71 return evaluator
72
73
74 def _convert_one_path(
75 path_elem: ET.Element,
76 cust_geom: ET.Element,
77 xfrm: Xfrm,
78 shape_evaluator: FormulaEvaluator,
79 ) -> str:
80 coordinate_width = _path_extent(
81 path_elem.get("w"),
82 xfrm.w,
83 shape_evaluator,
84 )
85 coordinate_height = _path_extent(
86 path_elem.get("h"),
87 xfrm.h,
88 shape_evaluator,
89 )
90 path_evaluator = _custom_geometry_evaluator(
91 cust_geom,
92 coordinate_width,
93 coordinate_height,
94 )
95 commands = tuple(
96 _evaluate_path_command(command, path_evaluator)
97 for command in path_elem
98 if isinstance(command.tag, str)
99 )
100 return render_evaluated_path(
101 commands,
102 x=xfrm.x,
103 y=xfrm.y,
104 width=xfrm.w,
105 height=xfrm.h,
106 coordinate_width=coordinate_width,
107 coordinate_height=coordinate_height,
108 )
109
110
111 def _evaluate_path_command(
112 element: ET.Element,
113 evaluator: FormulaEvaluator,
114 ) -> _EvaluatedCommand:
115 name = element.tag.rsplit("}", 1)[-1]
116 if name == "arcTo":
117 values = tuple(
118 evaluator.evaluate_value(_required_attr(element, attr))
119 for attr in ("wR", "hR", "stAng", "swAng")
120 )
121 return _EvaluatedCommand(name=name, parameters=values)
122 expected_points = {
123 "moveTo": 1,
124 "lnTo": 1,
125 "quadBezTo": 2,
126 "cubicBezTo": 3,
127 "close": 0,
128 }.get(name)
129 if expected_points is None:
130 raise FormulaEvaluationError(
131 f"Unsupported custom geometry path command: {name!r}"
132 )
133 points = element.findall("a:pt", NS)
134 if len(points) != expected_points:
135 raise FormulaEvaluationError(
136 f"Custom path command {name!r} expects {expected_points} point(s), "
137 f"found {len(points)}"
138 )
139 values = tuple(
140 evaluator.evaluate_value(_required_attr(point, coordinate))
141 for point in points
142 for coordinate in ("x", "y")
143 )
144 return _EvaluatedCommand(name=name, parameters=values)
145
146
147 def _path_extent(
148 raw: str | None,
149 shape_extent: float,
150 evaluator: FormulaEvaluator,
151 ) -> float:
152 if raw is None:
153 return shape_extent
154 value = evaluator.evaluate_value(raw)
155 return shape_extent if value == 0 else value
156
157
158 def _required_attr(element: ET.Element, name: str) -> str:
159 value = element.attrib.get(name)
160 if value is None or not value.strip():
161 raise FormulaEvaluationError(
162 f"Custom geometry element {element.tag.rsplit('}', 1)[-1]!r} "
163 f"requires attribute {name!r}"
164 )
165 return value.strip()
166
166 lines PYTHON