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/shapes/base.py
ADDED
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
"""Base shape-related objects such as BaseShape."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable, cast
|
|
7
|
+
|
|
8
|
+
from pptx2.action import ActionSetting
|
|
9
|
+
from pptx2.dml.effect import (
|
|
10
|
+
BlurFormat,
|
|
11
|
+
GlowFormat,
|
|
12
|
+
InnerShadowFormat,
|
|
13
|
+
PresetShadowFormat,
|
|
14
|
+
ReflectionFormat,
|
|
15
|
+
ShadowFormat,
|
|
16
|
+
SoftEdgeFormat,
|
|
17
|
+
)
|
|
18
|
+
from pptx2.dml.three_d import ThreeDFormat
|
|
19
|
+
from pptx2.shared import ElementProxy
|
|
20
|
+
from pptx2.util import _coerce_emu, lazyproperty
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pptx2.design.style import ShapeStyle
|
|
24
|
+
from pptx2.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
|
|
25
|
+
from pptx2.oxml.shapes import ShapeElement
|
|
26
|
+
from pptx2.oxml.shapes.shared import CT_Placeholder
|
|
27
|
+
from pptx2.parts.slide import BaseSlidePart
|
|
28
|
+
from pptx2.types import ProvidesPart
|
|
29
|
+
from pptx2.util import Length
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BaseShape(object):
|
|
33
|
+
"""Base class for shape objects.
|
|
34
|
+
|
|
35
|
+
Subclasses include |Shape|, |Picture|, and |GraphicFrame|.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, shape_elm: ShapeElement, parent: ProvidesPart):
|
|
39
|
+
super().__init__()
|
|
40
|
+
self._element = shape_elm
|
|
41
|
+
self._parent = parent
|
|
42
|
+
|
|
43
|
+
def __eq__(self, other: object) -> bool:
|
|
44
|
+
"""|True| if this shape object proxies the same element as *other*.
|
|
45
|
+
|
|
46
|
+
Equality for proxy objects is defined as referring to the same XML element, whether or not
|
|
47
|
+
they are the same proxy object instance.
|
|
48
|
+
"""
|
|
49
|
+
if not isinstance(other, BaseShape):
|
|
50
|
+
return False
|
|
51
|
+
return self._element is other._element
|
|
52
|
+
|
|
53
|
+
def __ne__(self, other: object) -> bool:
|
|
54
|
+
if not isinstance(other, BaseShape):
|
|
55
|
+
return True
|
|
56
|
+
return self._element is not other._element
|
|
57
|
+
|
|
58
|
+
@lazyproperty
|
|
59
|
+
def click_action(self) -> ActionSetting:
|
|
60
|
+
"""|ActionSetting| instance providing access to click behaviors.
|
|
61
|
+
|
|
62
|
+
Click behaviors are hyperlink-like behaviors including jumping to a hyperlink (web page)
|
|
63
|
+
or to another slide in the presentation. The click action is that defined on the overall
|
|
64
|
+
shape, not a run of text within the shape. An |ActionSetting| object is always returned,
|
|
65
|
+
even when no click behavior is defined on the shape.
|
|
66
|
+
"""
|
|
67
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
68
|
+
return ActionSetting(cNvPr, self)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def element(self) -> ShapeElement:
|
|
72
|
+
"""`lxml` element for this shape, e.g. a CT_Shape instance.
|
|
73
|
+
|
|
74
|
+
Note that manipulating this element improperly can produce an invalid presentation file.
|
|
75
|
+
Make sure you know what you're doing if you use this to change the underlying XML.
|
|
76
|
+
"""
|
|
77
|
+
return self._element
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def has_chart(self) -> bool:
|
|
81
|
+
"""|True| if this shape is a graphic frame containing a chart object.
|
|
82
|
+
|
|
83
|
+
|False| otherwise. When |True|, the chart object can be accessed using the ``.chart``
|
|
84
|
+
property.
|
|
85
|
+
"""
|
|
86
|
+
# This implementation is unconditionally False, the True version is
|
|
87
|
+
# on GraphicFrame subclass.
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def has_table(self) -> bool:
|
|
92
|
+
"""|True| if this shape is a graphic frame containing a table object.
|
|
93
|
+
|
|
94
|
+
|False| otherwise. When |True|, the table object can be accessed using the ``.table``
|
|
95
|
+
property.
|
|
96
|
+
"""
|
|
97
|
+
# This implementation is unconditionally False, the True version is
|
|
98
|
+
# on GraphicFrame subclass.
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def has_text_frame(self) -> bool:
|
|
103
|
+
"""|True| if this shape can contain text."""
|
|
104
|
+
# overridden on Shape to return True. Only <p:sp> has text frame
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def height(self) -> Length:
|
|
109
|
+
"""Read/write. Integer distance between top and bottom extents of shape in EMUs."""
|
|
110
|
+
return self._element.cy
|
|
111
|
+
|
|
112
|
+
@height.setter
|
|
113
|
+
def height(self, value: Length):
|
|
114
|
+
self._element.cy = _coerce_emu(value)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def is_placeholder(self) -> bool:
|
|
118
|
+
"""True if this shape is a placeholder.
|
|
119
|
+
|
|
120
|
+
A shape is a placeholder if it has a <p:ph> element.
|
|
121
|
+
"""
|
|
122
|
+
return self._element.has_ph_elm
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def left(self) -> Length:
|
|
126
|
+
"""Integer distance of the left edge of this shape from the left edge of the slide.
|
|
127
|
+
|
|
128
|
+
Read/write. Expressed in English Metric Units (EMU)
|
|
129
|
+
"""
|
|
130
|
+
return self._element.x
|
|
131
|
+
|
|
132
|
+
@left.setter
|
|
133
|
+
def left(self, value: Length):
|
|
134
|
+
self._element.x = _coerce_emu(value)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def name(self) -> str:
|
|
138
|
+
"""Name of this shape, e.g. 'Picture 7'."""
|
|
139
|
+
return self._element.shape_name
|
|
140
|
+
|
|
141
|
+
@name.setter
|
|
142
|
+
def name(self, value: str):
|
|
143
|
+
self._element._nvXxPr.cNvPr.name = value # pyright: ignore[reportPrivateUsage]
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def alt_text(self) -> str:
|
|
147
|
+
"""Accessibility description (alt text) for this shape.
|
|
148
|
+
|
|
149
|
+
Read/write ``str``. Maps to the ``descr`` attribute of the
|
|
150
|
+
shape's ``<p:cNvPr>`` element — the OOXML-sanctioned alt-text
|
|
151
|
+
slot that screen readers announce and that PowerPoint surfaces
|
|
152
|
+
in its *Alt Text* pane. Reading returns ``""`` when no
|
|
153
|
+
description has been set.
|
|
154
|
+
|
|
155
|
+
Example::
|
|
156
|
+
|
|
157
|
+
picture.alt_text = "Bar chart of Q3 revenue by region."
|
|
158
|
+
|
|
159
|
+
Assigning ``""`` (or ``None``) clears the description.
|
|
160
|
+
"""
|
|
161
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
162
|
+
return cNvPr.get("descr") or ""
|
|
163
|
+
|
|
164
|
+
@alt_text.setter
|
|
165
|
+
def alt_text(self, value: str | None):
|
|
166
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
167
|
+
if value is None or value == "":
|
|
168
|
+
if "descr" in cNvPr.attrib:
|
|
169
|
+
del cNvPr.attrib["descr"]
|
|
170
|
+
return
|
|
171
|
+
if not isinstance(value, str):
|
|
172
|
+
raise TypeError(f"alt_text must be a string or None; got {type(value).__name__}")
|
|
173
|
+
cNvPr.set("descr", value)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def title_text(self) -> str:
|
|
177
|
+
"""Accessibility title for this shape.
|
|
178
|
+
|
|
179
|
+
Read/write ``str``. Maps to the ``title`` attribute of the
|
|
180
|
+
shape's ``<p:cNvPr>`` element — a short one-line label that
|
|
181
|
+
complements the longer :attr:`alt_text` description. Reading
|
|
182
|
+
returns ``""`` when no title has been set; assigning ``""`` (or
|
|
183
|
+
``None``) clears it.
|
|
184
|
+
"""
|
|
185
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
186
|
+
return cNvPr.get("title") or ""
|
|
187
|
+
|
|
188
|
+
@title_text.setter
|
|
189
|
+
def title_text(self, value: str | None):
|
|
190
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
191
|
+
if value is None or value == "":
|
|
192
|
+
if "title" in cNvPr.attrib:
|
|
193
|
+
del cNvPr.attrib["title"]
|
|
194
|
+
return
|
|
195
|
+
if not isinstance(value, str):
|
|
196
|
+
raise TypeError(f"title_text must be a string or None; got {type(value).__name__}")
|
|
197
|
+
cNvPr.set("title", value)
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def part(self) -> BaseSlidePart:
|
|
201
|
+
"""The package part containing this shape.
|
|
202
|
+
|
|
203
|
+
A |BaseSlidePart| subclass in this case. Access to a slide part should only be required if
|
|
204
|
+
you are extending the behavior of |pp| API objects.
|
|
205
|
+
"""
|
|
206
|
+
return cast("BaseSlidePart", self._parent.part)
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def placeholder_format(self) -> _PlaceholderFormat:
|
|
210
|
+
"""Provides access to placeholder-specific properties such as placeholder type.
|
|
211
|
+
|
|
212
|
+
Raises |ValueError| on access if the shape is not a placeholder.
|
|
213
|
+
"""
|
|
214
|
+
ph = self._element.ph
|
|
215
|
+
if ph is None:
|
|
216
|
+
raise ValueError("shape is not a placeholder")
|
|
217
|
+
return _PlaceholderFormat(ph)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def rotation(self) -> float:
|
|
221
|
+
"""Degrees of clockwise rotation.
|
|
222
|
+
|
|
223
|
+
Read/write float. Negative values can be assigned to indicate counter-clockwise rotation,
|
|
224
|
+
e.g. assigning -45.0 will change setting to 315.0.
|
|
225
|
+
"""
|
|
226
|
+
return self._element.rot
|
|
227
|
+
|
|
228
|
+
@rotation.setter
|
|
229
|
+
def rotation(self, value: float):
|
|
230
|
+
self._element.rot = value
|
|
231
|
+
|
|
232
|
+
@lazyproperty
|
|
233
|
+
def blur(self) -> BlurFormat:
|
|
234
|
+
"""|BlurFormat| object providing access to the Gaussian blur effect.
|
|
235
|
+
|
|
236
|
+
Always returned, even when no blur is explicitly set. Reading
|
|
237
|
+
``blur.radius`` returns None in that case.
|
|
238
|
+
"""
|
|
239
|
+
return BlurFormat(self._element.spPr)
|
|
240
|
+
|
|
241
|
+
@lazyproperty
|
|
242
|
+
def glow(self) -> GlowFormat:
|
|
243
|
+
"""|GlowFormat| object providing access to glow effect for this shape.
|
|
244
|
+
|
|
245
|
+
A |GlowFormat| object is always returned even when no glow is explicitly
|
|
246
|
+
defined. Reading ``glow.radius`` returns None in that case.
|
|
247
|
+
"""
|
|
248
|
+
return GlowFormat(self._element.spPr)
|
|
249
|
+
|
|
250
|
+
@lazyproperty
|
|
251
|
+
def reflection(self) -> ReflectionFormat:
|
|
252
|
+
"""|ReflectionFormat| object providing access to the reflection effect.
|
|
253
|
+
|
|
254
|
+
Always returned, even when no reflection is explicitly set. Reads of
|
|
255
|
+
the individual properties return None in that case.
|
|
256
|
+
"""
|
|
257
|
+
return ReflectionFormat(self._element.spPr)
|
|
258
|
+
|
|
259
|
+
@lazyproperty
|
|
260
|
+
def shadow(self) -> ShadowFormat | None:
|
|
261
|
+
"""|ShadowFormat| object providing access to shadow for this shape.
|
|
262
|
+
|
|
263
|
+
For ordinary shapes (autoshapes, pictures, group shapes, connectors)
|
|
264
|
+
a |ShadowFormat| facade is always returned, even when no shadow is
|
|
265
|
+
explicitly defined — its individual properties return ``None`` in
|
|
266
|
+
that case.
|
|
267
|
+
|
|
268
|
+
:class:`~pptx2.shapes.graphfrm.GraphicFrame` returns ``None``
|
|
269
|
+
instead of a facade: charts and tables expose effects at
|
|
270
|
+
content-specific locations that the unified |ShadowFormat| API
|
|
271
|
+
doesn't apply to. Callers that probe ``shape.shadow`` across every
|
|
272
|
+
shape on a slide should branch on ``if shape.shadow is None`` to
|
|
273
|
+
skip GraphicFrames cleanly.
|
|
274
|
+
"""
|
|
275
|
+
return ShadowFormat(self._element.spPr)
|
|
276
|
+
|
|
277
|
+
@lazyproperty
|
|
278
|
+
def inner_shadow(self) -> InnerShadowFormat:
|
|
279
|
+
"""|InnerShadowFormat| object providing access to the inner-shadow effect.
|
|
280
|
+
|
|
281
|
+
An |InnerShadowFormat| facade is always returned, even when no inner
|
|
282
|
+
shadow is explicitly defined — its individual properties
|
|
283
|
+
(``blur_radius``, ``distance``, ``direction``, ``color``) return
|
|
284
|
+
``None`` in that case.
|
|
285
|
+
"""
|
|
286
|
+
return InnerShadowFormat(self._element.spPr)
|
|
287
|
+
|
|
288
|
+
@lazyproperty
|
|
289
|
+
def preset_shadow(self) -> PresetShadowFormat:
|
|
290
|
+
"""|PresetShadowFormat| object providing access to the preset-shadow effect.
|
|
291
|
+
|
|
292
|
+
A |PresetShadowFormat| facade is always returned, even when no preset
|
|
293
|
+
shadow is explicitly defined — ``preset`` returns ``None`` in that
|
|
294
|
+
case. Assign ``preset_shadow.preset`` an :class:`MSO_PRESET_SHADOW`
|
|
295
|
+
member or a ``"shdw1".."shdw20"`` string to apply one.
|
|
296
|
+
"""
|
|
297
|
+
return PresetShadowFormat(self._element.spPr)
|
|
298
|
+
|
|
299
|
+
@lazyproperty
|
|
300
|
+
def soft_edges(self) -> SoftEdgeFormat:
|
|
301
|
+
"""|SoftEdgeFormat| object providing access to soft-edge effect for this shape.
|
|
302
|
+
|
|
303
|
+
A |SoftEdgeFormat| object is always returned even when no soft-edge is
|
|
304
|
+
explicitly defined. Reading ``soft_edges.radius`` returns None in that case.
|
|
305
|
+
"""
|
|
306
|
+
return SoftEdgeFormat(self._element.spPr)
|
|
307
|
+
|
|
308
|
+
@lazyproperty
|
|
309
|
+
def style(self) -> ShapeStyle:
|
|
310
|
+
"""Token-resolving design-system facade for this shape.
|
|
311
|
+
|
|
312
|
+
Returns a :class:`pptx2.design.style.ShapeStyle` whose setters
|
|
313
|
+
accept :class:`pptx2.design.tokens` values (palette colors,
|
|
314
|
+
shadow tokens, typography tokens) and fan them out into the
|
|
315
|
+
shape's underlying ``fill`` / ``line`` / ``shadow`` proxies.
|
|
316
|
+
|
|
317
|
+
Example::
|
|
318
|
+
|
|
319
|
+
shape.style.fill = tokens.palette["primary"]
|
|
320
|
+
shape.style.shadow = tokens.shadows["card"]
|
|
321
|
+
shape.style.font = tokens.typography["body"]
|
|
322
|
+
"""
|
|
323
|
+
from pptx2.design.style import ShapeStyle
|
|
324
|
+
|
|
325
|
+
return ShapeStyle(self)
|
|
326
|
+
|
|
327
|
+
@lazyproperty
|
|
328
|
+
def three_d(self) -> ThreeDFormat:
|
|
329
|
+
"""|ThreeDFormat| object providing access to 3-D formatting for this shape.
|
|
330
|
+
|
|
331
|
+
A |ThreeDFormat| object is always returned even when no 3-D properties are
|
|
332
|
+
explicitly defined. Reading e.g. ``three_d.bevel_top.preset`` returns None in that case.
|
|
333
|
+
|
|
334
|
+
Example::
|
|
335
|
+
|
|
336
|
+
from pptx2.enum.dml import BevelPreset, PresetMaterial
|
|
337
|
+
from pptx2.util import Pt
|
|
338
|
+
|
|
339
|
+
shape.three_d.bevel_top.preset = BevelPreset.CIRCLE
|
|
340
|
+
shape.three_d.bevel_top.width = Pt(4)
|
|
341
|
+
shape.three_d.extrusion_height = Pt(6)
|
|
342
|
+
shape.three_d.preset_material = PresetMaterial.MATTE
|
|
343
|
+
"""
|
|
344
|
+
return ThreeDFormat(self._element.spPr)
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def shape_id(self) -> int:
|
|
348
|
+
"""Read-only positive integer identifying this shape.
|
|
349
|
+
|
|
350
|
+
The id of a shape is unique among all shapes on a slide.
|
|
351
|
+
"""
|
|
352
|
+
return self._element.shape_id
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def lint_group(self) -> str | None:
|
|
356
|
+
"""Group tag consulted by the layout linter to suppress same-group collisions.
|
|
357
|
+
|
|
358
|
+
Shapes that share a non-empty ``lint_group`` may overlap without
|
|
359
|
+
producing a :class:`~pptx2.lint.ShapeCollision` warning. Shapes
|
|
360
|
+
with ``lint_group is None`` (the default) and shapes belonging to
|
|
361
|
+
different groups continue to warn on overlap.
|
|
362
|
+
|
|
363
|
+
The value is round-tripped through save/load via an ``<a:ext>``
|
|
364
|
+
element under the shape's ``cNvPr/extLst`` — the OOXML-sanctioned
|
|
365
|
+
extension mechanism. PowerPoint preserves the element verbatim and
|
|
366
|
+
does not flag it as unrecognised content.
|
|
367
|
+
|
|
368
|
+
Example::
|
|
369
|
+
|
|
370
|
+
card.lint_group = "kpi-card-1"
|
|
371
|
+
accent_bar.lint_group = "kpi-card-1"
|
|
372
|
+
# card and accent_bar may overlap without a lint warning.
|
|
373
|
+
|
|
374
|
+
Assigning ``None`` clears the tag.
|
|
375
|
+
"""
|
|
376
|
+
from pptx2.lint import _read_lint_group
|
|
377
|
+
|
|
378
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
379
|
+
return _read_lint_group(cNvPr)
|
|
380
|
+
|
|
381
|
+
@lint_group.setter
|
|
382
|
+
def lint_group(self, value: str | None) -> None:
|
|
383
|
+
from pptx2.lint import _clear_lint_group, _write_lint_group
|
|
384
|
+
|
|
385
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
386
|
+
if value is None:
|
|
387
|
+
_clear_lint_group(cNvPr)
|
|
388
|
+
return
|
|
389
|
+
if not isinstance(value, str):
|
|
390
|
+
raise ValueError("lint_group must be a string, an empty string, or None")
|
|
391
|
+
# Empty string is the explicit "no group" sentinel — overrides
|
|
392
|
+
# any implicit name-prefix group the linter would otherwise
|
|
393
|
+
# infer from a dotted shape name. Persist it verbatim rather
|
|
394
|
+
# than clearing so the override round-trips.
|
|
395
|
+
_write_lint_group(cNvPr, value)
|
|
396
|
+
|
|
397
|
+
def animate(
|
|
398
|
+
self,
|
|
399
|
+
*,
|
|
400
|
+
entry: str | None = None,
|
|
401
|
+
exit: str | None = None,
|
|
402
|
+
emphasis: str | None = None,
|
|
403
|
+
trigger: str = "on_click",
|
|
404
|
+
delay_ms: int = 0,
|
|
405
|
+
duration_ms: int = 500,
|
|
406
|
+
direction: str | None = None,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Add a constrained-subset animation to this shape.
|
|
409
|
+
|
|
410
|
+
A small façade over the full :mod:`pptx2.animation` API for
|
|
411
|
+
the five most common cases. Heavy animation use is rarely
|
|
412
|
+
appropriate in a professional deck, so the surface is
|
|
413
|
+
deliberately narrow:
|
|
414
|
+
|
|
415
|
+
Pass exactly one of ``entry``, ``exit``, or ``emphasis``.
|
|
416
|
+
Recognised presets:
|
|
417
|
+
|
|
418
|
+
* ``entry``: ``"fade"``, ``"appear"``, ``"fly_in"``,
|
|
419
|
+
``"float_in"``, ``"wipe"``, ``"zoom"``, ``"wheel"``,
|
|
420
|
+
``"random_bars"``.
|
|
421
|
+
* ``exit``: ``"fade"``, ``"disappear"``, ``"fly_out"``,
|
|
422
|
+
``"float_out"``, ``"wipe"``, ``"zoom"``, ``"wheel"``,
|
|
423
|
+
``"random_bars"``.
|
|
424
|
+
* ``emphasis``: ``"pulse"``, ``"spin"``, ``"teeter"``.
|
|
425
|
+
|
|
426
|
+
``trigger`` is one of ``"on_click"``, ``"with_previous"``,
|
|
427
|
+
``"after_previous"``. ``delay_ms`` and ``duration_ms`` are
|
|
428
|
+
OOXML milliseconds. ``direction`` is consumed by ``fly_in`` /
|
|
429
|
+
``fly_out`` / ``wipe`` (``"left"``, ``"right"``, ``"top"``,
|
|
430
|
+
``"bottom"``); ignored otherwise.
|
|
431
|
+
|
|
432
|
+
For animation types not covered here, drop down to
|
|
433
|
+
:class:`pptx2.animation.Entrance` /
|
|
434
|
+
:class:`~pptx2.animation.Exit` /
|
|
435
|
+
:class:`~pptx2.animation.Emphasis` directly.
|
|
436
|
+
"""
|
|
437
|
+
kinds_set = sum(1 for v in (entry, exit, emphasis) if v is not None)
|
|
438
|
+
if kinds_set != 1:
|
|
439
|
+
raise ValueError(
|
|
440
|
+
"Pass exactly one of entry=, exit=, emphasis=; "
|
|
441
|
+
f"got entry={entry!r}, exit={exit!r}, emphasis={emphasis!r}"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
from pptx2.animation import Emphasis, Entrance, Exit
|
|
445
|
+
from pptx2.enum.animation import PP_ANIM_TRIGGER
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
slide = self.part.slide # type: ignore[attr-defined]
|
|
449
|
+
except AttributeError as exc:
|
|
450
|
+
raise ValueError(
|
|
451
|
+
"shape.animate() requires the shape to be on a slide"
|
|
452
|
+
) from exc
|
|
453
|
+
|
|
454
|
+
trigger_map = {
|
|
455
|
+
"on_click": PP_ANIM_TRIGGER.ON_CLICK,
|
|
456
|
+
"with_previous": PP_ANIM_TRIGGER.WITH_PREVIOUS,
|
|
457
|
+
"after_previous": PP_ANIM_TRIGGER.AFTER_PREVIOUS,
|
|
458
|
+
}
|
|
459
|
+
if trigger not in trigger_map:
|
|
460
|
+
raise ValueError(
|
|
461
|
+
f"trigger must be one of {sorted(trigger_map)}; got {trigger!r}"
|
|
462
|
+
)
|
|
463
|
+
trig = trigger_map[trigger]
|
|
464
|
+
|
|
465
|
+
common_kwargs = {"trigger": trig, "delay": int(delay_ms)}
|
|
466
|
+
|
|
467
|
+
def _call(facade_method, preset, *, supports_direction=False):
|
|
468
|
+
kwargs = dict(common_kwargs)
|
|
469
|
+
if preset != "appear" and preset != "disappear":
|
|
470
|
+
kwargs["duration"] = int(duration_ms)
|
|
471
|
+
if supports_direction and direction is not None:
|
|
472
|
+
kwargs["direction"] = direction
|
|
473
|
+
facade_method(slide, self, **kwargs)
|
|
474
|
+
|
|
475
|
+
if entry is not None:
|
|
476
|
+
preset = entry
|
|
477
|
+
method = getattr(Entrance, preset, None)
|
|
478
|
+
if method is None:
|
|
479
|
+
raise ValueError(f"unknown entry preset: {preset!r}")
|
|
480
|
+
_call(method, preset, supports_direction=preset in ("fly_in", "wipe"))
|
|
481
|
+
elif exit is not None:
|
|
482
|
+
preset = exit
|
|
483
|
+
method = getattr(Exit, preset, None)
|
|
484
|
+
if method is None:
|
|
485
|
+
raise ValueError(f"unknown exit preset: {preset!r}")
|
|
486
|
+
_call(method, preset, supports_direction=preset in ("fly_out", "wipe"))
|
|
487
|
+
else: # emphasis
|
|
488
|
+
preset = emphasis # type: ignore[assignment]
|
|
489
|
+
method = getattr(Emphasis, preset, None)
|
|
490
|
+
if method is None:
|
|
491
|
+
raise ValueError(f"unknown emphasis preset: {preset!r}")
|
|
492
|
+
_call(method, preset)
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def lint_skip(self) -> frozenset[str]:
|
|
496
|
+
"""Lint check codes silenced on this shape.
|
|
497
|
+
|
|
498
|
+
Per-shape opt-out for the linter: any :class:`LintIssue` whose
|
|
499
|
+
``code`` is in this set is dropped from the report when ``slide.lint()``
|
|
500
|
+
is called. Cross-shape issues (e.g. ``ShapeCollision``,
|
|
501
|
+
``ZOrderAnomaly``) are only suppressed when *both* shapes opt out —
|
|
502
|
+
a one-sided opt-out keeps the warning, since the other shape may
|
|
503
|
+
still want it surfaced.
|
|
504
|
+
|
|
505
|
+
Example — silence intentional 8pt chrome::
|
|
506
|
+
|
|
507
|
+
footer_label.lint_skip = {"MinFontSize"}
|
|
508
|
+
rag_pill.lint_skip = {"MinFontSize"}
|
|
509
|
+
|
|
510
|
+
Stored alongside ``lint_group`` in the same ``cNvPr/extLst/ext``
|
|
511
|
+
block so it round-trips through save/load. Assign ``set()`` /
|
|
512
|
+
``frozenset()`` to clear.
|
|
513
|
+
"""
|
|
514
|
+
from pptx2.lint import _read_lint_skip
|
|
515
|
+
|
|
516
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
517
|
+
return _read_lint_skip(cNvPr)
|
|
518
|
+
|
|
519
|
+
@lint_skip.setter
|
|
520
|
+
def lint_skip(self, value) -> None:
|
|
521
|
+
from pptx2.lint import _write_lint_skip
|
|
522
|
+
|
|
523
|
+
if value is None:
|
|
524
|
+
value = frozenset()
|
|
525
|
+
if not isinstance(value, (set, frozenset, list, tuple)):
|
|
526
|
+
raise TypeError(
|
|
527
|
+
"lint_skip must be a set/frozenset/list/tuple of issue "
|
|
528
|
+
f"codes; got {type(value).__name__}"
|
|
529
|
+
)
|
|
530
|
+
# Validate each code: must be a non-empty trimmed string with no
|
|
531
|
+
# commas (the on-disk form is comma-joined, so a comma in a code
|
|
532
|
+
# would corrupt the round-trip). Trim whitespace so callers
|
|
533
|
+
# don't have to be precious about formatting.
|
|
534
|
+
codes: set[str] = set()
|
|
535
|
+
for raw in value:
|
|
536
|
+
if not isinstance(raw, str):
|
|
537
|
+
raise TypeError(
|
|
538
|
+
"lint_skip codes must be strings; got "
|
|
539
|
+
f"{type(raw).__name__}"
|
|
540
|
+
)
|
|
541
|
+
code = raw.strip()
|
|
542
|
+
if not code:
|
|
543
|
+
raise ValueError("lint_skip codes must be non-empty strings")
|
|
544
|
+
if "," in code:
|
|
545
|
+
raise ValueError(
|
|
546
|
+
f"lint_skip code {raw!r} contains ',', which is reserved "
|
|
547
|
+
"as the on-disk separator"
|
|
548
|
+
)
|
|
549
|
+
codes.add(code)
|
|
550
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
551
|
+
_write_lint_skip(cNvPr, frozenset(codes))
|
|
552
|
+
|
|
553
|
+
def allow_overlap_with(self, *shapes: "BaseShape") -> None:
|
|
554
|
+
"""Declare that overlapping each shape in *shapes* is intentional.
|
|
555
|
+
|
|
556
|
+
The narrow counterpart to :attr:`lint_group`. A ``lint_group`` is
|
|
557
|
+
n-ary and symmetric — every shape sharing the tag may overlap every
|
|
558
|
+
other one. An allowance licenses exactly one pair, which is what you
|
|
559
|
+
want for "this badge may sit on this card, but nothing else"::
|
|
560
|
+
|
|
561
|
+
badge.allow_overlap_with(card)
|
|
562
|
+
|
|
563
|
+
The declaration is one-sided to write but read symmetrically: it
|
|
564
|
+
takes only one of the pair to vouch for the overlap. Calling it on
|
|
565
|
+
either shape is equivalent, and calling it on both is harmless.
|
|
566
|
+
|
|
567
|
+
Allowances accumulate, so repeated calls add to the set rather than
|
|
568
|
+
replacing it. Clear them with :meth:`disallow_overlap_with` (one
|
|
569
|
+
pair) or by assigning ``shape.overlap_allowances = ()``.
|
|
570
|
+
|
|
571
|
+
Stored as shape ids in the same ``cNvPr/extLst/ext`` block as
|
|
572
|
+
``lint_group`` and ``lint_skip``, so it round-trips through
|
|
573
|
+
save/load.
|
|
574
|
+
|
|
575
|
+
Raises:
|
|
576
|
+
ValueError: if any argument is this same shape, or if either
|
|
577
|
+
shape has no usable shape id.
|
|
578
|
+
"""
|
|
579
|
+
from pptx2.lint import _read_lint_allow, _write_lint_allow
|
|
580
|
+
|
|
581
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
582
|
+
ids = set(_read_lint_allow(cNvPr))
|
|
583
|
+
for other in shapes:
|
|
584
|
+
other_id = self._require_shape_id(other)
|
|
585
|
+
if other_id == self.shape_id:
|
|
586
|
+
raise ValueError(
|
|
587
|
+
"a shape cannot be given an overlap allowance with itself"
|
|
588
|
+
)
|
|
589
|
+
ids.add(other_id)
|
|
590
|
+
_write_lint_allow(cNvPr, ids)
|
|
591
|
+
|
|
592
|
+
def disallow_overlap_with(self, *shapes: "BaseShape") -> None:
|
|
593
|
+
"""Revoke the overlap allowance for each shape in *shapes*.
|
|
594
|
+
|
|
595
|
+
The inverse of :meth:`allow_overlap_with`. Revoking an allowance
|
|
596
|
+
that was never granted is a no-op rather than an error, so callers
|
|
597
|
+
can clear defensively.
|
|
598
|
+
|
|
599
|
+
Note this only clears the allowance recorded *on this shape*. If the
|
|
600
|
+
pair was vouched for from the other side as well, the overlap stays
|
|
601
|
+
suppressed until that one is revoked too.
|
|
602
|
+
"""
|
|
603
|
+
from pptx2.lint import _read_lint_allow, _write_lint_allow
|
|
604
|
+
|
|
605
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
606
|
+
ids = set(_read_lint_allow(cNvPr))
|
|
607
|
+
for other in shapes:
|
|
608
|
+
ids.discard(self._require_shape_id(other))
|
|
609
|
+
_write_lint_allow(cNvPr, ids)
|
|
610
|
+
|
|
611
|
+
def _require_shape_id(self, shape: "BaseShape") -> int:
|
|
612
|
+
"""Return *shape*'s id, raising a useful error when it is unusable.
|
|
613
|
+
|
|
614
|
+
Two things make a shape unusable as an allowance target. It may have
|
|
615
|
+
no ``cNvPr`` and therefore no id to key on. Or it may live on a
|
|
616
|
+
different slide: shape ids are unique only *within* a slide, so an
|
|
617
|
+
id borrowed from another slide either collides with this shape's own
|
|
618
|
+
id — reading as a bogus self-reference — or silently matches an
|
|
619
|
+
unrelated shape here and suppresses a collision that was real.
|
|
620
|
+
"""
|
|
621
|
+
try:
|
|
622
|
+
shape_id = shape.shape_id
|
|
623
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
624
|
+
raise ValueError(
|
|
625
|
+
f"{type(shape).__name__} has no shape id, so it cannot take "
|
|
626
|
+
"part in an overlap allowance; tag both shapes with a shared "
|
|
627
|
+
"lint_group instead"
|
|
628
|
+
) from exc
|
|
629
|
+
if not self._on_same_slide_as(shape):
|
|
630
|
+
raise ValueError(
|
|
631
|
+
f"cannot record an overlap allowance with {shape.name!r}: it "
|
|
632
|
+
"is on a different slide. Shape ids are only unique within a "
|
|
633
|
+
"slide, so an allowance can only name a shape on this one."
|
|
634
|
+
)
|
|
635
|
+
return shape_id
|
|
636
|
+
|
|
637
|
+
def _on_same_slide_as(self, shape: "BaseShape") -> bool:
|
|
638
|
+
"""Return True unless *shape* is known to live on another slide.
|
|
639
|
+
|
|
640
|
+
Shapes on one slide share a part, including shapes nested in groups.
|
|
641
|
+
When either part cannot be resolved — a shape built outside a
|
|
642
|
+
package, as unit tests do — this answers ``True``: the check exists
|
|
643
|
+
to catch a real mistake, not to make detached shapes unusable.
|
|
644
|
+
"""
|
|
645
|
+
try:
|
|
646
|
+
return self.part is shape.part
|
|
647
|
+
except Exception:
|
|
648
|
+
return True
|
|
649
|
+
|
|
650
|
+
@property
|
|
651
|
+
def overlap_allowances(self) -> frozenset[int]:
|
|
652
|
+
"""Shape ids this shape has been cleared to overlap.
|
|
653
|
+
|
|
654
|
+
Read the set granted by :meth:`allow_overlap_with`. Note this
|
|
655
|
+
reflects only the allowances recorded on *this* shape — an overlap
|
|
656
|
+
may also be suppressed by an allowance held on the other shape, or
|
|
657
|
+
by a shared :attr:`lint_group`.
|
|
658
|
+
|
|
659
|
+
Assign an iterable of shape ids (or an empty one to clear). Most
|
|
660
|
+
callers want :meth:`allow_overlap_with` instead, which takes shapes
|
|
661
|
+
rather than raw ids and accumulates.
|
|
662
|
+
"""
|
|
663
|
+
from pptx2.lint import _read_lint_allow
|
|
664
|
+
|
|
665
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
666
|
+
return _read_lint_allow(cNvPr)
|
|
667
|
+
|
|
668
|
+
@overlap_allowances.setter
|
|
669
|
+
def overlap_allowances(self, value) -> None:
|
|
670
|
+
from pptx2.lint import _write_lint_allow
|
|
671
|
+
|
|
672
|
+
if value is None:
|
|
673
|
+
value = ()
|
|
674
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Iterable):
|
|
675
|
+
raise TypeError(
|
|
676
|
+
"overlap_allowances must be an iterable of shape ids; got "
|
|
677
|
+
f"{type(value).__name__}"
|
|
678
|
+
)
|
|
679
|
+
ids: set[int] = set()
|
|
680
|
+
for raw in value:
|
|
681
|
+
# bool is an int subclass, and True/False are never valid ids.
|
|
682
|
+
if isinstance(raw, bool) or not isinstance(raw, int):
|
|
683
|
+
raise TypeError(
|
|
684
|
+
"overlap_allowances entries must be integer shape ids; "
|
|
685
|
+
f"got {type(raw).__name__}"
|
|
686
|
+
)
|
|
687
|
+
ids.add(raw)
|
|
688
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
689
|
+
_write_lint_allow(cNvPr, ids)
|
|
690
|
+
|
|
691
|
+
@property
|
|
692
|
+
def layer(self) -> str | None:
|
|
693
|
+
"""Name of the visual layer this shape belongs to.
|
|
694
|
+
|
|
695
|
+
Layer hints are the third way to declare an intentional overlap, and
|
|
696
|
+
the only one that asserts a *direction*. A shape names its own layer
|
|
697
|
+
with :attr:`layer`; a shape that means to sit on top of that layer
|
|
698
|
+
names it in :attr:`layer_above`::
|
|
699
|
+
|
|
700
|
+
card.layer = "card"
|
|
701
|
+
badge.layer_above = "card"
|
|
702
|
+
|
|
703
|
+
Overlaps that agree with the declaration are treated as intentional
|
|
704
|
+
and stay out of the report. An overlap that *contradicts* it — the
|
|
705
|
+
shape claiming to be on top is drawn underneath — is reported as a
|
|
706
|
+
:class:`~pptx2.lint.LayerOrderViolation` error, since the
|
|
707
|
+
declaration records what the author meant and the drawing order is
|
|
708
|
+
what fails to deliver it.
|
|
709
|
+
|
|
710
|
+
Unlike :attr:`lint_group`, a layer name may be shared by any number
|
|
711
|
+
of unrelated shapes: it describes a stratum of the design, not one
|
|
712
|
+
grouped cluster. Assign ``None`` to clear.
|
|
713
|
+
"""
|
|
714
|
+
from pptx2.lint import _read_lint_layer
|
|
715
|
+
|
|
716
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
717
|
+
return _read_lint_layer(cNvPr)[0]
|
|
718
|
+
|
|
719
|
+
@layer.setter
|
|
720
|
+
def layer(self, value: str | None) -> None:
|
|
721
|
+
from pptx2.lint import _read_lint_layer, _write_lint_layer
|
|
722
|
+
|
|
723
|
+
value = self._validate_layer_name(value, "layer")
|
|
724
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
725
|
+
_, above = _read_lint_layer(cNvPr)
|
|
726
|
+
_write_lint_layer(cNvPr, name=value, above=above)
|
|
727
|
+
|
|
728
|
+
@property
|
|
729
|
+
def layer_above(self) -> str | None:
|
|
730
|
+
"""Name of the layer this shape declares it is drawn on top of.
|
|
731
|
+
|
|
732
|
+
See :attr:`layer` for the full picture. Assign ``None`` to clear.
|
|
733
|
+
"""
|
|
734
|
+
from pptx2.lint import _read_lint_layer
|
|
735
|
+
|
|
736
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
737
|
+
return _read_lint_layer(cNvPr)[1]
|
|
738
|
+
|
|
739
|
+
@layer_above.setter
|
|
740
|
+
def layer_above(self, value: str | None) -> None:
|
|
741
|
+
from pptx2.lint import _read_lint_layer, _write_lint_layer
|
|
742
|
+
|
|
743
|
+
value = self._validate_layer_name(value, "layer_above")
|
|
744
|
+
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
|
|
745
|
+
name, _ = _read_lint_layer(cNvPr)
|
|
746
|
+
_write_lint_layer(cNvPr, name=name, above=value)
|
|
747
|
+
|
|
748
|
+
@staticmethod
|
|
749
|
+
def _validate_layer_name(value: str | None, attr: str) -> str | None:
|
|
750
|
+
"""Normalise a layer name, or raise if it is unusable.
|
|
751
|
+
|
|
752
|
+
An all-whitespace string is treated as ``None`` (a clear) rather
|
|
753
|
+
than as a layer literally named ``" "``, which is never what the
|
|
754
|
+
caller meant.
|
|
755
|
+
"""
|
|
756
|
+
if value is None:
|
|
757
|
+
return None
|
|
758
|
+
if not isinstance(value, str):
|
|
759
|
+
raise TypeError(
|
|
760
|
+
f"{attr} must be a string or None; got {type(value).__name__}"
|
|
761
|
+
)
|
|
762
|
+
value = value.strip()
|
|
763
|
+
return value or None
|
|
764
|
+
|
|
765
|
+
def delete(self) -> None:
|
|
766
|
+
"""Remove this shape from its slide and clean up dependent state.
|
|
767
|
+
|
|
768
|
+
In addition to removing the shape's XML element, this purges the
|
|
769
|
+
references other parts of the slide still hold to it:
|
|
770
|
+
|
|
771
|
+
* animation entries in the timing tree that targeted this shape.
|
|
772
|
+
PowerPoint silently "repairs" decks with orphan timing
|
|
773
|
+
references on open, but a clean tree avoids the prompt.
|
|
774
|
+
* overlap allowances naming this shape's id. Ids are recycled --
|
|
775
|
+
the allocator hands out ``max(existing) + 1``, so deleting the
|
|
776
|
+
highest-id shape frees its id for the next shape added after a
|
|
777
|
+
save/reopen. A leftover allowance would then match that
|
|
778
|
+
unrelated newcomer and silently suppress a real
|
|
779
|
+
:class:`~pptx2.lint.ShapeCollision`.
|
|
780
|
+
|
|
781
|
+
Equivalent in spirit to::
|
|
782
|
+
|
|
783
|
+
shape._element.getparent().remove(shape._element)
|
|
784
|
+
|
|
785
|
+
but with the cleanup passes that the manual idiom misses.
|
|
786
|
+
"""
|
|
787
|
+
# Snapshot the slide reference and this shape's id *before*
|
|
788
|
+
# detaching the element, because once detached the parent walk
|
|
789
|
+
# would fail.
|
|
790
|
+
slide = None
|
|
791
|
+
try:
|
|
792
|
+
slide = self.part.slide # type: ignore[attr-defined]
|
|
793
|
+
except Exception:
|
|
794
|
+
slide = None
|
|
795
|
+
# Deleting a group takes its descendants with it, so every id
|
|
796
|
+
# about to disappear has to be collected, not just this one.
|
|
797
|
+
deleted_ids = self._descendant_shape_ids()
|
|
798
|
+
|
|
799
|
+
parent = self._element.getparent()
|
|
800
|
+
if parent is not None:
|
|
801
|
+
parent.remove(self._element)
|
|
802
|
+
|
|
803
|
+
if slide is not None:
|
|
804
|
+
try:
|
|
805
|
+
slide.animations.purge_orphans()
|
|
806
|
+
except Exception:
|
|
807
|
+
pass
|
|
808
|
+
if deleted_ids:
|
|
809
|
+
self._purge_overlap_allowances(slide, deleted_ids)
|
|
810
|
+
|
|
811
|
+
def _descendant_shape_ids(self) -> "frozenset[int]":
|
|
812
|
+
"""Return this shape's id plus every id nested beneath it.
|
|
813
|
+
|
|
814
|
+
Removing a group's element removes its members with it, so all of
|
|
815
|
+
their ids go stale at once. Best-effort: an id that cannot be read
|
|
816
|
+
is skipped rather than failing the delete.
|
|
817
|
+
"""
|
|
818
|
+
ids: set[int] = set()
|
|
819
|
+
|
|
820
|
+
def _collect(shape) -> None:
|
|
821
|
+
with contextlib.suppress(Exception):
|
|
822
|
+
ids.add(shape.shape_id)
|
|
823
|
+
nested = getattr(shape, "shapes", None)
|
|
824
|
+
if nested is None:
|
|
825
|
+
return
|
|
826
|
+
try:
|
|
827
|
+
members = list(nested)
|
|
828
|
+
except Exception:
|
|
829
|
+
return
|
|
830
|
+
for member in members:
|
|
831
|
+
_collect(member)
|
|
832
|
+
|
|
833
|
+
_collect(self)
|
|
834
|
+
return frozenset(ids)
|
|
835
|
+
|
|
836
|
+
@staticmethod
|
|
837
|
+
def _purge_overlap_allowances(slide, deleted_ids: "frozenset[int]") -> None:
|
|
838
|
+
"""Drop every id in *deleted_ids* from allowances on *slide*.
|
|
839
|
+
|
|
840
|
+
Best-effort and never fatal: deleting a shape must not start
|
|
841
|
+
raising because some sibling has unreadable lint metadata.
|
|
842
|
+
"""
|
|
843
|
+
from pptx2.lint import _read_lint_allow, _shape_cNvPr, _write_lint_allow
|
|
844
|
+
|
|
845
|
+
def _walk(shapes):
|
|
846
|
+
for shape in shapes:
|
|
847
|
+
yield shape
|
|
848
|
+
nested = getattr(shape, "shapes", None)
|
|
849
|
+
if nested is not None:
|
|
850
|
+
try:
|
|
851
|
+
yield from _walk(nested)
|
|
852
|
+
except Exception:
|
|
853
|
+
continue
|
|
854
|
+
|
|
855
|
+
try:
|
|
856
|
+
shapes = list(_walk(slide.shapes))
|
|
857
|
+
except Exception:
|
|
858
|
+
return
|
|
859
|
+
for shape in shapes:
|
|
860
|
+
try:
|
|
861
|
+
cNvPr = _shape_cNvPr(shape)
|
|
862
|
+
if cNvPr is None:
|
|
863
|
+
continue
|
|
864
|
+
allowances = _read_lint_allow(cNvPr)
|
|
865
|
+
if allowances & deleted_ids:
|
|
866
|
+
_write_lint_allow(cNvPr, allowances - deleted_ids)
|
|
867
|
+
except Exception:
|
|
868
|
+
continue
|
|
869
|
+
|
|
870
|
+
@property
|
|
871
|
+
def shape_type(self) -> MSO_SHAPE_TYPE:
|
|
872
|
+
"""A member of MSO_SHAPE_TYPE classifying this shape by type.
|
|
873
|
+
|
|
874
|
+
Like ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses.
|
|
875
|
+
"""
|
|
876
|
+
raise NotImplementedError(f"{type(self).__name__} does not implement `.shape_type`")
|
|
877
|
+
|
|
878
|
+
@property
|
|
879
|
+
def top(self) -> Length:
|
|
880
|
+
"""Distance from the top edge of the slide to the top edge of this shape.
|
|
881
|
+
|
|
882
|
+
Read/write. Expressed in English Metric Units (EMU)
|
|
883
|
+
"""
|
|
884
|
+
return self._element.y
|
|
885
|
+
|
|
886
|
+
@top.setter
|
|
887
|
+
def top(self, value: Length):
|
|
888
|
+
self._element.y = _coerce_emu(value)
|
|
889
|
+
|
|
890
|
+
@property
|
|
891
|
+
def width(self) -> Length:
|
|
892
|
+
"""Distance between left and right extents of this shape.
|
|
893
|
+
|
|
894
|
+
Read/write. Expressed in English Metric Units (EMU).
|
|
895
|
+
"""
|
|
896
|
+
return self._element.cx
|
|
897
|
+
|
|
898
|
+
@width.setter
|
|
899
|
+
def width(self, value: Length):
|
|
900
|
+
self._element.cx = _coerce_emu(value)
|
|
901
|
+
|
|
902
|
+
@property
|
|
903
|
+
def bbox(self):
|
|
904
|
+
"""Return the shape's geometry as an immutable :class:`BBox`.
|
|
905
|
+
|
|
906
|
+
``shape.bbox`` is a snapshot — mutating the shape afterwards
|
|
907
|
+
does not update the box. Use :meth:`BBox.apply_to` to push a
|
|
908
|
+
new box back onto the shape.
|
|
909
|
+
|
|
910
|
+
Example::
|
|
911
|
+
|
|
912
|
+
from pptx2 import BBox
|
|
913
|
+
|
|
914
|
+
inner = shape.bbox.inset(all=Inches(0.2))
|
|
915
|
+
slide.shapes.add_textbox(*inner)
|
|
916
|
+
"""
|
|
917
|
+
from pptx2.geometry import BBox
|
|
918
|
+
|
|
919
|
+
return BBox.from_shape(self)
|
|
920
|
+
|
|
921
|
+
def fill_hex(self, hex_color: "str | None") -> "BaseShape":
|
|
922
|
+
"""Set a solid fill from a hex string (``"#RRGGBB"`` or ``"RRGGBB"``).
|
|
923
|
+
|
|
924
|
+
Convenience for the three-line ``shape.fill.solid();
|
|
925
|
+
shape.fill.fore_color.rgb = RGBColor(...)`` dance. Returns
|
|
926
|
+
``self`` so calls can be chained.
|
|
927
|
+
|
|
928
|
+
Example::
|
|
929
|
+
|
|
930
|
+
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *box).fill_hex("#0B5CFF")
|
|
931
|
+
|
|
932
|
+
Pass ``None`` to clear the fill (the shape inherits from its
|
|
933
|
+
theme afterwards). Hex strings, ``RGBColor`` instances, and
|
|
934
|
+
``(r, g, b)`` tuples are all accepted.
|
|
935
|
+
"""
|
|
936
|
+
from pptx2._color import coerce_color
|
|
937
|
+
|
|
938
|
+
if hex_color is None:
|
|
939
|
+
# ``fill.background()`` produces a transparent (no-fill)
|
|
940
|
+
# solid; the closest thing to "clear" without ripping the
|
|
941
|
+
# element out wholesale.
|
|
942
|
+
try:
|
|
943
|
+
self.fill.background() # type: ignore[attr-defined]
|
|
944
|
+
except AttributeError as exc:
|
|
945
|
+
raise AttributeError(
|
|
946
|
+
f"{type(self).__name__} does not support fill"
|
|
947
|
+
) from exc
|
|
948
|
+
return self
|
|
949
|
+
try:
|
|
950
|
+
fill = self.fill # type: ignore[attr-defined]
|
|
951
|
+
except AttributeError as exc:
|
|
952
|
+
raise AttributeError(
|
|
953
|
+
f"{type(self).__name__} does not support fill"
|
|
954
|
+
) from exc
|
|
955
|
+
fill.solid()
|
|
956
|
+
fill.fore_color.rgb = coerce_color(hex_color)
|
|
957
|
+
return self
|
|
958
|
+
|
|
959
|
+
def line_hex(
|
|
960
|
+
self,
|
|
961
|
+
hex_color: "str | None",
|
|
962
|
+
*,
|
|
963
|
+
weight_pt: float | None = None,
|
|
964
|
+
) -> "BaseShape":
|
|
965
|
+
"""Set the line stroke from a hex string (``"#RRGGBB"``).
|
|
966
|
+
|
|
967
|
+
Optional ``weight_pt`` sets the stroke width in points. Returns
|
|
968
|
+
``self`` so calls can be chained.
|
|
969
|
+
|
|
970
|
+
Example::
|
|
971
|
+
|
|
972
|
+
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *box).line_hex(
|
|
973
|
+
"#0D0D0D", weight_pt=1.25,
|
|
974
|
+
)
|
|
975
|
+
"""
|
|
976
|
+
from pptx2._color import coerce_color
|
|
977
|
+
from pptx2.util import Pt
|
|
978
|
+
|
|
979
|
+
try:
|
|
980
|
+
line = self.line # type: ignore[attr-defined]
|
|
981
|
+
except AttributeError as exc:
|
|
982
|
+
raise AttributeError(
|
|
983
|
+
f"{type(self).__name__} does not support line"
|
|
984
|
+
) from exc
|
|
985
|
+
if hex_color is None:
|
|
986
|
+
line.fill.background()
|
|
987
|
+
else:
|
|
988
|
+
line.color.rgb = coerce_color(hex_color)
|
|
989
|
+
if weight_pt is not None:
|
|
990
|
+
line.width = Pt(float(weight_pt))
|
|
991
|
+
return self
|
|
992
|
+
|
|
993
|
+
def set_text_preserving_format(self, new_text: str) -> "BaseShape":
|
|
994
|
+
"""Replace all text in this shape with ``new_text``, keeping run formatting.
|
|
995
|
+
|
|
996
|
+
Captures the first run's character properties (``<a:rPr>``) and
|
|
997
|
+
the first paragraph's properties (``<a:pPr>``), rebuilds the
|
|
998
|
+
text body to hold ``new_text`` (one paragraph per ``\\n``), then
|
|
999
|
+
re-applies those properties to every new run and paragraph.
|
|
1000
|
+
|
|
1001
|
+
Font face / size / colour / bold / italic on that first run are
|
|
1002
|
+
preserved verbatim — useful when overwriting a templated
|
|
1003
|
+
placeholder (e.g. ``"<TITLE>"``) without losing the designer's
|
|
1004
|
+
font choices.
|
|
1005
|
+
|
|
1006
|
+
Example::
|
|
1007
|
+
|
|
1008
|
+
shape.set_text_preserving_format("Q4 revenue overview")
|
|
1009
|
+
|
|
1010
|
+
Raises :class:`ValueError` if the shape has no text frame.
|
|
1011
|
+
"""
|
|
1012
|
+
if not getattr(self, "has_text_frame", False):
|
|
1013
|
+
raise ValueError(
|
|
1014
|
+
f"shape {self.name!r} has no text frame; can't replace text"
|
|
1015
|
+
)
|
|
1016
|
+
tf = self.text_frame # type: ignore[attr-defined]
|
|
1017
|
+
|
|
1018
|
+
from copy import deepcopy
|
|
1019
|
+
|
|
1020
|
+
rPr_template = None
|
|
1021
|
+
pPr_template = None
|
|
1022
|
+
first_para = tf.paragraphs[0] if tf.paragraphs else None
|
|
1023
|
+
if first_para is not None:
|
|
1024
|
+
pPr = first_para._p.pPr # type: ignore[attr-defined]
|
|
1025
|
+
if pPr is not None:
|
|
1026
|
+
pPr_template = deepcopy(pPr)
|
|
1027
|
+
if first_para.runs:
|
|
1028
|
+
rPr = first_para.runs[0]._r.rPr # type: ignore[attr-defined]
|
|
1029
|
+
if rPr is not None:
|
|
1030
|
+
rPr_template = deepcopy(rPr)
|
|
1031
|
+
|
|
1032
|
+
# Rebuild the body using the high-level text setter; this gives
|
|
1033
|
+
# us one paragraph per "\n" with a single run per paragraph.
|
|
1034
|
+
tf.text = new_text if new_text else ""
|
|
1035
|
+
|
|
1036
|
+
for para in tf.paragraphs:
|
|
1037
|
+
p_elm = para._p # type: ignore[attr-defined]
|
|
1038
|
+
if pPr_template is not None:
|
|
1039
|
+
existing_pPr = p_elm.pPr
|
|
1040
|
+
if existing_pPr is not None:
|
|
1041
|
+
p_elm._remove_pPr()
|
|
1042
|
+
p_elm._insert_pPr(deepcopy(pPr_template))
|
|
1043
|
+
if rPr_template is not None:
|
|
1044
|
+
for run in para.runs:
|
|
1045
|
+
r_elm = run._r # type: ignore[attr-defined]
|
|
1046
|
+
if r_elm.rPr is not None:
|
|
1047
|
+
r_elm._remove_rPr()
|
|
1048
|
+
r_elm._insert_rPr(deepcopy(rPr_template))
|
|
1049
|
+
return self
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
class _PlaceholderFormat(ElementProxy):
|
|
1053
|
+
"""Provides properties specific to placeholders, such as the placeholder type.
|
|
1054
|
+
|
|
1055
|
+
Accessed via the :attr:`~.BaseShape.placeholder_format` property of a placeholder shape,
|
|
1056
|
+
"""
|
|
1057
|
+
|
|
1058
|
+
def __init__(self, element: CT_Placeholder):
|
|
1059
|
+
super().__init__(element)
|
|
1060
|
+
self._ph = element
|
|
1061
|
+
|
|
1062
|
+
@property
|
|
1063
|
+
def element(self) -> CT_Placeholder:
|
|
1064
|
+
"""The `p:ph` element proxied by this object."""
|
|
1065
|
+
return self._ph
|
|
1066
|
+
|
|
1067
|
+
@property
|
|
1068
|
+
def idx(self) -> int:
|
|
1069
|
+
"""Integer placeholder 'idx' attribute."""
|
|
1070
|
+
return self._ph.idx
|
|
1071
|
+
|
|
1072
|
+
@property
|
|
1073
|
+
def type(self) -> PP_PLACEHOLDER:
|
|
1074
|
+
"""Placeholder type.
|
|
1075
|
+
|
|
1076
|
+
A member of the :ref:`PpPlaceholderType` enumeration, e.g. PP_PLACEHOLDER.CHART
|
|
1077
|
+
"""
|
|
1078
|
+
return self._ph.type
|