python-pptx2 2.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. pptx2/__init__.py +152 -0
  2. pptx2/_color.py +75 -0
  3. pptx2/_slide_importer.py +597 -0
  4. pptx2/_svg.py +155 -0
  5. pptx2/_template_applier.py +292 -0
  6. pptx2/_textstyle.py +187 -0
  7. pptx2/accessibility.py +365 -0
  8. pptx2/action.py +270 -0
  9. pptx2/animation.py +2237 -0
  10. pptx2/api.py +49 -0
  11. pptx2/audit.py +258 -0
  12. pptx2/chart/__init__.py +0 -0
  13. pptx2/chart/analytics.py +381 -0
  14. pptx2/chart/axis.py +543 -0
  15. pptx2/chart/category.py +200 -0
  16. pptx2/chart/chart.py +670 -0
  17. pptx2/chart/data.py +864 -0
  18. pptx2/chart/datalabel.py +406 -0
  19. pptx2/chart/legend.py +86 -0
  20. pptx2/chart/marker.py +70 -0
  21. pptx2/chart/palettes.py +129 -0
  22. pptx2/chart/plot.py +462 -0
  23. pptx2/chart/point.py +101 -0
  24. pptx2/chart/quick_layouts.py +325 -0
  25. pptx2/chart/series.py +334 -0
  26. pptx2/chart/xlsx.py +272 -0
  27. pptx2/chart/xmlwriter.py +1845 -0
  28. pptx2/compose/__init__.py +28 -0
  29. pptx2/compose/from_spec.py +1094 -0
  30. pptx2/design/__init__.py +8 -0
  31. pptx2/design/components.py +607 -0
  32. pptx2/design/figures.py +389 -0
  33. pptx2/design/layout.py +370 -0
  34. pptx2/design/recipes.py +1967 -0
  35. pptx2/design/style.py +209 -0
  36. pptx2/design/tokens.py +915 -0
  37. pptx2/diagrams.py +754 -0
  38. pptx2/dml/__init__.py +0 -0
  39. pptx2/dml/chtfmt.py +40 -0
  40. pptx2/dml/color.py +496 -0
  41. pptx2/dml/effect.py +909 -0
  42. pptx2/dml/fill.py +691 -0
  43. pptx2/dml/line.py +287 -0
  44. pptx2/dml/picture.py +212 -0
  45. pptx2/dml/three_d.py +381 -0
  46. pptx2/enum/__init__.py +0 -0
  47. pptx2/enum/action.py +71 -0
  48. pptx2/enum/animation.py +31 -0
  49. pptx2/enum/base.py +218 -0
  50. pptx2/enum/chart.py +574 -0
  51. pptx2/enum/dml.py +740 -0
  52. pptx2/enum/lang.py +685 -0
  53. pptx2/enum/presentation.py +133 -0
  54. pptx2/enum/shapes.py +1029 -0
  55. pptx2/enum/text.py +230 -0
  56. pptx2/exc.py +42 -0
  57. pptx2/formats.py +139 -0
  58. pptx2/geometry.py +420 -0
  59. pptx2/inherit.py +109 -0
  60. pptx2/lint.py +2256 -0
  61. pptx2/math.py +177 -0
  62. pptx2/media.py +197 -0
  63. pptx2/opc/__init__.py +0 -0
  64. pptx2/opc/constants.py +332 -0
  65. pptx2/opc/oxml.py +188 -0
  66. pptx2/opc/package.py +762 -0
  67. pptx2/opc/packuri.py +109 -0
  68. pptx2/opc/serialized.py +296 -0
  69. pptx2/opc/shared.py +20 -0
  70. pptx2/opc/spec.py +45 -0
  71. pptx2/oxml/__init__.py +555 -0
  72. pptx2/oxml/action.py +53 -0
  73. pptx2/oxml/chart/__init__.py +0 -0
  74. pptx2/oxml/chart/axis.py +337 -0
  75. pptx2/oxml/chart/chart.py +481 -0
  76. pptx2/oxml/chart/datalabel.py +253 -0
  77. pptx2/oxml/chart/legend.py +72 -0
  78. pptx2/oxml/chart/marker.py +61 -0
  79. pptx2/oxml/chart/plot.py +365 -0
  80. pptx2/oxml/chart/series.py +425 -0
  81. pptx2/oxml/chart/shared.py +220 -0
  82. pptx2/oxml/coreprops.py +288 -0
  83. pptx2/oxml/dml/__init__.py +0 -0
  84. pptx2/oxml/dml/color.py +135 -0
  85. pptx2/oxml/dml/effect.py +213 -0
  86. pptx2/oxml/dml/fill.py +316 -0
  87. pptx2/oxml/dml/line.py +12 -0
  88. pptx2/oxml/dml/three_d.py +110 -0
  89. pptx2/oxml/ns.py +135 -0
  90. pptx2/oxml/presentation.py +313 -0
  91. pptx2/oxml/shapes/__init__.py +19 -0
  92. pptx2/oxml/shapes/autoshape.py +467 -0
  93. pptx2/oxml/shapes/connector.py +107 -0
  94. pptx2/oxml/shapes/graphfrm.py +347 -0
  95. pptx2/oxml/shapes/groupshape.py +329 -0
  96. pptx2/oxml/shapes/picture.py +270 -0
  97. pptx2/oxml/shapes/shared.py +577 -0
  98. pptx2/oxml/simpletypes.py +1027 -0
  99. pptx2/oxml/slide.py +563 -0
  100. pptx2/oxml/table.py +650 -0
  101. pptx2/oxml/text.py +815 -0
  102. pptx2/oxml/theme.py +36 -0
  103. pptx2/oxml/xmlchemy.py +717 -0
  104. pptx2/package.py +222 -0
  105. pptx2/parts/__init__.py +0 -0
  106. pptx2/parts/chart.py +95 -0
  107. pptx2/parts/coreprops.py +167 -0
  108. pptx2/parts/diagram.py +37 -0
  109. pptx2/parts/embeddedpackage.py +93 -0
  110. pptx2/parts/image.py +275 -0
  111. pptx2/parts/media.py +37 -0
  112. pptx2/parts/presentation.py +136 -0
  113. pptx2/parts/slide.py +371 -0
  114. pptx2/presentation.py +408 -0
  115. pptx2/py.typed +0 -0
  116. pptx2/render.py +586 -0
  117. pptx2/section.py +272 -0
  118. pptx2/shapes/__init__.py +26 -0
  119. pptx2/shapes/autoshape.py +442 -0
  120. pptx2/shapes/base.py +1078 -0
  121. pptx2/shapes/connector.py +297 -0
  122. pptx2/shapes/freeform.py +337 -0
  123. pptx2/shapes/graphfrm.py +316 -0
  124. pptx2/shapes/group.py +264 -0
  125. pptx2/shapes/picture.py +422 -0
  126. pptx2/shapes/placeholder.py +468 -0
  127. pptx2/shapes/shapetree.py +2027 -0
  128. pptx2/shared.py +82 -0
  129. pptx2/skill/SKILL.md +450 -0
  130. pptx2/skill/__init__.py +78 -0
  131. pptx2/skill/__main__.py +64 -0
  132. pptx2/skill/references/animations.md +189 -0
  133. pptx2/skill/references/basics.md +421 -0
  134. pptx2/skill/references/charts.md +254 -0
  135. pptx2/skill/references/compose.md +234 -0
  136. pptx2/skill/references/design.md +366 -0
  137. pptx2/skill/references/effects.md +249 -0
  138. pptx2/skill/references/end-to-end-deck.md +231 -0
  139. pptx2/skill/references/geometry-and-arrows.md +334 -0
  140. pptx2/skill/references/lint.md +275 -0
  141. pptx2/skill/references/math.md +86 -0
  142. pptx2/skill/references/picture-effects.md +129 -0
  143. pptx2/skill/references/render.md +151 -0
  144. pptx2/skill/references/smart-art.md +75 -0
  145. pptx2/skill/references/space-aware-authoring.md +249 -0
  146. pptx2/skill/references/tables.md +244 -0
  147. pptx2/skill/references/theme.md +127 -0
  148. pptx2/skill/references/three-d.md +109 -0
  149. pptx2/skill/references/transitions.md +100 -0
  150. pptx2/slide.py +1244 -0
  151. pptx2/smart_art.py +220 -0
  152. pptx2/spec.py +633 -0
  153. pptx2/table.py +1181 -0
  154. pptx2/table_styles.py +184 -0
  155. pptx2/templates/default.pptx +0 -0
  156. pptx2/templates/docx-icon.emf +0 -0
  157. pptx2/templates/generic-icon.emf +0 -0
  158. pptx2/templates/notes.xml +23 -0
  159. pptx2/templates/notesMaster.xml +352 -0
  160. pptx2/templates/pptx-icon.emf +0 -0
  161. pptx2/templates/theme.xml +321 -0
  162. pptx2/templates/xlsx-icon.emf +0 -0
  163. pptx2/text/__init__.py +0 -0
  164. pptx2/text/fonts.py +482 -0
  165. pptx2/text/layout.py +374 -0
  166. pptx2/text/text.py +1272 -0
  167. pptx2/theme.py +721 -0
  168. pptx2/types.py +36 -0
  169. pptx2/util.py +263 -0
  170. python_pptx2-2.13.0.dist-info/METADATA +351 -0
  171. python_pptx2-2.13.0.dist-info/RECORD +175 -0
  172. python_pptx2-2.13.0.dist-info/WHEEL +5 -0
  173. python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
  174. python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
  175. python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/dml/effect.py ADDED
@@ -0,0 +1,909 @@
1
+ """Visual effects on a shape such as shadow, glow, and soft-edges."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from typing import TYPE_CHECKING, Callable
7
+
8
+ from pptx2.dml.color import ColorFormat
9
+ from pptx2.enum.dml import MSO_THEME_COLOR
10
+ from pptx2.oxml.ns import qn
11
+
12
+ # `<a:outerShdw>`, `<a:innerShdw>`, and `<a:glow>` each require *exactly one*
13
+ # EG_ColorChoice child per the OOXML schema (minOccurs=1). PowerPoint reports a
14
+ # deck as broken when one of these effect elements is written without a colour,
15
+ # even though lxml / python-pptx / LibreOffice accept it (the same failure mode
16
+ # as an empty `<a:scene3d>`). Geometry-only setters such as
17
+ # ``shadow.blur_radius`` must therefore guarantee a colour child exists.
18
+ _EFFECT_COLOR_TAGS = frozenset(
19
+ qn(t) for t in ("a:scrgbClr", "a:srgbClr", "a:hslClr", "a:sysClr", "a:schemeClr", "a:prstClr")
20
+ )
21
+
22
+
23
+ def _ensure_effect_color(el) -> None:
24
+ """Add a default opaque-black colour child to *el* when it has none.
25
+
26
+ Idempotent: a no-op when the effect element already carries a colour, so a
27
+ caller who sets ``.color.rgb`` keeps their colour and a caller who only sets
28
+ geometry still produces schema-valid (PowerPoint-openable) XML.
29
+ """
30
+ if any(child.tag in _EFFECT_COLOR_TAGS for child in el):
31
+ return
32
+ from pptx2.dml.color import RGBColor
33
+
34
+ ColorFormat.from_colorchoice_parent(el).rgb = RGBColor(0x00, 0x00, 0x00)
35
+
36
+
37
+ #: The three shadow effects, wherever they appear — a flat `<a:effectLst>` or
38
+ #: nested anywhere inside an `<a:effectDag>` container tree.
39
+ _SHADOW_TAGS = tuple(qn(t) for t in ("a:outerShdw", "a:innerShdw", "a:prstShdw"))
40
+
41
+
42
+ def _suppress_theme_effect_ref(spPr) -> None:
43
+ """Point the shape's `<a:effectRef>` at the "no effect" style-matrix slot.
44
+
45
+ Auto shapes created by ``shapes.add_shape()`` carry a ``<p:style>`` with
46
+ ``<a:effectRef idx="2"/>``, which resolves against the theme's effect-style
47
+ list — in most themes a soft drop shadow. An empty ``<a:effectLst/>`` in
48
+ ``<p:spPr>`` is *supposed* to override that, but renderers disagree (the
49
+ "phantom shadow I never asked for" bug), so clearing a shadow also has to
50
+ re-point the style reference at ``idx="0"``, the well-known empty slot.
51
+
52
+ A no-op for shapes with no ``<p:style>`` (text boxes, placeholders,
53
+ pictures) and for group shapes, whose ``grpSpPr`` has no style sibling.
54
+ """
55
+ sp = spPr.getparent()
56
+ if sp is None:
57
+ return
58
+ style = sp.find(qn("p:style"))
59
+ if style is None:
60
+ return
61
+ effectRef = style.find(qn("a:effectRef"))
62
+ if effectRef is None:
63
+ return
64
+ effectRef.set("idx", "0")
65
+
66
+
67
+ if TYPE_CHECKING:
68
+ from pptx2.dml.color import RGBColor
69
+ from pptx2.enum.dml import MSO_COLOR_TYPE, MSO_PRESET_SHADOW
70
+ from pptx2.oxml.dml.effect import (
71
+ CT_BlurEffect,
72
+ CT_EffectList,
73
+ CT_GlowEffect,
74
+ CT_InnerShadowEffect,
75
+ CT_OuterShadowEffect,
76
+ CT_PresetShadowEffect,
77
+ CT_ReflectionEffect,
78
+ CT_SoftEdgesEffect,
79
+ )
80
+ from pptx2.oxml.shapes.shared import CT_ShapeProperties
81
+ from pptx2.util import Length
82
+
83
+ # Effect elements that carry exactly one EG_ColorChoice child and can back a
84
+ # `_LazyEffectColorFormat` (outer/inner/preset shadow, glow).
85
+ _EffectColorParent = (
86
+ CT_OuterShadowEffect | CT_InnerShadowEffect | CT_PresetShadowEffect | CT_GlowEffect
87
+ )
88
+
89
+
90
+ class _LazyEffectColorFormat:
91
+ """Non-mutating ColorFormat proxy for visual-effect elements (shadow, glow).
92
+
93
+ Reads (`type`, `rgb`, `theme_color`, `brightness`, `alpha`) peek at the
94
+ existing effect element without touching the XML. When the element doesn't
95
+ exist yet, reads return the appropriate "no color" sentinel values.
96
+
97
+ Writes (`rgb=`, `theme_color=`) lazily create the effectLst + effect element
98
+ hierarchy on first assignment, then delegate to a real `ColorFormat`.
99
+
100
+ `peek()` must return the existing effect element or None without any side
101
+ effects; `ensure()` must return the element (creating it if absent).
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ peek: Callable[[], _EffectColorParent | None],
107
+ ensure: Callable[[], _EffectColorParent],
108
+ ):
109
+ self._peek = peek
110
+ self._ensure = ensure
111
+
112
+ @property
113
+ def type(self) -> MSO_COLOR_TYPE | None:
114
+ cf = self._existing_cf()
115
+ return cf.type if cf is not None else None
116
+
117
+ @property
118
+ def rgb(self) -> RGBColor | None:
119
+ cf = self._existing_cf()
120
+ return cf.rgb if cf is not None else None
121
+
122
+ @rgb.setter
123
+ def rgb(self, value: RGBColor):
124
+ self._ensure_cf().rgb = value
125
+
126
+ @property
127
+ def theme_color(self) -> MSO_THEME_COLOR:
128
+ cf = self._existing_cf()
129
+ return cf.theme_color if cf is not None else MSO_THEME_COLOR.NOT_THEME_COLOR
130
+
131
+ @theme_color.setter
132
+ def theme_color(self, value: MSO_THEME_COLOR):
133
+ self._ensure_cf().theme_color = value
134
+
135
+ @property
136
+ def brightness(self) -> float:
137
+ cf = self._existing_cf()
138
+ return cf.brightness if cf is not None else 0.0
139
+
140
+ @brightness.setter
141
+ def brightness(self, value: float):
142
+ cf = self._existing_cf()
143
+ if cf is None:
144
+ raise ValueError(
145
+ "can't set brightness when color.type is None. Set color.rgb or .theme_color first."
146
+ )
147
+ cf.brightness = value
148
+
149
+ @property
150
+ def alpha(self) -> float:
151
+ cf = self._existing_cf()
152
+ return cf.alpha if cf is not None else 1.0
153
+
154
+ @alpha.setter
155
+ def alpha(self, value: float | None):
156
+ cf = self._existing_cf()
157
+ if cf is None:
158
+ # No colour set yet. Rather than force callers to set ``.rgb``
159
+ # first, default to opaque black — shadows (and glows) are almost
160
+ # always black, so an alpha-only assignment is the common case.
161
+ from pptx2.dml.color import RGBColor
162
+
163
+ cf = self._ensure_cf()
164
+ cf.rgb = RGBColor(0x00, 0x00, 0x00)
165
+ cf.alpha = value
166
+
167
+ def _existing_cf(self) -> ColorFormat | None:
168
+ """ColorFormat for the effect element if it exists, else None."""
169
+ el = self._peek()
170
+ return None if el is None else ColorFormat.from_colorchoice_parent(el)
171
+
172
+ def _ensure_cf(self) -> ColorFormat:
173
+ """ColorFormat for the effect element, creating the element if needed."""
174
+ return ColorFormat.from_colorchoice_parent(self._ensure())
175
+
176
+
177
+ class ShadowFormat(object):
178
+ """Provides access to outer-shadow effect on a shape.
179
+
180
+ All property reads are non-mutating: if no explicit shadow is set, None is
181
+ returned rather than writing a default into the XML. Assigning to a
182
+ property creates the `<a:effectLst>`/`<a:outerShdw>` hierarchy on demand.
183
+
184
+ The legacy `inherit` read/write property is retained for backward
185
+ compatibility but is deprecated; prefer reading individual properties for
186
+ None.
187
+ """
188
+
189
+ def __init__(self, spPr: CT_ShapeProperties):
190
+ self._element = spPr
191
+
192
+ # ------------------------------------------------------------------
193
+ # Legacy back-compat property
194
+ # ------------------------------------------------------------------
195
+
196
+ @property
197
+ def inherit(self) -> bool:
198
+ """True if shape inherits shadow settings (no explicit effectLst).
199
+
200
+ Assigning True removes any explicit `<a:effectLst>` (restoring
201
+ inheritance for *all* effects). Assigning False ensures the element
202
+ is present but leaves it empty (no visible effect).
203
+
204
+ .. deprecated:: 1.1
205
+ Read individual properties (``shadow.blur_radius`` etc.) for
206
+ ``None`` instead. ``inherit`` is scheduled for removal in 2.0.
207
+ """
208
+ warnings.warn(
209
+ "ShadowFormat.inherit is deprecated; read individual properties "
210
+ "(blur_radius, distance, direction, color) for None instead. "
211
+ "Will be removed in python-pptx2 2.0.",
212
+ DeprecationWarning,
213
+ stacklevel=2,
214
+ )
215
+ return self._element.effectLst is None
216
+
217
+ @inherit.setter
218
+ def inherit(self, value: bool):
219
+ warnings.warn(
220
+ "ShadowFormat.inherit is deprecated; assign individual properties "
221
+ "to None to clear them, or call ShadowFormat.clear() to remove the "
222
+ "shadow entirely — `inherit = False` only writes an empty "
223
+ "<a:effectLst/> and leaves an inherited theme shadow rendering. "
224
+ "Will be removed in python-pptx2 2.0.",
225
+ DeprecationWarning,
226
+ stacklevel=2,
227
+ )
228
+ # Deliberately symmetric: `False` writes the empty `<a:effectLst/>` and
229
+ # `True` removes it again, so a round-trip through this deprecated
230
+ # property leaves the shape's XML as it found it. Suppressing the
231
+ # theme effect style is what `clear()` is for — it edits `<p:style>`,
232
+ # which `inherit = True` could not put back (the original `effectRef`
233
+ # index isn't recoverable once overwritten).
234
+ if bool(value):
235
+ self._element._remove_effectLst() # pyright: ignore[reportPrivateUsage]
236
+ else:
237
+ self._element.get_or_add_effectLst()
238
+
239
+ # ------------------------------------------------------------------
240
+ # Suppression
241
+ # ------------------------------------------------------------------
242
+
243
+ def clear(self) -> "ShadowFormat":
244
+ """Guarantee this shape renders with no shadow, and return self.
245
+
246
+ Removes every explicit shadow element (`<a:outerShdw>`,
247
+ `<a:innerShdw>`, `<a:prstShdw>`) from the shape's `<a:effectLst>`,
248
+ writes the empty `<a:effectLst/>` that overrides inherited effects,
249
+ and re-points any `<a:effectRef>` in the shape's `<p:style>` at the
250
+ theme's empty effect slot (``idx="0"``).
251
+
252
+ That last step is what assigning ``None`` to the individual shadow
253
+ properties (or the deprecated ``shadow.inherit = False``) does not do:
254
+ auto shapes ship with ``<a:effectRef idx="2"/>``, which in most themes
255
+ is a soft drop shadow, so clearing only the explicit element leaves a
256
+ phantom shadow behind::
257
+
258
+ card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, *box)
259
+ card.shadow.clear() # flat card, no theme shadow
260
+
261
+ Non-shadow effects written **on the shape** (glow, soft edges, blur,
262
+ reflection in its own `<a:effectLst>`) are preserved. Theme-derived
263
+ effects are not: `<a:effectRef>` is a single all-or-nothing reference
264
+ to one entry in the theme's effect-style list, so a theme whose
265
+ referenced style pairs its shadow with a glow loses that glow too.
266
+ There is no way to keep one and drop the other through the reference
267
+ itself; re-apply the effect explicitly on the shape if you need it.
268
+ (Stock Office themes reference shadow-only styles, so this is only a
269
+ consideration for custom themes.)
270
+
271
+ Idempotent, and safe on shapes that never had a shadow.
272
+
273
+ A shape whose effects are expressed as an `<a:effectDag>` — legal, and
274
+ seen on decks authored outside PowerPoint — has its shadow nodes pruned
275
+ from that tree instead. `<a:effectLst>` and `<a:effectDag>` are the two
276
+ arms of one `EG_EffectProperties` choice, so writing a sibling list
277
+ would make the deck schema-invalid *and* leave the DAG's own shadow
278
+ rendering.
279
+ """
280
+ effectDag = self._element.find(qn("a:effectDag"))
281
+ if effectDag is not None:
282
+ for tag in _SHADOW_TAGS:
283
+ for node in list(effectDag.iterdescendants(tag)):
284
+ node.getparent().remove(node)
285
+ else:
286
+ effectLst = self._element.get_or_add_effectLst()
287
+ for remove in (
288
+ "_remove_outerShdw",
289
+ "_remove_innerShdw",
290
+ "_remove_prstShdw",
291
+ ):
292
+ getattr(effectLst, remove)()
293
+ _suppress_theme_effect_ref(self._element)
294
+ return self
295
+
296
+ # ------------------------------------------------------------------
297
+ # New Phase-3 properties — all non-mutating on read
298
+ # ------------------------------------------------------------------
299
+
300
+ @property
301
+ def blur_radius(self) -> Length | None:
302
+ """Blur radius of the shadow in EMU, or None if not explicitly set."""
303
+ outerShdw = self._outerShdw
304
+ return None if outerShdw is None else outerShdw.blurRad
305
+
306
+ @blur_radius.setter
307
+ def blur_radius(self, value: Length | None):
308
+ if value is None:
309
+ if self._outerShdw is not None:
310
+ self._outerShdw.blurRad = None # type: ignore[assignment]
311
+ else:
312
+ self._get_or_add_outerShdw().blurRad = value # type: ignore[assignment]
313
+
314
+ @property
315
+ def distance(self) -> Length | None:
316
+ """Shadow offset distance in EMU, or None if not explicitly set."""
317
+ outerShdw = self._outerShdw
318
+ return None if outerShdw is None else outerShdw.dist
319
+
320
+ @distance.setter
321
+ def distance(self, value: Length | None):
322
+ if value is None:
323
+ if self._outerShdw is not None:
324
+ self._outerShdw.dist = None # type: ignore[assignment]
325
+ else:
326
+ self._get_or_add_outerShdw().dist = value # type: ignore[assignment]
327
+
328
+ @property
329
+ def direction(self) -> float | None:
330
+ """Shadow direction in degrees (0–360), or None if not explicitly set."""
331
+ outerShdw = self._outerShdw
332
+ return None if outerShdw is None else outerShdw.dir
333
+
334
+ @direction.setter
335
+ def direction(self, value: float | None):
336
+ if value is None:
337
+ if self._outerShdw is not None:
338
+ self._outerShdw.dir = None # type: ignore[assignment]
339
+ else:
340
+ self._get_or_add_outerShdw().dir = value # type: ignore[assignment]
341
+
342
+ @property
343
+ def color(self) -> _LazyEffectColorFormat:
344
+ """Non-mutating color accessor for the shadow color.
345
+
346
+ Reading any sub-property (``type``, ``rgb``, ``theme_color``) on a
347
+ shape with no explicit shadow returns the appropriate "no color"
348
+ sentinel without touching the XML. Writing to ``color.rgb`` or
349
+ ``color.theme_color`` lazily creates the ``<a:outerShdw>`` hierarchy.
350
+ """
351
+ return _LazyEffectColorFormat(lambda: self._outerShdw, self._get_or_add_outerShdw)
352
+
353
+ @color.setter
354
+ def color(self, value: RGBColor) -> None:
355
+ # Convenience setter so ``shape.shadow.color = RGBColor(...)`` works in
356
+ # addition to ``shape.shadow.color.rgb = RGBColor(...)`` (parity with
357
+ # ThreeDFormat.extrusion_color / contour_color).
358
+ self.color.rgb = value
359
+
360
+ # ------------------------------------------------------------------
361
+ # Internals
362
+ # ------------------------------------------------------------------
363
+
364
+ @property
365
+ def _outerShdw(self) -> CT_OuterShadowEffect | None:
366
+ effectLst: CT_EffectList | None = self._element.effectLst
367
+ if effectLst is None:
368
+ return None
369
+ return effectLst.outerShdw
370
+
371
+ def _get_or_add_outerShdw(self) -> CT_OuterShadowEffect:
372
+ effectLst: CT_EffectList = self._element.get_or_add_effectLst()
373
+ outerShdw = effectLst.outerShdw
374
+ if outerShdw is None:
375
+ outerShdw = effectLst.get_or_add_outerShdw()
376
+ # <a:outerShdw> requires a colour child; guarantee one so a
377
+ # geometry-only shadow doesn't make PowerPoint flag the deck as broken.
378
+ _ensure_effect_color(outerShdw)
379
+ return outerShdw
380
+
381
+
382
+ class InnerShadowFormat(object):
383
+ """Provides access to the inner-shadow effect on a shape.
384
+
385
+ Inner shadow (``<a:innerShdw>``) is the sibling of the outer shadow that
386
+ casts *into* the shape rather than behind it. Its API mirrors
387
+ :class:`ShadowFormat` — ``blur_radius``, ``distance``, ``direction``, and
388
+ ``color`` — minus the outer-only ``rotWithShape``/alignment attributes the
389
+ inner element doesn't have.
390
+
391
+ All property reads are non-mutating: when no explicit inner shadow is set,
392
+ each property returns ``None`` (or a "no color" sentinel) rather than
393
+ writing a default into the XML. Assigning to any property lazily creates
394
+ the ``<a:effectLst>``/``<a:innerShdw>`` hierarchy and guarantees the
395
+ schema-required colour child.
396
+ """
397
+
398
+ def __init__(self, spPr: CT_ShapeProperties):
399
+ self._element = spPr
400
+
401
+ @property
402
+ def blur_radius(self) -> Length | None:
403
+ """Blur radius of the inner shadow in EMU, or None if not set."""
404
+ innerShdw = self._innerShdw
405
+ return None if innerShdw is None else innerShdw.blurRad
406
+
407
+ @blur_radius.setter
408
+ def blur_radius(self, value: Length | None):
409
+ if value is None:
410
+ if self._innerShdw is not None:
411
+ self._innerShdw.blurRad = None # type: ignore[assignment]
412
+ else:
413
+ self._get_or_add_innerShdw().blurRad = value # type: ignore[assignment]
414
+
415
+ @property
416
+ def distance(self) -> Length | None:
417
+ """Inner-shadow offset distance in EMU, or None if not set."""
418
+ innerShdw = self._innerShdw
419
+ return None if innerShdw is None else innerShdw.dist
420
+
421
+ @distance.setter
422
+ def distance(self, value: Length | None):
423
+ if value is None:
424
+ if self._innerShdw is not None:
425
+ self._innerShdw.dist = None # type: ignore[assignment]
426
+ else:
427
+ self._get_or_add_innerShdw().dist = value # type: ignore[assignment]
428
+
429
+ @property
430
+ def direction(self) -> float | None:
431
+ """Inner-shadow direction in degrees (0–360), or None if not set."""
432
+ innerShdw = self._innerShdw
433
+ return None if innerShdw is None else innerShdw.dir
434
+
435
+ @direction.setter
436
+ def direction(self, value: float | None):
437
+ if value is None:
438
+ if self._innerShdw is not None:
439
+ self._innerShdw.dir = None # type: ignore[assignment]
440
+ else:
441
+ self._get_or_add_innerShdw().dir = value # type: ignore[assignment]
442
+
443
+ @property
444
+ def color(self) -> _LazyEffectColorFormat:
445
+ """Non-mutating color accessor for the inner-shadow color.
446
+
447
+ Reading any sub-property (``type``, ``rgb``, ``theme_color``) on a
448
+ shape with no explicit inner shadow returns the appropriate "no color"
449
+ sentinel without touching the XML. Writing to ``color.rgb`` or
450
+ ``color.theme_color`` lazily creates the ``<a:innerShdw>`` hierarchy.
451
+ """
452
+ return _LazyEffectColorFormat(lambda: self._innerShdw, self._get_or_add_innerShdw)
453
+
454
+ @color.setter
455
+ def color(self, value: RGBColor) -> None:
456
+ # Convenience setter so ``shape.inner_shadow.color = RGBColor(...)``
457
+ # works in addition to ``shape.inner_shadow.color.rgb = RGBColor(...)``.
458
+ self.color.rgb = value
459
+
460
+ # ------------------------------------------------------------------
461
+ # Internals
462
+ # ------------------------------------------------------------------
463
+
464
+ @property
465
+ def _innerShdw(self) -> CT_InnerShadowEffect | None:
466
+ effectLst: CT_EffectList | None = self._element.effectLst
467
+ if effectLst is None:
468
+ return None
469
+ return effectLst.innerShdw
470
+
471
+ def _get_or_add_innerShdw(self) -> CT_InnerShadowEffect:
472
+ effectLst: CT_EffectList = self._element.get_or_add_effectLst()
473
+ innerShdw = effectLst.innerShdw
474
+ if innerShdw is None:
475
+ innerShdw = effectLst.get_or_add_innerShdw()
476
+ # <a:innerShdw> requires exactly one EG_ColorChoice child; guarantee one
477
+ # so a geometry-only inner shadow stays schema-valid (PowerPoint flags a
478
+ # colour-less shadow as broken).
479
+ _ensure_effect_color(innerShdw)
480
+ return innerShdw
481
+
482
+
483
+ class PresetShadowFormat(object):
484
+ """Provides access to the preset-shadow effect on a shape.
485
+
486
+ Preset shadow (``<a:prstShdw>``) selects one of twenty canned shadow looks
487
+ (``shdw1`` .. ``shdw20``) via the schema-*required* ``prst`` attribute,
488
+ with optional ``distance``/``direction`` overrides and a colour.
489
+
490
+ The ``prst`` attribute is mandatory, so this proxy never materialises a
491
+ ``<a:prstShdw>`` element without one: setting ``distance``/``direction``/
492
+ ``color`` before a preset defaults the preset to ``shdw1``. All reads are
493
+ non-mutating and return ``None`` (or the "no color" sentinel) when no
494
+ explicit preset shadow is present.
495
+ """
496
+
497
+ def __init__(self, spPr: CT_ShapeProperties):
498
+ self._element = spPr
499
+
500
+ @property
501
+ def preset(self) -> MSO_PRESET_SHADOW | None:
502
+ """The preset-shadow style as an :class:`MSO_PRESET_SHADOW` member, or None."""
503
+ prstShdw = self._prstShdw
504
+ return None if prstShdw is None else prstShdw.prst
505
+
506
+ @preset.setter
507
+ def preset(self, value: MSO_PRESET_SHADOW | str | None):
508
+ if value is None:
509
+ # The whole element hinges on `prst`; clearing the preset removes
510
+ # the element so theme inheritance is restored.
511
+ effectLst: CT_EffectList | None = self._element.effectLst
512
+ if effectLst is not None and effectLst.prstShdw is not None:
513
+ effectLst._remove_prstShdw() # pyright: ignore[reportPrivateUsage]
514
+ return
515
+ self._get_or_add_prstShdw().prst = self._coerce_preset(value) # type: ignore[assignment]
516
+
517
+ @property
518
+ def distance(self) -> Length | None:
519
+ """Preset-shadow offset distance in EMU, or None if not set."""
520
+ prstShdw = self._prstShdw
521
+ return None if prstShdw is None else prstShdw.dist
522
+
523
+ @distance.setter
524
+ def distance(self, value: Length | None):
525
+ if value is None:
526
+ if self._prstShdw is not None:
527
+ self._prstShdw.dist = None # type: ignore[assignment]
528
+ else:
529
+ self._get_or_add_prstShdw().dist = value # type: ignore[assignment]
530
+
531
+ @property
532
+ def direction(self) -> float | None:
533
+ """Preset-shadow direction in degrees (0–360), or None if not set."""
534
+ prstShdw = self._prstShdw
535
+ return None if prstShdw is None else prstShdw.dir
536
+
537
+ @direction.setter
538
+ def direction(self, value: float | None):
539
+ if value is None:
540
+ if self._prstShdw is not None:
541
+ self._prstShdw.dir = None # type: ignore[assignment]
542
+ else:
543
+ self._get_or_add_prstShdw().dir = value # type: ignore[assignment]
544
+
545
+ @property
546
+ def color(self) -> _LazyEffectColorFormat:
547
+ """Non-mutating color accessor for the preset-shadow color.
548
+
549
+ Reading any sub-property on a shape with no explicit preset shadow
550
+ returns the appropriate "no color" sentinel without touching the XML.
551
+ Writing to ``color.rgb`` or ``color.theme_color`` lazily creates the
552
+ ``<a:prstShdw>`` hierarchy (defaulting the preset to ``shdw1``).
553
+ """
554
+ return _LazyEffectColorFormat(lambda: self._prstShdw, self._get_or_add_prstShdw)
555
+
556
+ @color.setter
557
+ def color(self, value: RGBColor) -> None:
558
+ self.color.rgb = value
559
+
560
+ # ------------------------------------------------------------------
561
+ # Internals
562
+ # ------------------------------------------------------------------
563
+
564
+ @staticmethod
565
+ def _coerce_preset(value: MSO_PRESET_SHADOW | str) -> MSO_PRESET_SHADOW:
566
+ """Accept an :class:`MSO_PRESET_SHADOW` member or a ``"shdw1".."shdw20"`` string."""
567
+ from pptx2.enum.dml import MSO_PRESET_SHADOW
568
+
569
+ if isinstance(value, str):
570
+ return MSO_PRESET_SHADOW.from_xml(value)
571
+ return value
572
+
573
+ @property
574
+ def _prstShdw(self) -> CT_PresetShadowEffect | None:
575
+ effectLst: CT_EffectList | None = self._element.effectLst
576
+ if effectLst is None:
577
+ return None
578
+ return effectLst.prstShdw
579
+
580
+ def _get_or_add_prstShdw(self) -> CT_PresetShadowEffect:
581
+ from pptx2.enum.dml import MSO_PRESET_SHADOW
582
+
583
+ effectLst: CT_EffectList = self._element.get_or_add_effectLst()
584
+ prstShdw = effectLst.prstShdw
585
+ if prstShdw is None:
586
+ prstShdw = effectLst.get_or_add_prstShdw()
587
+ # `prst` is schema-REQUIRED; never let the element exist without it.
588
+ if prstShdw.get("prst") is None:
589
+ prstShdw.prst = MSO_PRESET_SHADOW.SHADOW_1 # type: ignore[assignment]
590
+ # <a:prstShdw> also requires exactly one EG_ColorChoice child.
591
+ _ensure_effect_color(prstShdw)
592
+ return prstShdw
593
+
594
+
595
+ class GlowFormat(object):
596
+ """Provides access to the glow effect on a shape.
597
+
598
+ All property reads are non-mutating; assigning a non-None value lazily
599
+ creates the `<a:effectLst>`/`<a:glow>` hierarchy.
600
+ """
601
+
602
+ def __init__(self, spPr: CT_ShapeProperties):
603
+ self._element = spPr
604
+
605
+ @property
606
+ def radius(self) -> Length | None:
607
+ """Glow radius in EMU, or None when no explicit glow is set."""
608
+ glow = self._glow
609
+ return None if glow is None else glow.rad
610
+
611
+ @radius.setter
612
+ def radius(self, value: Length | None):
613
+ if value is None:
614
+ # Only remove the attribute — preserves any explicitly set color.
615
+ if self._glow is not None:
616
+ self._glow.rad = None # type: ignore[assignment]
617
+ else:
618
+ self._get_or_add_glow().rad = value # type: ignore[assignment]
619
+
620
+ @property
621
+ def color(self) -> _LazyEffectColorFormat:
622
+ """Non-mutating color accessor for the glow color.
623
+
624
+ Reading any sub-property on a shape with no explicit glow returns the
625
+ appropriate "no color" sentinel without touching the XML. Writing to
626
+ ``color.rgb`` or ``color.theme_color`` lazily creates the
627
+ ``<a:glow>`` hierarchy.
628
+ """
629
+ return _LazyEffectColorFormat(lambda: self._glow, self._get_or_add_glow)
630
+
631
+ @color.setter
632
+ def color(self, value: RGBColor) -> None:
633
+ # Convenience setter so ``shape.glow.color = RGBColor(...)`` works in
634
+ # addition to ``shape.glow.color.rgb = RGBColor(...)``.
635
+ self.color.rgb = value
636
+
637
+ # ------------------------------------------------------------------
638
+ # Internals
639
+ # ------------------------------------------------------------------
640
+
641
+ @property
642
+ def _glow(self) -> CT_GlowEffect | None:
643
+ effectLst: CT_EffectList | None = self._element.effectLst
644
+ if effectLst is None:
645
+ return None
646
+ return effectLst.glow
647
+
648
+ def _get_or_add_glow(self) -> CT_GlowEffect:
649
+ effectLst: CT_EffectList = self._element.get_or_add_effectLst()
650
+ glow = effectLst.glow
651
+ if glow is None:
652
+ glow = effectLst.get_or_add_glow()
653
+ # <a:glow> requires a colour child (a glow with no colour is invalid and
654
+ # is rejected by PowerPoint); guarantee one.
655
+ _ensure_effect_color(glow)
656
+ return glow
657
+
658
+
659
+ class SoftEdgeFormat(object):
660
+ """Provides access to the soft-edge effect on a shape.
661
+
662
+ All property reads are non-mutating. Assigning a non-None radius lazily
663
+ creates the `<a:effectLst>`/`<a:softEdge>` hierarchy.
664
+ """
665
+
666
+ def __init__(self, spPr: CT_ShapeProperties):
667
+ self._element = spPr
668
+
669
+ @property
670
+ def radius(self) -> Length | None:
671
+ """Soft-edge blur radius in EMU, or None when no explicit soft-edge is set."""
672
+ softEdge = self._softEdge
673
+ return None if softEdge is None else softEdge.rad
674
+
675
+ @radius.setter
676
+ def radius(self, value: Length | None):
677
+ if value is None:
678
+ if self._softEdge is not None:
679
+ effectLst: CT_EffectList | None = self._element.effectLst
680
+ if effectLst is not None:
681
+ effectLst._remove_softEdge() # pyright: ignore[reportPrivateUsage]
682
+ else:
683
+ self._get_or_add_softEdge().rad = value # type: ignore[assignment]
684
+
685
+ # ------------------------------------------------------------------
686
+ # Internals
687
+ # ------------------------------------------------------------------
688
+
689
+ @property
690
+ def _softEdge(self) -> CT_SoftEdgesEffect | None:
691
+ effectLst: CT_EffectList | None = self._element.effectLst
692
+ if effectLst is None:
693
+ return None
694
+ return effectLst.softEdge
695
+
696
+ def _get_or_add_softEdge(self) -> CT_SoftEdgesEffect:
697
+ effectLst: CT_EffectList = self._element.get_or_add_effectLst()
698
+ softEdge = effectLst.softEdge
699
+ if softEdge is None:
700
+ softEdge = effectLst.get_or_add_softEdge()
701
+ return softEdge
702
+
703
+
704
+ class BlurFormat(object):
705
+ """Provides access to the Gaussian blur effect on a shape.
706
+
707
+ All property reads are non-mutating; assigning a non-None value lazily
708
+ creates the `<a:effectLst>`/`<a:blur>` hierarchy. Clearing the last
709
+ explicit attribute drops the `<a:blur>` element again so theme
710
+ inheritance is preserved.
711
+ """
712
+
713
+ def __init__(self, spPr: CT_ShapeProperties):
714
+ self._element = spPr
715
+
716
+ @property
717
+ def radius(self) -> Length | None:
718
+ """Blur radius in EMU, or None when no explicit blur is set."""
719
+ blur = self._blur
720
+ return None if blur is None else blur.rad
721
+
722
+ @radius.setter
723
+ def radius(self, value: Length | None):
724
+ if value is None:
725
+ if self._blur is not None:
726
+ self._blur.rad = None # type: ignore[assignment]
727
+ self._maybe_drop_blur()
728
+ else:
729
+ self._get_or_add_blur().rad = value # type: ignore[assignment]
730
+
731
+ @property
732
+ def grow(self) -> bool | None:
733
+ """True when the bounding box expands to accommodate the blur.
734
+
735
+ Returns None when no `<a:blur>` element is present. PowerPoint
736
+ treats absence of the attribute as `True`, but we surface the raw
737
+ value so a round-trip through python-pptx never silently flips a
738
+ deck-author's choice.
739
+ """
740
+ blur = self._blur
741
+ return None if blur is None else blur.grow
742
+
743
+ @grow.setter
744
+ def grow(self, value: bool | None):
745
+ if value is None:
746
+ if self._blur is not None:
747
+ self._blur.grow = None # type: ignore[assignment]
748
+ self._maybe_drop_blur()
749
+ else:
750
+ self._get_or_add_blur().grow = bool(value) # type: ignore[assignment]
751
+
752
+ # ------------------------------------------------------------------
753
+ # Internals
754
+ # ------------------------------------------------------------------
755
+
756
+ @property
757
+ def _blur(self) -> CT_BlurEffect | None:
758
+ effectLst: CT_EffectList | None = self._element.effectLst
759
+ if effectLst is None:
760
+ return None
761
+ return effectLst.blur
762
+
763
+ def _get_or_add_blur(self) -> CT_BlurEffect:
764
+ effectLst: CT_EffectList = self._element.get_or_add_effectLst()
765
+ blur = effectLst.blur
766
+ if blur is None:
767
+ blur = effectLst.get_or_add_blur()
768
+ return blur
769
+
770
+ def _maybe_drop_blur(self) -> None:
771
+ """Remove `<a:blur>` when no explicit attributes remain.
772
+
773
+ Keeps theme inheritance intact when a caller clears every property
774
+ they previously assigned.
775
+ """
776
+ blur = self._blur
777
+ if blur is None:
778
+ return
779
+ if not blur.attrib:
780
+ effectLst = self._element.effectLst
781
+ if effectLst is not None:
782
+ effectLst._remove_blur() # pyright: ignore[reportPrivateUsage]
783
+
784
+
785
+ class ReflectionFormat(object):
786
+ """Provides access to the reflection effect on a shape.
787
+
788
+ Reflection is the "mirror image fading downward" effect commonly seen on
789
+ photo cards. The full OOXML schema for `<a:reflection>` exposes 14
790
+ attributes; we surface the four that control the look users typically
791
+ care about — blur radius, offset distance, direction, and the start /
792
+ end alpha that drive the fade — and leave the rest accessible via the
793
+ underlying element for power users.
794
+
795
+ All reads are non-mutating; the `<a:effectLst>`/`<a:reflection>`
796
+ hierarchy is created lazily on first write, and clearing the last
797
+ explicit attribute drops the element again so theme inheritance is
798
+ preserved.
799
+ """
800
+
801
+ def __init__(self, spPr: CT_ShapeProperties):
802
+ self._element = spPr
803
+
804
+ @property
805
+ def blur_radius(self) -> Length | None:
806
+ """Blur radius applied to the reflection in EMU, or None."""
807
+ reflection = self._reflection
808
+ return None if reflection is None else reflection.blurRad
809
+
810
+ @blur_radius.setter
811
+ def blur_radius(self, value: Length | None):
812
+ if value is None:
813
+ if self._reflection is not None:
814
+ self._reflection.blurRad = None # type: ignore[assignment]
815
+ self._maybe_drop_reflection()
816
+ else:
817
+ self._get_or_add_reflection().blurRad = value # type: ignore[assignment]
818
+
819
+ @property
820
+ def distance(self) -> Length | None:
821
+ """Distance the reflection is offset from the shape, in EMU, or None."""
822
+ reflection = self._reflection
823
+ return None if reflection is None else reflection.dist
824
+
825
+ @distance.setter
826
+ def distance(self, value: Length | None):
827
+ if value is None:
828
+ if self._reflection is not None:
829
+ self._reflection.dist = None # type: ignore[assignment]
830
+ self._maybe_drop_reflection()
831
+ else:
832
+ self._get_or_add_reflection().dist = value # type: ignore[assignment]
833
+
834
+ @property
835
+ def direction(self) -> float | None:
836
+ """Direction of the reflection offset in degrees (0–360), or None."""
837
+ reflection = self._reflection
838
+ return None if reflection is None else reflection.dir
839
+
840
+ @direction.setter
841
+ def direction(self, value: float | None):
842
+ if value is None:
843
+ if self._reflection is not None:
844
+ self._reflection.dir = None # type: ignore[assignment]
845
+ self._maybe_drop_reflection()
846
+ else:
847
+ self._get_or_add_reflection().dir = value # type: ignore[assignment]
848
+
849
+ @property
850
+ def start_alpha(self) -> float | None:
851
+ """Alpha at the top of the reflection in `[0.0, 1.0]`, or None."""
852
+ reflection = self._reflection
853
+ return None if reflection is None else reflection.stA
854
+
855
+ @start_alpha.setter
856
+ def start_alpha(self, value: float | None):
857
+ if value is None:
858
+ if self._reflection is not None:
859
+ self._reflection.stA = None # type: ignore[assignment]
860
+ self._maybe_drop_reflection()
861
+ else:
862
+ self._get_or_add_reflection().stA = value # type: ignore[assignment]
863
+
864
+ @property
865
+ def end_alpha(self) -> float | None:
866
+ """Alpha at the bottom of the reflection in `[0.0, 1.0]`, or None."""
867
+ reflection = self._reflection
868
+ return None if reflection is None else reflection.endA
869
+
870
+ @end_alpha.setter
871
+ def end_alpha(self, value: float | None):
872
+ if value is None:
873
+ if self._reflection is not None:
874
+ self._reflection.endA = None # type: ignore[assignment]
875
+ self._maybe_drop_reflection()
876
+ else:
877
+ self._get_or_add_reflection().endA = value # type: ignore[assignment]
878
+
879
+ # ------------------------------------------------------------------
880
+ # Internals
881
+ # ------------------------------------------------------------------
882
+
883
+ @property
884
+ def _reflection(self) -> CT_ReflectionEffect | None:
885
+ effectLst: CT_EffectList | None = self._element.effectLst
886
+ if effectLst is None:
887
+ return None
888
+ return effectLst.reflection
889
+
890
+ def _get_or_add_reflection(self) -> CT_ReflectionEffect:
891
+ effectLst: CT_EffectList = self._element.get_or_add_effectLst()
892
+ reflection = effectLst.reflection
893
+ if reflection is None:
894
+ reflection = effectLst.get_or_add_reflection()
895
+ return reflection
896
+
897
+ def _maybe_drop_reflection(self) -> None:
898
+ """Remove `<a:reflection>` when no explicit attributes remain.
899
+
900
+ Keeps theme inheritance intact when a caller clears every property
901
+ they previously assigned.
902
+ """
903
+ reflection = self._reflection
904
+ if reflection is None:
905
+ return
906
+ if not reflection.attrib:
907
+ effectLst = self._element.effectLst
908
+ if effectLst is not None:
909
+ effectLst._remove_reflection() # pyright: ignore[reportPrivateUsage]