| 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 |