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.
Files changed (175) hide show
  1. pptx2/__init__.py +152 -0
  2. pptx2/_color.py +75 -0
  3. pptx2/_slide_importer.py +597 -0
  4. pptx2/_svg.py +155 -0
  5. pptx2/_template_applier.py +292 -0
  6. pptx2/_textstyle.py +187 -0
  7. pptx2/accessibility.py +365 -0
  8. pptx2/action.py +270 -0
  9. pptx2/animation.py +2237 -0
  10. pptx2/api.py +49 -0
  11. pptx2/audit.py +258 -0
  12. pptx2/chart/__init__.py +0 -0
  13. pptx2/chart/analytics.py +381 -0
  14. pptx2/chart/axis.py +543 -0
  15. pptx2/chart/category.py +200 -0
  16. pptx2/chart/chart.py +670 -0
  17. pptx2/chart/data.py +864 -0
  18. pptx2/chart/datalabel.py +406 -0
  19. pptx2/chart/legend.py +86 -0
  20. pptx2/chart/marker.py +70 -0
  21. pptx2/chart/palettes.py +129 -0
  22. pptx2/chart/plot.py +462 -0
  23. pptx2/chart/point.py +101 -0
  24. pptx2/chart/quick_layouts.py +325 -0
  25. pptx2/chart/series.py +334 -0
  26. pptx2/chart/xlsx.py +272 -0
  27. pptx2/chart/xmlwriter.py +1845 -0
  28. pptx2/compose/__init__.py +28 -0
  29. pptx2/compose/from_spec.py +1094 -0
  30. pptx2/design/__init__.py +8 -0
  31. pptx2/design/components.py +607 -0
  32. pptx2/design/figures.py +389 -0
  33. pptx2/design/layout.py +370 -0
  34. pptx2/design/recipes.py +1967 -0
  35. pptx2/design/style.py +209 -0
  36. pptx2/design/tokens.py +915 -0
  37. pptx2/diagrams.py +754 -0
  38. pptx2/dml/__init__.py +0 -0
  39. pptx2/dml/chtfmt.py +40 -0
  40. pptx2/dml/color.py +496 -0
  41. pptx2/dml/effect.py +909 -0
  42. pptx2/dml/fill.py +691 -0
  43. pptx2/dml/line.py +287 -0
  44. pptx2/dml/picture.py +212 -0
  45. pptx2/dml/three_d.py +381 -0
  46. pptx2/enum/__init__.py +0 -0
  47. pptx2/enum/action.py +71 -0
  48. pptx2/enum/animation.py +31 -0
  49. pptx2/enum/base.py +218 -0
  50. pptx2/enum/chart.py +574 -0
  51. pptx2/enum/dml.py +740 -0
  52. pptx2/enum/lang.py +685 -0
  53. pptx2/enum/presentation.py +133 -0
  54. pptx2/enum/shapes.py +1029 -0
  55. pptx2/enum/text.py +230 -0
  56. pptx2/exc.py +42 -0
  57. pptx2/formats.py +139 -0
  58. pptx2/geometry.py +420 -0
  59. pptx2/inherit.py +109 -0
  60. pptx2/lint.py +2256 -0
  61. pptx2/math.py +177 -0
  62. pptx2/media.py +197 -0
  63. pptx2/opc/__init__.py +0 -0
  64. pptx2/opc/constants.py +332 -0
  65. pptx2/opc/oxml.py +188 -0
  66. pptx2/opc/package.py +762 -0
  67. pptx2/opc/packuri.py +109 -0
  68. pptx2/opc/serialized.py +296 -0
  69. pptx2/opc/shared.py +20 -0
  70. pptx2/opc/spec.py +45 -0
  71. pptx2/oxml/__init__.py +555 -0
  72. pptx2/oxml/action.py +53 -0
  73. pptx2/oxml/chart/__init__.py +0 -0
  74. pptx2/oxml/chart/axis.py +337 -0
  75. pptx2/oxml/chart/chart.py +481 -0
  76. pptx2/oxml/chart/datalabel.py +253 -0
  77. pptx2/oxml/chart/legend.py +72 -0
  78. pptx2/oxml/chart/marker.py +61 -0
  79. pptx2/oxml/chart/plot.py +365 -0
  80. pptx2/oxml/chart/series.py +425 -0
  81. pptx2/oxml/chart/shared.py +220 -0
  82. pptx2/oxml/coreprops.py +288 -0
  83. pptx2/oxml/dml/__init__.py +0 -0
  84. pptx2/oxml/dml/color.py +135 -0
  85. pptx2/oxml/dml/effect.py +213 -0
  86. pptx2/oxml/dml/fill.py +316 -0
  87. pptx2/oxml/dml/line.py +12 -0
  88. pptx2/oxml/dml/three_d.py +110 -0
  89. pptx2/oxml/ns.py +135 -0
  90. pptx2/oxml/presentation.py +313 -0
  91. pptx2/oxml/shapes/__init__.py +19 -0
  92. pptx2/oxml/shapes/autoshape.py +467 -0
  93. pptx2/oxml/shapes/connector.py +107 -0
  94. pptx2/oxml/shapes/graphfrm.py +347 -0
  95. pptx2/oxml/shapes/groupshape.py +329 -0
  96. pptx2/oxml/shapes/picture.py +270 -0
  97. pptx2/oxml/shapes/shared.py +577 -0
  98. pptx2/oxml/simpletypes.py +1027 -0
  99. pptx2/oxml/slide.py +563 -0
  100. pptx2/oxml/table.py +650 -0
  101. pptx2/oxml/text.py +815 -0
  102. pptx2/oxml/theme.py +36 -0
  103. pptx2/oxml/xmlchemy.py +717 -0
  104. pptx2/package.py +222 -0
  105. pptx2/parts/__init__.py +0 -0
  106. pptx2/parts/chart.py +95 -0
  107. pptx2/parts/coreprops.py +167 -0
  108. pptx2/parts/diagram.py +37 -0
  109. pptx2/parts/embeddedpackage.py +93 -0
  110. pptx2/parts/image.py +275 -0
  111. pptx2/parts/media.py +37 -0
  112. pptx2/parts/presentation.py +136 -0
  113. pptx2/parts/slide.py +371 -0
  114. pptx2/presentation.py +408 -0
  115. pptx2/py.typed +0 -0
  116. pptx2/render.py +586 -0
  117. pptx2/section.py +272 -0
  118. pptx2/shapes/__init__.py +26 -0
  119. pptx2/shapes/autoshape.py +442 -0
  120. pptx2/shapes/base.py +1078 -0
  121. pptx2/shapes/connector.py +297 -0
  122. pptx2/shapes/freeform.py +337 -0
  123. pptx2/shapes/graphfrm.py +316 -0
  124. pptx2/shapes/group.py +264 -0
  125. pptx2/shapes/picture.py +422 -0
  126. pptx2/shapes/placeholder.py +468 -0
  127. pptx2/shapes/shapetree.py +2027 -0
  128. pptx2/shared.py +82 -0
  129. pptx2/skill/SKILL.md +450 -0
  130. pptx2/skill/__init__.py +78 -0
  131. pptx2/skill/__main__.py +64 -0
  132. pptx2/skill/references/animations.md +189 -0
  133. pptx2/skill/references/basics.md +421 -0
  134. pptx2/skill/references/charts.md +254 -0
  135. pptx2/skill/references/compose.md +234 -0
  136. pptx2/skill/references/design.md +366 -0
  137. pptx2/skill/references/effects.md +249 -0
  138. pptx2/skill/references/end-to-end-deck.md +231 -0
  139. pptx2/skill/references/geometry-and-arrows.md +334 -0
  140. pptx2/skill/references/lint.md +275 -0
  141. pptx2/skill/references/math.md +86 -0
  142. pptx2/skill/references/picture-effects.md +129 -0
  143. pptx2/skill/references/render.md +151 -0
  144. pptx2/skill/references/smart-art.md +75 -0
  145. pptx2/skill/references/space-aware-authoring.md +249 -0
  146. pptx2/skill/references/tables.md +244 -0
  147. pptx2/skill/references/theme.md +127 -0
  148. pptx2/skill/references/three-d.md +109 -0
  149. pptx2/skill/references/transitions.md +100 -0
  150. pptx2/slide.py +1244 -0
  151. pptx2/smart_art.py +220 -0
  152. pptx2/spec.py +633 -0
  153. pptx2/table.py +1181 -0
  154. pptx2/table_styles.py +184 -0
  155. pptx2/templates/default.pptx +0 -0
  156. pptx2/templates/docx-icon.emf +0 -0
  157. pptx2/templates/generic-icon.emf +0 -0
  158. pptx2/templates/notes.xml +23 -0
  159. pptx2/templates/notesMaster.xml +352 -0
  160. pptx2/templates/pptx-icon.emf +0 -0
  161. pptx2/templates/theme.xml +321 -0
  162. pptx2/templates/xlsx-icon.emf +0 -0
  163. pptx2/text/__init__.py +0 -0
  164. pptx2/text/fonts.py +482 -0
  165. pptx2/text/layout.py +374 -0
  166. pptx2/text/text.py +1272 -0
  167. pptx2/theme.py +721 -0
  168. pptx2/types.py +36 -0
  169. pptx2/util.py +263 -0
  170. python_pptx2-2.13.0.dist-info/METADATA +351 -0
  171. python_pptx2-2.13.0.dist-info/RECORD +175 -0
  172. python_pptx2-2.13.0.dist-info/WHEEL +5 -0
  173. python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
  174. python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
  175. python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/slide.py ADDED
@@ -0,0 +1,1244 @@
1
+ """Slide-related objects, including masters, layouts, and notes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import contextmanager
6
+ from typing import TYPE_CHECKING, Iterator, Sequence, cast
7
+
8
+ from pptx2.dml.fill import FillFormat
9
+ from pptx2.enum.presentation import (
10
+ MSO_TRANSITION_TYPE,
11
+ P14_TRANSITION_NAMES,
12
+ P159_TRANSITION_NAMES,
13
+ )
14
+ from pptx2.enum.shapes import PP_PLACEHOLDER
15
+ from pptx2.oxml.ns import qn
16
+ from pptx2.shapes.shapetree import (
17
+ LayoutPlaceholders,
18
+ LayoutShapes,
19
+ MasterPlaceholders,
20
+ MasterShapes,
21
+ NotesSlidePlaceholders,
22
+ NotesSlideShapes,
23
+ SlidePlaceholders,
24
+ SlideShapes,
25
+ )
26
+ from pptx2.shared import ElementProxy, ParentedElementProxy, PartElementProxy
27
+ from pptx2.util import lazyproperty
28
+
29
+ if TYPE_CHECKING:
30
+ from pptx2.animation import SlideAnimations
31
+ from pptx2.lint import SlideLintReport
32
+ from pptx2.oxml.presentation import CT_SlideIdList, CT_SlideMasterIdList
33
+ from pptx2.oxml.slide import (
34
+ CT_CommonSlideData,
35
+ CT_NotesSlide,
36
+ CT_Slide,
37
+ CT_SlideLayoutIdList,
38
+ CT_SlideMaster,
39
+ )
40
+ from pptx2.parts.presentation import PresentationPart
41
+ from pptx2.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart
42
+ from pptx2.presentation import Presentation
43
+ from pptx2.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder
44
+ from pptx2.shapes.shapetree import NotesSlidePlaceholder
45
+ from pptx2.smart_art import SmartArtCollection
46
+ from pptx2.text.text import TextFrame
47
+
48
+
49
+ class _BaseSlide(PartElementProxy):
50
+ """Base class for slide objects, including masters, layouts and notes."""
51
+
52
+ _element: CT_Slide
53
+
54
+ @lazyproperty
55
+ def background(self) -> _Background:
56
+ """|_Background| object providing slide background properties.
57
+
58
+ This property returns a |_Background| object whether or not the
59
+ slide, master, or layout has an explicitly defined background.
60
+
61
+ The same |_Background| object is returned on every call for the same
62
+ slide object.
63
+ """
64
+ return _Background(self._element.cSld)
65
+
66
+ @property
67
+ def name(self) -> str:
68
+ """String representing the internal name of this slide.
69
+
70
+ Returns an empty string (`''`) if no name is assigned. Assigning an empty string or |None|
71
+ to this property causes any name to be removed.
72
+ """
73
+ return self._element.cSld.name
74
+
75
+ @name.setter
76
+ def name(self, value: str | None):
77
+ new_value = "" if value is None else value
78
+ self._element.cSld.name = new_value
79
+
80
+
81
+ class _BaseMaster(_BaseSlide):
82
+ """Base class for master objects such as |SlideMaster| and |NotesMaster|.
83
+
84
+ Provides access to placeholders and regular shapes.
85
+ """
86
+
87
+ @lazyproperty
88
+ def placeholders(self) -> MasterPlaceholders:
89
+ """|MasterPlaceholders| collection of placeholder shapes in this master.
90
+
91
+ Sequence sorted in `idx` order.
92
+ """
93
+ return MasterPlaceholders(self._element.spTree, self)
94
+
95
+ @lazyproperty
96
+ def shapes(self):
97
+ """
98
+ Instance of |MasterShapes| containing sequence of shape objects
99
+ appearing on this slide.
100
+ """
101
+ return MasterShapes(self._element.spTree, self)
102
+
103
+
104
+ class NotesMaster(_BaseMaster):
105
+ """Proxy for the notes master XML document.
106
+
107
+ Provides access to shapes, the most commonly used of which are placeholders.
108
+ """
109
+
110
+
111
+ class NotesSlide(_BaseSlide):
112
+ """Notes slide object.
113
+
114
+ Provides access to slide notes placeholder and other shapes on the notes handout
115
+ page.
116
+ """
117
+
118
+ element: CT_NotesSlide # pyright: ignore[reportIncompatibleMethodOverride]
119
+
120
+ def clone_master_placeholders(self, notes_master: NotesMaster) -> None:
121
+ """Selectively add placeholder shape elements from `notes_master`.
122
+
123
+ Selected placeholder shape elements from `notes_master` are added to the shapes
124
+ collection of this notes slide. Z-order of placeholders is preserved. Certain
125
+ placeholders (header, date, footer) are not cloned.
126
+ """
127
+
128
+ def iter_cloneable_placeholders() -> Iterator[MasterPlaceholder]:
129
+ """Generate a reference to each cloneable placeholder in `notes_master`.
130
+
131
+ These are the placeholders that should be cloned to a notes slide when the a new notes
132
+ slide is created.
133
+ """
134
+ cloneable = (
135
+ PP_PLACEHOLDER.SLIDE_IMAGE,
136
+ PP_PLACEHOLDER.BODY,
137
+ PP_PLACEHOLDER.SLIDE_NUMBER,
138
+ )
139
+ for placeholder in notes_master.placeholders:
140
+ if placeholder.element.ph_type in cloneable:
141
+ yield placeholder
142
+
143
+ shapes = self.shapes
144
+ for placeholder in iter_cloneable_placeholders():
145
+ shapes.clone_placeholder(cast("LayoutPlaceholder", placeholder))
146
+
147
+ @property
148
+ def notes_placeholder(self) -> NotesSlidePlaceholder | None:
149
+ """the notes placeholder on this notes slide, the shape that contains the actual notes text.
150
+
151
+ Return |None| if no notes placeholder is present; while this is probably uncommon, it can
152
+ happen if the notes master does not have a body placeholder, or if the notes placeholder
153
+ has been deleted from the notes slide.
154
+ """
155
+ for placeholder in self.placeholders:
156
+ if placeholder.placeholder_format.type == PP_PLACEHOLDER.BODY:
157
+ return placeholder
158
+ return None
159
+
160
+ @property
161
+ def notes_text_frame(self) -> TextFrame | None:
162
+ """The text frame of the notes placeholder on this notes slide.
163
+
164
+ |None| if there is no notes placeholder. This is a shortcut to accommodate the common case
165
+ of simply adding "notes" text to the notes "page".
166
+ """
167
+ notes_placeholder = self.notes_placeholder
168
+ if notes_placeholder is None:
169
+ return None
170
+ return notes_placeholder.text_frame
171
+
172
+ @lazyproperty
173
+ def placeholders(self) -> NotesSlidePlaceholders:
174
+ """Instance of |NotesSlidePlaceholders| for this notes-slide.
175
+
176
+ Contains the sequence of placeholder shapes in this notes slide.
177
+ """
178
+ return NotesSlidePlaceholders(self.element.spTree, self)
179
+
180
+ @lazyproperty
181
+ def shapes(self) -> NotesSlideShapes:
182
+ """Sequence of shape objects appearing on this notes slide."""
183
+ return NotesSlideShapes(self._element.spTree, self)
184
+
185
+
186
+ class SlideTransition(object):
187
+ """Provides access to the transition into a slide.
188
+
189
+ A |SlideTransition| object is returned by :attr:`Slide.transition`
190
+ whether or not an explicit ``<p:transition>`` element is present on the
191
+ slide; reads on properties of an absent transition return |None| and
192
+ never mutate the underlying XML, so theme inheritance is preserved.
193
+
194
+ Setting any property creates the ``<p:transition>`` element on demand;
195
+ use :meth:`clear` to remove the element entirely (restoring the default
196
+ "no explicit transition" state).
197
+ """
198
+
199
+ def __init__(self, sld_elm):
200
+ self._sld = sld_elm
201
+
202
+ @property
203
+ def kind(self) -> MSO_TRANSITION_TYPE | None:
204
+ """Transition kind as :ref:`MsoTransitionType`, or |None| if not set."""
205
+ transition = self._sld.transition
206
+ if transition is None:
207
+ return None
208
+ kind_elm = transition.kind_element
209
+ if kind_elm is None:
210
+ # explicit `<p:transition/>` with no child means "cut" / no animation
211
+ return MSO_TRANSITION_TYPE.NONE
212
+ local = kind_elm.tag.rsplit("}", 1)[-1]
213
+ try:
214
+ return MSO_TRANSITION_TYPE.from_xml(local)
215
+ except ValueError:
216
+ return None
217
+
218
+ @kind.setter
219
+ def kind(self, value: MSO_TRANSITION_TYPE | None) -> None:
220
+ if value is None:
221
+ self.clear()
222
+ return
223
+ if not isinstance(value, MSO_TRANSITION_TYPE):
224
+ raise TypeError(
225
+ "kind must be a MSO_TRANSITION_TYPE member or None, got %r" % (value,)
226
+ )
227
+ transition = self._sld.get_or_add_transition()
228
+ # remove any pre-existing kind child
229
+ existing = transition.kind_element
230
+ if existing is not None:
231
+ transition.remove(existing)
232
+ if value is MSO_TRANSITION_TYPE.NONE:
233
+ return
234
+ local = value.xml_value
235
+ if local in P159_TRANSITION_NAMES:
236
+ prefix = "p159"
237
+ elif local in P14_TRANSITION_NAMES:
238
+ prefix = "p14"
239
+ else:
240
+ prefix = "p"
241
+ kind_elm = etree.Element(
242
+ qn("%s:%s" % (prefix, local)),
243
+ nsmap={prefix: _PREFIX_TO_URI[prefix]},
244
+ )
245
+ # insert at position 0 (before any sndAc/extLst)
246
+ transition.insert(0, kind_elm)
247
+
248
+ @property
249
+ def duration(self) -> int | None:
250
+ """Transition duration in milliseconds, or |None| if not explicitly set.
251
+
252
+ Resolves the ``p14:dur`` attribute (PowerPoint 2010+ extension) if
253
+ present; falls back to mapping the legacy ``spd`` bucket
254
+ (``slow``/``med``/``fast`` ↔ 1000/750/500 ms) otherwise.
255
+ """
256
+ transition = self._sld.transition
257
+ if transition is None:
258
+ return None
259
+ dur_attr = transition.get(qn("p14:dur"))
260
+ if dur_attr is not None:
261
+ try:
262
+ return int(dur_attr)
263
+ except ValueError:
264
+ return None
265
+ spd = transition.spd
266
+ if spd is None:
267
+ return None
268
+ return _SPD_TO_MS.get(spd)
269
+
270
+ @duration.setter
271
+ def duration(self, ms: int | None) -> None:
272
+ if ms is None:
273
+ # clearing on a slide that inherits should be a no-op, not a
274
+ # mutation that introduces an empty `<p:transition>` element
275
+ transition = self._sld.transition
276
+ if transition is None:
277
+ return
278
+ transition.attrib.pop(qn("p14:dur"), None)
279
+ # also drop the legacy `spd` bucket; otherwise the getter falls
280
+ # back to it and reads as still-explicitly-set
281
+ transition.spd = None
282
+ return
283
+ if ms < 0:
284
+ raise ValueError("duration must be a non-negative integer (milliseconds)")
285
+ transition = self._sld.get_or_add_transition()
286
+ transition.set(qn("p14:dur"), str(int(ms)))
287
+ # writing an explicit ms duration supersedes any legacy bucket
288
+ transition.spd = None
289
+
290
+ @property
291
+ def advance_on_click(self) -> bool | None:
292
+ """Whether the slide advances on mouse-click; |None| if unset."""
293
+ transition = self._sld.transition
294
+ if transition is None:
295
+ return None
296
+ return transition.advClick
297
+
298
+ @advance_on_click.setter
299
+ def advance_on_click(self, value: bool | None) -> None:
300
+ if value is None:
301
+ transition = self._sld.transition
302
+ if transition is None:
303
+ return
304
+ transition.advClick = None
305
+ return
306
+ transition = self._sld.get_or_add_transition()
307
+ transition.advClick = bool(value)
308
+
309
+ @property
310
+ def advance_after(self) -> int | None:
311
+ """Auto-advance time (milliseconds), or |None| if not auto-advancing."""
312
+ transition = self._sld.transition
313
+ if transition is None:
314
+ return None
315
+ return transition.advTm
316
+
317
+ @advance_after.setter
318
+ def advance_after(self, ms: int | None) -> None:
319
+ if ms is None:
320
+ transition = self._sld.transition
321
+ if transition is None:
322
+ return
323
+ transition.advTm = None
324
+ return
325
+ if ms < 0:
326
+ raise ValueError("advance_after must be a non-negative integer (milliseconds)")
327
+ transition = self._sld.get_or_add_transition()
328
+ transition.advTm = int(ms)
329
+
330
+ def clear(self) -> None:
331
+ """Remove the ``<p:transition>`` element entirely.
332
+
333
+ After this call, the slide has no explicit transition; reads return
334
+ |None| again. Idempotent: safe to call when no transition is set.
335
+ """
336
+ self._sld._remove_transition()
337
+
338
+
339
+ _SPD_TO_MS = {"slow": 1000, "med": 750, "fast": 500}
340
+
341
+
342
+ _PREFIX_TO_URI = {
343
+ "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
344
+ "p14": "http://schemas.microsoft.com/office/powerpoint/2010/main",
345
+ "p159": "http://schemas.microsoft.com/office/powerpoint/2015/09/main",
346
+ }
347
+
348
+
349
+ # -- imported here to avoid a circular import at module load time --
350
+ from lxml import etree # noqa: E402
351
+
352
+
353
+ class Slide(_BaseSlide):
354
+ """Slide object. Provides access to shapes and slide-level properties."""
355
+
356
+ part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride]
357
+
358
+ @lazyproperty
359
+ def animations(self) -> SlideAnimations:
360
+ """Return a |SlideAnimations| object for adding animation effects to this slide.
361
+
362
+ Animations are appended to the slide's timing tree in the order they
363
+ are added. Existing animations (e.g. authored in PowerPoint) are
364
+ preserved and new effects are appended after them.
365
+
366
+ Example::
367
+
368
+ from pptx2.animation import Entrance, Trigger
369
+
370
+ Entrance.fade(slide, shape)
371
+ Entrance.fly_in(slide, shape2, direction="left",
372
+ trigger=Trigger.WITH_PREVIOUS)
373
+ """
374
+ from pptx2.animation import SlideAnimations
375
+
376
+ return SlideAnimations(self)
377
+
378
+ def render_thumbnail(self, **kwargs):
379
+ """Render this slide to a PNG via headless LibreOffice.
380
+
381
+ Thin wrapper around :func:`pptx2.render.render_slide_thumbnail`.
382
+ Forwards `out_path`, `soffice_bin`, `timeout`, and `return_bytes`
383
+ keyword arguments. Requires ``soffice`` on PATH.
384
+ """
385
+ from pptx2.render import render_slide_thumbnail
386
+
387
+ return render_slide_thumbnail(self, **kwargs)
388
+
389
+ def lint_group(self, name: str | None, *shapes) -> None:
390
+ """Tag every shape in *shapes* with ``lint_group = name``.
391
+
392
+ Convenience batch form of ``shape.lint_group = name``. Shapes that
393
+ share a non-empty ``lint_group`` are allowed to overlap without
394
+ producing :class:`~pptx2.lint.ShapeCollision` warnings.
395
+
396
+ Example::
397
+
398
+ slide.lint_group("kpi-card-1", card, accent_bar, label_box, value_box)
399
+
400
+ Passing ``name=None`` clears the tag on each supplied shape.
401
+ """
402
+ for shape in shapes:
403
+ shape.lint_group = name
404
+
405
+ def lint_group_overlaps(self, *shapes, name: str | None = None) -> str:
406
+ """Tag *shapes* as a co-overlapping design group, returning the group name.
407
+
408
+ Convenience over :meth:`lint_group` that also auto-generates a
409
+ unique-on-the-slide group name when one isn't supplied, so
410
+ callers don't have to invent ``"kpi-card-1"`` / ``"kpi-card-2"``
411
+ labels by hand. Inspired by IMPROVEMENT_PLAN.md item 12 — the
412
+ single-line equivalent of:
413
+
414
+ slide.lint_group(f"design-group-{n}", *shapes)
415
+
416
+ Example::
417
+
418
+ slide.lint_group_overlaps(card, accent_bar, label, value)
419
+
420
+ When *name* is supplied it is used verbatim (matching
421
+ :meth:`lint_group`). Otherwise a name of the form
422
+ ``"design-group-N"`` is chosen, where ``N`` starts at 1 and
423
+ increments to the smallest positive integer that doesn't
424
+ already appear as a ``lint_group`` tag on this slide.
425
+ """
426
+ if not shapes:
427
+ raise ValueError(
428
+ "lint_group_overlaps requires at least one shape; got 0"
429
+ )
430
+ if name is None:
431
+ existing = {
432
+ getattr(s, "lint_group", None)
433
+ for s in self.shapes
434
+ } - {None, ""}
435
+ n = 1
436
+ while f"design-group-{n}" in existing:
437
+ n += 1
438
+ name = f"design-group-{n}"
439
+ for shape in shapes:
440
+ shape.lint_group = name
441
+ return name
442
+
443
+ @contextmanager
444
+ def design_group(self, name: str):
445
+ """Context manager that auto-tags shapes added inside the block.
446
+
447
+ Any shape appended to this slide's shape tree while the block is
448
+ active receives ``lint_group = name`` (provided it doesn't already
449
+ have a non-empty group, so nested ``design_group`` calls behave
450
+ intuitively — the innermost label wins).
451
+
452
+ Example::
453
+
454
+ with slide.design_group("kpi-card-1"):
455
+ slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, ...) # card
456
+ slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, ...) # accent
457
+ slide.shapes.add_textbox(...) # label
458
+ slide.shapes.add_textbox(...) # value
459
+ # All four now share ``lint_group = "kpi-card-1"``.
460
+ """
461
+ if not isinstance(name, str) or not name:
462
+ raise ValueError("design_group name must be a non-empty string")
463
+
464
+ sp_tree = self._element.spTree
465
+ before = {id(elm) for elm in sp_tree.iter_shape_elms()}
466
+ try:
467
+ yield
468
+ finally:
469
+ from pptx2.lint import _read_lint_group, _write_lint_group
470
+
471
+ for elm in sp_tree.iter_shape_elms():
472
+ if id(elm) in before:
473
+ continue
474
+ try:
475
+ cNvPr = elm._nvXxPr.cNvPr
476
+ except AttributeError:
477
+ continue
478
+ # Don't overwrite an explicit group set by the caller or by
479
+ # an inner ``design_group`` block that already tagged this
480
+ # shape.
481
+ if _read_lint_group(cNvPr):
482
+ continue
483
+ _write_lint_group(cNvPr, name)
484
+
485
+ def lint(
486
+ self,
487
+ *,
488
+ include_effect_bleed: bool = False,
489
+ disable=(),
490
+ min_severity="info",
491
+ ) -> SlideLintReport:
492
+ """Inspect this slide for geometric and typographic issues.
493
+
494
+ Returns a |SlideLintReport| with a list of detected issues (text
495
+ overflow, shapes off-slide, shape collisions). The report is
496
+ generated fresh on each call.
497
+
498
+ *include_effect_bleed* (opt-in, default |False|) widens each
499
+ shape's bbox by its shadow blur radius before the OffSlide and
500
+ ShapeCollision checks run. Bleed-only issues are emitted as
501
+ ``OffSlideShadow`` / ``ShapeCollisionShadow`` so callers can
502
+ suppress them via ``shape.lint_skip`` without losing real
503
+ geometry warnings.
504
+
505
+ *disable* is an iterable of issue ``code`` values to skip
506
+ entirely — e.g. ``disable=["ShapeCollision"]``.
507
+
508
+ *min_severity* drops issues below the named threshold from the
509
+ report (``"info"`` / ``"warning"`` / ``"error"``).
510
+
511
+ Example::
512
+
513
+ report = slide.lint()
514
+ if report.has_errors:
515
+ print(report.summary())
516
+ """
517
+ from pptx2.lint import lint_slide
518
+
519
+ return lint_slide(
520
+ self,
521
+ include_effect_bleed=include_effect_bleed,
522
+ disable=disable,
523
+ min_severity=min_severity,
524
+ )
525
+
526
+ @property
527
+ def follow_master_background(self):
528
+ """|True| if this slide inherits the slide master background.
529
+
530
+ Read-only. Inheritance is broken as a side-effect of giving the
531
+ slide its own background rather than by assigning here: touching
532
+ :attr:`background` adds a ``<p:bg>`` element, after which this
533
+ reports |False|.
534
+
535
+ ::
536
+
537
+ slide.follow_master_background # True
538
+ slide.background.fill.solid()
539
+ slide.background.fill.fore_color.rgb = "#102030"
540
+ slide.follow_master_background # now False
541
+
542
+ To restore inheritance, remove the slide's own background.
543
+ """
544
+ return self._element.bg is None
545
+
546
+ @property
547
+ def has_notes_slide(self) -> bool:
548
+ """`True` if this slide has a notes slide, `False` otherwise.
549
+
550
+ A notes slide is created by :attr:`.notes_slide` when one doesn't exist; use this property
551
+ to test for a notes slide without the possible side effect of creating one.
552
+ """
553
+ return self.part.has_notes_slide
554
+
555
+ @property
556
+ def notes_slide(self) -> NotesSlide:
557
+ """The |NotesSlide| instance for this slide.
558
+
559
+ If the slide does not have a notes slide, one is created. The same single instance is
560
+ returned on each call.
561
+ """
562
+ return self.part.notes_slide
563
+
564
+ @property
565
+ def notes(self) -> str:
566
+ """The speaker-notes text for this slide as a |str|.
567
+
568
+ Returns an empty string when this slide has no notes slide or its notes
569
+ placeholder is empty. This is a first-class, LLM-friendly shortcut over
570
+ :attr:`.notes_slide` / :attr:`~.NotesSlide.notes_text_frame`; reading it
571
+ never creates a notes slide.
572
+
573
+ Assigning a string writes the notes text, creating the notes slide (and
574
+ therefore its notes placeholder) on demand::
575
+
576
+ slide.notes = "Remember to thank the sponsors."
577
+ """
578
+ if not self.has_notes_slide:
579
+ return ""
580
+ text_frame = self.notes_slide.notes_text_frame
581
+ if text_frame is None:
582
+ return ""
583
+ return text_frame.text
584
+
585
+ @notes.setter
586
+ def notes(self, text: str) -> None:
587
+ text_frame = self.notes_slide.notes_text_frame
588
+ if text_frame is None:
589
+ raise ValueError("notes slide has no notes placeholder; cannot set notes text")
590
+ text_frame.text = text
591
+
592
+ @lazyproperty
593
+ def placeholders(self) -> SlidePlaceholders:
594
+ """Sequence of placeholder shapes in this slide."""
595
+ return SlidePlaceholders(self._element.spTree, self)
596
+
597
+ @lazyproperty
598
+ def shapes(self) -> SlideShapes:
599
+ """Sequence of shape objects appearing on this slide."""
600
+ return SlideShapes(self._element.spTree, self)
601
+
602
+ def slide_bbox(self):
603
+ """Return the slide's full area as a :class:`~pptx2.geometry.BBox`."""
604
+ from pptx2.geometry import BBox
605
+
606
+ return BBox.from_slide(self)
607
+
608
+ def content_bbox(self, *, include_decorative: bool = False):
609
+ """Return the bounding box covering all non-decorative shapes.
610
+
611
+ ``include_decorative=False`` (the default) skips slide-spanning
612
+ backgrounds (any shape whose width and height each exceed 95%
613
+ of the slide area), so the returned box reflects "where the
614
+ real content is" rather than the full slide.
615
+
616
+ Returns ``None`` when the slide has no qualifying shapes.
617
+ """
618
+ from pptx2.geometry import BBox
619
+
620
+ slide_box = BBox.from_slide(self)
621
+ union_box: BBox | None = None
622
+ threshold_w = int(slide_box.width) * 0.95
623
+ threshold_h = int(slide_box.height) * 0.95
624
+ for shape in self.shapes:
625
+ try:
626
+ box = BBox.from_shape(shape)
627
+ except Exception:
628
+ continue
629
+ if not include_decorative:
630
+ if (
631
+ int(box.width) >= threshold_w
632
+ and int(box.height) >= threshold_h
633
+ ):
634
+ continue
635
+ union_box = box if union_box is None else union_box.union(box)
636
+ return union_box
637
+
638
+ def find_empty_region(
639
+ self,
640
+ *,
641
+ near=None,
642
+ min_width=0,
643
+ min_height=0,
644
+ ):
645
+ """Return a :class:`BBox` of an unused region on the slide.
646
+
647
+ Walks a coarse grid over the slide and returns the largest cell
648
+ (or cluster of cells) that doesn't overlap any existing
649
+ shape. ``near`` is an optional BBox / Shape; when given, the
650
+ cell whose centre is nearest its centre is preferred over
651
+ strictly the largest free area.
652
+
653
+ ``min_width`` / ``min_height`` filter out tiny free pockets in
654
+ EMU. Returns ``None`` when no region meets the criteria.
655
+
656
+ Approximate by design — for one-off LLM placement decisions,
657
+ not pixel-perfect packing.
658
+ """
659
+ from pptx2.geometry import BBox
660
+
661
+ slide_box = BBox.from_slide(self)
662
+ # 12×8 sample grid is fine-grained enough for typical decks.
663
+ cells = slide_box.grid(12, 8)
664
+ existing = []
665
+ for shape in self.shapes:
666
+ try:
667
+ existing.append(BBox.from_shape(shape))
668
+ except Exception:
669
+ pass
670
+
671
+ free = [c for c in cells if not any(c.intersects(s) for s in existing)]
672
+ if not free:
673
+ return None
674
+
675
+ # Merge horizontally-adjacent free cells in the same row.
676
+ merged: list[BBox] = []
677
+ for c in free:
678
+ if merged:
679
+ last = merged[-1]
680
+ if (
681
+ int(last.top) == int(c.top)
682
+ and int(last.right) == int(c.left)
683
+ ):
684
+ merged[-1] = last.union(c)
685
+ continue
686
+ merged.append(c)
687
+
688
+ candidates = [
689
+ m for m in merged
690
+ if int(m.width) >= int(min_width)
691
+ and int(m.height) >= int(min_height)
692
+ ]
693
+ if not candidates:
694
+ return None
695
+
696
+ if near is not None:
697
+ from pptx2.shapes.base import BaseShape
698
+
699
+ if isinstance(near, BaseShape):
700
+ target = BBox.from_shape(near)
701
+ elif isinstance(near, BBox):
702
+ target = near
703
+ else:
704
+ raise TypeError(
705
+ "find_empty_region(near=...) must be a BaseShape or BBox; "
706
+ f"got {type(near).__name__}"
707
+ )
708
+ tx, ty = int(target.cx), int(target.cy)
709
+ candidates.sort(
710
+ key=lambda b: (int(b.cx) - tx) ** 2 + (int(b.cy) - ty) ** 2
711
+ )
712
+ return candidates[0]
713
+ candidates.sort(key=lambda b: -b.area)
714
+ return candidates[0]
715
+
716
+ def tidy(
717
+ self,
718
+ *,
719
+ fix_offslide: bool = True,
720
+ fix_overflow: bool = True,
721
+ fix_grid_drift: bool = False,
722
+ fix_layer_order: bool = True,
723
+ ) -> list[str]:
724
+ """One-call cleanup: lint then auto-fix the safe subset.
725
+
726
+ Wraps :meth:`lint` + :meth:`SlideLintReport.auto_fix` with the
727
+ flags most decks want by default. Returns the list of fixes
728
+ applied (the same shape as ``auto_fix()``).
729
+
730
+ * ``fix_offslide`` (default ``True``) clamps shapes back
731
+ on-slide.
732
+ * ``fix_overflow`` (default ``True``) flips overflowing text
733
+ frames to ``MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE``.
734
+ * ``fix_grid_drift`` (default ``False``) snaps minor grid drift
735
+ — off by default because the snap can move a shape by several
736
+ EMU when the inferred grid is wrong.
737
+ * ``fix_layer_order`` (default ``True``) restacks shapes whose
738
+ ``layer_above`` declaration is contradicted by the drawing
739
+ order. On by default because it only enforces an ordering
740
+ the author already declared, and it never moves geometry.
741
+ """
742
+ disable: list[str] = []
743
+ if not fix_offslide:
744
+ disable.append("OffSlide")
745
+ if not fix_overflow:
746
+ disable.append("TextOverflow")
747
+ if not fix_grid_drift:
748
+ disable.append("OffGridDrift")
749
+ if not fix_layer_order:
750
+ disable.append("LayerOrderViolation")
751
+ report = self.lint(disable=disable)
752
+ return report.auto_fix()
753
+
754
+ @property
755
+ def smart_art(self) -> SmartArtCollection:
756
+ """Return a |SmartArtCollection| for this slide.
757
+
758
+ Provides indexed access to SmartArt graphics on the slide. Each item
759
+ is a |SmartArtShape| whose :attr:`~SmartArtShape.texts` property gives
760
+ the current text list and :meth:`~SmartArtShape.set_text` replaces it.
761
+
762
+ Example::
763
+
764
+ org_chart = slide.smart_art[0]
765
+ print(org_chart.texts) # ['CEO', 'CTO', 'CFO']
766
+ org_chart.set_text(['Alice', 'Bob', 'Carol'])
767
+
768
+ Returns an empty collection (length 0) when there are no SmartArt
769
+ shapes on the slide.
770
+ """
771
+ from pptx2.smart_art import SmartArtCollection
772
+
773
+ return SmartArtCollection(self)
774
+
775
+ @property
776
+ def slide_id(self) -> int:
777
+ """Integer value that uniquely identifies this slide within this presentation.
778
+
779
+ The slide id does not change if the position of this slide in the slide sequence is changed
780
+ by adding, rearranging, or deleting slides.
781
+ """
782
+ return self.part.slide_id
783
+
784
+ @property
785
+ def slide_layout(self) -> SlideLayout:
786
+ """|SlideLayout| object this slide inherits appearance from."""
787
+ return self.part.slide_layout
788
+
789
+ @property
790
+ def color_variant(self) -> str | None:
791
+ """Per-slide color-mapping variant: ``"light"`` / ``"dark"`` / |None|.
792
+
793
+ Reads / writes the ``<p:clrMapOvr>`` element to apply a built-in
794
+ light or dark variant of the deck's master color map. ``"light"``
795
+ is the master's default mapping (``bg1=lt1``, ``tx1=dk1``, …);
796
+ ``"dark"`` swaps backgrounds and text (``bg1=dk1``, ``tx1=lt1``,
797
+ …) for a dark-on-light slide without changing the deck theme.
798
+
799
+ Reading returns:
800
+
801
+ * ``"dark"`` — slide has an explicit override that swaps bg/tx.
802
+ * ``"light"`` — slide inherits from the master (or has an
803
+ explicit ``<a:masterClrMapping/>`` element).
804
+ * ``None`` — slide has a custom override that doesn't match
805
+ either of the two named variants.
806
+
807
+ Assigning ``None`` removes the override entirely (returning to
808
+ master inheritance).
809
+
810
+ For more flexible control, use :meth:`set_clr_map_override`.
811
+ """
812
+ clr_ovr = self._element.clrMapOvr
813
+ if clr_ovr is None:
814
+ return "light"
815
+ # If <a:masterClrMapping/> is present we're inheriting.
816
+ for child in clr_ovr:
817
+ local = child.tag.rsplit("}", 1)[-1]
818
+ if local == "masterClrMapping":
819
+ return "light"
820
+ if local == "overrideClrMapping":
821
+ if (
822
+ child.get("bg1") == "dk1"
823
+ and child.get("tx1") == "lt1"
824
+ and child.get("bg2") == "dk2"
825
+ and child.get("tx2") == "lt2"
826
+ ):
827
+ return "dark"
828
+ return None
829
+ return None
830
+
831
+ @color_variant.setter
832
+ def color_variant(self, value: str | None) -> None:
833
+ if value is None:
834
+ self._element._remove_clrMapOvr()
835
+ return
836
+ if value == "light":
837
+ self.set_clr_map_override(masterClrMapping=True)
838
+ return
839
+ if value == "dark":
840
+ self.set_clr_map_override(
841
+ bg1="dk1", tx1="lt1", bg2="dk2", tx2="lt2",
842
+ accent1="accent1", accent2="accent2", accent3="accent3",
843
+ accent4="accent4", accent5="accent5", accent6="accent6",
844
+ hlink="hlink", folHlink="folHlink",
845
+ )
846
+ return
847
+ raise ValueError(
848
+ f"color_variant must be 'light', 'dark', or None; got {value!r}"
849
+ )
850
+
851
+ def set_clr_map_override(self, *, masterClrMapping: bool = False, **mapping: str) -> None:
852
+ """Set this slide's ``<p:clrMapOvr>`` element directly.
853
+
854
+ With ``masterClrMapping=True`` (and no other args), removes any
855
+ existing override and writes ``<a:masterClrMapping/>`` so the
856
+ slide inherits the master's color map.
857
+
858
+ Otherwise, writes an ``<a:overrideClrMapping>`` with the supplied
859
+ attributes. Standard mapping attributes are ``bg1``, ``tx1``,
860
+ ``bg2``, ``tx2``, ``accent1``..``accent6``, ``hlink``,
861
+ ``folHlink``; each value is the slot it should resolve to
862
+ (e.g. ``bg1="dk1"`` redirects "background 1" lookups to the
863
+ ``dk1`` palette slot).
864
+
865
+ Use :attr:`color_variant` for the common light/dark presets.
866
+ """
867
+ clr_ovr = self._element.get_or_add_clrMapOvr()
868
+ # Clear existing children.
869
+ for child in list(clr_ovr):
870
+ clr_ovr.remove(child)
871
+
872
+ a_ns = "http://schemas.openxmlformats.org/drawingml/2006/main"
873
+ if masterClrMapping and not mapping:
874
+ child = etree.SubElement(clr_ovr, "{%s}masterClrMapping" % a_ns)
875
+ return
876
+ child = etree.SubElement(clr_ovr, "{%s}overrideClrMapping" % a_ns)
877
+ for k, v in mapping.items():
878
+ child.set(k, v)
879
+
880
+ @lazyproperty
881
+ def transition(self) -> SlideTransition:
882
+ """|SlideTransition| object describing the transition into this slide.
883
+
884
+ The same instance is returned on each call. Reads on individual
885
+ properties of the returned object are non-mutating; the underlying
886
+ ``<p:transition>`` element is created only when a property is
887
+ assigned.
888
+ """
889
+ return SlideTransition(self._element)
890
+
891
+
892
+ class Slides(ParentedElementProxy):
893
+ """Sequence of slides belonging to an instance of |Presentation|.
894
+
895
+ Has list semantics for access to individual slides. Supports indexed access, len(), and
896
+ iteration.
897
+ """
898
+
899
+ part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride]
900
+
901
+ def __init__(self, sldIdLst: CT_SlideIdList, prs: Presentation):
902
+ super(Slides, self).__init__(sldIdLst, prs)
903
+ self._sldIdLst = sldIdLst
904
+
905
+ def __getitem__(self, idx: int) -> Slide:
906
+ """Provide indexed access, (e.g. 'slides[0]')."""
907
+ try:
908
+ sldId = self._sldIdLst.sldId_lst[idx]
909
+ except IndexError:
910
+ raise IndexError("slide index out of range")
911
+ return self.part.related_slide(sldId.rId)
912
+
913
+ def __iter__(self) -> Iterator[Slide]:
914
+ """Support iteration, e.g. `for slide in slides:`."""
915
+ for sldId in self._sldIdLst.sldId_lst:
916
+ yield self.part.related_slide(sldId.rId)
917
+
918
+ def __len__(self) -> int:
919
+ """Support len() built-in function, e.g. `len(slides) == 4`."""
920
+ return len(self._sldIdLst)
921
+
922
+ def add_slide(self, slide_layout: SlideLayout) -> Slide:
923
+ """Return a newly added slide that inherits layout from `slide_layout`."""
924
+ rId, slide = self.part.add_slide(slide_layout)
925
+ slide.shapes.clone_layout_placeholders(slide_layout)
926
+ sldId = self._sldIdLst.add_sldId(rId)
927
+ self._add_to_final_section(sldId.id)
928
+ return slide
929
+
930
+ def _add_to_final_section(self, slide_id: int) -> None:
931
+ """Register a newly appended slide with the deck's final section.
932
+
933
+ PowerPoint keeps the 2010 section extension a complete partition of
934
+ the deck — every slide belongs to exactly one section. A slide
935
+ appended at the end of a sectioned deck therefore falls into the
936
+ last section, so mirror that here to keep the `p14:sectionLst` in
937
+ sync. A no-op when the deck has no sections.
938
+ """
939
+ prs_elm = self._sldIdLst.getparent()
940
+ sectionLst = getattr(prs_elm, "sectionLst", None)
941
+ if sectionLst is None or not sectionLst.section_lst:
942
+ return
943
+ sectionLst.section_lst[-1].add_sldId(slide_id)
944
+
945
+ def get(self, slide_id: int, default: Slide | None = None) -> Slide | None:
946
+ """Return the slide identified by int `slide_id` in this presentation.
947
+
948
+ Returns `default` if not found.
949
+ """
950
+ slide = self.part.get_slide(slide_id)
951
+ if slide is None:
952
+ return default
953
+ return slide
954
+
955
+ def index(self, slide: Slide) -> int:
956
+ """Map `slide` to its zero-based position in this slide sequence.
957
+
958
+ Raises |ValueError| on *slide* not present.
959
+ """
960
+ for idx, this_slide in enumerate(self):
961
+ if this_slide == slide:
962
+ return idx
963
+ raise ValueError("%s is not in slide collection" % slide)
964
+
965
+ def move(self, old_index: int, new_index: int) -> None:
966
+ """Relocate the slide at `old_index` to `new_index`, shifting the rest.
967
+
968
+ Both indices are zero-based and must be in range. PowerPoint slide order
969
+ follows the order of `p:sldId` children in `p:sldIdLst`, so this simply
970
+ detaches the corresponding element and re-inserts it at the new
971
+ position::
972
+
973
+ prs.slides.move(0, 2) # send the first slide to position 2
974
+
975
+ Raises |IndexError| if either index is out of range.
976
+ """
977
+ count = len(self._sldIdLst)
978
+ old_index = self._normalized_index(old_index, count)
979
+ new_index = self._normalized_index(new_index, count)
980
+ sldId_lst = self._sldIdLst.sldId_lst
981
+ sldId = sldId_lst[old_index]
982
+ self._sldIdLst.remove(sldId)
983
+ # -- recompute the insertion reference against the post-removal order --
984
+ remaining = self._sldIdLst.sldId_lst
985
+ if new_index >= len(remaining):
986
+ self._sldIdLst.append(sldId)
987
+ else:
988
+ remaining[new_index].addprevious(sldId)
989
+
990
+ def reorder(self, new_order: "Sequence[int | Slide]") -> None:
991
+ """Rearrange the slides into the permutation given by `new_order`.
992
+
993
+ `new_order` is a full permutation of the collection, expressed either as
994
+ zero-based indices into the *current* order or as the |Slide| objects
995
+ themselves (the two forms may not be mixed). After the call, the slide
996
+ that was at ``new_order[0]`` becomes the first slide, and so on::
997
+
998
+ prs.slides.reorder([2, 0, 1]) # by index
999
+ prs.slides.reorder([s2, s0, s1]) # by Slide object
1000
+
1001
+ Raises |ValueError| if `new_order` is not a permutation of exactly the
1002
+ slides in this collection (wrong length, duplicates, or unknown items).
1003
+ """
1004
+ count = len(self._sldIdLst)
1005
+ order = list(new_order)
1006
+ if len(order) != count:
1007
+ raise ValueError(
1008
+ "new_order must contain exactly %d items, got %d" % (count, len(order))
1009
+ )
1010
+
1011
+ sldId_lst = self._sldIdLst.sldId_lst
1012
+ indices: list[int] = []
1013
+ for item in order:
1014
+ if isinstance(item, Slide):
1015
+ indices.append(self.index(item))
1016
+ else:
1017
+ indices.append(self._normalized_index(int(item), count))
1018
+
1019
+ if sorted(indices) != list(range(count)):
1020
+ raise ValueError("new_order must be a permutation of the slides in this collection")
1021
+
1022
+ ordered_sldIds = [sldId_lst[i] for i in indices]
1023
+ for sldId in ordered_sldIds:
1024
+ self._sldIdLst.remove(sldId)
1025
+ for sldId in ordered_sldIds:
1026
+ self._sldIdLst.append(sldId)
1027
+
1028
+ @staticmethod
1029
+ def _normalized_index(idx: int, count: int) -> int:
1030
+ """Return `idx` resolved against `count`, supporting negative indexing.
1031
+
1032
+ Raises |IndexError| when out of range.
1033
+ """
1034
+ original = idx
1035
+ if idx < 0:
1036
+ idx += count
1037
+ if idx < 0 or idx >= count:
1038
+ raise IndexError("slide index out of range: %r" % original)
1039
+ return idx
1040
+
1041
+
1042
+ class SlideLayout(_BaseSlide):
1043
+ """Slide layout object.
1044
+
1045
+ Provides access to placeholders, regular shapes, and slide layout-level properties.
1046
+ """
1047
+
1048
+ part: SlideLayoutPart # pyright: ignore[reportIncompatibleMethodOverride]
1049
+
1050
+ def iter_cloneable_placeholders(self) -> Iterator[LayoutPlaceholder]:
1051
+ """Generate layout-placeholders on this slide-layout that should be cloned to a new slide.
1052
+
1053
+ Used when creating a new slide from this slide-layout.
1054
+ """
1055
+ latent_ph_types = (
1056
+ PP_PLACEHOLDER.DATE,
1057
+ PP_PLACEHOLDER.FOOTER,
1058
+ PP_PLACEHOLDER.SLIDE_NUMBER,
1059
+ )
1060
+ for ph in self.placeholders:
1061
+ if ph.element.ph_type not in latent_ph_types:
1062
+ yield ph
1063
+
1064
+ @lazyproperty
1065
+ def placeholders(self) -> LayoutPlaceholders:
1066
+ """Sequence of placeholder shapes in this slide layout.
1067
+
1068
+ Placeholders appear in `idx` order.
1069
+ """
1070
+ return LayoutPlaceholders(self._element.spTree, self)
1071
+
1072
+ @lazyproperty
1073
+ def shapes(self) -> LayoutShapes:
1074
+ """Sequence of shapes appearing on this slide layout."""
1075
+ return LayoutShapes(self._element.spTree, self)
1076
+
1077
+ @property
1078
+ def slide_master(self) -> SlideMaster:
1079
+ """Slide master from which this slide-layout inherits properties."""
1080
+ return self.part.slide_master
1081
+
1082
+ @property
1083
+ def used_by_slides(self):
1084
+ """Tuple of slide objects based on this slide layout."""
1085
+ # ---getting Slides collection requires going around the horn a bit---
1086
+ slides = self.part.package.presentation_part.presentation.slides
1087
+ return tuple(s for s in slides if s.slide_layout == self)
1088
+
1089
+
1090
+ class SlideLayouts(ParentedElementProxy):
1091
+ """Sequence of slide layouts belonging to a slide-master.
1092
+
1093
+ Supports indexed access, len(), iteration, index() and remove().
1094
+ """
1095
+
1096
+ part: SlideMasterPart # pyright: ignore[reportIncompatibleMethodOverride]
1097
+
1098
+ def __init__(self, sldLayoutIdLst: CT_SlideLayoutIdList, parent: SlideMaster):
1099
+ super(SlideLayouts, self).__init__(sldLayoutIdLst, parent)
1100
+ self._sldLayoutIdLst = sldLayoutIdLst
1101
+
1102
+ def __getitem__(self, idx: int) -> SlideLayout:
1103
+ """Provides indexed access, e.g. `slide_layouts[2]`."""
1104
+ try:
1105
+ sldLayoutId = self._sldLayoutIdLst.sldLayoutId_lst[idx]
1106
+ except IndexError:
1107
+ raise IndexError("slide layout index out of range")
1108
+ return self.part.related_slide_layout(sldLayoutId.rId)
1109
+
1110
+ def __iter__(self) -> Iterator[SlideLayout]:
1111
+ """Generate each |SlideLayout| in the collection, in sequence."""
1112
+ for sldLayoutId in self._sldLayoutIdLst.sldLayoutId_lst:
1113
+ yield self.part.related_slide_layout(sldLayoutId.rId)
1114
+
1115
+ def __len__(self) -> int:
1116
+ """Support len() built-in function, e.g. `len(slides) == 4`."""
1117
+ return len(self._sldLayoutIdLst)
1118
+
1119
+ def get_by_name(self, name: str, default: SlideLayout | None = None) -> SlideLayout | None:
1120
+ """Return SlideLayout object having `name`, or `default` if not found."""
1121
+ for slide_layout in self:
1122
+ if slide_layout.name == name:
1123
+ return slide_layout
1124
+ return default
1125
+
1126
+ def index(self, slide_layout: SlideLayout) -> int:
1127
+ """Return zero-based index of `slide_layout` in this collection.
1128
+
1129
+ Raises `ValueError` if `slide_layout` is not present in this collection.
1130
+ """
1131
+ for idx, this_layout in enumerate(self):
1132
+ if slide_layout == this_layout:
1133
+ return idx
1134
+ raise ValueError("layout not in this SlideLayouts collection")
1135
+
1136
+ def remove(self, slide_layout: SlideLayout) -> None:
1137
+ """Remove `slide_layout` from the collection.
1138
+
1139
+ Raises ValueError when `slide_layout` is in use; a slide layout which is the basis for one
1140
+ or more slides cannot be removed.
1141
+ """
1142
+ # ---raise if layout is in use---
1143
+ if slide_layout.used_by_slides:
1144
+ raise ValueError("cannot remove slide-layout in use by one or more slides")
1145
+
1146
+ # ---target layout is identified by its index in this collection---
1147
+ target_idx = self.index(slide_layout)
1148
+
1149
+ # --remove layout from p:sldLayoutIds of its master
1150
+ # --this stops layout from showing up, but doesn't remove it from package
1151
+ target_sldLayoutId = self._sldLayoutIdLst.sldLayoutId_lst[target_idx]
1152
+ self._sldLayoutIdLst.remove(target_sldLayoutId)
1153
+
1154
+ # --drop relationship from master to layout
1155
+ # --this removes layout from package, along with everything (only) it refers to,
1156
+ # --including images (not used elsewhere) and hyperlinks
1157
+ slide_layout.slide_master.part.drop_rel(target_sldLayoutId.rId)
1158
+
1159
+
1160
+ class SlideMaster(_BaseMaster):
1161
+ """Slide master object.
1162
+
1163
+ Provides access to slide layouts. Access to placeholders, regular shapes, and slide master-level
1164
+ properties is inherited from |_BaseMaster|.
1165
+ """
1166
+
1167
+ _element: CT_SlideMaster # pyright: ignore[reportIncompatibleVariableOverride]
1168
+
1169
+ @lazyproperty
1170
+ def slide_layouts(self) -> SlideLayouts:
1171
+ """|SlideLayouts| object providing access to this slide-master's layouts."""
1172
+ return SlideLayouts(self._element.get_or_add_sldLayoutIdLst(), self)
1173
+
1174
+
1175
+ class SlideMasters(ParentedElementProxy):
1176
+ """Sequence of |SlideMaster| objects belonging to a presentation.
1177
+
1178
+ Has list access semantics, supporting indexed access, len(), and iteration.
1179
+ """
1180
+
1181
+ part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride]
1182
+
1183
+ def __init__(self, sldMasterIdLst: CT_SlideMasterIdList, parent: Presentation):
1184
+ super(SlideMasters, self).__init__(sldMasterIdLst, parent)
1185
+ self._sldMasterIdLst = sldMasterIdLst
1186
+
1187
+ def __getitem__(self, idx: int) -> SlideMaster:
1188
+ """Provides indexed access, e.g. `slide_masters[2]`."""
1189
+ try:
1190
+ sldMasterId = self._sldMasterIdLst.sldMasterId_lst[idx]
1191
+ except IndexError:
1192
+ raise IndexError("slide master index out of range")
1193
+ return self.part.related_slide_master(sldMasterId.rId)
1194
+
1195
+ def __iter__(self):
1196
+ """Generate each |SlideMaster| instance in the collection, in sequence."""
1197
+ for smi in self._sldMasterIdLst.sldMasterId_lst:
1198
+ yield self.part.related_slide_master(smi.rId)
1199
+
1200
+ def __len__(self):
1201
+ """Support len() built-in function, e.g. `len(slide_masters) == 4`."""
1202
+ return len(self._sldMasterIdLst)
1203
+
1204
+
1205
+ class _Background(ElementProxy):
1206
+ """Provides access to slide background properties.
1207
+
1208
+ Note that the presence of this object does not by itself imply an
1209
+ explicitly-defined background; a slide with an inherited background still
1210
+ has a |_Background| object.
1211
+ """
1212
+
1213
+ def __init__(self, cSld: CT_CommonSlideData):
1214
+ super(_Background, self).__init__(cSld)
1215
+ self._cSld = cSld
1216
+
1217
+ @lazyproperty
1218
+ def fill(self):
1219
+ """|FillFormat| instance for this background.
1220
+
1221
+ This |FillFormat| object is used to interrogate or specify the fill
1222
+ of the slide background.
1223
+
1224
+ Note that accessing this property is potentially destructive. A slide
1225
+ background can also be specified by a background style reference and
1226
+ accessing this property will remove that reference, if present, and
1227
+ replace it with NoFill. This is frequently the case for a slide
1228
+ master background.
1229
+
1230
+ This is also the case when there is no explicitly defined background
1231
+ (background is inherited); merely accessing this property will cause
1232
+ the background to be set to NoFill and the inheritance link will be
1233
+ interrupted. This is frequently the case for a slide background.
1234
+
1235
+ Of course, if you are accessing this property in order to set the
1236
+ fill, then these changes are of no consequence, but the existing
1237
+ background cannot be reliably interrogated using this property unless
1238
+ you have already established it is an explicit fill.
1239
+
1240
+ If the background is already a fill, then accessing this property
1241
+ makes no changes to the current background.
1242
+ """
1243
+ bgPr = self._cSld.get_or_add_bgPr()
1244
+ return FillFormat.from_fill_parent(bgPr)