forked from scanny/python-pptx
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbase.py
More file actions
342 lines (267 loc) · 12.3 KB
/
Copy pathbase.py
File metadata and controls
342 lines (267 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
"""Base shape-related objects such as BaseShape."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from lxml.etree import _Element # pyright: ignore[reportPrivateUsage]
from pptx.action import ActionSetting
from pptx.dml.effect import ShadowFormat
from pptx.shared import ElementProxy
from pptx.util import lazyproperty
# ---bound to the lxml base method so `find_by_xpath(..., namespaces=ns)` can
# ---honor the caller's prefix map without going through the project's
# ---`BaseOxmlElement.xpath` override (which auto-applies the project nsmap
# ---and rejects `namespaces=` kwarg).
_LXML_XPATH = _Element.xpath
if TYPE_CHECKING:
from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
from pptx.oxml.shapes import ShapeElement
from pptx.oxml.shapes.shared import CT_Placeholder
from pptx.parts.slide import BaseSlidePart
from pptx.types import ProvidesPart
from pptx.util import Length
class BaseShape(object):
"""Base class for shape objects.
Subclasses include |Shape|, |Picture|, and |GraphicFrame|.
"""
def __init__(self, shape_elm: ShapeElement, parent: ProvidesPart):
super().__init__()
self._element = shape_elm
self._parent = parent
def __eq__(self, other: object) -> bool:
"""|True| if this shape object proxies the same element as *other*.
Equality for proxy objects is defined as referring to the same XML element, whether or not
they are the same proxy object instance.
"""
if not isinstance(other, BaseShape):
return False
return self._element is other._element
def __ne__(self, other: object) -> bool:
if not isinstance(other, BaseShape):
return True
return self._element is not other._element
@lazyproperty
def click_action(self) -> ActionSetting:
"""|ActionSetting| instance providing access to click behaviors.
Click behaviors are hyperlink-like behaviors including jumping to a hyperlink (web page)
or to another slide in the presentation. The click action is that defined on the overall
shape, not a run of text within the shape. An |ActionSetting| object is always returned,
even when no click behavior is defined on the shape.
"""
cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
return ActionSetting(cNvPr, self)
@property
def element(self) -> ShapeElement:
"""`lxml` element for this shape, e.g. a CT_Shape instance.
Note that manipulating this element improperly can produce an invalid presentation file.
Make sure you know what you're doing if you use this to change the underlying XML.
"""
return self._element
def find_by_xpath(self, xpath: str, namespaces: "dict[str, str] | None" = None) -> list:
"""Power-user XPath escape hatch over this shape's element subtree.
Returns whatever ``lxml.etree._Element.xpath`` returns — typically a
list of matching elements, or an empty list when the expression
matches nothing. When ``namespaces`` is |None| (default), the
project's standard namespace map is used so common prefixes
(``a:``, ``p:``, ``r:``, ``xsi:``, ``adec:``, ``p14:``, etc.) work
without explicit declaration. Pass a custom dict to override.
Example::
for t_elm in shape.find_by_xpath(".//a:t"):
print(t_elm.text)
"""
if namespaces is None:
# ---project's BaseOxmlElement.xpath auto-applies the standard nsmap---
return self._element.xpath(xpath)
# ---custom nsmap: bypass the project wrapper (see _LXML_XPATH note above)---
return _LXML_XPATH(self._element, xpath, namespaces=namespaces)
@property
def has_chart(self) -> bool:
"""|True| if this shape is a graphic frame containing a chart object.
|False| otherwise. When |True|, the chart object can be accessed using the ``.chart``
property.
"""
# This implementation is unconditionally False, the True version is
# on GraphicFrame subclass.
return False
@property
def has_table(self) -> bool:
"""|True| if this shape is a graphic frame containing a table object.
|False| otherwise. When |True|, the table object can be accessed using the ``.table``
property.
"""
# This implementation is unconditionally False, the True version is
# on GraphicFrame subclass.
return False
@property
def has_text_frame(self) -> bool:
"""|True| if this shape can contain text."""
# overridden on Shape to return True. Only <p:sp> has text frame
return False
@property
def height(self) -> Length:
"""Read/write. Integer distance between top and bottom extents of shape in EMUs."""
return self._element.cy
@height.setter
def height(self, value: Length):
self._element.cy = value
@property
def is_placeholder(self) -> bool:
"""True if this shape is a placeholder.
A shape is a placeholder if it has a <p:ph> element.
"""
return self._element.has_ph_elm
@property
def left(self) -> Length:
"""Integer distance of the left edge of this shape from the left edge of the slide.
Read/write. Expressed in English Metric Units (EMU)
"""
return self._element.x
@left.setter
def left(self, value: Length):
self._element.x = value
@property
def name(self) -> str:
"""Name of this shape, e.g. 'Picture 7'."""
return self._element.shape_name
@name.setter
def name(self, value: str):
self._element._nvXxPr.cNvPr.name = value # pyright: ignore[reportPrivateUsage]
@property
def alt_text(self) -> str | None:
"""Alternative text describing this shape, used by screen readers and accessibility tools.
Read/write. Returns the value of the `descr` attribute on the shape's
`<p:cNvPr>` element. None if the attribute is not present (the shape has no
alt text). Assigning None removes the attribute. Assigning an empty string
is a meaningful, distinct value — it preserves the attribute as `descr=""`,
useful for callers who want to round-trip an explicit "no description"
marker.
See Microsoft Accessibility guidance: prefer `alt_text` for the description
and `alt_title` for a short heading, when both are needed.
"""
return self._element._nvXxPr.cNvPr.descr # pyright: ignore[reportPrivateUsage]
@alt_text.setter
def alt_text(self, value: str | None):
self._element._nvXxPr.cNvPr.descr = value # pyright: ignore[reportPrivateUsage]
@property
def alt_title(self) -> str | None:
"""Short title (heading) for this shape's alternative text, used for accessibility.
Read/write. Returns the value of the `title` attribute on the shape's
`<p:cNvPr>` element. None if the attribute is not present. Assigning None
removes the attribute. Microsoft accessibility guidance recommends a brief
title plus a longer `alt_text` description, mirroring the two-field UX in
PowerPoint's "Alt Text" pane.
"""
return self._element._nvXxPr.cNvPr.title # pyright: ignore[reportPrivateUsage]
@alt_title.setter
def alt_title(self, value: str | None):
self._element._nvXxPr.cNvPr.title = value # pyright: ignore[reportPrivateUsage]
@property
def is_decorative(self) -> bool:
"""True if this shape is marked as decorative (Office 2019+ accessibility flag).
Read/write boolean. Decorative shapes are skipped by screen readers; they
carry no semantic meaning beyond visual decoration (background grids,
ornaments, dividers). Backed by an `<adec:decorative val="1"/>` extension
inside `<p:cNvPr>/<a:extLst>`. Setting to False removes the extension; the
attribute defaults to False on shapes that have never been touched.
"""
return self._element._nvXxPr.cNvPr.decorative # pyright: ignore[reportPrivateUsage]
@is_decorative.setter
def is_decorative(self, value: bool):
self._element._nvXxPr.cNvPr.decorative = bool(value) # pyright: ignore[reportPrivateUsage]
@property
def is_hidden_from_accessibility(self) -> bool:
"""Convenience alias for :attr:`is_decorative`.
Read/write. Decorative shapes (the official OOXML term — `<adec:decorative
val="1"/>`) are exactly those that are hidden from accessibility tools
such as screen readers. Some accessibility documentation (and a number of
third-party authoring tools) use the wording "hidden from accessibility"
for the same flag; this property exists so the API reads naturally for
either audience.
"""
return self.is_decorative
@is_hidden_from_accessibility.setter
def is_hidden_from_accessibility(self, value: bool):
self.is_decorative = bool(value)
@property
def part(self) -> BaseSlidePart:
"""The package part containing this shape.
A |BaseSlidePart| subclass in this case. Access to a slide part should only be required if
you are extending the behavior of |pp| API objects.
"""
return cast("BaseSlidePart", self._parent.part)
@property
def placeholder_format(self) -> _PlaceholderFormat:
"""Provides access to placeholder-specific properties such as placeholder type.
Raises |ValueError| on access if the shape is not a placeholder.
"""
ph = self._element.ph
if ph is None:
raise ValueError("shape is not a placeholder")
return _PlaceholderFormat(ph)
@property
def rotation(self) -> float:
"""Degrees of clockwise rotation.
Read/write float. Negative values can be assigned to indicate counter-clockwise rotation,
e.g. assigning -45.0 will change setting to 315.0.
"""
return self._element.rot
@rotation.setter
def rotation(self, value: float):
self._element.rot = value
@lazyproperty
def shadow(self) -> ShadowFormat:
"""|ShadowFormat| object providing access to shadow for this shape.
A |ShadowFormat| object is always returned, even when no shadow is
explicitly defined on this shape (i.e. it inherits its shadow
behavior).
"""
return ShadowFormat(self._element.spPr)
@property
def shape_id(self) -> int:
"""Read-only positive integer identifying this shape.
The id of a shape is unique among all shapes on a slide.
"""
return self._element.shape_id
@property
def shape_type(self) -> MSO_SHAPE_TYPE:
"""A member of MSO_SHAPE_TYPE classifying this shape by type.
Like ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses.
"""
raise NotImplementedError(f"{type(self).__name__} does not implement `.shape_type`")
@property
def top(self) -> Length:
"""Distance from the top edge of the slide to the top edge of this shape.
Read/write. Expressed in English Metric Units (EMU)
"""
return self._element.y
@top.setter
def top(self, value: Length):
self._element.y = value
@property
def width(self) -> Length:
"""Distance between left and right extents of this shape.
Read/write. Expressed in English Metric Units (EMU).
"""
return self._element.cx
@width.setter
def width(self, value: Length):
self._element.cx = value
class _PlaceholderFormat(ElementProxy):
"""Provides properties specific to placeholders, such as the placeholder type.
Accessed via the :attr:`~.BaseShape.placeholder_format` property of a placeholder shape,
"""
def __init__(self, element: CT_Placeholder):
super().__init__(element)
self._ph = element
@property
def element(self) -> CT_Placeholder:
"""The `p:ph` element proxied by this object."""
return self._ph
@property
def idx(self) -> int:
"""Integer placeholder 'idx' attribute."""
return self._ph.idx
@property
def type(self) -> PP_PLACEHOLDER:
"""Placeholder type.
A member of the :ref:`PpPlaceholderType` enumeration, e.g. PP_PLACEHOLDER.CHART
"""
return self._ph.type