返回 ppt-master
prstgeom_to_svg.py
根目录 / skills / ppt-master / scripts / pptx_to_svg / prstgeom_to_svg.py
1 """DrawingML <a:prstGeom> -> SVG geometry conversion.
2
3 The visible SVG geometry is accompanied by the source preset name and every
4 explicit ``a:avLst`` guide formula. This metadata is rendering-neutral and
5 lets the reverse converter distinguish a native PowerPoint shape from an
6 arbitrary SVG path without guessing from its appearance.
7
8 The standard preset catalog is evaluated by the shared data-driven geometry
9 engine. Presets outside that locked catalog retain their source semantics on
10 an explicitly marked bounding-box fallback; they never masquerade as ``rect``.
11
12 The evaluated result carries one or more painted SVG path layers plus the
13 rendering-neutral metadata used by the reverse converter.
14 """
15
16 from __future__ import annotations
17
18 from dataclasses import dataclass, field
19 from xml.etree import ElementTree as ET
20
21 from pptx_shapes import get_preset_registry
22
23 from .emu_units import NS, Xfrm, fmt_num
24 from .preset_registry_to_svg import SvgPresetPath, render_preset_geometry
25
26
27 # ---------------------------------------------------------------------------
28 # GeomResult
29 # ---------------------------------------------------------------------------
30
31 @dataclass
32 class GeomResult:
33 """Result of converting a prst preset to SVG.
34
35 `tag` is the SVG element tag (rect / ellipse / line / polygon / path /
36 polyline). `attrs` are absolute SVG coordinates already in slide space.
37 `path_d` (when tag == 'path') is the d attribute. The slide assembler
38 merges fill/stroke attrs from fill_to_svg/ln_to_svg.
39 """
40
41 tag: str
42 attrs: dict[str, str] = field(default_factory=dict)
43 # When tag == 'path' use path_d for the d attribute.
44 path_d: str | None = None
45 # When tag == 'polygon' / 'polyline' use points for the points attribute.
46 points: str | None = None
47 # Standard presets can contain multiple independently painted path layers.
48 layers: tuple[SvgPresetPath, ...] = ()
49
50
51 # ---------------------------------------------------------------------------
52 # Explicit fallback for presets outside the locked standard catalog
53 # ---------------------------------------------------------------------------
54
55 def _rect(xfrm: Xfrm) -> GeomResult:
56 return GeomResult(
57 tag="rect",
58 attrs={
59 "x": fmt_num(xfrm.x),
60 "y": fmt_num(xfrm.y),
61 "width": fmt_num(xfrm.w),
62 "height": fmt_num(xfrm.h),
63 },
64 )
65
66
67 # ---------------------------------------------------------------------------
68 # Dispatch
69 # ---------------------------------------------------------------------------
70
71 def convert_prst_geom(
72 prst: str,
73 xfrm: Xfrm,
74 sp_pr: ET.Element | None,
75 ) -> GeomResult | None:
76 """Convert <a:prstGeom prst="..."> to a GeomResult.
77
78 Every emitted result carries ``data-pptx-prst`` plus one
79 ``data-pptx-av-<name>`` attribute per explicit adjustment guide. Unknown
80 presets use a visibly neutral bounding-box fallback with diagnostic
81 metadata instead of silently changing their semantic type to ``rect``.
82
83 Returns ``None`` only when the logical frame cannot produce geometry.
84 """
85 metadata = _preset_metadata(prst, sp_pr)
86 registry = get_preset_registry()
87 if prst not in registry:
88 result = _rect(xfrm)
89 result.attrs.update(metadata)
90 result.attrs.update({
91 "data-pptx-geometry-status": "unsupported",
92 "data-pptx-geometry-reason": f"unsupported-preset:{prst}",
93 })
94 return result
95 if xfrm.w < 0 or xfrm.h < 0 or (xfrm.w == 0 and xfrm.h == 0):
96 return None
97
98 try:
99 rendered = render_preset_geometry(
100 prst,
101 xfrm,
102 _preset_adjustments(sp_pr),
103 )
104 except ValueError as exc:
105 result = _rect(xfrm)
106 result.attrs.update(metadata)
107 result.attrs.update({
108 "data-pptx-geometry-status": "unsupported",
109 "data-pptx-geometry-reason": (
110 f"preset-evaluation-error:{type(exc).__name__}"
111 ),
112 })
113 return result
114 if not rendered.paths:
115 return None
116 return GeomResult(
117 tag="path",
118 attrs=metadata,
119 path_d=" ".join(path.d for path in rendered.paths),
120 layers=rendered.paths,
121 )
122
123
124 def _preset_metadata(
125 prst: str,
126 prst_geom: ET.Element | None,
127 ) -> dict[str, str]:
128 """Return rendering-neutral SVG attributes for native preset semantics."""
129 attrs = {"data-pptx-prst": prst}
130 if prst_geom is None:
131 return attrs
132
133 av_lst = prst_geom.find("a:avLst", NS)
134 if av_lst is None:
135 return attrs
136 for guide in av_lst.findall("a:gd", NS):
137 name = guide.attrib.get("name", "")
138 if not name:
139 continue
140 attrs[f"data-pptx-av-{name}"] = guide.attrib.get("fmla", "")
141 return attrs
142
143
144 def _preset_adjustments(
145 prst_geom: ET.Element | None,
146 ) -> dict[str, str]:
147 """Return explicit instance adjustment formulas for registry evaluation."""
148 if prst_geom is None:
149 return {}
150 av_lst = prst_geom.find("a:avLst", NS)
151 if av_lst is None:
152 return {}
153 return {
154 guide.attrib["name"]: guide.attrib.get("fmla", "")
155 for guide in av_lst.findall("a:gd", NS)
156 if guide.attrib.get("name")
157 }
158
159
160 def supported_presets() -> set[str]:
161 """Return the set of recognized prst values for diagnostics."""
162 return set(get_preset_registry().names)
163
163 lines PYTHON