aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py1190
1 files changed, 1190 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py b/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py
new file mode 100644
index 00000000..29623f1f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py
@@ -0,0 +1,1190 @@
+"""The shape tree, the structure that holds a slide's shapes."""
+
+from __future__ import annotations
+
+import io
+import os
+from typing import IO, TYPE_CHECKING, Callable, Iterable, Iterator, cast
+
+from pptx.enum.shapes import PP_PLACEHOLDER, PROG_ID
+from pptx.media import SPEAKER_IMAGE_BYTES, Video
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.oxml.ns import qn
+from pptx.oxml.shapes.autoshape import CT_Shape
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.oxml.simpletypes import ST_Direction
+from pptx.shapes.autoshape import AutoShapeType, Shape
+from pptx.shapes.base import BaseShape
+from pptx.shapes.connector import Connector
+from pptx.shapes.freeform import FreeformBuilder
+from pptx.shapes.graphfrm import GraphicFrame
+from pptx.shapes.group import GroupShape
+from pptx.shapes.picture import Movie, Picture
+from pptx.shapes.placeholder import (
+ ChartPlaceholder,
+ LayoutPlaceholder,
+ MasterPlaceholder,
+ NotesSlidePlaceholder,
+ PicturePlaceholder,
+ PlaceholderGraphicFrame,
+ PlaceholderPicture,
+ SlidePlaceholder,
+ TablePlaceholder,
+)
+from pptx.shared import ParentedElementProxy
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.chart.chart import Chart
+ from pptx.chart.data import ChartData
+ from pptx.enum.chart import XL_CHART_TYPE
+ from pptx.enum.shapes import MSO_CONNECTOR_TYPE, MSO_SHAPE
+ from pptx.oxml.shapes import ShapeElement
+ from pptx.oxml.shapes.connector import CT_Connector
+ from pptx.oxml.shapes.groupshape import CT_GroupShape
+ from pptx.parts.image import ImagePart
+ from pptx.parts.slide import SlidePart
+ from pptx.slide import Slide, SlideLayout
+ from pptx.types import ProvidesPart
+ from pptx.util import Length
+
+# +-- _BaseShapes
+# | |
+# | +-- _BaseGroupShapes
+# | | |
+# | | +-- GroupShapes
+# | | |
+# | | +-- SlideShapes
+# | |
+# | +-- LayoutShapes
+# | |
+# | +-- MasterShapes
+# | |
+# | +-- NotesSlideShapes
+# | |
+# | +-- BasePlaceholders
+# | |
+# | +-- LayoutPlaceholders
+# | |
+# | +-- MasterPlaceholders
+# | |
+# | +-- NotesSlidePlaceholders
+# |
+# +-- SlidePlaceholders
+
+
+class _BaseShapes(ParentedElementProxy):
+ """Base class for a shape collection appearing in a slide-type object.
+
+ Subclasses include Slide, SlideLayout, and SlideMaster. Provides common methods.
+ """
+
+ def __init__(self, spTree: CT_GroupShape, parent: ProvidesPart):
+ super(_BaseShapes, self).__init__(spTree, parent)
+ self._spTree = spTree
+ self._cached_max_shape_id = None
+
+ def __getitem__(self, idx: int) -> BaseShape:
+ """Return shape at `idx` in sequence, e.g. `shapes[2]`."""
+ shape_elms = list(self._iter_member_elms())
+ try:
+ shape_elm = shape_elms[idx]
+ except IndexError:
+ raise IndexError("shape index out of range")
+ return self._shape_factory(shape_elm)
+
+ def __iter__(self) -> Iterator[BaseShape]:
+ """Generate a reference to each shape in the collection, in sequence."""
+ for shape_elm in self._iter_member_elms():
+ yield self._shape_factory(shape_elm)
+
+ def __len__(self) -> int:
+ """Return count of shapes in this shape tree.
+
+ A group shape contributes 1 to the total, without regard to the number of shapes contained
+ in the group.
+ """
+ shape_elms = list(self._iter_member_elms())
+ return len(shape_elms)
+
+ def clone_placeholder(self, placeholder: LayoutPlaceholder) -> None:
+ """Add a new placeholder shape based on `placeholder`."""
+ sp = placeholder.element
+ ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx)
+ id_ = self._next_shape_id
+ name = self._next_ph_name(ph_type, id_, orient)
+ self._spTree.add_placeholder(id_, name, ph_type, orient, sz, idx)
+
+ def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str:
+ """Return the base name for a placeholder of `ph_type` in this shape collection.
+
+ There is some variance between slide types, for example a notes slide uses a different
+ name for the body placeholder, so this method can be overriden by subclasses.
+ """
+ return {
+ PP_PLACEHOLDER.BITMAP: "ClipArt Placeholder",
+ PP_PLACEHOLDER.BODY: "Text Placeholder",
+ PP_PLACEHOLDER.CENTER_TITLE: "Title",
+ PP_PLACEHOLDER.CHART: "Chart Placeholder",
+ PP_PLACEHOLDER.DATE: "Date Placeholder",
+ PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
+ PP_PLACEHOLDER.HEADER: "Header Placeholder",
+ PP_PLACEHOLDER.MEDIA_CLIP: "Media Placeholder",
+ PP_PLACEHOLDER.OBJECT: "Content Placeholder",
+ PP_PLACEHOLDER.ORG_CHART: "SmartArt Placeholder",
+ PP_PLACEHOLDER.PICTURE: "Picture Placeholder",
+ PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
+ PP_PLACEHOLDER.SUBTITLE: "Subtitle",
+ PP_PLACEHOLDER.TABLE: "Table Placeholder",
+ PP_PLACEHOLDER.TITLE: "Title",
+ }[ph_type]
+
+ @property
+ def turbo_add_enabled(self) -> bool:
+ """True if "turbo-add" mode is enabled. Read/Write.
+
+ EXPERIMENTAL: This feature can radically improve performance when adding large numbers
+ (hundreds of shapes) to a slide. It works by caching the last shape ID used and
+ incrementing that value to assign the next shape id. This avoids repeatedly searching all
+ shape ids in the slide each time a new ID is required.
+
+ Performance is not noticeably improved for a slide with a relatively small number of
+ shapes, but because the search time rises with the square of the shape count, this option
+ can be useful for optimizing generation of a slide composed of many shapes.
+
+ Shape-id collisions can occur (causing a repair error on load) if more than one |Slide|
+ object is used to interact with the same slide in the presentation. Note that the |Slides|
+ collection creates a new |Slide| object each time a slide is accessed (e.g. `slide =
+ prs.slides[0]`, so you must be careful to limit use to a single |Slide| object.
+ """
+ return self._cached_max_shape_id is not None
+
+ @turbo_add_enabled.setter
+ def turbo_add_enabled(self, value: bool):
+ enable = bool(value)
+ self._cached_max_shape_id = self._spTree.max_shape_id if enable else None
+
+ @staticmethod
+ def _is_member_elm(shape_elm: ShapeElement) -> bool:
+ """Return true if `shape_elm` represents a member of this collection, False otherwise."""
+ return True
+
+ def _iter_member_elms(self) -> Iterator[ShapeElement]:
+ """Generate each child of the `p:spTree` element that corresponds to a shape.
+
+ Items appear in XML document order.
+ """
+ for shape_elm in self._spTree.iter_shape_elms():
+ if self._is_member_elm(shape_elm):
+ yield shape_elm
+
+ def _next_ph_name(self, ph_type: PP_PLACEHOLDER, id: int, orient: str) -> str:
+ """Next unique placeholder name for placeholder shape of type `ph_type`.
+
+ Usually will be standard placeholder root name suffixed with id-1, e.g.
+ _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> 'Table Placeholder 3'. The number is
+ incremented as necessary to make the name unique within the collection. If `orient` is
+ `'vert'`, the placeholder name is prefixed with `'Vertical '`.
+ """
+ basename = self.ph_basename(ph_type)
+
+ # prefix rootname with 'Vertical ' if orient is 'vert'
+ if orient == ST_Direction.VERT:
+ basename = "Vertical %s" % basename
+
+ # increment numpart as necessary to make name unique
+ numpart = id - 1
+ names = self._spTree.xpath("//p:cNvPr/@name")
+ while True:
+ name = "%s %d" % (basename, numpart)
+ if name not in names:
+ break
+ numpart += 1
+
+ return name
+
+ @property
+ def _next_shape_id(self) -> int:
+ """Return a unique shape id suitable for use with a new shape.
+
+ The returned id is 1 greater than the maximum shape id used so far. In practice, the
+ minimum id is 2 because the spTree element is always assigned id="1".
+ """
+ # ---presence of cached-max-shape-id indicates turbo mode is on---
+ if self._cached_max_shape_id is not None:
+ self._cached_max_shape_id += 1
+ return self._cached_max_shape_id
+
+ return self._spTree.max_shape_id + 1
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return BaseShapeFactory(shape_elm, self)
+
+
+class _BaseGroupShapes(_BaseShapes):
+ """Base class for shape-trees that can add shapes."""
+
+ part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride]
+ _element: CT_GroupShape
+
+ def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart):
+ super(_BaseGroupShapes, self).__init__(grpSp, parent)
+ self._grpSp = grpSp
+
+ def add_chart(
+ self,
+ chart_type: XL_CHART_TYPE,
+ x: Length,
+ y: Length,
+ cx: Length,
+ cy: Length,
+ chart_data: ChartData,
+ ) -> Chart:
+ """Add a new chart of `chart_type` to the slide.
+
+ The chart is positioned at (`x`, `y`), has size (`cx`, `cy`), and depicts `chart_data`.
+ `chart_type` is one of the :ref:`XlChartType` enumeration values. `chart_data` is a
+ |ChartData| object populated with the categories and series values for the chart.
+
+ Note that a |GraphicFrame| shape object is returned, not the |Chart| object contained in
+ that graphic frame shape. The chart object may be accessed using the :attr:`chart`
+ property of the returned |GraphicFrame| object.
+ """
+ rId = self.part.add_chart_part(chart_type, chart_data)
+ graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy)
+ self._recalculate_extents()
+ return cast("Chart", self._shape_factory(graphicFrame))
+
+ def add_connector(
+ self,
+ connector_type: MSO_CONNECTOR_TYPE,
+ begin_x: Length,
+ begin_y: Length,
+ end_x: Length,
+ end_y: Length,
+ ) -> Connector:
+ """Add a newly created connector shape to the end of this shape tree.
+
+ `connector_type` is a member of the :ref:`MsoConnectorType` enumeration and the end-point
+ values are specified as EMU values. The returned connector is of type `connector_type` and
+ has begin and end points as specified.
+ """
+ cxnSp = self._add_cxnSp(connector_type, begin_x, begin_y, end_x, end_y)
+ self._recalculate_extents()
+ return cast(Connector, self._shape_factory(cxnSp))
+
+ def add_group_shape(self, shapes: Iterable[BaseShape] = ()) -> GroupShape:
+ """Return a |GroupShape| object newly appended to this shape tree.
+
+ The group shape is empty and must be populated with shapes using methods on its shape
+ tree, available on its `.shapes` property. The position and extents of the group shape are
+ determined by the shapes it contains; its position and extents are recalculated each time
+ a shape is added to it.
+ """
+ shapes = tuple(shapes)
+ grpSp = self._element.add_grpSp()
+ for shape in shapes:
+ grpSp.insert_element_before(
+ shape._element, "p:extLst" # pyright: ignore[reportPrivateUsage]
+ )
+ if shapes:
+ grpSp.recalculate_extents()
+ return cast(GroupShape, self._shape_factory(grpSp))
+
+ def add_ole_object(
+ self,
+ object_file: str | IO[bytes],
+ prog_id: str,
+ left: Length,
+ top: Length,
+ width: Length | None = None,
+ height: Length | None = None,
+ icon_file: str | IO[bytes] | None = None,
+ icon_width: Length | None = None,
+ icon_height: Length | None = None,
+ ) -> GraphicFrame:
+ """Return newly-created GraphicFrame shape embedding `object_file`.
+
+ The returned graphic-frame shape contains `object_file` as an embedded OLE object. It is
+ displayed as an icon at `left`, `top` with size `width`, `height`. `width` and `height`
+ may be omitted when `prog_id` is a member of `PROG_ID`, in which case the default icon
+ size is used. This is advised for best appearance where applicable because it avoids an
+ icon with a "stretched" appearance.
+
+ `object_file` may either be a str path to a file or file-like object (such as
+ `io.BytesIO`) containing the bytes of the object to be embedded (such as an Excel file).
+
+ `prog_id` can be either a member of `pptx.enum.shapes.PROG_ID` or a str value like
+ `"Adobe.Exchange.7"` determined by inspecting the XML generated by PowerPoint for an
+ object of the desired type.
+
+ `icon_file` may either be a str path to an image file or a file-like object containing the
+ image. The image provided will be displayed in lieu of the OLE object; double-clicking on
+ the image opens the object (subject to operating-system limitations). The image file can
+ be any supported image file. Those produced by PowerPoint itself are generally EMF and can
+ be harvested from a PPTX package that embeds such an object. PNG and JPG also work fine.
+
+ `icon_width` and `icon_height` are `Length` values (e.g. Emu() or Inches()) that describe
+ the size of the icon image within the shape. These should be omitted unless a custom
+ `icon_file` is provided. The dimensions must be discovered by inspecting the XML.
+ Automatic resizing of the OLE-object shape can occur when the icon is double-clicked if
+ these values are not as set by PowerPoint. This behavior may only manifest in the Windows
+ version of PowerPoint.
+ """
+ graphicFrame = _OleObjectElementCreator.graphicFrame(
+ self,
+ self._next_shape_id,
+ object_file,
+ prog_id,
+ left,
+ top,
+ width,
+ height,
+ icon_file,
+ icon_width,
+ icon_height,
+ )
+ self._spTree.append(graphicFrame)
+ self._recalculate_extents()
+ return cast(GraphicFrame, self._shape_factory(graphicFrame))
+
+ def add_picture(
+ self,
+ image_file: str | IO[bytes],
+ left: Length,
+ top: Length,
+ width: Length | None = None,
+ height: Length | None = None,
+ ) -> Picture:
+ """Add picture shape displaying image in `image_file`.
+
+ `image_file` can be either a path to a file (a string) or a file-like object. The picture
+ is positioned with its top-left corner at (`top`, `left`). If `width` and `height` are
+ both |None|, the native size of the image is used. If only one of `width` or `height` is
+ used, the unspecified dimension is calculated to preserve the aspect ratio of the image.
+ If both are specified, the picture is stretched to fit, without regard to its native
+ aspect ratio.
+ """
+ image_part, rId = self.part.get_or_add_image_part(image_file)
+ pic = self._add_pic_from_image_part(image_part, rId, left, top, width, height)
+ self._recalculate_extents()
+ return cast(Picture, self._shape_factory(pic))
+
+ def add_shape(
+ self, autoshape_type_id: MSO_SHAPE, left: Length, top: Length, width: Length, height: Length
+ ) -> Shape:
+ """Return new |Shape| object appended to this shape tree.
+
+ `autoshape_type_id` is a member of :ref:`MsoAutoShapeType` e.g. `MSO_SHAPE.RECTANGLE`
+ specifying the type of shape to be added. The remaining arguments specify the new shape's
+ position and size.
+ """
+ autoshape_type = AutoShapeType(autoshape_type_id)
+ sp = self._add_sp(autoshape_type, left, top, width, height)
+ self._recalculate_extents()
+ return cast(Shape, self._shape_factory(sp))
+
+ def add_textbox(self, left: Length, top: Length, width: Length, height: Length) -> Shape:
+ """Return newly added text box shape appended to this shape tree.
+
+ The text box is of the specified size, located at the specified position on the slide.
+ """
+ sp = self._add_textbox_sp(left, top, width, height)
+ self._recalculate_extents()
+ return cast(Shape, self._shape_factory(sp))
+
+ def build_freeform(
+ self, start_x: float = 0, start_y: float = 0, scale: tuple[float, float] | float = 1.0
+ ) -> FreeformBuilder:
+ """Return |FreeformBuilder| object to specify a freeform shape.
+
+ The optional `start_x` and `start_y` arguments specify the starting pen position in local
+ coordinates. They will be rounded to the nearest integer before use and each default to
+ zero.
+
+ The optional `scale` argument specifies the size of local coordinates proportional to
+ slide coordinates (EMU). If the vertical scale is different than the horizontal scale
+ (local coordinate units are "rectangular"), a pair of numeric values can be provided as
+ the `scale` argument, e.g. `scale=(1.0, 2.0)`. In this case the first number is
+ interpreted as the horizontal (X) scale and the second as the vertical (Y) scale.
+
+ A convenient method for calculating scale is to divide a |Length| object by an equivalent
+ count of local coordinate units, e.g. `scale = Inches(1)/1000` for 1000 local units per
+ inch.
+ """
+ x_scale, y_scale = scale if isinstance(scale, tuple) else (scale, scale)
+
+ return FreeformBuilder.new(self, start_x, start_y, x_scale, y_scale)
+
+ def index(self, shape: BaseShape) -> int:
+ """Return the index of `shape` in this sequence.
+
+ Raises |ValueError| if `shape` is not in the collection.
+ """
+ shape_elms = list(self._element.iter_shape_elms())
+ return shape_elms.index(shape.element)
+
+ def _add_chart_graphicFrame(
+ self, rId: str, x: Length, y: Length, cx: Length, cy: Length
+ ) -> CT_GraphicalObjectFrame:
+ """Return new `p:graphicFrame` element appended to this shape tree.
+
+ The `p:graphicFrame` element has the specified position and size and refers to the chart
+ part identified by `rId`.
+ """
+ shape_id = self._next_shape_id
+ name = "Chart %d" % (shape_id - 1)
+ graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame(
+ shape_id, name, rId, x, y, cx, cy
+ )
+ self._spTree.append(graphicFrame)
+ return graphicFrame
+
+ def _add_cxnSp(
+ self,
+ connector_type: MSO_CONNECTOR_TYPE,
+ begin_x: Length,
+ begin_y: Length,
+ end_x: Length,
+ end_y: Length,
+ ) -> CT_Connector:
+ """Return a newly-added `p:cxnSp` element as specified.
+
+ The `p:cxnSp` element is for a connector of `connector_type` beginning at (`begin_x`,
+ `begin_y`) and extending to (`end_x`, `end_y`).
+ """
+ id_ = self._next_shape_id
+ name = "Connector %d" % (id_ - 1)
+
+ flipH, flipV = begin_x > end_x, begin_y > end_y
+ x, y = min(begin_x, end_x), min(begin_y, end_y)
+ cx, cy = abs(end_x - begin_x), abs(end_y - begin_y)
+
+ return self._element.add_cxnSp(id_, name, connector_type, x, y, cx, cy, flipH, flipV)
+
+ def _add_pic_from_image_part(
+ self,
+ image_part: ImagePart,
+ rId: str,
+ x: Length,
+ y: Length,
+ cx: Length | None,
+ cy: Length | None,
+ ) -> CT_Picture:
+ """Return a newly appended `p:pic` element as specified.
+
+ The `p:pic` element displays the image in `image_part` with size and position specified by
+ `x`, `y`, `cx`, and `cy`. The element is appended to the shape tree, causing it to be
+ displayed first in z-order on the slide.
+ """
+ id_ = self._next_shape_id
+ scaled_cx, scaled_cy = image_part.scale(cx, cy)
+ name = "Picture %d" % (id_ - 1)
+ desc = image_part.desc
+ pic = self._grpSp.add_pic(id_, name, desc, rId, x, y, scaled_cx, scaled_cy)
+ return pic
+
+ def _add_sp(
+ self, autoshape_type: AutoShapeType, x: Length, y: Length, cx: Length, cy: Length
+ ) -> CT_Shape:
+ """Return newly-added `p:sp` element as specified.
+
+ `p:sp` element is of `autoshape_type` at position (`x`, `y`) and of size (`cx`, `cy`).
+ """
+ id_ = self._next_shape_id
+ name = "%s %d" % (autoshape_type.basename, id_ - 1)
+ sp = self._grpSp.add_autoshape(id_, name, autoshape_type.prst, x, y, cx, cy)
+ return sp
+
+ def _add_textbox_sp(self, x: Length, y: Length, cx: Length, cy: Length) -> CT_Shape:
+ """Return newly-appended textbox `p:sp` element.
+
+ Element has position (`x`, `y`) and size (`cx`, `cy`).
+ """
+ id_ = self._next_shape_id
+ name = "TextBox %d" % (id_ - 1)
+ sp = self._spTree.add_textbox(id_, name, x, y, cx, cy)
+ return sp
+
+ def _recalculate_extents(self) -> None:
+ """Adjust position and size to incorporate all contained shapes.
+
+ This would typically be called when a contained shape is added, removed, or its position
+ or size updated.
+ """
+ # ---default behavior is to do nothing, GroupShapes overrides to
+ # produce the distinctive behavior of groups and subgroups.---
+ pass
+
+
+class GroupShapes(_BaseGroupShapes):
+ """The sequence of child shapes belonging to a group shape.
+
+ Note that this collection can itself contain a group shape, making this part of a recursive,
+ tree data structure (acyclic graph).
+ """
+
+ def _recalculate_extents(self) -> None:
+ """Adjust position and size to incorporate all contained shapes.
+
+ This would typically be called when a contained shape is added, removed, or its position
+ or size updated.
+ """
+ self._grpSp.recalculate_extents()
+
+
+class SlideShapes(_BaseGroupShapes):
+ """Sequence of shapes appearing on a slide.
+
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+ Supports indexed access, len(), index(), and iteration.
+ """
+
+ parent: Slide # pyright: ignore[reportIncompatibleMethodOverride]
+
+ def add_movie(
+ self,
+ movie_file: str | IO[bytes],
+ left: Length,
+ top: Length,
+ width: Length,
+ height: Length,
+ poster_frame_image: str | IO[bytes] | None = None,
+ mime_type: str = CT.VIDEO,
+ ) -> GraphicFrame:
+ """Return newly added movie shape displaying video in `movie_file`.
+
+ **EXPERIMENTAL.** This method has important limitations:
+
+ * The size must be specified; no auto-scaling such as that provided by :meth:`add_picture`
+ is performed.
+ * The MIME type of the video file should be specified, e.g. 'video/mp4'. The provided
+ video file is not interrogated for its type. The MIME type `video/unknown` is used by
+ default (and works fine in tests as of this writing).
+ * A poster frame image must be provided, it cannot be automatically extracted from the
+ video file. If no poster frame is provided, the default "media loudspeaker" image will
+ be used.
+
+ Return a newly added movie shape to the slide, positioned at (`left`, `top`), having size
+ (`width`, `height`), and containing `movie_file`. Before the video is started,
+ `poster_frame_image` is displayed as a placeholder for the video.
+ """
+ movie_pic = _MoviePicElementCreator.new_movie_pic(
+ self,
+ self._next_shape_id,
+ movie_file,
+ left,
+ top,
+ width,
+ height,
+ poster_frame_image,
+ mime_type,
+ )
+ self._spTree.append(movie_pic)
+ self._add_video_timing(movie_pic)
+ return cast(GraphicFrame, self._shape_factory(movie_pic))
+
+ def add_table(
+ self, rows: int, cols: int, left: Length, top: Length, width: Length, height: Length
+ ) -> GraphicFrame:
+ """Add a |GraphicFrame| object containing a table.
+
+ The table has the specified number of `rows` and `cols` and the specified position and
+ size. `width` is evenly distributed between the columns of the new table. Likewise,
+ `height` is evenly distributed between the rows. Note that the `.table` property on the
+ returned |GraphicFrame| shape must be used to access the enclosed |Table| object.
+ """
+ graphicFrame = self._add_graphicFrame_containing_table(rows, cols, left, top, width, height)
+ return cast(GraphicFrame, self._shape_factory(graphicFrame))
+
+ def clone_layout_placeholders(self, slide_layout: SlideLayout) -> None:
+ """Add placeholder shapes based on those in `slide_layout`.
+
+ Z-order of placeholders is preserved. Latent placeholders (date, slide number, and footer)
+ are not cloned.
+ """
+ for placeholder in slide_layout.iter_cloneable_placeholders():
+ self.clone_placeholder(placeholder)
+
+ @property
+ def placeholders(self) -> SlidePlaceholders:
+ """Sequence of placeholder shapes in this slide."""
+ return self.parent.placeholders
+
+ @property
+ def title(self) -> Shape | None:
+ """The title placeholder shape on the slide.
+
+ |None| if the slide has no title placeholder.
+ """
+ for elm in self._spTree.iter_ph_elms():
+ if elm.ph_idx == 0:
+ return cast(Shape, self._shape_factory(elm))
+ return None
+
+ def _add_graphicFrame_containing_table(
+ self, rows: int, cols: int, x: Length, y: Length, cx: Length, cy: Length
+ ) -> CT_GraphicalObjectFrame:
+ """Return a newly added `p:graphicFrame` element containing a table as specified."""
+ _id = self._next_shape_id
+ name = "Table %d" % (_id - 1)
+ graphicFrame = self._spTree.add_table(_id, name, rows, cols, x, y, cx, cy)
+ return graphicFrame
+
+ def _add_video_timing(self, pic: CT_Picture) -> None:
+ """Add a `p:video` element under `p:sld/p:timing`.
+
+ The element will refer to the specified `pic` element by its shape id, and cause the video
+ play controls to appear for that video.
+ """
+ sld = self._spTree.xpath("/p:sld")[0]
+ childTnLst = sld.get_or_add_childTnLst()
+ childTnLst.add_video(pic.shape_id)
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return SlideShapeFactory(shape_elm, self)
+
+
+class LayoutShapes(_BaseShapes):
+ """Sequence of shapes appearing on a slide layout.
+
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+ Supports indexed access, len(), index(), and iteration.
+ """
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return _LayoutShapeFactory(shape_elm, self)
+
+
+class MasterShapes(_BaseShapes):
+ """Sequence of shapes appearing on a slide master.
+
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+ Supports indexed access, len(), and iteration.
+ """
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return _MasterShapeFactory(shape_elm, self)
+
+
+class NotesSlideShapes(_BaseShapes):
+ """Sequence of shapes appearing on a notes slide.
+
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+ Supports indexed access, len(), index(), and iteration.
+ """
+
+ def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str:
+ """Return the base name for a placeholder of `ph_type` in this shape collection.
+
+ A notes slide uses a different name for the body placeholder and has some unique
+ placeholder types, so this method overrides the default in the base class.
+ """
+ return {
+ PP_PLACEHOLDER.BODY: "Notes Placeholder",
+ PP_PLACEHOLDER.DATE: "Date Placeholder",
+ PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
+ PP_PLACEHOLDER.HEADER: "Header Placeholder",
+ PP_PLACEHOLDER.SLIDE_IMAGE: "Slide Image Placeholder",
+ PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
+ }[ph_type]
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` appearing on a notes slide."""
+ return _NotesSlideShapeFactory(shape_elm, self)
+
+
+class BasePlaceholders(_BaseShapes):
+ """Base class for placeholder collections.
+
+ Subclasses differentiate behaviors for a master, layout, and slide. By default, placeholder
+ shapes are constructed using |BaseShapeFactory|. Subclasses should override
+ :method:`_shape_factory` to use custom placeholder classes.
+ """
+
+ @staticmethod
+ def _is_member_elm(shape_elm: ShapeElement) -> bool:
+ """True if `shape_elm` is a placeholder shape, False otherwise."""
+ return shape_elm.has_ph_elm
+
+
+class LayoutPlaceholders(BasePlaceholders):
+ """Sequence of |LayoutPlaceholder| instance for each placeholder shape on a slide layout."""
+
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
+ [], Iterator[LayoutPlaceholder]
+ ]
+
+ def get(self, idx: int, default: LayoutPlaceholder | None = None) -> LayoutPlaceholder | None:
+ """The first placeholder shape with matching `idx` value, or `default` if not found."""
+ for placeholder in self:
+ if placeholder.element.ph_idx == idx:
+ return placeholder
+ return default
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return _LayoutShapeFactory(shape_elm, self)
+
+
+class MasterPlaceholders(BasePlaceholders):
+ """Sequence of MasterPlaceholder representing the placeholder shapes on a slide master."""
+
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
+ [], Iterator[MasterPlaceholder]
+ ]
+
+ def get(self, ph_type: PP_PLACEHOLDER, default: MasterPlaceholder | None = None):
+ """Return the first placeholder shape with type `ph_type` (e.g. 'body').
+
+ Returns `default` if no such placeholder shape is present in the collection.
+ """
+ for placeholder in self:
+ if placeholder.ph_type == ph_type:
+ return placeholder
+ return default
+
+ def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride]
+ self, placeholder_elm: CT_Shape
+ ) -> MasterPlaceholder:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return cast(MasterPlaceholder, _MasterShapeFactory(placeholder_elm, self))
+
+
+class NotesSlidePlaceholders(MasterPlaceholders):
+ """Sequence of placeholder shapes on a notes slide."""
+
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
+ [], Iterator[NotesSlidePlaceholder]
+ ]
+
+ def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride]
+ self, placeholder_elm: CT_Shape
+ ) -> NotesSlidePlaceholder:
+ """Return an instance of the appropriate placeholder proxy class for `placeholder_elm`."""
+ return cast(NotesSlidePlaceholder, _NotesSlideShapeFactory(placeholder_elm, self))
+
+
+class SlidePlaceholders(ParentedElementProxy):
+ """Collection of placeholder shapes on a slide.
+
+ Supports iteration, :func:`len`, and dictionary-style lookup on the `idx` value of the
+ placeholders it contains.
+ """
+
+ _element: CT_GroupShape
+
+ def __getitem__(self, idx: int):
+ """Access placeholder shape having `idx`.
+
+ Note that while this looks like list access, idx is actually a dictionary key and will
+ raise |KeyError| if no placeholder with that idx value is in the collection.
+ """
+ for e in self._element.iter_ph_elms():
+ if e.ph_idx == idx:
+ return SlideShapeFactory(e, self)
+ raise KeyError("no placeholder on this slide with idx == %d" % idx)
+
+ def __iter__(self):
+ """Generate placeholder shapes in `idx` order."""
+ ph_elms = sorted([e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx)
+ return (SlideShapeFactory(e, self) for e in ph_elms)
+
+ def __len__(self) -> int:
+ """Return count of placeholder shapes."""
+ return len(list(self._element.iter_ph_elms()))
+
+
+def BaseShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ tag = shape_elm.tag
+
+ if isinstance(shape_elm, CT_Picture):
+ videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile")
+ if videoFiles:
+ return Movie(shape_elm, parent)
+ return Picture(shape_elm, parent)
+
+ shape_cls = {
+ qn("p:cxnSp"): Connector,
+ qn("p:grpSp"): GroupShape,
+ qn("p:sp"): Shape,
+ qn("p:graphicFrame"): GraphicFrame,
+ }.get(tag, BaseShape)
+
+ return shape_cls(shape_elm, parent) # pyright: ignore[reportArgumentType]
+
+
+def _LayoutShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` on a slide layout."""
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+ return LayoutPlaceholder(shape_elm, parent)
+ return BaseShapeFactory(shape_elm, parent)
+
+
+def _MasterShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` on a slide master."""
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+ return MasterPlaceholder(shape_elm, parent)
+ return BaseShapeFactory(shape_elm, parent)
+
+
+def _NotesSlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` on a notes slide."""
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+ return NotesSlidePlaceholder(shape_elm, parent)
+ return BaseShapeFactory(shape_elm, parent)
+
+
+def _SlidePlaceholderFactory(shape_elm: ShapeElement, parent: ProvidesPart):
+ """Return a placeholder shape of the appropriate type for `shape_elm`."""
+ tag = shape_elm.tag
+ if tag == qn("p:sp"):
+ Constructor = {
+ PP_PLACEHOLDER.BITMAP: PicturePlaceholder,
+ PP_PLACEHOLDER.CHART: ChartPlaceholder,
+ PP_PLACEHOLDER.PICTURE: PicturePlaceholder,
+ PP_PLACEHOLDER.TABLE: TablePlaceholder,
+ }.get(shape_elm.ph_type, SlidePlaceholder)
+ elif tag == qn("p:graphicFrame"):
+ Constructor = PlaceholderGraphicFrame
+ elif tag == qn("p:pic"):
+ Constructor = PlaceholderPicture
+ else:
+ Constructor = BaseShapeFactory
+ return Constructor(shape_elm, parent) # pyright: ignore[reportArgumentType]
+
+
+def SlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` on a slide."""
+ if shape_elm.has_ph_elm:
+ return _SlidePlaceholderFactory(shape_elm, parent)
+ return BaseShapeFactory(shape_elm, parent)
+
+
+class _MoviePicElementCreator(object):
+ """Functional service object for creating a new movie p:pic element.
+
+ It's entire external interface is its :meth:`new_movie_pic` class method that returns a new
+ `p:pic` element containing the specified video. This class is not intended to be constructed
+ or an instance of it retained by the caller; it is a "one-shot" object, really a function
+ wrapped in a object such that its helper methods can be organized here.
+ """
+
+ def __init__(
+ self,
+ shapes: SlideShapes,
+ shape_id: int,
+ movie_file: str | IO[bytes],
+ x: Length,
+ y: Length,
+ cx: Length,
+ cy: Length,
+ poster_frame_file: str | IO[bytes] | None,
+ mime_type: str | None,
+ ):
+ super(_MoviePicElementCreator, self).__init__()
+ self._shapes = shapes
+ self._shape_id = shape_id
+ self._movie_file = movie_file
+ self._x, self._y, self._cx, self._cy = x, y, cx, cy
+ self._poster_frame_file = poster_frame_file
+ self._mime_type = mime_type
+
+ @classmethod
+ def new_movie_pic(
+ cls,
+ shapes: SlideShapes,
+ shape_id: int,
+ movie_file: str | IO[bytes],
+ x: Length,
+ y: Length,
+ cx: Length,
+ cy: Length,
+ poster_frame_image: str | IO[bytes] | None,
+ mime_type: str | None,
+ ) -> CT_Picture:
+ """Return a new `p:pic` element containing video in `movie_file`.
+
+ If `mime_type` is None, 'video/unknown' is used. If `poster_frame_file` is None, the
+ default "media loudspeaker" image is used.
+ """
+ return cls(shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type)._pic
+
+ @property
+ def _media_rId(self) -> str:
+ """Return the rId of RT.MEDIA relationship to video part.
+
+ For historical reasons, there are two relationships to the same part; one is the video rId
+ and the other is the media rId.
+ """
+ return self._video_part_rIds[0]
+
+ @lazyproperty
+ def _pic(self) -> CT_Picture:
+ """Return the new `p:pic` element referencing the video."""
+ return CT_Picture.new_video_pic(
+ self._shape_id,
+ self._shape_name,
+ self._video_rId,
+ self._media_rId,
+ self._poster_frame_rId,
+ self._x,
+ self._y,
+ self._cx,
+ self._cy,
+ )
+
+ @lazyproperty
+ def _poster_frame_image_file(self) -> str | IO[bytes]:
+ """Return the image file for video placeholder image.
+
+ If no poster frame file is provided, the default "media loudspeaker" image is used.
+ """
+ poster_frame_file = self._poster_frame_file
+ if poster_frame_file is None:
+ return io.BytesIO(SPEAKER_IMAGE_BYTES)
+ return poster_frame_file
+
+ @lazyproperty
+ def _poster_frame_rId(self) -> str:
+ """Return the rId of relationship to poster frame image.
+
+ The poster frame is the image used to represent the video before it's played.
+ """
+ _, poster_frame_rId = self._slide_part.get_or_add_image_part(self._poster_frame_image_file)
+ return poster_frame_rId
+
+ @property
+ def _shape_name(self) -> str:
+ """Return the appropriate shape name for the p:pic shape.
+
+ A movie shape is named with the base filename of the video.
+ """
+ return self._video.filename
+
+ @property
+ def _slide_part(self) -> SlidePart:
+ """Return SlidePart object for slide containing this movie."""
+ return self._shapes.part
+
+ @lazyproperty
+ def _video(self) -> Video:
+ """Return a |Video| object containing the movie file."""
+ return Video.from_path_or_file_like(self._movie_file, self._mime_type)
+
+ @lazyproperty
+ def _video_part_rIds(self) -> tuple[str, str]:
+ """Return the rIds for relationships to media part for video.
+
+ This is where the media part and its relationships to the slide are actually created.
+ """
+ media_rId, video_rId = self._slide_part.get_or_add_video_media_part(self._video)
+ return media_rId, video_rId
+
+ @property
+ def _video_rId(self) -> str:
+ """Return the rId of RT.VIDEO relationship to video part.
+
+ For historical reasons, there are two relationships to the same part; one is the video rId
+ and the other is the media rId.
+ """
+ return self._video_part_rIds[1]
+
+
+class _OleObjectElementCreator(object):
+ """Functional service object for creating a new OLE-object p:graphicFrame element.
+
+ It's entire external interface is its :meth:`graphicFrame` class method that returns a new
+ `p:graphicFrame` element containing the specified embedded OLE-object shape. This class is not
+ intended to be constructed or an instance of it retained by the caller; it is a "one-shot"
+ object, really a function wrapped in a object such that its helper methods can be organized
+ here.
+ """
+
+ def __init__(
+ self,
+ shapes: _BaseGroupShapes,
+ shape_id: int,
+ ole_object_file: str | IO[bytes],
+ prog_id: PROG_ID | str,
+ x: Length,
+ y: Length,
+ cx: Length | None,
+ cy: Length | None,
+ icon_file: str | IO[bytes] | None,
+ icon_width: Length | None,
+ icon_height: Length | None,
+ ):
+ self._shapes = shapes
+ self._shape_id = shape_id
+ self._ole_object_file = ole_object_file
+ self._prog_id_arg = prog_id
+ self._x = x
+ self._y = y
+ self._cx_arg = cx
+ self._cy_arg = cy
+ self._icon_file_arg = icon_file
+ self._icon_width_arg = icon_width
+ self._icon_height_arg = icon_height
+
+ @classmethod
+ def graphicFrame(
+ cls,
+ shapes: _BaseGroupShapes,
+ shape_id: int,
+ ole_object_file: str | IO[bytes],
+ prog_id: PROG_ID | str,
+ x: Length,
+ y: Length,
+ cx: Length | None,
+ cy: Length | None,
+ icon_file: str | IO[bytes] | None,
+ icon_width: Length | None,
+ icon_height: Length | None,
+ ) -> CT_GraphicalObjectFrame:
+ """Return new `p:graphicFrame` element containing embedded `ole_object_file`."""
+ return cls(
+ shapes,
+ shape_id,
+ ole_object_file,
+ prog_id,
+ x,
+ y,
+ cx,
+ cy,
+ icon_file,
+ icon_width,
+ icon_height,
+ )._graphicFrame
+
+ @lazyproperty
+ def _graphicFrame(self) -> CT_GraphicalObjectFrame:
+ """Newly-created `p:graphicFrame` element referencing embedded OLE-object."""
+ return CT_GraphicalObjectFrame.new_ole_object_graphicFrame(
+ self._shape_id,
+ self._shape_name,
+ self._ole_object_rId,
+ self._progId,
+ self._icon_rId,
+ self._x,
+ self._y,
+ self._cx,
+ self._cy,
+ self._icon_width,
+ self._icon_height,
+ )
+
+ @lazyproperty
+ def _cx(self) -> Length:
+ """Emu object specifying width of "show-as-icon" image for OLE shape."""
+ # --- a user-specified width overrides any default ---
+ if self._cx_arg is not None:
+ return self._cx_arg
+
+ # --- the default width is specified by the PROG_ID member if prog_id is one,
+ # --- otherwise it gets the default icon width.
+ return (
+ Emu(self._prog_id_arg.width) if isinstance(self._prog_id_arg, PROG_ID) else Emu(965200)
+ )
+
+ @lazyproperty
+ def _cy(self) -> Length:
+ """Emu object specifying height of "show-as-icon" image for OLE shape."""
+ # --- a user-specified width overrides any default ---
+ if self._cy_arg is not None:
+ return self._cy_arg
+
+ # --- the default height is specified by the PROG_ID member if prog_id is one,
+ # --- otherwise it gets the default icon height.
+ return (
+ Emu(self._prog_id_arg.height) if isinstance(self._prog_id_arg, PROG_ID) else Emu(609600)
+ )
+
+ @lazyproperty
+ def _icon_height(self) -> Length:
+ """Vertical size of enclosed EMF icon within the OLE graphic-frame.
+
+ This must be specified when a custom icon is used, to avoid stretching of the image and
+ possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it.
+
+ The correct size can be determined by creating an example PPTX using PowerPoint and then
+ inspecting the XML of the OLE graphics-frame (p:oleObj.imgH).
+ """
+ return self._icon_height_arg if self._icon_height_arg is not None else Emu(609600)
+
+ @lazyproperty
+ def _icon_image_file(self) -> str | IO[bytes]:
+ """Reference to image file containing icon to show in lieu of this object.
+
+ This can be either a str path or a file-like object (io.BytesIO typically).
+ """
+ # --- a user-specified icon overrides any default ---
+ if self._icon_file_arg is not None:
+ return self._icon_file_arg
+
+ # --- A prog_id belonging to PROG_ID gets its icon filename from there. A
+ # --- user-specified (str) prog_id gets the default icon.
+ icon_filename = (
+ self._prog_id_arg.icon_filename
+ if isinstance(self._prog_id_arg, PROG_ID)
+ else "generic-icon.emf"
+ )
+
+ _thisdir = os.path.split(__file__)[0]
+ return os.path.abspath(os.path.join(_thisdir, "..", "templates", icon_filename))
+
+ @lazyproperty
+ def _icon_rId(self) -> str:
+ """str rId like "rId7" of rel to icon (image) representing OLE-object part."""
+ _, rId = self._slide_part.get_or_add_image_part(self._icon_image_file)
+ return rId
+
+ @lazyproperty
+ def _icon_width(self) -> Length:
+ """Width of enclosed EMF icon within the OLE graphic-frame.
+
+ This must be specified when a custom icon is used, to avoid stretching of the image and
+ possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it.
+ """
+ return self._icon_width_arg if self._icon_width_arg is not None else Emu(965200)
+
+ @lazyproperty
+ def _ole_object_rId(self) -> str:
+ """str rId like "rId6" of relationship to embedded ole_object part.
+
+ This is where the ole_object part and its relationship to the slide are actually created.
+ """
+ return self._slide_part.add_embedded_ole_object_part(
+ self._prog_id_arg, self._ole_object_file
+ )
+
+ @lazyproperty
+ def _progId(self) -> str:
+ """str like "Excel.Sheet.12" identifying program used to open object.
+
+ This value appears in the `progId` attribute of the `p:oleObj` element for the object.
+ """
+ prog_id_arg = self._prog_id_arg
+
+ # --- member of PROG_ID enumeration knows its progId keyphrase, otherwise caller
+ # --- has specified it explicitly (as str)
+ return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg
+
+ @lazyproperty
+ def _shape_name(self) -> str:
+ """str name like "Object 1" for the embedded ole_object shape.
+
+ The name is formed from the prefix "Object " and the shape-id decremented by 1.
+ """
+ return "Object %d" % (self._shape_id - 1)
+
+ @lazyproperty
+ def _slide_part(self) -> SlidePart:
+ """SlidePart object for this slide."""
+ return self._shapes.part