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
@@ -0,0 +1,2027 @@
1
+ """The shape tree, the structure that holds a slide's shapes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import os
7
+ import warnings
8
+ from typing import IO, TYPE_CHECKING, Callable, Iterable, Iterator, cast
9
+
10
+ from pptx2.enum.shapes import PP_PLACEHOLDER, PROG_ID
11
+ from pptx2.media import SPEAKER_IMAGE_BYTES, Video
12
+ from pptx2.opc.constants import CONTENT_TYPE as CT
13
+ from pptx2.oxml.ns import qn
14
+ from pptx2.oxml.shapes.autoshape import CT_Shape
15
+ from pptx2.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
16
+ from pptx2.oxml.shapes.picture import CT_Picture
17
+ from pptx2.oxml.simpletypes import ST_Direction
18
+ from pptx2.shapes.autoshape import AutoShapeType, Shape
19
+ from pptx2.shapes.base import BaseShape
20
+ from pptx2.shapes.connector import Connector
21
+ from pptx2.shapes.freeform import FreeformBuilder
22
+ from pptx2.shapes.graphfrm import GraphicFrame
23
+ from pptx2.shapes.group import GroupShape
24
+ from pptx2.shapes.picture import Movie, Picture
25
+ from pptx2.shapes.placeholder import (
26
+ ChartPlaceholder,
27
+ LayoutPlaceholder,
28
+ MasterPlaceholder,
29
+ NotesSlidePlaceholder,
30
+ PicturePlaceholder,
31
+ PlaceholderGraphicFrame,
32
+ PlaceholderPicture,
33
+ SlidePlaceholder,
34
+ TablePlaceholder,
35
+ )
36
+ from pptx2.shared import ParentedElementProxy
37
+ from pptx2.util import Emu, _coerce_emu, lazyproperty
38
+
39
+ if TYPE_CHECKING:
40
+ from pptx2.chart.chart import Chart
41
+ from pptx2.chart.data import ChartData
42
+ from pptx2.enum.chart import XL_CHART_TYPE
43
+ from pptx2.enum.shapes import MSO_CONNECTOR_TYPE, MSO_SHAPE
44
+ from pptx2.oxml.shapes import ShapeElement
45
+ from pptx2.oxml.shapes.connector import CT_Connector
46
+ from pptx2.oxml.shapes.groupshape import CT_GroupShape
47
+ from pptx2.parts.image import ImagePart
48
+ from pptx2.parts.slide import SlidePart
49
+ from pptx2.slide import Slide, SlideLayout
50
+ from pptx2.types import ProvidesPart
51
+ from pptx2.util import Length
52
+
53
+ # Horizontal-bar chart types — those where category[0] sits at the
54
+ # bottom of the axis by default. We flip ``reverse_order`` on these
55
+ # at creation time so the first category renders at the top, matching
56
+ # the natural reading order. ``BAR_OF_PIE`` is excluded — it's a pie
57
+ # variant, not a horizontal-bar chart.
58
+ _HORIZONTAL_BAR_CHART_NAMES = frozenset(
59
+ {
60
+ "BAR_CLUSTERED",
61
+ "BAR_STACKED",
62
+ "BAR_STACKED_100",
63
+ "THREE_D_BAR_CLUSTERED",
64
+ "THREE_D_BAR_STACKED",
65
+ "THREE_D_BAR_STACKED_100",
66
+ "CONE_BAR_CLUSTERED",
67
+ "CONE_BAR_STACKED",
68
+ "CONE_BAR_STACKED_100",
69
+ "CYLINDER_BAR_CLUSTERED",
70
+ "CYLINDER_BAR_STACKED",
71
+ "CYLINDER_BAR_STACKED_100",
72
+ "PYRAMID_BAR_CLUSTERED",
73
+ "PYRAMID_BAR_STACKED",
74
+ "PYRAMID_BAR_STACKED_100",
75
+ }
76
+ )
77
+
78
+
79
+ # Anchor strings accepted by ``add_picture`` / ``add_shape`` /
80
+ # ``add_textbox``. The first token is vertical, the second horizontal;
81
+ # ``"center"`` matches both axes (``center-center`` and bare
82
+ # ``"center"`` are equivalent). The variant spellings keep both UK and
83
+ # US conventions usable.
84
+ _VERTICAL_ANCHORS = {"top", "middle", "center", "centre", "bottom"}
85
+ _HORIZONTAL_ANCHORS = {"left", "center", "centre", "right"}
86
+
87
+
88
+ def _resolve_anchor(anchor: str) -> tuple[str, str]:
89
+ """Return ``(vertical, horizontal)`` from a hyphenated anchor string.
90
+
91
+ Accepts ``"top-left"``, ``"top-center"``, ``"top-centre"``,
92
+ ``"middle-left"``, ``"middle-center"``, ``"middle-right"``,
93
+ ``"center"`` (== ``"middle-center"``), ``"bottom-left"``,
94
+ ``"bottom-center"``, ``"bottom-right"``, etc. ``"center-left"``
95
+ is also accepted as a synonym for ``"middle-left"`` since it's
96
+ a common typo.
97
+ """
98
+ raw = anchor.strip().lower()
99
+ if raw in {"center", "centre"}:
100
+ return ("middle", "center")
101
+ if "-" not in raw:
102
+ raise ValueError(
103
+ f"anchor must be 'center' or 'vertical-horizontal' "
104
+ f"(e.g. 'top-right'); got {anchor!r}"
105
+ )
106
+ parts = [p.strip() for p in raw.split("-", 1)]
107
+ v, h = parts[0], parts[1]
108
+ # Accept "center-left" etc. as synonyms for "middle-left".
109
+ if v == "center" or v == "centre":
110
+ v = "middle"
111
+ if v not in _VERTICAL_ANCHORS or h not in _HORIZONTAL_ANCHORS:
112
+ raise ValueError(
113
+ f"anchor must be one of top|middle|bottom dash "
114
+ f"left|center|right; got {anchor!r}"
115
+ )
116
+ # Normalise "centre" → "center" on the horizontal half.
117
+ if h == "centre":
118
+ h = "center"
119
+ return (v, h)
120
+
121
+
122
+ def _compute_anchor_left_top(
123
+ anchor: str,
124
+ container_w: int,
125
+ container_h: int,
126
+ shape_w: int,
127
+ shape_h: int,
128
+ margin: int = 0,
129
+ ) -> tuple[int, int]:
130
+ """Compute ``(left, top)`` EMU values for a shape under `anchor`.
131
+
132
+ The shape is positioned inside a container of size
133
+ (`container_w`, `container_h`), with `margin` EMU between the
134
+ shape and the matching container edges (margin is ignored on the
135
+ centre axes — centred shapes don't need an outer margin).
136
+ """
137
+ v, h = _resolve_anchor(anchor)
138
+
139
+ if h == "left":
140
+ left = margin
141
+ elif h == "right":
142
+ left = container_w - margin - shape_w
143
+ else: # center
144
+ left = (container_w - shape_w) // 2
145
+
146
+ if v == "top":
147
+ top = margin
148
+ elif v == "bottom":
149
+ top = container_h - margin - shape_h
150
+ else: # middle
151
+ top = (container_h - shape_h) // 2
152
+
153
+ return (left, top)
154
+
155
+
156
+ def _container_box(shapetree, container) -> tuple[int, int, int, int]:
157
+ """Return the slide-relative ``(left, top, width, height)`` of a container.
158
+
159
+ Shapes added through ``slide.shapes.add_*`` always live in the
160
+ slide's ``<p:spTree>`` and therefore use slide-relative
161
+ coordinates; nesting a shape inside a parent shape on the slide
162
+ is purely *visual*, not structural. So when ``container`` is a
163
+ parent shape, we need both its position *and* its size to compute
164
+ correct anchor coordinates — otherwise "centre inside this card"
165
+ silently means "centre inside a card-sized box at the slide
166
+ origin", which is wrong for any container not at ``(0, 0)``.
167
+
168
+ `container` may be:
169
+
170
+ * ``None`` — use the slide; origin ``(0, 0)``, extents from
171
+ the presentation.
172
+ * A slide-like object exposing ``part.package.presentation_part``
173
+ — same as ``None``, but resolves through the supplied object.
174
+ * Any object with ``.width`` and ``.height`` attributes; if it
175
+ also exposes ``.left`` / ``.top`` (i.e. a real shape) those
176
+ are honoured, otherwise the origin defaults to ``(0, 0)``
177
+ (useful for synthetic / virtual containers).
178
+
179
+ Raises ``ValueError`` if no usable extents can be derived.
180
+ """
181
+ if container is None:
182
+ prs = shapetree.part.package.presentation_part.presentation
183
+ return (0, 0, int(prs.slide_width), int(prs.slide_height))
184
+ # Shape-like: width / height attributes (with optional left / top).
185
+ if hasattr(container, "width") and hasattr(container, "height"):
186
+ w, h = container.width, container.height
187
+ if w is not None and h is not None:
188
+ left = getattr(container, "left", 0) or 0
189
+ top = getattr(container, "top", 0) or 0
190
+ return (int(left), int(top), int(w), int(h))
191
+ # Slide-like: try the same path as the default branch.
192
+ if hasattr(container, "part"):
193
+ try:
194
+ prs = container.part.package.presentation_part.presentation
195
+ return (0, 0, int(prs.slide_width), int(prs.slide_height))
196
+ except AttributeError:
197
+ pass
198
+ raise ValueError(
199
+ "container must be None, a slide, or a shape with .width/.height"
200
+ )
201
+
202
+
203
+ def _container_extents(shapetree, container) -> tuple[int, int]:
204
+ """Back-compat wrapper — return ``(width, height)`` only.
205
+
206
+ Prefer :func:`_container_box` in new code; it also returns the
207
+ container's slide-relative origin, which the anchor helpers need
208
+ for non-origin containers.
209
+ """
210
+ _, _, w, h = _container_box(shapetree, container)
211
+ return (w, h)
212
+
213
+
214
+ class _LintGroupScope:
215
+ """Context manager returned by :meth:`_BaseShapes.lint_group_scope`.
216
+
217
+ On enter it snapshots the current shape count of the tree; on
218
+ exit it tags every shape added in between with
219
+ ``shape.lint_group = name``. A diff-based approach keeps the
220
+ implementation independent of which ``add_*`` method the caller
221
+ uses — the proxy doesn't have to wrap each one individually, and
222
+ custom helpers that ultimately call into the tree just work.
223
+ """
224
+
225
+ def __init__(self, shapetree, name):
226
+ self._shapetree = shapetree
227
+ self._name = name
228
+ self._snapshot = 0
229
+
230
+ def __enter__(self):
231
+ self._snapshot = len(list(self._shapetree._iter_member_elms()))
232
+ return self._shapetree
233
+
234
+ def __exit__(self, exc_type, exc, tb):
235
+ # On exception, still tag — the shapes were added; bailing
236
+ # would leave them as untagged "real" overlaps in the lint
237
+ # report, which is the worse default.
238
+ from pptx2.shapes.base import BaseShape
239
+
240
+ elms = list(self._shapetree._iter_member_elms())
241
+ added = elms[self._snapshot :]
242
+ if not added:
243
+ return False # nothing to tag, propagate any exception
244
+
245
+ name = self._name
246
+ if name is None:
247
+ existing_groups = set()
248
+ for elm in elms:
249
+ shape = self._shapetree._shape_factory(elm)
250
+ tag = getattr(shape, "lint_group", None)
251
+ if tag:
252
+ existing_groups.add(tag)
253
+ n = 1
254
+ while f"design-group-{n}" in existing_groups:
255
+ n += 1
256
+ name = f"design-group-{n}"
257
+
258
+ for elm in added:
259
+ shape: BaseShape = self._shapetree._shape_factory(elm)
260
+ try:
261
+ shape.lint_group = name
262
+ except (AttributeError, NotImplementedError):
263
+ # Some shape kinds don't carry a cNvPr (rare); skip.
264
+ pass
265
+ return False # never suppress exceptions
266
+
267
+
268
+ def _apply_horizontal_bar_default(graphic_frame, chart_type) -> None:
269
+ """Reverse the category axis on horizontal-bar chart types.
270
+
271
+ OOXML's default places ``category[0]`` at the bottom of the axis,
272
+ which makes a chart fed ``["A", "B", "C"]`` render with ``A`` at
273
+ the bottom — counterintuitive for natural top-to-bottom reading.
274
+ This flips it for the bar types where users almost always want
275
+ top-to-bottom; column charts keep their default left-to-right
276
+ ordering. Caller can override post-creation if the legacy default
277
+ is wanted.
278
+ """
279
+ if getattr(chart_type, "name", None) not in _HORIZONTAL_BAR_CHART_NAMES:
280
+ return
281
+ try:
282
+ graphic_frame.chart.category_axis.reverse_order = True
283
+ except (AttributeError, ValueError):
284
+ # Defensive: never break chart creation on a styling tweak.
285
+ pass
286
+
287
+
288
+ # +-- _BaseShapes
289
+ # | |
290
+ # | +-- _BaseGroupShapes
291
+ # | | |
292
+ # | | +-- GroupShapes
293
+ # | | |
294
+ # | | +-- SlideShapes
295
+ # | |
296
+ # | +-- LayoutShapes
297
+ # | |
298
+ # | +-- MasterShapes
299
+ # | |
300
+ # | +-- NotesSlideShapes
301
+ # | |
302
+ # | +-- BasePlaceholders
303
+ # | |
304
+ # | +-- LayoutPlaceholders
305
+ # | |
306
+ # | +-- MasterPlaceholders
307
+ # | |
308
+ # | +-- NotesSlidePlaceholders
309
+ # |
310
+ # +-- SlidePlaceholders
311
+
312
+
313
+ def _endpoint_box(target):
314
+ """Return a ``(left, top, width, height)`` tuple for an arrow endpoint.
315
+
316
+ Accepts a ``BaseShape``, a ``BBox`` (or any 4-iterable thereof), or
317
+ ``None`` (in which case ``None`` is returned). Coordinate tuples
318
+ are not treated as boxes — callers pass them through verbatim via
319
+ the ``_resolve_endpoint`` path that follows.
320
+ """
321
+ from pptx2.geometry import BBox
322
+
323
+ if target is None:
324
+ return None
325
+ if isinstance(target, BaseShape):
326
+ return (int(target.left), int(target.top), int(target.width), int(target.height))
327
+ if isinstance(target, BBox):
328
+ return (int(target.left), int(target.top), int(target.width), int(target.height))
329
+ return None
330
+
331
+
332
+ def _resolve_endpoint(target, *, opposite, side: str, inset_emu: int):
333
+ """Return ``(x, y)`` for an arrow endpoint, snapping to the right edge.
334
+
335
+ * If ``target`` is a coordinate tuple ``(x, y)``, return it verbatim.
336
+ * If ``target`` is a Shape / BBox, choose a mid-edge anchor (the one
337
+ facing ``opposite``, unless ``side`` is a specific edge name), pull
338
+ the resulting point inward by ``inset_emu`` so an arrowhead won't
339
+ bleed past the target's stroke.
340
+ """
341
+ if isinstance(target, (tuple, list)) and len(target) == 2:
342
+ return (int(target[0]), int(target[1]))
343
+
344
+ box = _endpoint_box(target)
345
+ if box is None:
346
+ raise TypeError(
347
+ "arrow endpoint must be (x, y), a Shape, or a BBox; got %r"
348
+ % (target,)
349
+ )
350
+ left, top, width, height = box
351
+
352
+ if side in (None, "auto"):
353
+ opp_box = _endpoint_box(opposite)
354
+ if isinstance(opposite, (tuple, list)) and len(opposite) == 2:
355
+ opp_cx, opp_cy = int(opposite[0]), int(opposite[1])
356
+ elif opp_box is not None:
357
+ opp_cx = opp_box[0] + opp_box[2] // 2
358
+ opp_cy = opp_box[1] + opp_box[3] // 2
359
+ else:
360
+ opp_cx, opp_cy = left + width // 2, top + height // 2
361
+
362
+ cx = left + width // 2
363
+ cy = top + height // 2
364
+ # Pick whichever edge the opposite endpoint is closest to.
365
+ dx = opp_cx - cx
366
+ dy = opp_cy - cy
367
+ if abs(dx) >= abs(dy):
368
+ side = "right" if dx >= 0 else "left"
369
+ else:
370
+ side = "bottom" if dy >= 0 else "top"
371
+
372
+ if side == "right":
373
+ return (left + width - inset_emu, top + height // 2)
374
+ if side == "left":
375
+ return (left + inset_emu, top + height // 2)
376
+ if side == "top":
377
+ return (left + width // 2, top + inset_emu)
378
+ if side == "bottom":
379
+ return (left + width // 2, top + height - inset_emu)
380
+ raise ValueError(
381
+ f"side must be 'top'/'right'/'bottom'/'left'/'auto'; got {side!r}"
382
+ )
383
+
384
+
385
+ class _BaseShapes(ParentedElementProxy):
386
+ """Base class for a shape collection appearing in a slide-type object.
387
+
388
+ Subclasses include Slide, SlideLayout, and SlideMaster. Provides common methods.
389
+ """
390
+
391
+ def __init__(self, spTree: CT_GroupShape, parent: ProvidesPart):
392
+ super(_BaseShapes, self).__init__(spTree, parent)
393
+ self._spTree = spTree
394
+ self._turbo_add_enabled = False
395
+
396
+ def __getitem__(self, idx: int) -> BaseShape:
397
+ """Return shape at `idx` in sequence, e.g. `shapes[2]`."""
398
+ shape_elms = list(self._iter_member_elms())
399
+ try:
400
+ shape_elm = shape_elms[idx]
401
+ except IndexError:
402
+ raise IndexError("shape index out of range")
403
+ return self._shape_factory(shape_elm)
404
+
405
+ def __iter__(self) -> Iterator[BaseShape]:
406
+ """Generate a reference to each shape in the collection, in sequence."""
407
+ for shape_elm in self._iter_member_elms():
408
+ yield self._shape_factory(shape_elm)
409
+
410
+ def __len__(self) -> int:
411
+ """Return count of shapes in this shape tree.
412
+
413
+ A group shape contributes 1 to the total, without regard to the number of shapes contained
414
+ in the group.
415
+ """
416
+ shape_elms = list(self._iter_member_elms())
417
+ return len(shape_elms)
418
+
419
+ def lint_group_scope(self, name: str | None = None):
420
+ """Context manager that auto-tags shapes added inside it.
421
+
422
+ Every shape appended to this shape tree between ``__enter__``
423
+ and ``__exit__`` is tagged with ``shape.lint_group = name`` on
424
+ exit, so the linter treats them as one intentional overlap
425
+ group. Use it for hand-built composite UI elements (progress
426
+ bars, gauges, badges, custom KPI tiles) where the constituent
427
+ shapes deliberately overlap and the auto-emitted
428
+ ``ShapeCollision`` warnings would be noise::
429
+
430
+ with slide.shapes.lint_group_scope(name="progress_bar") as g:
431
+ track = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, ...)
432
+ fill = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, ...)
433
+ # both shapes now carry lint_group="progress_bar".
434
+
435
+ ``g`` (the yielded value) is this :class:`_BaseShapes`
436
+ instance, so calls inside the ``with`` block can also use
437
+ ``g.add_shape(...)`` for clarity.
438
+
439
+ When ``name`` is omitted a unique-on-this-tree name is
440
+ auto-generated (``"design-group-N"`` with smallest available
441
+ N), matching :meth:`Slide.lint_group_overlaps`.
442
+ """
443
+ return _LintGroupScope(self, name)
444
+
445
+ def clone_placeholder(self, placeholder: LayoutPlaceholder) -> None:
446
+ """Add a new placeholder shape based on `placeholder`."""
447
+ sp = placeholder.element
448
+ ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx)
449
+ id_ = self._next_shape_id
450
+ name = self._next_ph_name(ph_type, id_, orient)
451
+ self._spTree.add_placeholder(id_, name, ph_type, orient, sz, idx)
452
+
453
+ def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str:
454
+ """Return the base name for a placeholder of `ph_type` in this shape collection.
455
+
456
+ There is some variance between slide types, for example a notes slide uses a different
457
+ name for the body placeholder, so this method can be overriden by subclasses.
458
+ """
459
+ return {
460
+ PP_PLACEHOLDER.BITMAP: "ClipArt Placeholder",
461
+ PP_PLACEHOLDER.BODY: "Text Placeholder",
462
+ PP_PLACEHOLDER.CENTER_TITLE: "Title",
463
+ PP_PLACEHOLDER.CHART: "Chart Placeholder",
464
+ PP_PLACEHOLDER.DATE: "Date Placeholder",
465
+ PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
466
+ PP_PLACEHOLDER.HEADER: "Header Placeholder",
467
+ PP_PLACEHOLDER.MEDIA_CLIP: "Media Placeholder",
468
+ PP_PLACEHOLDER.OBJECT: "Content Placeholder",
469
+ PP_PLACEHOLDER.ORG_CHART: "SmartArt Placeholder",
470
+ PP_PLACEHOLDER.PICTURE: "Picture Placeholder",
471
+ PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
472
+ PP_PLACEHOLDER.SUBTITLE: "Subtitle",
473
+ PP_PLACEHOLDER.TABLE: "Table Placeholder",
474
+ PP_PLACEHOLDER.TITLE: "Title",
475
+ }[ph_type]
476
+
477
+ @property
478
+ def turbo_add_enabled(self) -> bool:
479
+ """Deprecated no-op. Read/Write.
480
+
481
+ Shape-id allocation is now always cached on the shape-tree element
482
+ (see :meth:`CT_GroupShape.allocate_shape_id`), so the historical
483
+ opt-in fast path is effectively always on. The setter is kept as a
484
+ no-op for back-compat and emits a :class:`DeprecationWarning`; the
485
+ getter still reflects whatever the user last assigned for the
486
+ benefit of round-tripping their own code.
487
+ """
488
+ return self._turbo_add_enabled
489
+
490
+ @turbo_add_enabled.setter
491
+ def turbo_add_enabled(self, value: bool):
492
+ warnings.warn(
493
+ "turbo_add_enabled is a deprecated no-op; shape-id allocation is now"
494
+ " always O(1) per add. The setting will be removed in a future release.",
495
+ DeprecationWarning,
496
+ stacklevel=2,
497
+ )
498
+ self._turbo_add_enabled = bool(value)
499
+
500
+ @staticmethod
501
+ def _is_member_elm(shape_elm: ShapeElement) -> bool:
502
+ """Return true if `shape_elm` represents a member of this collection, False otherwise."""
503
+ return True
504
+
505
+ def _iter_member_elms(self) -> Iterator[ShapeElement]:
506
+ """Generate each child of the `p:spTree` element that corresponds to a shape.
507
+
508
+ Items appear in XML document order.
509
+ """
510
+ for shape_elm in self._spTree.iter_shape_elms():
511
+ if self._is_member_elm(shape_elm):
512
+ yield shape_elm
513
+
514
+ def _next_ph_name(self, ph_type: PP_PLACEHOLDER, id: int, orient: str) -> str:
515
+ """Next unique placeholder name for placeholder shape of type `ph_type`.
516
+
517
+ Usually will be standard placeholder root name suffixed with id-1, e.g.
518
+ _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> 'Table Placeholder 3'. The number is
519
+ incremented as necessary to make the name unique within the collection. If `orient` is
520
+ `'vert'`, the placeholder name is prefixed with `'Vertical '`.
521
+ """
522
+ basename = self.ph_basename(ph_type)
523
+
524
+ # prefix rootname with 'Vertical ' if orient is 'vert'
525
+ if orient == ST_Direction.VERT:
526
+ basename = "Vertical %s" % basename
527
+
528
+ # increment numpart as necessary to make name unique
529
+ numpart = id - 1
530
+ names = self._spTree.xpath("//p:cNvPr/@name")
531
+ while True:
532
+ name = "%s %d" % (basename, numpart)
533
+ if name not in names:
534
+ break
535
+ numpart += 1
536
+
537
+ return name
538
+
539
+ @property
540
+ def _next_shape_id(self) -> int:
541
+ """Return a unique shape id suitable for use with a new shape.
542
+
543
+ The returned id is 1 greater than the maximum shape id used so far. In practice, the
544
+ minimum id is 2 because the spTree element is always assigned id="1".
545
+ """
546
+ return self._spTree.allocate_shape_id()
547
+
548
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
549
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
550
+ return BaseShapeFactory(shape_elm, self)
551
+
552
+
553
+ class _BaseGroupShapes(_BaseShapes):
554
+ """Base class for shape-trees that can add shapes."""
555
+
556
+ part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride]
557
+ _element: CT_GroupShape
558
+
559
+ def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart):
560
+ super(_BaseGroupShapes, self).__init__(grpSp, parent)
561
+ self._grpSp = grpSp
562
+
563
+ def add_chart(
564
+ self,
565
+ chart_type: XL_CHART_TYPE,
566
+ x: Length,
567
+ y: Length,
568
+ cx: Length,
569
+ cy: Length,
570
+ chart_data: ChartData,
571
+ ) -> Chart:
572
+ """Add a new chart of `chart_type` to the slide.
573
+
574
+ The chart is positioned at (`x`, `y`), has size (`cx`, `cy`), and depicts `chart_data`.
575
+ `chart_type` is one of the :ref:`XlChartType` enumeration values. `chart_data` is a
576
+ |ChartData| object populated with the categories and series values for the chart.
577
+
578
+ Note that a |GraphicFrame| shape object is returned, not the |Chart| object contained in
579
+ that graphic frame shape. The chart object may be accessed using the :attr:`chart`
580
+ property of the returned |GraphicFrame| object.
581
+
582
+ For horizontal bar charts (``BAR_*`` enum members), the category
583
+ axis is reversed by default so the first category renders at the
584
+ top — matching the natural reading order. Column charts retain
585
+ their default left-to-right ordering. Override by setting
586
+ ``chart.category_axis.reverse_order = False`` after creation.
587
+ """
588
+ x, y = _coerce_emu(x), _coerce_emu(y)
589
+ cx, cy = _coerce_emu(cx), _coerce_emu(cy)
590
+ rId = self.part.add_chart_part(chart_type, chart_data)
591
+ graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy)
592
+ self._recalculate_extents()
593
+ shape = self._shape_factory(graphicFrame)
594
+ _apply_horizontal_bar_default(shape, chart_type)
595
+ return cast("Chart", shape)
596
+
597
+ def add_connector(
598
+ self,
599
+ connector_type: MSO_CONNECTOR_TYPE,
600
+ begin_x: Length,
601
+ begin_y: Length,
602
+ end_x: Length,
603
+ end_y: Length,
604
+ ) -> Connector:
605
+ """Add a newly created connector shape to the end of this shape tree.
606
+
607
+ `connector_type` is a member of the :ref:`MsoConnectorType` enumeration and the end-point
608
+ values are specified as EMU values. The returned connector is of type `connector_type` and
609
+ has begin and end points as specified.
610
+ """
611
+ begin_x, begin_y = _coerce_emu(begin_x), _coerce_emu(begin_y)
612
+ end_x, end_y = _coerce_emu(end_x), _coerce_emu(end_y)
613
+ cxnSp = self._add_cxnSp(connector_type, begin_x, begin_y, end_x, end_y)
614
+ self._recalculate_extents()
615
+ return cast(Connector, self._shape_factory(cxnSp))
616
+
617
+ def add_group_shape(self, shapes: Iterable[BaseShape] = ()) -> GroupShape:
618
+ """Return a |GroupShape| object newly appended to this shape tree.
619
+
620
+ The group shape is empty and must be populated with shapes using methods on its shape
621
+ tree, available on its `.shapes` property. The position and extents of the group shape are
622
+ determined by the shapes it contains; its position and extents are recalculated each time
623
+ a shape is added to it.
624
+ """
625
+ shapes = tuple(shapes)
626
+ grpSp = self._element.add_grpSp()
627
+ for shape in shapes:
628
+ grpSp.insert_element_before(
629
+ shape._element, "p:extLst" # pyright: ignore[reportPrivateUsage]
630
+ )
631
+ if shapes:
632
+ grpSp.recalculate_extents()
633
+ return cast(GroupShape, self._shape_factory(grpSp))
634
+
635
+ def add_ole_object(
636
+ self,
637
+ object_file: str | IO[bytes],
638
+ prog_id: str,
639
+ left: Length,
640
+ top: Length,
641
+ width: Length | None = None,
642
+ height: Length | None = None,
643
+ icon_file: str | IO[bytes] | None = None,
644
+ icon_width: Length | None = None,
645
+ icon_height: Length | None = None,
646
+ ) -> GraphicFrame:
647
+ """Return newly-created GraphicFrame shape embedding `object_file`.
648
+
649
+ The returned graphic-frame shape contains `object_file` as an embedded OLE object. It is
650
+ displayed as an icon at `left`, `top` with size `width`, `height`. `width` and `height`
651
+ may be omitted when `prog_id` is a member of `PROG_ID`, in which case the default icon
652
+ size is used. This is advised for best appearance where applicable because it avoids an
653
+ icon with a "stretched" appearance.
654
+
655
+ `object_file` may either be a str path to a file or file-like object (such as
656
+ `io.BytesIO`) containing the bytes of the object to be embedded (such as an Excel file).
657
+
658
+ `prog_id` can be either a member of `pptx2.enum.shapes.PROG_ID` or a str value like
659
+ `"Adobe.Exchange.7"` determined by inspecting the XML generated by PowerPoint for an
660
+ object of the desired type.
661
+
662
+ `icon_file` may either be a str path to an image file or a file-like object containing the
663
+ image. The image provided will be displayed in lieu of the OLE object; double-clicking on
664
+ the image opens the object (subject to operating-system limitations). The image file can
665
+ be any supported image file. Those produced by PowerPoint itself are generally EMF and can
666
+ be harvested from a PPTX package that embeds such an object. PNG and JPG also work fine.
667
+
668
+ `icon_width` and `icon_height` are `Length` values (e.g. Emu() or Inches()) that describe
669
+ the size of the icon image within the shape. These should be omitted unless a custom
670
+ `icon_file` is provided. The dimensions must be discovered by inspecting the XML.
671
+ Automatic resizing of the OLE-object shape can occur when the icon is double-clicked if
672
+ these values are not as set by PowerPoint. This behavior may only manifest in the Windows
673
+ version of PowerPoint.
674
+ """
675
+ left, top = _coerce_emu(left), _coerce_emu(top)
676
+ width, height = _coerce_emu(width), _coerce_emu(height)
677
+ icon_width = _coerce_emu(icon_width)
678
+ icon_height = _coerce_emu(icon_height)
679
+ graphicFrame = _OleObjectElementCreator.graphicFrame(
680
+ self,
681
+ self._next_shape_id,
682
+ object_file,
683
+ prog_id,
684
+ left,
685
+ top,
686
+ width,
687
+ height,
688
+ icon_file,
689
+ icon_width,
690
+ icon_height,
691
+ )
692
+ self._spTree.append(graphicFrame)
693
+ self._recalculate_extents()
694
+ return cast(GraphicFrame, self._shape_factory(graphicFrame))
695
+
696
+ def add_picture(
697
+ self,
698
+ image_file: str | IO[bytes],
699
+ left: Length = Emu(0),
700
+ top: Length = Emu(0),
701
+ width: Length | None = None,
702
+ height: Length | None = None,
703
+ *,
704
+ anchor: str | None = None,
705
+ margin: Length = Emu(0),
706
+ container=None,
707
+ ) -> Picture:
708
+ """Add picture shape displaying image in `image_file`.
709
+
710
+ `image_file` can be either a path to a file (a string) or a file-like object. The picture
711
+ is positioned with its top-left corner at (`left`, `top`). If `width` and `height` are
712
+ both |None|, the native size of the image is used. If only one of `width` or `height` is
713
+ used, the unspecified dimension is calculated to preserve the aspect ratio of the image.
714
+ If both are specified, the picture is stretched to fit, without regard to its native
715
+ aspect ratio.
716
+
717
+ When `anchor` is given, ``left`` and ``top`` are recomputed
718
+ from the anchor + the picture's rendered dimensions. The
719
+ common case "logo at bottom-right with a 0.25" margin"
720
+ becomes a single call::
721
+
722
+ slide.shapes.add_picture(
723
+ logo_path,
724
+ anchor="bottom-right",
725
+ margin=Inches(0.25),
726
+ height=Inches(0.32),
727
+ )
728
+
729
+ `anchor` is one of ``"top-left"``, ``"top-center"``,
730
+ ``"top-right"``, ``"middle-left"``, ``"middle-center"`` (or
731
+ bare ``"center"``), ``"middle-right"``, ``"bottom-left"``,
732
+ ``"bottom-center"``, ``"bottom-right"``. Either spelling of
733
+ "center"/"centre" is accepted.
734
+
735
+ `container` is the box the anchor is relative to. ``None``
736
+ (the default) anchors against the slide; pass a parent shape
737
+ (or any object with ``.width`` / ``.height``) to anchor inside
738
+ a card / group / placeholder. `margin` is the gap between the
739
+ picture and the matching container edges; ignored on the
740
+ centred axes.
741
+ """
742
+ left, top = _coerce_emu(left), _coerce_emu(top)
743
+ width, height = _coerce_emu(width), _coerce_emu(height)
744
+ image_part, rId = self.part.get_or_add_image_part(image_file)
745
+ pic = self._add_pic_from_image_part(image_part, rId, left, top, width, height)
746
+ self._recalculate_extents()
747
+ picture = cast(Picture, self._shape_factory(pic))
748
+ if anchor is not None:
749
+ cl, ct, cw, ch = _container_box(self, container)
750
+ new_left, new_top = _compute_anchor_left_top(
751
+ anchor, cw, ch, int(picture.width), int(picture.height), int(margin)
752
+ )
753
+ picture.left = Emu(cl + new_left)
754
+ picture.top = Emu(ct + new_top)
755
+ return picture
756
+
757
+ def add_svg_picture(
758
+ self,
759
+ svg_file,
760
+ left: Length,
761
+ top: Length,
762
+ width: Length | None = None,
763
+ height: Length | None = None,
764
+ *,
765
+ png_fallback=None,
766
+ ) -> Picture:
767
+ """Add an SVG picture with a PNG fallback (Office 2016+ compatible).
768
+
769
+ Modern PowerPoint requires every embedded SVG to ship alongside
770
+ a raster fallback: the slide's ``<a:blip>`` references the PNG
771
+ and an ``<asvg:svgBlip>`` extension references the SVG. This
772
+ method handles both halves.
773
+
774
+ `svg_file` is a path, file-like object, or raw ``bytes`` blob
775
+ of SVG markup. `png_fallback` is the raster fallback in the
776
+ same forms. When `png_fallback` is ``None`` the SVG is
777
+ rasterised via ``cairosvg`` (an *optional* dependency); a
778
+ clear error is raised if it isn't installed.
779
+
780
+ `left` / `top` / `width` / `height` work exactly as in
781
+ :meth:`add_picture` — both extents default to the rasterised
782
+ PNG's native size when omitted.
783
+ """
784
+ left, top = _coerce_emu(left), _coerce_emu(top)
785
+ width, height = _coerce_emu(width), _coerce_emu(height)
786
+ from io import BytesIO
787
+
788
+ from pptx2._svg import (
789
+ add_svg_blip_extension,
790
+ add_svg_image_part,
791
+ load_image_blob,
792
+ looks_like_svg,
793
+ rasterize_svg,
794
+ )
795
+
796
+ svg_blob, svg_filename = load_image_blob(svg_file)
797
+ if not looks_like_svg(svg_blob):
798
+ raise ValueError(
799
+ "svg_file does not appear to contain SVG markup; pass an "
800
+ "SVG path, file-like, or bytes blob."
801
+ )
802
+
803
+ if png_fallback is None:
804
+ png_blob = rasterize_svg(svg_blob)
805
+ else:
806
+ png_blob, _ = load_image_blob(png_fallback)
807
+
808
+ # Register the PNG fallback through the existing
809
+ # Pillow-aware pipeline so dpi / size detection still works.
810
+ png_part, png_rId = self.part.get_or_add_image_part(BytesIO(png_blob))
811
+ pic = self._add_pic_from_image_part(png_part, png_rId, left, top, width, height)
812
+
813
+ # Register the SVG and inject the blip extension.
814
+ _, svg_rId = add_svg_image_part(self.part, svg_blob, svg_filename)
815
+ add_svg_blip_extension(pic, svg_rId)
816
+
817
+ self._recalculate_extents()
818
+ return cast(Picture, self._shape_factory(pic))
819
+
820
+ def add_shape(
821
+ self,
822
+ autoshape_type_id: MSO_SHAPE,
823
+ left: Length,
824
+ top: Length,
825
+ width: Length,
826
+ height: Length,
827
+ *,
828
+ anchor: str | None = None,
829
+ margin: Length = Emu(0),
830
+ container=None,
831
+ ) -> Shape:
832
+ """Return new |Shape| object appended to this shape tree.
833
+
834
+ `autoshape_type_id` is a member of :ref:`MsoAutoShapeType` e.g. `MSO_SHAPE.RECTANGLE`
835
+ specifying the type of shape to be added. The remaining arguments specify the new shape's
836
+ position and size.
837
+
838
+ See :meth:`add_picture` for the semantics of `anchor`,
839
+ `margin`, and `container`. With `anchor` set, the supplied
840
+ `left` / `top` are overwritten after creation by the
841
+ anchor-derived position.
842
+ """
843
+ left, top = _coerce_emu(left), _coerce_emu(top)
844
+ width, height = _coerce_emu(width), _coerce_emu(height)
845
+ autoshape_type = AutoShapeType(autoshape_type_id)
846
+ sp = self._add_sp(autoshape_type, left, top, width, height)
847
+ self._recalculate_extents()
848
+ shape = cast(Shape, self._shape_factory(sp))
849
+ if anchor is not None:
850
+ cl, ct, cw, ch = _container_box(self, container)
851
+ new_left, new_top = _compute_anchor_left_top(
852
+ anchor, cw, ch, int(shape.width), int(shape.height), int(margin)
853
+ )
854
+ shape.left = Emu(cl + new_left)
855
+ shape.top = Emu(ct + new_top)
856
+ return shape
857
+
858
+ def add_textbox(
859
+ self,
860
+ left: Length,
861
+ top: Length,
862
+ width: Length,
863
+ height: Length,
864
+ *,
865
+ anchor: str | None = None,
866
+ margin: Length = Emu(0),
867
+ container=None,
868
+ ) -> Shape:
869
+ """Return newly added text box shape appended to this shape tree.
870
+
871
+ The text box is of the specified size, located at the specified position on the slide.
872
+
873
+ See :meth:`add_picture` for `anchor` / `margin` / `container`
874
+ semantics.
875
+ """
876
+ left, top = _coerce_emu(left), _coerce_emu(top)
877
+ width, height = _coerce_emu(width), _coerce_emu(height)
878
+ sp = self._add_textbox_sp(left, top, width, height)
879
+ self._recalculate_extents()
880
+ textbox = cast(Shape, self._shape_factory(sp))
881
+ if anchor is not None:
882
+ cl, ct, cw, ch = _container_box(self, container)
883
+ new_left, new_top = _compute_anchor_left_top(
884
+ anchor, cw, ch, int(textbox.width), int(textbox.height), int(margin)
885
+ )
886
+ textbox.left = Emu(cl + new_left)
887
+ textbox.top = Emu(ct + new_top)
888
+ return textbox
889
+
890
+ def add_text(
891
+ self,
892
+ *bbox_or_positional,
893
+ text: str = "",
894
+ font: str | None = None,
895
+ size_pt: float | None = None,
896
+ bold: bool | None = None,
897
+ italic: bool | None = None,
898
+ color=None,
899
+ align: str | None = None,
900
+ anchor: str | None = None,
901
+ margin_pt: float | tuple[float, float, float, float] | None = None,
902
+ word_wrap: bool | None = True,
903
+ ) -> Shape:
904
+ """Add a textbox carrying *text* with one-call styling.
905
+
906
+ Accepts either a :class:`~pptx2.geometry.BBox` positionally
907
+ or the four ``(left, top, width, height)`` lengths::
908
+
909
+ slide.shapes.add_text(bbox, text="Hello", size_pt=24, bold=True,
910
+ color="#0B5CFF", align="center")
911
+ slide.shapes.add_text(Inches(1), Inches(2), Inches(4), Inches(1),
912
+ text="Hello")
913
+
914
+ Keyword args:
915
+
916
+ * ``font`` — typeface name (e.g. ``"Inter"``); ``None`` inherits.
917
+ * ``size_pt`` — font size in points; ``None`` inherits.
918
+ * ``bold`` / ``italic`` — ``True``/``False``/``None``.
919
+ * ``color`` — any "color-like" (``"#RRGGBB"``, ``RGBColor``,
920
+ ``(r, g, b)``).
921
+ * ``align`` — ``"left"`` / ``"center"`` / ``"right"`` /
922
+ ``"justify"``; ``None`` inherits.
923
+ * ``anchor`` — vertical anchor: ``"top"`` / ``"middle"`` /
924
+ ``"bottom"``; ``None`` inherits.
925
+ * ``margin_pt`` — uniform margin in points, or a 4-tuple
926
+ ``(top, right, bottom, left)``.
927
+ * ``word_wrap`` — defaults to ``True``.
928
+
929
+ Returns the textbox :class:`Shape` so further mutation works as
930
+ normal.
931
+ """
932
+ from pptx2._textstyle import apply_margins, apply_text_style, coerce_anchor
933
+ from pptx2.geometry import BBox
934
+
935
+ if len(bbox_or_positional) == 1 and isinstance(bbox_or_positional[0], BBox):
936
+ box = bbox_or_positional[0]
937
+ left, top, width, height = box.left, box.top, box.width, box.height
938
+ elif len(bbox_or_positional) == 4:
939
+ left, top, width, height = bbox_or_positional
940
+ else:
941
+ raise TypeError(
942
+ "add_text(): pass either a BBox or (left, top, width, "
943
+ "height); got %d positional arg(s)" % len(bbox_or_positional)
944
+ )
945
+
946
+ shape = self.add_textbox(left, top, width, height)
947
+ tf = shape.text_frame
948
+ if word_wrap is not None:
949
+ tf.word_wrap = bool(word_wrap)
950
+
951
+ if margin_pt is not None:
952
+ if isinstance(margin_pt, (tuple, list)) and len(margin_pt) != 4:
953
+ raise ValueError(
954
+ "margin_pt tuple must have 4 elements (top, right, bottom, left)"
955
+ )
956
+ apply_margins(tf, margin_pt)
957
+
958
+ if anchor is not None:
959
+ tf.vertical_anchor = coerce_anchor(anchor)
960
+
961
+ tf.text = text or ""
962
+
963
+ apply_text_style(
964
+ tf,
965
+ font=font,
966
+ size_pt=size_pt,
967
+ bold=bold,
968
+ italic=italic,
969
+ color=color,
970
+ align=align,
971
+ )
972
+
973
+ return shape
974
+
975
+ def add_equation(
976
+ self,
977
+ *bbox_or_positional,
978
+ latex: str,
979
+ display: bool = True,
980
+ font: str | None = None,
981
+ size_pt: float | None = None,
982
+ color=None,
983
+ align: str | None = "center",
984
+ anchor: str | None = "middle",
985
+ margin_pt: float | tuple[float, float, float, float] | None = None,
986
+ ) -> Shape:
987
+ """Add a text box containing a native PowerPoint equation from LaTeX.
988
+
989
+ Accepts either a :class:`~pptx2.geometry.BBox` or
990
+ ``(left, top, width, height)``::
991
+
992
+ slide.shapes.add_equation(bbox, latex=r"\\frac{a}{b}", size_pt=28)
993
+ slide.shapes.add_equation(
994
+ Inches(1), Inches(2), Inches(8), Inches(1.5),
995
+ latex=r"E = mc^2",
996
+ )
997
+
998
+ Requires ``latex2mathml`` and ``mathml2omml``
999
+ (``pip install "python-pptx2[math]"``). The equation is editable in
1000
+ PowerPoint's equation editor.
1001
+
1002
+ Keyword args match :meth:`add_text` for *font* / *size_pt* /
1003
+ *color* / *align* / *anchor* / *margin_pt*. *display* (default
1004
+ |True|) emits a display-math paragraph; set |False| for inline
1005
+ OMML.
1006
+ """
1007
+ from pptx2._textstyle import apply_margins, coerce_align, coerce_anchor
1008
+ from pptx2.geometry import BBox
1009
+
1010
+ if len(bbox_or_positional) == 1 and isinstance(bbox_or_positional[0], BBox):
1011
+ box = bbox_or_positional[0]
1012
+ left, top, width, height = box.left, box.top, box.width, box.height
1013
+ elif len(bbox_or_positional) == 4:
1014
+ left, top, width, height = bbox_or_positional
1015
+ else:
1016
+ raise TypeError(
1017
+ "add_equation(): pass either a BBox or (left, top, width, "
1018
+ "height); got %d positional arg(s)" % len(bbox_or_positional)
1019
+ )
1020
+
1021
+ shape = self.add_textbox(left, top, width, height)
1022
+ shape.name = "Equation %d" % shape.shape_id
1023
+ tf = shape.text_frame
1024
+ if anchor is not None:
1025
+ tf.vertical_anchor = coerce_anchor(anchor)
1026
+ if margin_pt is not None:
1027
+ apply_margins(tf, margin_pt)
1028
+
1029
+ paragraph = tf.paragraphs[0]
1030
+ if align is not None:
1031
+ paragraph.alignment = coerce_align(align)
1032
+ paragraph.add_math(
1033
+ latex,
1034
+ display=display,
1035
+ font=font,
1036
+ size_pt=size_pt,
1037
+ color=color,
1038
+ )
1039
+ return shape
1040
+
1041
+ def add_arrow(
1042
+ self,
1043
+ start,
1044
+ end,
1045
+ *,
1046
+ head: str | None = "triangle",
1047
+ tail: str | None = None,
1048
+ head_size: str = "medium",
1049
+ tail_size: str = "medium",
1050
+ color=None,
1051
+ weight_pt: float = 1.5,
1052
+ style: str = "solid",
1053
+ route: str = "straight",
1054
+ inset_pt: float = 0.0,
1055
+ end_side: str = "auto",
1056
+ start_side: str = "auto",
1057
+ ) -> Connector:
1058
+ """Add an arrow connector with proper arrowhead and inset routing.
1059
+
1060
+ ``start`` and ``end`` may each be:
1061
+
1062
+ * an ``(x, y)`` tuple of EMU or ``Length`` coordinates,
1063
+ * a :class:`~pptx2.geometry.BBox`,
1064
+ * a :class:`~pptx2.shapes.base.BaseShape`.
1065
+
1066
+ When the endpoint is a shape / BBox, the line is auto-routed to
1067
+ the nearest mid-edge (or the requested ``start_side`` / ``end_side``
1068
+ — one of ``"top"``, ``"right"``, ``"bottom"``, ``"left"``, ``"auto"``).
1069
+ ``inset_pt`` pulls the endpoint back from the shape edge by that many
1070
+ points so the arrowhead triangle doesn't bleed into a target box.
1071
+
1072
+ ``head`` and ``tail`` accept the short names from
1073
+ :class:`~pptx2.enum.dml.MSO_LINE_END_TYPE`:
1074
+ ``"triangle"``, ``"arrow"``, ``"stealth"``, ``"diamond"``,
1075
+ ``"oval"``, ``"none"`` (or ``None``).
1076
+
1077
+ ``style`` is ``"solid"`` / ``"dashed"`` / ``"dotted"``.
1078
+
1079
+ ``route`` is ``"straight"`` (default), ``"elbow"``, or
1080
+ ``"curved"`` — picks the underlying
1081
+ :class:`~pptx2.enum.shapes.MSO_CONNECTOR_TYPE`.
1082
+
1083
+ Returns the :class:`Connector` so callers can tweak further.
1084
+ """
1085
+ from pptx2._color import coerce_color
1086
+ from pptx2.enum.dml import (
1087
+ MSO_LINE_DASH_STYLE,
1088
+ MSO_LINE_END_SIZE,
1089
+ MSO_LINE_END_TYPE,
1090
+ )
1091
+ from pptx2.enum.shapes import MSO_CONNECTOR_TYPE
1092
+ from pptx2.util import Pt
1093
+
1094
+ _CONNECTOR = {
1095
+ "straight": MSO_CONNECTOR_TYPE.STRAIGHT,
1096
+ "elbow": MSO_CONNECTOR_TYPE.ELBOW,
1097
+ "curved": MSO_CONNECTOR_TYPE.CURVE,
1098
+ }
1099
+ if route not in _CONNECTOR:
1100
+ raise ValueError(
1101
+ f"route must be one of {sorted(_CONNECTOR)}; got {route!r}"
1102
+ )
1103
+
1104
+ _DASH = {
1105
+ "solid": MSO_LINE_DASH_STYLE.SOLID,
1106
+ "dashed": MSO_LINE_DASH_STYLE.DASH,
1107
+ "dotted": MSO_LINE_DASH_STYLE.ROUND_DOT,
1108
+ }
1109
+ if style not in _DASH:
1110
+ raise ValueError(
1111
+ f"style must be one of {sorted(_DASH)}; got {style!r}"
1112
+ )
1113
+
1114
+ _END_TYPE = {
1115
+ None: MSO_LINE_END_TYPE.NONE,
1116
+ "none": MSO_LINE_END_TYPE.NONE,
1117
+ "triangle": MSO_LINE_END_TYPE.TRIANGLE,
1118
+ "arrow": MSO_LINE_END_TYPE.ARROW,
1119
+ "stealth": MSO_LINE_END_TYPE.STEALTH,
1120
+ "diamond": MSO_LINE_END_TYPE.DIAMOND,
1121
+ "oval": MSO_LINE_END_TYPE.OVAL,
1122
+ }
1123
+ _END_SIZE = {
1124
+ "small": MSO_LINE_END_SIZE.SMALL,
1125
+ "medium": MSO_LINE_END_SIZE.MEDIUM,
1126
+ "large": MSO_LINE_END_SIZE.LARGE,
1127
+ }
1128
+
1129
+ # Strict validation up front — silently mapping an unknown name to
1130
+ # ``None`` would produce a headless line and make a typo
1131
+ # impossible to debug. Normalise case so ``"Triangle"`` works.
1132
+ def _norm(name):
1133
+ if name is None:
1134
+ return None
1135
+ if isinstance(name, str):
1136
+ return name.lower()
1137
+ return name
1138
+
1139
+ head_key = _norm(head)
1140
+ tail_key = _norm(tail)
1141
+ if head_key not in _END_TYPE:
1142
+ raise ValueError(
1143
+ f"head must be one of {sorted(k for k in _END_TYPE if k is not None)} "
1144
+ f"or None; got {head!r}"
1145
+ )
1146
+ if tail_key not in _END_TYPE:
1147
+ raise ValueError(
1148
+ f"tail must be one of {sorted(k for k in _END_TYPE if k is not None)} "
1149
+ f"or None; got {tail!r}"
1150
+ )
1151
+ head_size_key = _norm(head_size)
1152
+ tail_size_key = _norm(tail_size)
1153
+ if head_size_key not in _END_SIZE:
1154
+ raise ValueError(
1155
+ f"head_size must be one of {sorted(_END_SIZE)}; got {head_size!r}"
1156
+ )
1157
+ if tail_size_key not in _END_SIZE:
1158
+ raise ValueError(
1159
+ f"tail_size must be one of {sorted(_END_SIZE)}; got {tail_size!r}"
1160
+ )
1161
+
1162
+ bx, by = _resolve_endpoint(start, opposite=end, side=start_side, inset_emu=int(Pt(inset_pt)))
1163
+ ex, ey = _resolve_endpoint(end, opposite=start, side=end_side, inset_emu=int(Pt(inset_pt)))
1164
+
1165
+ conn = self.add_connector(_CONNECTOR[route], bx, by, ex, ey)
1166
+ line = conn.line
1167
+ line.width = Pt(float(weight_pt))
1168
+ line.dash_style = _DASH[style]
1169
+ if color is not None:
1170
+ line.color.rgb = coerce_color(color)
1171
+
1172
+ # The arrowhead is the tail by OOXML convention: tail = end-point.
1173
+ line.head_end.type = _END_TYPE[tail_key] # start
1174
+ line.tail_end.type = _END_TYPE[head_key] # end
1175
+ line.tail_end.width = _END_SIZE[head_size_key]
1176
+ line.tail_end.length = _END_SIZE[head_size_key]
1177
+ line.head_end.width = _END_SIZE[tail_size_key]
1178
+ line.head_end.length = _END_SIZE[tail_size_key]
1179
+ return conn
1180
+
1181
+ def build_freeform(
1182
+ self, start_x: float = 0, start_y: float = 0, scale: tuple[float, float] | float = 1.0
1183
+ ) -> FreeformBuilder:
1184
+ """Return |FreeformBuilder| object to specify a freeform shape.
1185
+
1186
+ The optional `start_x` and `start_y` arguments specify the starting pen position in local
1187
+ coordinates. They will be rounded to the nearest integer before use and each default to
1188
+ zero.
1189
+
1190
+ The optional `scale` argument specifies the size of local coordinates proportional to
1191
+ slide coordinates (EMU). If the vertical scale is different than the horizontal scale
1192
+ (local coordinate units are "rectangular"), a pair of numeric values can be provided as
1193
+ the `scale` argument, e.g. `scale=(1.0, 2.0)`. In this case the first number is
1194
+ interpreted as the horizontal (X) scale and the second as the vertical (Y) scale.
1195
+
1196
+ A convenient method for calculating scale is to divide a |Length| object by an equivalent
1197
+ count of local coordinate units, e.g. `scale = Inches(1)/1000` for 1000 local units per
1198
+ inch.
1199
+ """
1200
+ x_scale, y_scale = scale if isinstance(scale, tuple) else (scale, scale)
1201
+
1202
+ return FreeformBuilder.new(self, start_x, start_y, x_scale, y_scale)
1203
+
1204
+ def index(self, shape: BaseShape) -> int:
1205
+ """Return the index of `shape` in this sequence.
1206
+
1207
+ Raises |ValueError| if `shape` is not in the collection.
1208
+ """
1209
+ shape_elms = list(self._element.iter_shape_elms())
1210
+ return shape_elms.index(shape.element)
1211
+
1212
+ def _add_chart_graphicFrame(
1213
+ self, rId: str, x: Length, y: Length, cx: Length, cy: Length
1214
+ ) -> CT_GraphicalObjectFrame:
1215
+ """Return new `p:graphicFrame` element appended to this shape tree.
1216
+
1217
+ The `p:graphicFrame` element has the specified position and size and refers to the chart
1218
+ part identified by `rId`.
1219
+ """
1220
+ shape_id = self._next_shape_id
1221
+ name = "Chart %d" % (shape_id - 1)
1222
+ graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame(
1223
+ shape_id, name, rId, x, y, cx, cy
1224
+ )
1225
+ self._spTree.append(graphicFrame)
1226
+ return graphicFrame
1227
+
1228
+ def _add_cxnSp(
1229
+ self,
1230
+ connector_type: MSO_CONNECTOR_TYPE,
1231
+ begin_x: Length,
1232
+ begin_y: Length,
1233
+ end_x: Length,
1234
+ end_y: Length,
1235
+ ) -> CT_Connector:
1236
+ """Return a newly-added `p:cxnSp` element as specified.
1237
+
1238
+ The `p:cxnSp` element is for a connector of `connector_type` beginning at (`begin_x`,
1239
+ `begin_y`) and extending to (`end_x`, `end_y`).
1240
+ """
1241
+ id_ = self._next_shape_id
1242
+ name = "Connector %d" % (id_ - 1)
1243
+
1244
+ flipH, flipV = begin_x > end_x, begin_y > end_y
1245
+ x, y = min(begin_x, end_x), min(begin_y, end_y)
1246
+ cx, cy = abs(end_x - begin_x), abs(end_y - begin_y)
1247
+
1248
+ return self._element.add_cxnSp(id_, name, connector_type, x, y, cx, cy, flipH, flipV)
1249
+
1250
+ def _add_pic_from_image_part(
1251
+ self,
1252
+ image_part: ImagePart,
1253
+ rId: str,
1254
+ x: Length,
1255
+ y: Length,
1256
+ cx: Length | None,
1257
+ cy: Length | None,
1258
+ ) -> CT_Picture:
1259
+ """Return a newly appended `p:pic` element as specified.
1260
+
1261
+ The `p:pic` element displays the image in `image_part` with size and position specified by
1262
+ `x`, `y`, `cx`, and `cy`. The element is appended to the shape tree, causing it to be
1263
+ displayed first in z-order on the slide.
1264
+ """
1265
+ id_ = self._next_shape_id
1266
+ scaled_cx, scaled_cy = image_part.scale(cx, cy)
1267
+ name = "Picture %d" % (id_ - 1)
1268
+ desc = image_part.desc
1269
+ pic = self._grpSp.add_pic(id_, name, desc, rId, x, y, scaled_cx, scaled_cy)
1270
+ return pic
1271
+
1272
+ def _add_sp(
1273
+ self, autoshape_type: AutoShapeType, x: Length, y: Length, cx: Length, cy: Length
1274
+ ) -> CT_Shape:
1275
+ """Return newly-added `p:sp` element as specified.
1276
+
1277
+ `p:sp` element is of `autoshape_type` at position (`x`, `y`) and of size (`cx`, `cy`).
1278
+ """
1279
+ id_ = self._next_shape_id
1280
+ name = "%s %d" % (autoshape_type.basename, id_ - 1)
1281
+ sp = self._grpSp.add_autoshape(id_, name, autoshape_type.prst, x, y, cx, cy)
1282
+ return sp
1283
+
1284
+ def _add_textbox_sp(self, x: Length, y: Length, cx: Length, cy: Length) -> CT_Shape:
1285
+ """Return newly-appended textbox `p:sp` element.
1286
+
1287
+ Element has position (`x`, `y`) and size (`cx`, `cy`).
1288
+ """
1289
+ id_ = self._next_shape_id
1290
+ name = "TextBox %d" % (id_ - 1)
1291
+ sp = self._spTree.add_textbox(id_, name, x, y, cx, cy)
1292
+ return sp
1293
+
1294
+ def add_table(
1295
+ self,
1296
+ rows: int,
1297
+ cols: int,
1298
+ left: Length,
1299
+ top: Length,
1300
+ width: Length,
1301
+ height: Length,
1302
+ *,
1303
+ style: str = "default",
1304
+ ) -> GraphicFrame:
1305
+ """Add a |GraphicFrame| object containing a table.
1306
+
1307
+ The table has the specified number of `rows` and `cols` and the specified position and
1308
+ size. `width` is evenly distributed between the columns of the new table. Likewise,
1309
+ `height` is evenly distributed between the rows. Note that the `.table` property on the
1310
+ returned |GraphicFrame| shape must be used to access the enclosed |Table| object.
1311
+
1312
+ Available on a slide's shape tree and on a group's, so a table can be bundled into a
1313
+ group alongside a caption or badge.
1314
+
1315
+ ``style`` controls the inherited table-style flags applied at
1316
+ construction time:
1317
+
1318
+ * ``"default"`` (back-compat) — leave PowerPoint's
1319
+ inherited-style flags alone. Behaves as before this argument
1320
+ existed.
1321
+ * ``"clean"`` — disable every inherited style flag
1322
+ (``first_row``, ``first_col``, ``last_row``, ``last_col``,
1323
+ ``horz_banding``, ``vert_banding``). Use when applying custom
1324
+ cell borders or fills, since the inherited style otherwise
1325
+ overlays them and renders inconsistently across PowerPoint
1326
+ and LibreOffice.
1327
+ """
1328
+ if style not in ("default", "clean"):
1329
+ raise ValueError(f"style must be 'default' or 'clean'; got {style!r}")
1330
+ left, top = _coerce_emu(left), _coerce_emu(top)
1331
+ width, height = _coerce_emu(width), _coerce_emu(height)
1332
+ graphicFrame = self._add_graphicFrame_containing_table(rows, cols, left, top, width, height)
1333
+ self._recalculate_extents()
1334
+ shape = cast(GraphicFrame, self._shape_factory(graphicFrame))
1335
+ if style == "clean":
1336
+ tbl = shape.table
1337
+ tbl.first_row = False
1338
+ tbl.first_col = False
1339
+ tbl.last_row = False
1340
+ tbl.last_col = False
1341
+ tbl.horz_banding = False
1342
+ tbl.vert_banding = False
1343
+ return shape
1344
+
1345
+ def add_movie(
1346
+ self,
1347
+ movie_file: str | IO[bytes],
1348
+ left: Length,
1349
+ top: Length,
1350
+ width: Length,
1351
+ height: Length,
1352
+ poster_frame_image: str | IO[bytes] | None = None,
1353
+ mime_type: str = CT.VIDEO,
1354
+ ) -> GraphicFrame:
1355
+ """Return newly added movie shape displaying video in `movie_file`.
1356
+
1357
+ **EXPERIMENTAL.** This method has important limitations:
1358
+
1359
+ * The size must be specified; no auto-scaling such as that provided by :meth:`add_picture`
1360
+ is performed.
1361
+ * The MIME type of the video file should be specified, e.g. 'video/mp4'. The provided
1362
+ video file is not interrogated for its type. The MIME type `video/unknown` is used by
1363
+ default (and works fine in tests as of this writing).
1364
+ * A poster frame image must be provided, it cannot be automatically extracted from the
1365
+ video file. If no poster frame is provided, the default "media loudspeaker" image will
1366
+ be used.
1367
+
1368
+ Return a newly added movie shape, positioned at (`left`, `top`), having size
1369
+ (`width`, `height`), and containing `movie_file`. Before the video is started,
1370
+ `poster_frame_image` is displayed as a placeholder for the video. The video play-controls
1371
+ timing is registered on the enclosing slide even when the movie is added to a group.
1372
+ """
1373
+ left, top = _coerce_emu(left), _coerce_emu(top)
1374
+ width, height = _coerce_emu(width), _coerce_emu(height)
1375
+ movie_pic = _MoviePicElementCreator.new_movie_pic(
1376
+ self,
1377
+ self._next_shape_id,
1378
+ movie_file,
1379
+ left,
1380
+ top,
1381
+ width,
1382
+ height,
1383
+ poster_frame_image,
1384
+ mime_type,
1385
+ )
1386
+ self._spTree.append(movie_pic)
1387
+ self._add_video_timing(movie_pic)
1388
+ self._recalculate_extents()
1389
+ return cast(GraphicFrame, self._shape_factory(movie_pic))
1390
+
1391
+ def _add_graphicFrame_containing_table(
1392
+ self, rows: int, cols: int, x: Length, y: Length, cx: Length, cy: Length
1393
+ ) -> CT_GraphicalObjectFrame:
1394
+ """Return a newly added `p:graphicFrame` element containing a table as specified."""
1395
+ _id = self._next_shape_id
1396
+ name = "Table %d" % (_id - 1)
1397
+ graphicFrame = self._spTree.add_table(_id, name, rows, cols, x, y, cx, cy)
1398
+ return graphicFrame
1399
+
1400
+ def _add_video_timing(self, pic: CT_Picture) -> None:
1401
+ """Add a `p:video` element under `p:sld/p:timing`.
1402
+
1403
+ The element will refer to the specified `pic` element by its shape id, and cause the video
1404
+ play controls to appear for that video. Resolved via an absolute XPath so it also works
1405
+ when the movie lives inside a group on the slide.
1406
+ """
1407
+ sld = self._spTree.xpath("/p:sld")[0]
1408
+ childTnLst = sld.get_or_add_childTnLst()
1409
+ childTnLst.add_video(pic.shape_id)
1410
+
1411
+ def _recalculate_extents(self) -> None:
1412
+ """Adjust position and size to incorporate all contained shapes.
1413
+
1414
+ This would typically be called when a contained shape is added, removed, or its position
1415
+ or size updated.
1416
+ """
1417
+ # ---default behavior is to do nothing, GroupShapes overrides to
1418
+ # produce the distinctive behavior of groups and subgroups.---
1419
+ pass
1420
+
1421
+
1422
+ class GroupShapes(_BaseGroupShapes):
1423
+ """The sequence of child shapes belonging to a group shape.
1424
+
1425
+ Note that this collection can itself contain a group shape, making this part of a recursive,
1426
+ tree data structure (acyclic graph).
1427
+ """
1428
+
1429
+ def _recalculate_extents(self) -> None:
1430
+ """Adjust position and size to incorporate all contained shapes.
1431
+
1432
+ This would typically be called when a contained shape is added, removed, or its position
1433
+ or size updated.
1434
+ """
1435
+ self._grpSp.recalculate_extents()
1436
+
1437
+
1438
+ class SlideShapes(_BaseGroupShapes):
1439
+ """Sequence of shapes appearing on a slide.
1440
+
1441
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
1442
+ Supports indexed access, len(), index(), and iteration.
1443
+ """
1444
+
1445
+ parent: Slide # pyright: ignore[reportIncompatibleMethodOverride]
1446
+
1447
+ def clone_layout_placeholders(self, slide_layout: SlideLayout) -> None:
1448
+ """Add placeholder shapes based on those in `slide_layout`.
1449
+
1450
+ Z-order of placeholders is preserved. Latent placeholders (date, slide number, and footer)
1451
+ are not cloned.
1452
+ """
1453
+ for placeholder in slide_layout.iter_cloneable_placeholders():
1454
+ self.clone_placeholder(placeholder)
1455
+
1456
+ @property
1457
+ def placeholders(self) -> SlidePlaceholders:
1458
+ """Sequence of placeholder shapes in this slide."""
1459
+ return self.parent.placeholders
1460
+
1461
+ @property
1462
+ def title(self) -> Shape | None:
1463
+ """The title placeholder shape on the slide.
1464
+
1465
+ |None| if the slide has no title placeholder.
1466
+ """
1467
+ for elm in self._spTree.iter_ph_elms():
1468
+ if elm.ph_idx == 0:
1469
+ return cast(Shape, self._shape_factory(elm))
1470
+ return None
1471
+
1472
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
1473
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
1474
+ return SlideShapeFactory(shape_elm, self)
1475
+
1476
+
1477
+ class LayoutShapes(_BaseShapes):
1478
+ """Sequence of shapes appearing on a slide layout.
1479
+
1480
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
1481
+ Supports indexed access, len(), index(), and iteration.
1482
+ """
1483
+
1484
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
1485
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
1486
+ return _LayoutShapeFactory(shape_elm, self)
1487
+
1488
+
1489
+ class MasterShapes(_BaseShapes):
1490
+ """Sequence of shapes appearing on a slide master.
1491
+
1492
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
1493
+ Supports indexed access, len(), and iteration.
1494
+ """
1495
+
1496
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
1497
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
1498
+ return _MasterShapeFactory(shape_elm, self)
1499
+
1500
+
1501
+ class NotesSlideShapes(_BaseShapes):
1502
+ """Sequence of shapes appearing on a notes slide.
1503
+
1504
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
1505
+ Supports indexed access, len(), index(), and iteration.
1506
+ """
1507
+
1508
+ def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str:
1509
+ """Return the base name for a placeholder of `ph_type` in this shape collection.
1510
+
1511
+ A notes slide uses a different name for the body placeholder and has some unique
1512
+ placeholder types, so this method overrides the default in the base class.
1513
+ """
1514
+ return {
1515
+ PP_PLACEHOLDER.BODY: "Notes Placeholder",
1516
+ PP_PLACEHOLDER.DATE: "Date Placeholder",
1517
+ PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
1518
+ PP_PLACEHOLDER.HEADER: "Header Placeholder",
1519
+ PP_PLACEHOLDER.SLIDE_IMAGE: "Slide Image Placeholder",
1520
+ PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
1521
+ }[ph_type]
1522
+
1523
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
1524
+ """Return appropriate shape object for `shape_elm` appearing on a notes slide."""
1525
+ return _NotesSlideShapeFactory(shape_elm, self)
1526
+
1527
+
1528
+ class BasePlaceholders(_BaseShapes):
1529
+ """Base class for placeholder collections.
1530
+
1531
+ Subclasses differentiate behaviors for a master, layout, and slide. By default, placeholder
1532
+ shapes are constructed using |BaseShapeFactory|. Subclasses should override
1533
+ :method:`_shape_factory` to use custom placeholder classes.
1534
+ """
1535
+
1536
+ @staticmethod
1537
+ def _is_member_elm(shape_elm: ShapeElement) -> bool:
1538
+ """True if `shape_elm` is a placeholder shape, False otherwise."""
1539
+ return shape_elm.has_ph_elm
1540
+
1541
+
1542
+ class LayoutPlaceholders(BasePlaceholders):
1543
+ """Sequence of |LayoutPlaceholder| instance for each placeholder shape on a slide layout."""
1544
+
1545
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
1546
+ [], Iterator[LayoutPlaceholder]
1547
+ ]
1548
+
1549
+ def get(self, idx: int, default: LayoutPlaceholder | None = None) -> LayoutPlaceholder | None:
1550
+ """The first placeholder shape with matching `idx` value, or `default` if not found."""
1551
+ for placeholder in self:
1552
+ if placeholder.element.ph_idx == idx:
1553
+ return placeholder
1554
+ return default
1555
+
1556
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
1557
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
1558
+ return _LayoutShapeFactory(shape_elm, self)
1559
+
1560
+
1561
+ class MasterPlaceholders(BasePlaceholders):
1562
+ """Sequence of MasterPlaceholder representing the placeholder shapes on a slide master."""
1563
+
1564
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
1565
+ [], Iterator[MasterPlaceholder]
1566
+ ]
1567
+
1568
+ def get(self, ph_type: PP_PLACEHOLDER, default: MasterPlaceholder | None = None):
1569
+ """Return the first placeholder shape with type `ph_type` (e.g. 'body').
1570
+
1571
+ Returns `default` if no such placeholder shape is present in the collection.
1572
+ """
1573
+ for placeholder in self:
1574
+ if placeholder.ph_type == ph_type:
1575
+ return placeholder
1576
+ return default
1577
+
1578
+ def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride]
1579
+ self, placeholder_elm: CT_Shape
1580
+ ) -> MasterPlaceholder:
1581
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
1582
+ return cast(MasterPlaceholder, _MasterShapeFactory(placeholder_elm, self))
1583
+
1584
+
1585
+ class NotesSlidePlaceholders(MasterPlaceholders):
1586
+ """Sequence of placeholder shapes on a notes slide."""
1587
+
1588
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
1589
+ [], Iterator[NotesSlidePlaceholder]
1590
+ ]
1591
+
1592
+ def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride]
1593
+ self, placeholder_elm: CT_Shape
1594
+ ) -> NotesSlidePlaceholder:
1595
+ """Return an instance of the appropriate placeholder proxy class for `placeholder_elm`."""
1596
+ return cast(NotesSlidePlaceholder, _NotesSlideShapeFactory(placeholder_elm, self))
1597
+
1598
+
1599
+ class SlidePlaceholders(ParentedElementProxy):
1600
+ """Collection of placeholder shapes on a slide.
1601
+
1602
+ Supports iteration, :func:`len`, and dictionary-style lookup on the `idx` value of the
1603
+ placeholders it contains.
1604
+ """
1605
+
1606
+ _element: CT_GroupShape
1607
+
1608
+ def __getitem__(self, idx: int):
1609
+ """Access placeholder shape having `idx`.
1610
+
1611
+ Note that while this looks like list access, idx is actually a dictionary key and will
1612
+ raise |KeyError| if no placeholder with that idx value is in the collection.
1613
+ """
1614
+ for e in self._element.iter_ph_elms():
1615
+ if e.ph_idx == idx:
1616
+ return SlideShapeFactory(e, self)
1617
+ raise KeyError("no placeholder on this slide with idx == %d" % idx)
1618
+
1619
+ def __iter__(self):
1620
+ """Generate placeholder shapes in `idx` order."""
1621
+ ph_elms = sorted([e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx)
1622
+ return (SlideShapeFactory(e, self) for e in ph_elms)
1623
+
1624
+ def __len__(self) -> int:
1625
+ """Return count of placeholder shapes."""
1626
+ return len(list(self._element.iter_ph_elms()))
1627
+
1628
+
1629
+ def BaseShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
1630
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
1631
+ tag = shape_elm.tag
1632
+
1633
+ if isinstance(shape_elm, CT_Picture):
1634
+ videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile")
1635
+ if videoFiles:
1636
+ return Movie(shape_elm, parent)
1637
+ return Picture(shape_elm, parent)
1638
+
1639
+ shape_cls = {
1640
+ qn("p:cxnSp"): Connector,
1641
+ qn("p:grpSp"): GroupShape,
1642
+ qn("p:sp"): Shape,
1643
+ qn("p:graphicFrame"): GraphicFrame,
1644
+ }.get(tag, BaseShape)
1645
+
1646
+ return shape_cls(shape_elm, parent) # pyright: ignore[reportArgumentType]
1647
+
1648
+
1649
+ def _LayoutShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
1650
+ """Return appropriate shape object for `shape_elm` on a slide layout."""
1651
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
1652
+ return LayoutPlaceholder(shape_elm, parent)
1653
+ return BaseShapeFactory(shape_elm, parent)
1654
+
1655
+
1656
+ def _MasterShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
1657
+ """Return appropriate shape object for `shape_elm` on a slide master."""
1658
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
1659
+ return MasterPlaceholder(shape_elm, parent)
1660
+ return BaseShapeFactory(shape_elm, parent)
1661
+
1662
+
1663
+ def _NotesSlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
1664
+ """Return appropriate shape object for `shape_elm` on a notes slide."""
1665
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
1666
+ return NotesSlidePlaceholder(shape_elm, parent)
1667
+ return BaseShapeFactory(shape_elm, parent)
1668
+
1669
+
1670
+ def _SlidePlaceholderFactory(shape_elm: ShapeElement, parent: ProvidesPart):
1671
+ """Return a placeholder shape of the appropriate type for `shape_elm`."""
1672
+ tag = shape_elm.tag
1673
+ if tag == qn("p:sp"):
1674
+ Constructor = {
1675
+ PP_PLACEHOLDER.BITMAP: PicturePlaceholder,
1676
+ PP_PLACEHOLDER.CHART: ChartPlaceholder,
1677
+ PP_PLACEHOLDER.PICTURE: PicturePlaceholder,
1678
+ PP_PLACEHOLDER.TABLE: TablePlaceholder,
1679
+ }.get(shape_elm.ph_type, SlidePlaceholder)
1680
+ elif tag == qn("p:graphicFrame"):
1681
+ Constructor = PlaceholderGraphicFrame
1682
+ elif tag == qn("p:pic"):
1683
+ Constructor = PlaceholderPicture
1684
+ else:
1685
+ Constructor = BaseShapeFactory
1686
+ return Constructor(shape_elm, parent) # pyright: ignore[reportArgumentType]
1687
+
1688
+
1689
+ def SlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
1690
+ """Return appropriate shape object for `shape_elm` on a slide."""
1691
+ if shape_elm.has_ph_elm:
1692
+ return _SlidePlaceholderFactory(shape_elm, parent)
1693
+ return BaseShapeFactory(shape_elm, parent)
1694
+
1695
+
1696
+ class _MoviePicElementCreator(object):
1697
+ """Functional service object for creating a new movie p:pic element.
1698
+
1699
+ It's entire external interface is its :meth:`new_movie_pic` class method that returns a new
1700
+ `p:pic` element containing the specified video. This class is not intended to be constructed
1701
+ or an instance of it retained by the caller; it is a "one-shot" object, really a function
1702
+ wrapped in a object such that its helper methods can be organized here.
1703
+ """
1704
+
1705
+ def __init__(
1706
+ self,
1707
+ shapes: _BaseGroupShapes,
1708
+ shape_id: int,
1709
+ movie_file: str | IO[bytes],
1710
+ x: Length,
1711
+ y: Length,
1712
+ cx: Length,
1713
+ cy: Length,
1714
+ poster_frame_file: str | IO[bytes] | None,
1715
+ mime_type: str | None,
1716
+ ):
1717
+ super(_MoviePicElementCreator, self).__init__()
1718
+ self._shapes = shapes
1719
+ self._shape_id = shape_id
1720
+ self._movie_file = movie_file
1721
+ self._x, self._y, self._cx, self._cy = x, y, cx, cy
1722
+ self._poster_frame_file = poster_frame_file
1723
+ self._mime_type = mime_type
1724
+
1725
+ @classmethod
1726
+ def new_movie_pic(
1727
+ cls,
1728
+ shapes: _BaseGroupShapes,
1729
+ shape_id: int,
1730
+ movie_file: str | IO[bytes],
1731
+ x: Length,
1732
+ y: Length,
1733
+ cx: Length,
1734
+ cy: Length,
1735
+ poster_frame_image: str | IO[bytes] | None,
1736
+ mime_type: str | None,
1737
+ ) -> CT_Picture:
1738
+ """Return a new `p:pic` element containing video in `movie_file`.
1739
+
1740
+ If `mime_type` is None, 'video/unknown' is used. If `poster_frame_file` is None, the
1741
+ default "media loudspeaker" image is used.
1742
+ """
1743
+ return cls(shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type)._pic
1744
+
1745
+ @property
1746
+ def _media_rId(self) -> str:
1747
+ """Return the rId of RT.MEDIA relationship to video part.
1748
+
1749
+ For historical reasons, there are two relationships to the same part; one is the video rId
1750
+ and the other is the media rId.
1751
+ """
1752
+ return self._video_part_rIds[0]
1753
+
1754
+ @lazyproperty
1755
+ def _pic(self) -> CT_Picture:
1756
+ """Return the new `p:pic` element referencing the video."""
1757
+ return CT_Picture.new_video_pic(
1758
+ self._shape_id,
1759
+ self._shape_name,
1760
+ self._video_rId,
1761
+ self._media_rId,
1762
+ self._poster_frame_rId,
1763
+ self._x,
1764
+ self._y,
1765
+ self._cx,
1766
+ self._cy,
1767
+ )
1768
+
1769
+ @lazyproperty
1770
+ def _poster_frame_image_file(self) -> str | IO[bytes]:
1771
+ """Return the image file for video placeholder image.
1772
+
1773
+ If no poster frame file is provided, the default "media loudspeaker" image is used.
1774
+ """
1775
+ poster_frame_file = self._poster_frame_file
1776
+ if poster_frame_file is None:
1777
+ return io.BytesIO(SPEAKER_IMAGE_BYTES)
1778
+ return poster_frame_file
1779
+
1780
+ @lazyproperty
1781
+ def _poster_frame_rId(self) -> str:
1782
+ """Return the rId of relationship to poster frame image.
1783
+
1784
+ The poster frame is the image used to represent the video before it's played.
1785
+ """
1786
+ _, poster_frame_rId = self._slide_part.get_or_add_image_part(self._poster_frame_image_file)
1787
+ return poster_frame_rId
1788
+
1789
+ @property
1790
+ def _shape_name(self) -> str:
1791
+ """Return the appropriate shape name for the p:pic shape.
1792
+
1793
+ A movie shape is named with the base filename of the video.
1794
+ """
1795
+ return self._video.filename
1796
+
1797
+ @property
1798
+ def _slide_part(self) -> SlidePart:
1799
+ """Return SlidePart object for slide containing this movie."""
1800
+ return self._shapes.part
1801
+
1802
+ @lazyproperty
1803
+ def _video(self) -> Video:
1804
+ """Return a |Video| object containing the movie file."""
1805
+ return Video.from_path_or_file_like(self._movie_file, self._mime_type)
1806
+
1807
+ @lazyproperty
1808
+ def _video_part_rIds(self) -> tuple[str, str]:
1809
+ """Return the rIds for relationships to media part for video.
1810
+
1811
+ This is where the media part and its relationships to the slide are actually created.
1812
+ """
1813
+ media_rId, video_rId = self._slide_part.get_or_add_video_media_part(self._video)
1814
+ return media_rId, video_rId
1815
+
1816
+ @property
1817
+ def _video_rId(self) -> str:
1818
+ """Return the rId of RT.VIDEO relationship to video part.
1819
+
1820
+ For historical reasons, there are two relationships to the same part; one is the video rId
1821
+ and the other is the media rId.
1822
+ """
1823
+ return self._video_part_rIds[1]
1824
+
1825
+
1826
+ class _OleObjectElementCreator(object):
1827
+ """Functional service object for creating a new OLE-object p:graphicFrame element.
1828
+
1829
+ It's entire external interface is its :meth:`graphicFrame` class method that returns a new
1830
+ `p:graphicFrame` element containing the specified embedded OLE-object shape. This class is not
1831
+ intended to be constructed or an instance of it retained by the caller; it is a "one-shot"
1832
+ object, really a function wrapped in a object such that its helper methods can be organized
1833
+ here.
1834
+ """
1835
+
1836
+ def __init__(
1837
+ self,
1838
+ shapes: _BaseGroupShapes,
1839
+ shape_id: int,
1840
+ ole_object_file: str | IO[bytes],
1841
+ prog_id: PROG_ID | str,
1842
+ x: Length,
1843
+ y: Length,
1844
+ cx: Length | None,
1845
+ cy: Length | None,
1846
+ icon_file: str | IO[bytes] | None,
1847
+ icon_width: Length | None,
1848
+ icon_height: Length | None,
1849
+ ):
1850
+ self._shapes = shapes
1851
+ self._shape_id = shape_id
1852
+ self._ole_object_file = ole_object_file
1853
+ self._prog_id_arg = prog_id
1854
+ self._x = x
1855
+ self._y = y
1856
+ self._cx_arg = cx
1857
+ self._cy_arg = cy
1858
+ self._icon_file_arg = icon_file
1859
+ self._icon_width_arg = icon_width
1860
+ self._icon_height_arg = icon_height
1861
+
1862
+ @classmethod
1863
+ def graphicFrame(
1864
+ cls,
1865
+ shapes: _BaseGroupShapes,
1866
+ shape_id: int,
1867
+ ole_object_file: str | IO[bytes],
1868
+ prog_id: PROG_ID | str,
1869
+ x: Length,
1870
+ y: Length,
1871
+ cx: Length | None,
1872
+ cy: Length | None,
1873
+ icon_file: str | IO[bytes] | None,
1874
+ icon_width: Length | None,
1875
+ icon_height: Length | None,
1876
+ ) -> CT_GraphicalObjectFrame:
1877
+ """Return new `p:graphicFrame` element containing embedded `ole_object_file`."""
1878
+ return cls(
1879
+ shapes,
1880
+ shape_id,
1881
+ ole_object_file,
1882
+ prog_id,
1883
+ x,
1884
+ y,
1885
+ cx,
1886
+ cy,
1887
+ icon_file,
1888
+ icon_width,
1889
+ icon_height,
1890
+ )._graphicFrame
1891
+
1892
+ @lazyproperty
1893
+ def _graphicFrame(self) -> CT_GraphicalObjectFrame:
1894
+ """Newly-created `p:graphicFrame` element referencing embedded OLE-object."""
1895
+ return CT_GraphicalObjectFrame.new_ole_object_graphicFrame(
1896
+ self._shape_id,
1897
+ self._shape_name,
1898
+ self._ole_object_rId,
1899
+ self._progId,
1900
+ self._icon_rId,
1901
+ self._x,
1902
+ self._y,
1903
+ self._cx,
1904
+ self._cy,
1905
+ self._icon_width,
1906
+ self._icon_height,
1907
+ self._pic_id,
1908
+ )
1909
+
1910
+ @lazyproperty
1911
+ def _cx(self) -> Length:
1912
+ """Emu object specifying width of "show-as-icon" image for OLE shape."""
1913
+ # --- a user-specified width overrides any default ---
1914
+ if self._cx_arg is not None:
1915
+ return self._cx_arg
1916
+
1917
+ # --- the default width is specified by the PROG_ID member if prog_id is one,
1918
+ # --- otherwise it gets the default icon width.
1919
+ return (
1920
+ Emu(self._prog_id_arg.width) if isinstance(self._prog_id_arg, PROG_ID) else Emu(965200)
1921
+ )
1922
+
1923
+ @lazyproperty
1924
+ def _cy(self) -> Length:
1925
+ """Emu object specifying height of "show-as-icon" image for OLE shape."""
1926
+ # --- a user-specified width overrides any default ---
1927
+ if self._cy_arg is not None:
1928
+ return self._cy_arg
1929
+
1930
+ # --- the default height is specified by the PROG_ID member if prog_id is one,
1931
+ # --- otherwise it gets the default icon height.
1932
+ return (
1933
+ Emu(self._prog_id_arg.height) if isinstance(self._prog_id_arg, PROG_ID) else Emu(609600)
1934
+ )
1935
+
1936
+ @lazyproperty
1937
+ def _icon_height(self) -> Length:
1938
+ """Vertical size of enclosed EMF icon within the OLE graphic-frame.
1939
+
1940
+ This must be specified when a custom icon is used, to avoid stretching of the image and
1941
+ possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it.
1942
+
1943
+ The correct size can be determined by creating an example PPTX using PowerPoint and then
1944
+ inspecting the XML of the OLE graphics-frame (p:oleObj.imgH).
1945
+ """
1946
+ return self._icon_height_arg if self._icon_height_arg is not None else Emu(609600)
1947
+
1948
+ @lazyproperty
1949
+ def _icon_image_file(self) -> str | IO[bytes]:
1950
+ """Reference to image file containing icon to show in lieu of this object.
1951
+
1952
+ This can be either a str path or a file-like object (io.BytesIO typically).
1953
+ """
1954
+ # --- a user-specified icon overrides any default ---
1955
+ if self._icon_file_arg is not None:
1956
+ return self._icon_file_arg
1957
+
1958
+ # --- A prog_id belonging to PROG_ID gets its icon filename from there. A
1959
+ # --- user-specified (str) prog_id gets the default icon.
1960
+ icon_filename = (
1961
+ self._prog_id_arg.icon_filename
1962
+ if isinstance(self._prog_id_arg, PROG_ID)
1963
+ else "generic-icon.emf"
1964
+ )
1965
+
1966
+ _thisdir = os.path.split(__file__)[0]
1967
+ return os.path.abspath(os.path.join(_thisdir, "..", "templates", icon_filename))
1968
+
1969
+ @lazyproperty
1970
+ def _icon_rId(self) -> str:
1971
+ """str rId like "rId7" of rel to icon (image) representing OLE-object part."""
1972
+ _, rId = self._slide_part.get_or_add_image_part(self._icon_image_file)
1973
+ return rId
1974
+
1975
+ @lazyproperty
1976
+ def _icon_width(self) -> Length:
1977
+ """Width of enclosed EMF icon within the OLE graphic-frame.
1978
+
1979
+ This must be specified when a custom icon is used, to avoid stretching of the image and
1980
+ possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it.
1981
+ """
1982
+ return self._icon_width_arg if self._icon_width_arg is not None else Emu(965200)
1983
+
1984
+ @lazyproperty
1985
+ def _ole_object_rId(self) -> str:
1986
+ """str rId like "rId6" of relationship to embedded ole_object part.
1987
+
1988
+ This is where the ole_object part and its relationship to the slide are actually created.
1989
+ """
1990
+ return self._slide_part.add_embedded_ole_object_part(
1991
+ self._prog_id_arg, self._ole_object_file
1992
+ )
1993
+
1994
+ @lazyproperty
1995
+ def _progId(self) -> str:
1996
+ """str like "Excel.Sheet.12" identifying program used to open object.
1997
+
1998
+ This value appears in the `progId` attribute of the `p:oleObj` element for the object.
1999
+ """
2000
+ prog_id_arg = self._prog_id_arg
2001
+
2002
+ # --- member of PROG_ID enumeration knows its progId keyphrase, otherwise caller
2003
+ # --- has specified it explicitly (as str)
2004
+ return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg
2005
+
2006
+ @lazyproperty
2007
+ def _shape_name(self) -> str:
2008
+ """str name like "Object 1" for the embedded ole_object shape.
2009
+
2010
+ The name is formed from the prefix "Object " and the shape-id decremented by 1.
2011
+ """
2012
+ return "Object %d" % (self._shape_id - 1)
2013
+
2014
+ @lazyproperty
2015
+ def _pic_id(self) -> int:
2016
+ """Unique shape id for the inner "show-as-icon" ``p:pic`` element.
2017
+
2018
+ Allocated separately from the graphic-frame's own id so two OLE objects on the same
2019
+ slide don't both emit the hardcoded ``id="0"`` used previously — a duplicate shape id
2020
+ that makes PowerPoint report the deck as needing repair.
2021
+ """
2022
+ return self._shapes._next_shape_id
2023
+
2024
+ @lazyproperty
2025
+ def _slide_part(self) -> SlidePart:
2026
+ """SlidePart object for this slide."""
2027
+ return self._shapes.part