271 lines
8.7 KiB
Python
271 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PPT Master - Preset Shape Registry
|
|
|
|
Expose the complete preset catalog and evaluate shape geometry instances.
|
|
|
|
Usage:
|
|
Import get_preset_registry from pptx_shapes.registry.
|
|
|
|
Examples:
|
|
geometry = get_preset_registry().evaluate("rightArrow", 320, 160)
|
|
|
|
Dependencies:
|
|
None (only uses standard library)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from functools import lru_cache
|
|
from typing import Mapping
|
|
|
|
from .errors import (
|
|
FormulaEvaluationError,
|
|
PresetShapeDataError,
|
|
UnknownPresetShapeError,
|
|
)
|
|
from .formula import FormulaEvaluator
|
|
from .loader import load_preset_shape_definitions, load_shape_type_values
|
|
from .models import (
|
|
AdjustHandleDefinition,
|
|
EvaluatedAdjustHandle,
|
|
EvaluatedConnectionSite,
|
|
EvaluatedPathCommand,
|
|
EvaluatedPoint,
|
|
EvaluatedPresetGeometry,
|
|
EvaluatedShapePath,
|
|
EvaluatedTextRectangle,
|
|
PointExpression,
|
|
PresetShapeDefinition,
|
|
immutable_mapping,
|
|
)
|
|
|
|
|
|
AdjustmentValue = str | int | float
|
|
CONNECTOR_PRESET_TYPES = frozenset(
|
|
{
|
|
"line",
|
|
"lineInv",
|
|
"straightConnector1",
|
|
"bentConnector2",
|
|
"bentConnector3",
|
|
"bentConnector4",
|
|
"bentConnector5",
|
|
"curvedConnector2",
|
|
"curvedConnector3",
|
|
"curvedConnector4",
|
|
"curvedConnector5",
|
|
}
|
|
)
|
|
|
|
|
|
class PresetShapeRegistry:
|
|
"""Read-only registry for all standard DrawingML preset geometries."""
|
|
|
|
def __init__(
|
|
self,
|
|
definitions: tuple[PresetShapeDefinition, ...],
|
|
expected_names: tuple[str, ...],
|
|
) -> None:
|
|
self._definitions = {
|
|
definition.name: definition for definition in definitions
|
|
}
|
|
self._names = tuple(expected_names)
|
|
if len(self._definitions) != len(definitions):
|
|
raise PresetShapeDataError("Preset registry contains duplicate names")
|
|
missing = sorted(set(expected_names) - set(self._definitions))
|
|
extra = sorted(set(self._definitions) - set(expected_names))
|
|
if missing or extra:
|
|
raise PresetShapeDataError(
|
|
"Preset registry differs from ShapeTypeValues: "
|
|
f"missing={missing}, extra={extra}"
|
|
)
|
|
|
|
@classmethod
|
|
def bundled(cls) -> PresetShapeRegistry:
|
|
"""Load the hash-locked catalog shipped with PPT Master."""
|
|
|
|
names = load_shape_type_values()
|
|
definitions = load_preset_shape_definitions(expected_names=names)
|
|
return cls(definitions, names)
|
|
|
|
@property
|
|
def names(self) -> tuple[str, ...]:
|
|
"""Return all names in official ``ShapeTypeValues`` order."""
|
|
|
|
return self._names
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._definitions)
|
|
|
|
def __contains__(self, name: object) -> bool:
|
|
return isinstance(name, str) and name in self._definitions
|
|
|
|
def contains(self, name: str) -> bool:
|
|
"""Return whether ``name`` is a standard preset in this catalog."""
|
|
|
|
return name in self._definitions
|
|
|
|
def get(self, name: str) -> PresetShapeDefinition:
|
|
"""Return one immutable source definition or raise a precise error."""
|
|
|
|
try:
|
|
return self._definitions[name]
|
|
except KeyError as exc:
|
|
raise UnknownPresetShapeError(
|
|
f"Unknown DrawingML preset shape: {name!r}"
|
|
) from exc
|
|
|
|
def evaluate(
|
|
self,
|
|
name: str,
|
|
width: float,
|
|
height: float,
|
|
*,
|
|
adjustments: Mapping[str, AdjustmentValue] | None = None,
|
|
) -> EvaluatedPresetGeometry:
|
|
"""Evaluate every guide, handle, connection, text rect, and path."""
|
|
|
|
definition = self.get(name)
|
|
evaluator = FormulaEvaluator(width, height)
|
|
supplied = dict(adjustments or {})
|
|
adjustment_names = {guide.name for guide in definition.adjustments}
|
|
unknown = sorted(set(supplied) - adjustment_names)
|
|
if unknown:
|
|
raise FormulaEvaluationError(
|
|
f"Preset {name!r} has no adjustments named {unknown}"
|
|
)
|
|
|
|
evaluated_adjustments: dict[str, float] = {}
|
|
for guide in definition.adjustments:
|
|
source = supplied.get(guide.name, guide.formula)
|
|
value = evaluator.evaluate_value(source)
|
|
evaluated_adjustments[guide.name] = evaluator.bind(guide.name, value)
|
|
|
|
evaluated_guides: dict[str, float] = {}
|
|
for guide in definition.guides:
|
|
value = evaluator.evaluate(guide.formula)
|
|
evaluated_guides[guide.name] = evaluator.bind(guide.name, value)
|
|
|
|
return EvaluatedPresetGeometry(
|
|
name=name,
|
|
width=evaluator.resolve("w"),
|
|
height=evaluator.resolve("h"),
|
|
left=evaluator.resolve("l"),
|
|
top=evaluator.resolve("t"),
|
|
adjustments=immutable_mapping(evaluated_adjustments),
|
|
guides=immutable_mapping(evaluated_guides),
|
|
handles=tuple(
|
|
_evaluate_handle(handle, evaluator)
|
|
for handle in definition.handles
|
|
),
|
|
connections=tuple(
|
|
EvaluatedConnectionSite(
|
|
angle=evaluator.evaluate_value(connection.angle),
|
|
position=_evaluate_point(connection.position, evaluator),
|
|
)
|
|
for connection in definition.connections
|
|
),
|
|
text_rectangle=_evaluate_text_rectangle(definition, evaluator),
|
|
paths=tuple(
|
|
EvaluatedShapePath(
|
|
coordinate_width=_path_extent(
|
|
path.coordinate_width,
|
|
evaluator.resolve("w"),
|
|
evaluator,
|
|
),
|
|
coordinate_height=_path_extent(
|
|
path.coordinate_height,
|
|
evaluator.resolve("h"),
|
|
evaluator,
|
|
),
|
|
fill=path.fill,
|
|
stroke=path.stroke,
|
|
extrusion_ok=path.extrusion_ok,
|
|
commands=tuple(
|
|
EvaluatedPathCommand(
|
|
name=command.name,
|
|
parameters=tuple(
|
|
evaluator.evaluate_value(parameter)
|
|
for parameter in command.parameters
|
|
),
|
|
)
|
|
for command in path.commands
|
|
),
|
|
)
|
|
for path in definition.paths
|
|
),
|
|
)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_preset_registry() -> PresetShapeRegistry:
|
|
"""Return the process-wide, lazily loaded bundled registry."""
|
|
|
|
return PresetShapeRegistry.bundled()
|
|
|
|
|
|
def _evaluate_point(
|
|
point: PointExpression,
|
|
evaluator: FormulaEvaluator,
|
|
) -> EvaluatedPoint:
|
|
return EvaluatedPoint(
|
|
x=evaluator.evaluate_value(point.x),
|
|
y=evaluator.evaluate_value(point.y),
|
|
)
|
|
|
|
|
|
def _evaluate_handle(
|
|
handle: AdjustHandleDefinition,
|
|
evaluator: FormulaEvaluator,
|
|
) -> EvaluatedAdjustHandle:
|
|
return EvaluatedAdjustHandle(
|
|
kind=handle.kind,
|
|
position=_evaluate_point(handle.position, evaluator),
|
|
x_reference=handle.x_reference,
|
|
minimum_x=_optional_value(handle.minimum_x, evaluator),
|
|
maximum_x=_optional_value(handle.maximum_x, evaluator),
|
|
y_reference=handle.y_reference,
|
|
minimum_y=_optional_value(handle.minimum_y, evaluator),
|
|
maximum_y=_optional_value(handle.maximum_y, evaluator),
|
|
angle_reference=handle.angle_reference,
|
|
minimum_angle=_optional_value(handle.minimum_angle, evaluator),
|
|
maximum_angle=_optional_value(handle.maximum_angle, evaluator),
|
|
radius_reference=handle.radius_reference,
|
|
minimum_radius=_optional_value(handle.minimum_radius, evaluator),
|
|
maximum_radius=_optional_value(handle.maximum_radius, evaluator),
|
|
)
|
|
|
|
|
|
def _evaluate_text_rectangle(
|
|
definition: PresetShapeDefinition,
|
|
evaluator: FormulaEvaluator,
|
|
) -> EvaluatedTextRectangle | None:
|
|
rectangle = definition.text_rectangle
|
|
if rectangle is None:
|
|
return None
|
|
return EvaluatedTextRectangle(
|
|
left=evaluator.evaluate_value(rectangle.left),
|
|
top=evaluator.evaluate_value(rectangle.top),
|
|
right=evaluator.evaluate_value(rectangle.right),
|
|
bottom=evaluator.evaluate_value(rectangle.bottom),
|
|
)
|
|
|
|
|
|
def _optional_value(
|
|
value: str | None,
|
|
evaluator: FormulaEvaluator,
|
|
) -> float | None:
|
|
return None if value is None else evaluator.evaluate_value(value)
|
|
|
|
|
|
def _path_extent(
|
|
value: str | None,
|
|
shape_extent: float,
|
|
evaluator: FormulaEvaluator,
|
|
) -> float:
|
|
if value is None:
|
|
return shape_extent
|
|
resolved = evaluator.evaluate_value(value)
|
|
return shape_extent if resolved == 0 else resolved
|