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
|
"""Slide-related custom element classes, including those for masters."""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, cast
from pptx.oxml import parse_from_template, parse_xml
from pptx.oxml.dml.fill import CT_GradientFillProperties
from pptx.oxml.ns import nsdecls
from pptx.oxml.simpletypes import XsdString
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Choice,
OneAndOnlyOne,
OptionalAttribute,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
ZeroOrOneChoice,
)
if TYPE_CHECKING:
from pptx.oxml.shapes.groupshape import CT_GroupShape
class _BaseSlideElement(BaseOxmlElement):
"""Base class for the six slide types, providing common methods."""
cSld: CT_CommonSlideData
@property
def spTree(self) -> CT_GroupShape:
"""Return required `p:cSld/p:spTree` grandchild."""
return self.cSld.spTree
class CT_Background(BaseOxmlElement):
"""`p:bg` element."""
_insert_bgPr: Callable[[CT_BackgroundProperties], None]
# ---these two are actually a choice, not a sequence, but simpler for
# ---present purposes this way.
_tag_seq = ("p:bgPr", "p:bgRef")
bgPr: CT_BackgroundProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:bgPr", successors=()
)
bgRef = ZeroOrOne("p:bgRef", successors=())
del _tag_seq
def add_noFill_bgPr(self):
"""Return a new `p:bgPr` element with noFill properties."""
xml = "<p:bgPr %s>\n" " <a:noFill/>\n" " <a:effectLst/>\n" "</p:bgPr>" % nsdecls("a", "p")
bgPr = cast(CT_BackgroundProperties, parse_xml(xml))
self._insert_bgPr(bgPr)
return bgPr
class CT_BackgroundProperties(BaseOxmlElement):
"""`p:bgPr` element."""
_tag_seq = (
"a:noFill",
"a:solidFill",
"a:gradFill",
"a:blipFill",
"a:pattFill",
"a:grpFill",
"a:effectLst",
"a:effectDag",
"a:extLst",
)
eg_fillProperties = ZeroOrOneChoice(
(
Choice("a:noFill"),
Choice("a:solidFill"),
Choice("a:gradFill"),
Choice("a:blipFill"),
Choice("a:pattFill"),
Choice("a:grpFill"),
),
successors=_tag_seq[6:],
)
del _tag_seq
def _new_gradFill(self):
"""Override default to add default gradient subtree."""
return CT_GradientFillProperties.new_gradFill()
class CT_CommonSlideData(BaseOxmlElement):
"""`p:cSld` element."""
_remove_bg: Callable[[], None]
get_or_add_bg: Callable[[], CT_Background]
_tag_seq = ("p:bg", "p:spTree", "p:custDataLst", "p:controls", "p:extLst")
bg: CT_Background | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:bg", successors=_tag_seq[1:]
)
spTree: CT_GroupShape = OneAndOnlyOne("p:spTree") # pyright: ignore[reportAssignmentType]
del _tag_seq
name: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"name", XsdString, default=""
)
def get_or_add_bgPr(self) -> CT_BackgroundProperties:
"""Return `p:bg/p:bgPr` grandchild.
If no such grandchild is present, any existing `p:bg` child is first removed and a new
default `p:bg` with noFill settings is added.
"""
bg = self.bg
if bg is None or bg.bgPr is None:
bg = self._change_to_noFill_bg()
return cast(CT_BackgroundProperties, bg.bgPr)
def _change_to_noFill_bg(self) -> CT_Background:
"""Establish a `p:bg` child with no-fill settings.
Any existing `p:bg` child is first removed.
"""
self._remove_bg()
bg = self.get_or_add_bg()
bg.add_noFill_bgPr()
return bg
class CT_NotesMaster(_BaseSlideElement):
"""`p:notesMaster` element, root of a notes master part."""
_tag_seq = ("p:cSld", "p:clrMap", "p:hf", "p:notesStyle", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
del _tag_seq
@classmethod
def new_default(cls) -> CT_NotesMaster:
"""Return a new `p:notesMaster` element based on the built-in default template."""
return cast(CT_NotesMaster, parse_from_template("notesMaster"))
class CT_NotesSlide(_BaseSlideElement):
"""`p:notes` element, root of a notes slide part."""
_tag_seq = ("p:cSld", "p:clrMapOvr", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
del _tag_seq
@classmethod
def new(cls) -> CT_NotesSlide:
"""Return a new ``<p:notes>`` element based on the default template.
Note that the template does not include placeholders, which must be subsequently cloned
from the notes master.
"""
return cast(CT_NotesSlide, parse_from_template("notes"))
class CT_Slide(_BaseSlideElement):
"""`p:sld` element, root element of a slide part (XML document)."""
_tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
clrMapOvr = ZeroOrOne("p:clrMapOvr", successors=_tag_seq[2:])
timing = ZeroOrOne("p:timing", successors=_tag_seq[4:])
del _tag_seq
@classmethod
def new(cls) -> CT_Slide:
"""Return new `p:sld` element configured as base slide shape."""
return cast(CT_Slide, parse_xml(cls._sld_xml()))
@property
def bg(self):
"""Return `p:bg` grandchild or None if not present."""
return self.cSld.bg
def get_or_add_childTnLst(self):
"""Return parent element for a new `p:video` child element.
The `p:video` element causes play controls to appear under a video
shape (pic shape containing video). There can be more than one video
shape on a slide, which causes the precondition to vary. It needs to
handle the case when there is no `p:sld/p:timing` element and when
that element already exists. If the case isn't simple, it just nukes
what's there and adds a fresh one. This could theoretically remove
desired existing timing information, but there isn't any evidence
available to me one way or the other, so I've taken the simple
approach.
"""
childTnLst = self._childTnLst
if childTnLst is None:
childTnLst = self._add_childTnLst()
return childTnLst
def _add_childTnLst(self):
"""Add `./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst` descendant.
Any existing `p:timing` child element is ruthlessly removed and
replaced.
"""
self.remove(self.get_or_add_timing())
timing = parse_xml(self._childTnLst_timing_xml())
self._insert_timing(timing)
return timing.xpath("./p:tnLst/p:par/p:cTn/p:childTnLst")[0]
@property
def _childTnLst(self):
"""Return `./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst` descendant.
Return None if that element is not present.
"""
childTnLsts = self.xpath("./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst")
if not childTnLsts:
return None
return childTnLsts[0]
@staticmethod
def _childTnLst_timing_xml():
return (
"<p:timing %s>\n"
" <p:tnLst>\n"
" <p:par>\n"
' <p:cTn id="1" dur="indefinite" restart="never" nodeType="'
'tmRoot">\n'
" <p:childTnLst/>\n"
" </p:cTn>\n"
" </p:par>\n"
" </p:tnLst>\n"
"</p:timing>" % nsdecls("p")
)
@staticmethod
def _sld_xml():
return (
"<p:sld %s>\n"
" <p:cSld>\n"
" <p:spTree>\n"
" <p:nvGrpSpPr>\n"
' <p:cNvPr id="1" name=""/>\n'
" <p:cNvGrpSpPr/>\n"
" <p:nvPr/>\n"
" </p:nvGrpSpPr>\n"
" <p:grpSpPr/>\n"
" </p:spTree>\n"
" </p:cSld>\n"
" <p:clrMapOvr>\n"
" <a:masterClrMapping/>\n"
" </p:clrMapOvr>\n"
"</p:sld>" % nsdecls("a", "p", "r")
)
class CT_SlideLayout(_BaseSlideElement):
"""`p:sldLayout` element, root of a slide layout part."""
_tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:hf", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
del _tag_seq
class CT_SlideLayoutIdList(BaseOxmlElement):
"""`p:sldLayoutIdLst` element, child of `p:sldMaster`.
Contains references to the slide layouts that inherit from the slide master.
"""
sldLayoutId_lst: list[CT_SlideLayoutIdListEntry]
sldLayoutId = ZeroOrMore("p:sldLayoutId")
class CT_SlideLayoutIdListEntry(BaseOxmlElement):
"""`p:sldLayoutId` element, child of `p:sldLayoutIdLst`.
Contains a reference to a slide layout.
"""
rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
class CT_SlideMaster(_BaseSlideElement):
"""`p:sldMaster` element, root of a slide master part."""
get_or_add_sldLayoutIdLst: Callable[[], CT_SlideLayoutIdList]
_tag_seq = (
"p:cSld",
"p:clrMap",
"p:sldLayoutIdLst",
"p:transition",
"p:timing",
"p:hf",
"p:txStyles",
"p:extLst",
)
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
sldLayoutIdLst: CT_SlideLayoutIdList = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:sldLayoutIdLst", successors=_tag_seq[3:]
)
del _tag_seq
class CT_SlideTiming(BaseOxmlElement):
"""`p:timing` element, specifying animations and timed behaviors."""
_tag_seq = ("p:tnLst", "p:bldLst", "p:extLst")
tnLst = ZeroOrOne("p:tnLst", successors=_tag_seq[1:])
del _tag_seq
class CT_TimeNodeList(BaseOxmlElement):
"""`p:tnLst` or `p:childTnList` element."""
def add_video(self, shape_id):
"""Add a new `p:video` child element for movie having *shape_id*."""
video_xml = (
"<p:video %s>\n"
' <p:cMediaNode vol="80000">\n'
' <p:cTn id="%d" fill="hold" display="0">\n'
" <p:stCondLst>\n"
' <p:cond delay="indefinite"/>\n'
" </p:stCondLst>\n"
" </p:cTn>\n"
" <p:tgtEl>\n"
' <p:spTgt spid="%d"/>\n'
" </p:tgtEl>\n"
" </p:cMediaNode>\n"
"</p:video>\n" % (nsdecls("p"), self._next_cTn_id, shape_id)
)
video = parse_xml(video_xml)
self.append(video)
@property
def _next_cTn_id(self):
"""Return the next available unique ID (int) for p:cTn element."""
cTn_id_strs = self.xpath("/p:sld/p:timing//p:cTn/@id")
ids = [int(id_str) for id_str in cTn_id_strs]
return max(ids) + 1
class CT_TLMediaNodeVideo(BaseOxmlElement):
"""`p:video` element, specifying video media details."""
_tag_seq = ("p:cMediaNode",)
cMediaNode = OneAndOnlyOne("p:cMediaNode")
del _tag_seq
|