返回 ppt-master
registry.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Preset Shape Registry
4
5 Expose the complete preset catalog and evaluate shape geometry instances.
6
7 Usage:
8 Import get_preset_registry from pptx_shapes.registry.
9
10 Examples:
11 geometry = get_preset_registry().evaluate("rightArrow", 320, 160)
12
13 Dependencies:
14 None (only uses standard library)
15 """
16
17 from __future__ import annotations
18
19 from functools import lru_cache
20 from typing import Mapping
21
22 from .errors import (
23 FormulaEvaluationError,
24 PresetShapeDataError,
25 UnknownPresetShapeError,
26 )
27 from .formula import FormulaEvaluator
28 from .loader import load_preset_shape_definitions, load_shape_type_values
29 from .models import (
30 AdjustHandleDefinition,
31 EvaluatedAdjustHandle,
32 EvaluatedConnectionSite,
33 EvaluatedPathCommand,
34 EvaluatedPoint,
35 EvaluatedPresetGeometry,
36 EvaluatedShapePath,
37 EvaluatedTextRectangle,
38 PointExpression,
39 PresetShapeDefinition,
40 immutable_mapping,
41 )
42
43
44 AdjustmentValue = str | int | float
45 CONNECTOR_PRESET_TYPES = frozenset(
46 {
47 "line",
48 "lineInv",
49 "straightConnector1",
50 "bentConnector2",
51 "bentConnector3",
52 "bentConnector4",
53 "bentConnector5",
54 "curvedConnector2",
55 "curvedConnector3",
56 "curvedConnector4",
57 "curvedConnector5",
58 }
59 )
60
61
62 class PresetShapeRegistry:
63 """Read-only registry for all standard DrawingML preset geometries."""
64
65 def __init__(
66 self,
67 definitions: tuple[PresetShapeDefinition, ...],
68 expected_names: tuple[str, ...],
69 ) -> None:
70 self._definitions = {
71 definition.name: definition for definition in definitions
72 }
73 self._names = tuple(expected_names)
74 if len(self._definitions) != len(definitions):
75 raise PresetShapeDataError("Preset registry contains duplicate names")
76 missing = sorted(set(expected_names) - set(self._definitions))
77 extra = sorted(set(self._definitions) - set(expected_names))
78 if missing or extra:
79 raise PresetShapeDataError(
80 "Preset registry differs from ShapeTypeValues: "
81 f"missing={missing}, extra={extra}"
82 )
83
84 @classmethod
85 def bundled(cls) -> PresetShapeRegistry:
86 """Load the hash-locked catalog shipped with PPT Master."""
87
88 names = load_shape_type_values()
89 definitions = load_preset_shape_definitions(expected_names=names)
90 return cls(definitions, names)
91
92 @property
93 def names(self) -> tuple[str, ...]:
94 """Return all names in official ``ShapeTypeValues`` order."""
95
96 return self._names
97
98 def __len__(self) -> int:
99 return len(self._definitions)
100
101 def __contains__(self, name: object) -> bool:
102 return isinstance(name, str) and name in self._definitions
103
104 def contains(self, name: str) -> bool:
105 """Return whether ``name`` is a standard preset in this catalog."""
106
107 return name in self._definitions
108
109 def get(self, name: str) -> PresetShapeDefinition:
110 """Return one immutable source definition or raise a precise error."""
111
112 try:
113 return self._definitions[name]
114 except KeyError as exc:
115 raise UnknownPresetShapeError(
116 f"Unknown DrawingML preset shape: {name!r}"
117 ) from exc
118
119 def evaluate(
120 self,
121 name: str,
122 width: float,
123 height: float,
124 *,
125 adjustments: Mapping[str, AdjustmentValue] | None = None,
126 ) -> EvaluatedPresetGeometry:
127 """Evaluate every guide, handle, connection, text rect, and path."""
128
129 definition = self.get(name)
130 evaluator = FormulaEvaluator(width, height)
131 supplied = dict(adjustments or {})
132 adjustment_names = {guide.name for guide in definition.adjustments}
133 unknown = sorted(set(supplied) - adjustment_names)
134 if unknown:
135 raise FormulaEvaluationError(
136 f"Preset {name!r} has no adjustments named {unknown}"
137 )
138
139 evaluated_adjustments: dict[str, float] = {}
140 for guide in definition.adjustments:
141 source = supplied.get(guide.name, guide.formula)
142 value = evaluator.evaluate_value(source)
143 evaluated_adjustments[guide.name] = evaluator.bind(guide.name, value)
144
145 evaluated_guides: dict[str, float] = {}
146 for guide in definition.guides:
147 value = evaluator.evaluate(guide.formula)
148 evaluated_guides[guide.name] = evaluator.bind(guide.name, value)
149
150 return EvaluatedPresetGeometry(
151 name=name,
152 width=evaluator.resolve("w"),
153 height=evaluator.resolve("h"),
154 left=evaluator.resolve("l"),
155 top=evaluator.resolve("t"),
156 adjustments=immutable_mapping(evaluated_adjustments),
157 guides=immutable_mapping(evaluated_guides),
158 handles=tuple(
159 _evaluate_handle(handle, evaluator)
160 for handle in definition.handles
161 ),
162 connections=tuple(
163 EvaluatedConnectionSite(
164 angle=evaluator.evaluate_value(connection.angle),
165 position=_evaluate_point(connection.position, evaluator),
166 )
167 for connection in definition.connections
168 ),
169 text_rectangle=_evaluate_text_rectangle(definition, evaluator),
170 paths=tuple(
171 EvaluatedShapePath(
172 coordinate_width=_path_extent(
173 path.coordinate_width,
174 evaluator.resolve("w"),
175 evaluator,
176 ),
177 coordinate_height=_path_extent(
178 path.coordinate_height,
179 evaluator.resolve("h"),
180 evaluator,
181 ),
182 fill=path.fill,
183 stroke=path.stroke,
184 extrusion_ok=path.extrusion_ok,
185 commands=tuple(
186 EvaluatedPathCommand(
187 name=command.name,
188 parameters=tuple(
189 evaluator.evaluate_value(parameter)
190 for parameter in command.parameters
191 ),
192 )
193 for command in path.commands
194 ),
195 )
196 for path in definition.paths
197 ),
198 )
199
200
201 @lru_cache(maxsize=1)
202 def get_preset_registry() -> PresetShapeRegistry:
203 """Return the process-wide, lazily loaded bundled registry."""
204
205 return PresetShapeRegistry.bundled()
206
207
208 def _evaluate_point(
209 point: PointExpression,
210 evaluator: FormulaEvaluator,
211 ) -> EvaluatedPoint:
212 return EvaluatedPoint(
213 x=evaluator.evaluate_value(point.x),
214 y=evaluator.evaluate_value(point.y),
215 )
216
217
218 def _evaluate_handle(
219 handle: AdjustHandleDefinition,
220 evaluator: FormulaEvaluator,
221 ) -> EvaluatedAdjustHandle:
222 return EvaluatedAdjustHandle(
223 kind=handle.kind,
224 position=_evaluate_point(handle.position, evaluator),
225 x_reference=handle.x_reference,
226 minimum_x=_optional_value(handle.minimum_x, evaluator),
227 maximum_x=_optional_value(handle.maximum_x, evaluator),
228 y_reference=handle.y_reference,
229 minimum_y=_optional_value(handle.minimum_y, evaluator),
230 maximum_y=_optional_value(handle.maximum_y, evaluator),
231 angle_reference=handle.angle_reference,
232 minimum_angle=_optional_value(handle.minimum_angle, evaluator),
233 maximum_angle=_optional_value(handle.maximum_angle, evaluator),
234 radius_reference=handle.radius_reference,
235 minimum_radius=_optional_value(handle.minimum_radius, evaluator),
236 maximum_radius=_optional_value(handle.maximum_radius, evaluator),
237 )
238
239
240 def _evaluate_text_rectangle(
241 definition: PresetShapeDefinition,
242 evaluator: FormulaEvaluator,
243 ) -> EvaluatedTextRectangle | None:
244 rectangle = definition.text_rectangle
245 if rectangle is None:
246 return None
247 return EvaluatedTextRectangle(
248 left=evaluator.evaluate_value(rectangle.left),
249 top=evaluator.evaluate_value(rectangle.top),
250 right=evaluator.evaluate_value(rectangle.right),
251 bottom=evaluator.evaluate_value(rectangle.bottom),
252 )
253
254
255 def _optional_value(
256 value: str | None,
257 evaluator: FormulaEvaluator,
258 ) -> float | None:
259 return None if value is None else evaluator.evaluate_value(value)
260
261
262 def _path_extent(
263 value: str | None,
264 shape_extent: float,
265 evaluator: FormulaEvaluator,
266 ) -> float:
267 if value is None:
268 return shape_extent
269 resolved = evaluator.evaluate_value(value)
270 return shape_extent if resolved == 0 else resolved
271
271 lines PYTHON