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.
- pptx2/__init__.py +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- 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]
|