Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""DrawingML <a:custGeom> -> SVG <path d="..."> conversion.
|
||||
|
||||
Reverse of svg_to_pptx/drawingml/paths.py path_commands_to_drawingml.
|
||||
|
||||
Path command mapping:
|
||||
<a:moveTo> -> M
|
||||
<a:lnTo> -> L
|
||||
<a:cubicBezTo> -> C
|
||||
<a:quadBezTo> -> Q
|
||||
<a:arcTo> -> A (DrawingML uses center + sweep angles; we convert
|
||||
to SVG endpoint parameterization)
|
||||
<a:close/> -> Z
|
||||
|
||||
DrawingML <a:path w="..." h="..."> defines a shape-local coordinate system.
|
||||
Each path resolves guide formulas against that local coordinate system, then
|
||||
its coordinates are projected into the shape's absolute SVG frame.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from pptx_shapes import FormulaEvaluationError, FormulaEvaluator
|
||||
|
||||
from .emu_units import NS, Xfrm
|
||||
from .preset_registry_to_svg import render_evaluated_path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _EvaluatedCommand:
|
||||
name: str
|
||||
parameters: tuple[float, ...]
|
||||
|
||||
|
||||
def convert_custom_geom(
|
||||
cust_geom: ET.Element,
|
||||
xfrm: Xfrm,
|
||||
) -> str | None:
|
||||
"""Evaluate a complete custom geometry and return absolute SVG path data."""
|
||||
path_lst = cust_geom.find("a:pathLst", NS)
|
||||
if path_lst is None:
|
||||
return None
|
||||
shape_evaluator = _custom_geometry_evaluator(cust_geom, xfrm.w, xfrm.h)
|
||||
paths = [
|
||||
_convert_one_path(path_elem, cust_geom, xfrm, shape_evaluator)
|
||||
for path_elem in path_lst.findall("a:path", NS)
|
||||
]
|
||||
rendered = [path for path in paths if path]
|
||||
return " ".join(rendered) if rendered else None
|
||||
|
||||
|
||||
def _custom_geometry_evaluator(
|
||||
cust_geom: ET.Element,
|
||||
width: float,
|
||||
height: float,
|
||||
) -> FormulaEvaluator:
|
||||
evaluator = FormulaEvaluator(width, height)
|
||||
for list_name in ("avLst", "gdLst"):
|
||||
guide_list = cust_geom.find(f"a:{list_name}", NS)
|
||||
if guide_list is None:
|
||||
continue
|
||||
for guide in guide_list.findall("a:gd", NS):
|
||||
name = guide.attrib.get("name", "").strip()
|
||||
formula = guide.attrib.get("fmla", "").strip()
|
||||
if not name or not formula:
|
||||
raise FormulaEvaluationError(
|
||||
f"Custom geometry {list_name} contains an incomplete guide"
|
||||
)
|
||||
evaluator.bind(name, evaluator.evaluate(formula))
|
||||
return evaluator
|
||||
|
||||
|
||||
def _convert_one_path(
|
||||
path_elem: ET.Element,
|
||||
cust_geom: ET.Element,
|
||||
xfrm: Xfrm,
|
||||
shape_evaluator: FormulaEvaluator,
|
||||
) -> str:
|
||||
coordinate_width = _path_extent(
|
||||
path_elem.get("w"),
|
||||
xfrm.w,
|
||||
shape_evaluator,
|
||||
)
|
||||
coordinate_height = _path_extent(
|
||||
path_elem.get("h"),
|
||||
xfrm.h,
|
||||
shape_evaluator,
|
||||
)
|
||||
path_evaluator = _custom_geometry_evaluator(
|
||||
cust_geom,
|
||||
coordinate_width,
|
||||
coordinate_height,
|
||||
)
|
||||
commands = tuple(
|
||||
_evaluate_path_command(command, path_evaluator)
|
||||
for command in path_elem
|
||||
if isinstance(command.tag, str)
|
||||
)
|
||||
return render_evaluated_path(
|
||||
commands,
|
||||
x=xfrm.x,
|
||||
y=xfrm.y,
|
||||
width=xfrm.w,
|
||||
height=xfrm.h,
|
||||
coordinate_width=coordinate_width,
|
||||
coordinate_height=coordinate_height,
|
||||
)
|
||||
|
||||
|
||||
def _evaluate_path_command(
|
||||
element: ET.Element,
|
||||
evaluator: FormulaEvaluator,
|
||||
) -> _EvaluatedCommand:
|
||||
name = element.tag.rsplit("}", 1)[-1]
|
||||
if name == "arcTo":
|
||||
values = tuple(
|
||||
evaluator.evaluate_value(_required_attr(element, attr))
|
||||
for attr in ("wR", "hR", "stAng", "swAng")
|
||||
)
|
||||
return _EvaluatedCommand(name=name, parameters=values)
|
||||
expected_points = {
|
||||
"moveTo": 1,
|
||||
"lnTo": 1,
|
||||
"quadBezTo": 2,
|
||||
"cubicBezTo": 3,
|
||||
"close": 0,
|
||||
}.get(name)
|
||||
if expected_points is None:
|
||||
raise FormulaEvaluationError(
|
||||
f"Unsupported custom geometry path command: {name!r}"
|
||||
)
|
||||
points = element.findall("a:pt", NS)
|
||||
if len(points) != expected_points:
|
||||
raise FormulaEvaluationError(
|
||||
f"Custom path command {name!r} expects {expected_points} point(s), "
|
||||
f"found {len(points)}"
|
||||
)
|
||||
values = tuple(
|
||||
evaluator.evaluate_value(_required_attr(point, coordinate))
|
||||
for point in points
|
||||
for coordinate in ("x", "y")
|
||||
)
|
||||
return _EvaluatedCommand(name=name, parameters=values)
|
||||
|
||||
|
||||
def _path_extent(
|
||||
raw: str | None,
|
||||
shape_extent: float,
|
||||
evaluator: FormulaEvaluator,
|
||||
) -> float:
|
||||
if raw is None:
|
||||
return shape_extent
|
||||
value = evaluator.evaluate_value(raw)
|
||||
return shape_extent if value == 0 else value
|
||||
|
||||
|
||||
def _required_attr(element: ET.Element, name: str) -> str:
|
||||
value = element.attrib.get(name)
|
||||
if value is None or not value.strip():
|
||||
raise FormulaEvaluationError(
|
||||
f"Custom geometry element {element.tag.rsplit('}', 1)[-1]!r} "
|
||||
f"requires attribute {name!r}"
|
||||
)
|
||||
return value.strip()
|
||||
Reference in New Issue
Block a user