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
343
344
345
346
347
348
349
350
351
352
353
354
355
|
"""Autoshape-related objects such as Shape and Adjustment."""
from __future__ import annotations
from numbers import Number
from typing import TYPE_CHECKING, Iterable
from xml.sax import saxutils
from pptx.dml.fill import FillFormat
from pptx.dml.line import LineFormat
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_SHAPE_TYPE
from pptx.shapes.base import BaseShape
from pptx.spec import autoshape_types
from pptx.text.text import TextFrame
from pptx.util import lazyproperty
if TYPE_CHECKING:
from pptx.oxml.shapes.autoshape import CT_GeomGuide, CT_PresetGeometry2D, CT_Shape
from pptx.spec import AdjustmentValue
from pptx.types import ProvidesPart
class Adjustment:
"""An adjustment value for an autoshape.
An adjustment value corresponds to the position of an adjustment handle on an auto shape.
Adjustment handles are the small yellow diamond-shaped handles that appear on certain auto
shapes and allow the outline of the shape to be adjusted. For example, a rounded rectangle has
an adjustment handle that allows the radius of its corner rounding to be adjusted.
Values are |float| and generally range from 0.0 to 1.0, although the value can be negative or
greater than 1.0 in certain circumstances.
"""
def __init__(self, name: str, def_val: int, actual: int | None = None):
super(Adjustment, self).__init__()
self.name = name
self.def_val = def_val
self.actual = actual
@property
def effective_value(self) -> float:
"""Read/write |float| representing normalized adjustment value for this adjustment.
Actual values are a large-ish integer expressed in shape coordinates, nominally between 0
and 100,000. The effective value is normalized to a corresponding value nominally between
0.0 and 1.0. Intuitively this represents the proportion of the width or height of the shape
at which the adjustment value is located from its starting point. For simple shapes such as
a rounded rectangle, this intuitive correspondence holds. For more complicated shapes and
at more extreme shape proportions (e.g. width is much greater than height), the value can
become negative or greater than 1.0.
"""
raw_value = self.actual if self.actual is not None else self.def_val
return self._normalize(raw_value)
@effective_value.setter
def effective_value(self, value: float):
if not isinstance(value, Number):
raise ValueError(f"adjustment value must be numeric, got {repr(value)}")
self.actual = self._denormalize(value)
@staticmethod
def _denormalize(value: float) -> int:
"""Return integer corresponding to normalized `raw_value` on unit basis of 100,000.
See Adjustment.normalize for additional details.
"""
return int(value * 100000.0)
@staticmethod
def _normalize(raw_value: int) -> float:
"""Return normalized value for `raw_value`.
A normalized value is a |float| between 0.0 and 1.0 for nominal raw values between 0 and
100,000. Raw values less than 0 and greater than 100,000 are valid and return values
calculated on the same unit basis of 100,000.
"""
return raw_value / 100000.0
@property
def val(self) -> int:
"""Denormalized effective value.
Expressed in shape coordinates, this is suitable for using in the XML.
"""
return self.actual if self.actual is not None else self.def_val
class AdjustmentCollection:
"""Sequence of |Adjustment| instances for an auto shape.
Each represents an available adjustment for a shape of its type. Supports `len()` and indexed
access, e.g. `shape.adjustments[1] = 0.15`.
"""
def __init__(self, prstGeom: CT_PresetGeometry2D):
super(AdjustmentCollection, self).__init__()
self._adjustments_ = self._initialized_adjustments(prstGeom)
self._prstGeom = prstGeom
def __getitem__(self, idx: int) -> float:
"""Provides indexed access, (e.g. 'adjustments[9]')."""
return self._adjustments_[idx].effective_value
def __setitem__(self, idx: int, value: float):
"""Provides item assignment via an indexed expression, e.g. `adjustments[9] = 999.9`.
Causes all adjustment values in collection to be written to the XML.
"""
self._adjustments_[idx].effective_value = value
self._rewrite_guides()
def _initialized_adjustments(self, prstGeom: CT_PresetGeometry2D | None) -> list[Adjustment]:
"""Return an initialized list of adjustment values based on the contents of `prstGeom`."""
if prstGeom is None:
return []
davs = AutoShapeType.default_adjustment_values(prstGeom.prst)
adjustments = [Adjustment(name, def_val) for name, def_val in davs]
self._update_adjustments_with_actuals(adjustments, prstGeom.gd_lst)
return adjustments
def _rewrite_guides(self):
"""Write `a:gd` elements to the XML, one for each adjustment value.
Any existing guide elements are overwritten.
"""
guides = [(adj.name, adj.val) for adj in self._adjustments_]
self._prstGeom.rewrite_guides(guides)
@staticmethod
def _update_adjustments_with_actuals(
adjustments: Iterable[Adjustment], guides: Iterable[CT_GeomGuide]
):
"""Update |Adjustment| instances in `adjustments` with actual values held in `guides`.
`guides` is a list of `a:gd` elements. Guides with a name that does not match an adjustment
object are skipped.
"""
adjustments_by_name = dict((adj.name, adj) for adj in adjustments)
for gd in guides:
name = gd.name
actual = int(gd.fmla[4:])
try:
adjustment = adjustments_by_name[name]
except KeyError:
continue
adjustment.actual = actual
return
@property
def _adjustments(self) -> tuple[Adjustment, ...]:
"""Sequence of |Adjustment| objects contained in collection."""
return tuple(self._adjustments_)
def __len__(self):
"""Implement built-in function len()"""
return len(self._adjustments_)
class AutoShapeType:
"""Provides access to metadata for an auto-shape of type identified by `autoshape_type_id`.
Instances are cached, so no more than one instance for a particular auto shape type is in
memory.
Instances provide the following attributes:
.. attribute:: autoshape_type_id
Integer uniquely identifying this auto shape type. Corresponds to a
value in `pptx.constants.MSO` like `MSO_SHAPE.ROUNDED_RECTANGLE`.
.. attribute:: basename
Base part of shape name for auto shapes of this type, e.g. `Rounded
Rectangle` becomes `Rounded Rectangle 99` when the distinguishing
integer is added to the shape name.
.. attribute:: prst
String identifier for this auto shape type used in the `a:prstGeom`
element.
"""
_instances: dict[MSO_AUTO_SHAPE_TYPE, AutoShapeType] = {}
def __new__(cls, autoshape_type_id: MSO_AUTO_SHAPE_TYPE) -> AutoShapeType:
"""Only create new instance on first call for content_type.
After that, use cached instance.
"""
# -- if there's not a matching instance in the cache, create one --
if autoshape_type_id not in cls._instances:
inst = super(AutoShapeType, cls).__new__(cls)
cls._instances[autoshape_type_id] = inst
# -- return the instance; note that __init__() gets called either way --
return cls._instances[autoshape_type_id]
def __init__(self, autoshape_type_id: MSO_AUTO_SHAPE_TYPE):
"""Initialize attributes from constant values in `pptx.spec`."""
# -- skip loading if this instance is from the cache --
if hasattr(self, "_loaded"):
return
# -- raise on bad autoshape_type_id --
if autoshape_type_id not in autoshape_types:
raise KeyError(
"no autoshape type with id '%s' in pptx.spec.autoshape_types" % autoshape_type_id
)
# -- otherwise initialize new instance --
autoshape_type = autoshape_types[autoshape_type_id]
self._autoshape_type_id = autoshape_type_id
self._basename = autoshape_type["basename"]
self._loaded = True
@property
def autoshape_type_id(self) -> MSO_AUTO_SHAPE_TYPE:
"""MSO_AUTO_SHAPE_TYPE enumeration member identifying this auto shape type."""
return self._autoshape_type_id
@property
def basename(self) -> str:
"""Base of shape name for this auto shape type.
A shape name is like "Rounded Rectangle 7" and appears as an XML attribute for example at
`p:sp/p:nvSpPr/p:cNvPr{name}`. This basename value is the name less the distinguishing
integer. This value is escaped because at least one autoshape-type name includes double
quotes ('"No" Symbol').
"""
return saxutils.escape(self._basename, {'"': """})
@classmethod
def default_adjustment_values(cls, prst: MSO_AUTO_SHAPE_TYPE) -> tuple[AdjustmentValue, ...]:
"""Sequence of (name, value) pair adjustment value defaults for `prst` autoshape-type."""
return autoshape_types[prst]["avLst"]
@classmethod
def id_from_prst(cls, prst: str) -> MSO_AUTO_SHAPE_TYPE:
"""Select auto shape type with matching `prst`.
e.g. `MSO_SHAPE.RECTANGLE` corresponding to preset geometry keyword `"rect"`.
"""
return MSO_AUTO_SHAPE_TYPE.from_xml(prst)
@property
def prst(self):
"""
Preset geometry identifier string for this auto shape. Used in the
`prst` attribute of `a:prstGeom` element to specify the geometry
to be used in rendering the shape, for example `'roundRect'`.
"""
return MSO_AUTO_SHAPE_TYPE.to_xml(self._autoshape_type_id)
class Shape(BaseShape):
"""A shape that can appear on a slide.
Corresponds to the `p:sp` element that can appear in any of the slide-type parts
(slide, slideLayout, slideMaster, notesPage, notesMaster, handoutMaster).
"""
def __init__(self, sp: CT_Shape, parent: ProvidesPart):
super(Shape, self).__init__(sp, parent)
self._sp = sp
@lazyproperty
def adjustments(self) -> AdjustmentCollection:
"""Read-only reference to |AdjustmentCollection| instance for this shape."""
return AdjustmentCollection(self._sp.prstGeom)
@property
def auto_shape_type(self):
"""Enumeration value identifying the type of this auto shape.
Like `MSO_SHAPE.ROUNDED_RECTANGLE`. Raises |ValueError| if this shape is not an auto shape.
"""
if not self._sp.is_autoshape:
raise ValueError("shape is not an auto shape")
return self._sp.prst
@lazyproperty
def fill(self):
"""|FillFormat| instance for this shape.
Provides access to fill properties such as fill color.
"""
return FillFormat.from_fill_parent(self._sp.spPr)
def get_or_add_ln(self):
"""Return the `a:ln` element containing the line format properties XML for this shape."""
return self._sp.get_or_add_ln()
@property
def has_text_frame(self) -> bool:
"""|True| if this shape can contain text. Always |True| for an AutoShape."""
return True
@lazyproperty
def line(self):
"""|LineFormat| instance for this shape.
Provides access to line properties such as line color.
"""
return LineFormat(self)
@property
def ln(self):
"""The `a:ln` element containing the line format properties such as line color and width.
|None| if no `a:ln` element is present.
"""
return self._sp.ln
@property
def shape_type(self) -> MSO_SHAPE_TYPE:
"""Unique integer identifying the type of this shape, like `MSO_SHAPE_TYPE.TEXT_BOX`."""
if self.is_placeholder:
return MSO_SHAPE_TYPE.PLACEHOLDER
if self._sp.has_custom_geometry:
return MSO_SHAPE_TYPE.FREEFORM
if self._sp.is_autoshape:
return MSO_SHAPE_TYPE.AUTO_SHAPE
if self._sp.is_textbox:
return MSO_SHAPE_TYPE.TEXT_BOX
raise NotImplementedError("Shape instance of unrecognized shape type")
@property
def text(self) -> str:
"""Read/write. Text in shape as a single string.
The returned string will contain a newline character (`"\\n"`) separating each paragraph
and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the
shape's text.
Assignment to `text` replaces any text previously contained in the shape, along with any
paragraph or font formatting applied to it. A newline character (`"\\n"`) in the assigned
text causes a new paragraph to be started. A vertical-tab (`"\\v"`) character in the
assigned text causes a line-break (soft carriage-return) to be inserted. (The vertical-tab
character appears in clipboard text copied from PowerPoint as its str encoding of
line-breaks.)
"""
return self.text_frame.text
@text.setter
def text(self, text: str):
self.text_frame.text = text
@property
def text_frame(self):
"""|TextFrame| instance for this shape.
Contains the text of the shape and provides access to text formatting properties.
"""
txBody = self._sp.get_or_add_txBody()
return TextFrame(txBody, self)
|