Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user