Files
Templated-PPT-Master/runtime/ppt-master/scripts/pptx_shapes/registry.py
T

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