about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pptx/shapes
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/shapes')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py26
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py355
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/base.py244
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/connector.py297
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py337
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py166
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/group.py69
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/picture.py203
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py407
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py1190
10 files changed, 3294 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py b/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py
new file mode 100644
index 00000000..332109a3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py
@@ -0,0 +1,26 @@
+"""Objects used across sub-package."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pptx.opc.package import XmlPart
+    from pptx.types import ProvidesPart
+
+
+class Subshape(object):
+    """Provides access to the containing part for drawing elements that occur below a shape.
+
+    Access to the part is required for example to add or drop a relationship. Provides
+    `self._parent` attribute to subclasses.
+    """
+
+    def __init__(self, parent: ProvidesPart):
+        super(Subshape, self).__init__()
+        self._parent = parent
+
+    @property
+    def part(self) -> XmlPart:
+        """The package part containing this object."""
+        return self._parent.part
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py b/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py
new file mode 100644
index 00000000..c7f8cd93
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py
@@ -0,0 +1,355 @@
+"""Autoshape-related objects such as Shape and Adjustment."""
+
+from __future__ import annotations
+
+from numbers import Number
+from typing import TYPE_CHECKING, Iterable
+from xml.sax import saxutils
+
+from pptx.dml.fill import FillFormat
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.spec import autoshape_types
+from pptx.text.text import TextFrame
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.autoshape import CT_GeomGuide, CT_PresetGeometry2D, CT_Shape
+    from pptx.spec import AdjustmentValue
+    from pptx.types import ProvidesPart
+
+
+class Adjustment:
+    """An adjustment value for an autoshape.
+
+    An adjustment value corresponds to the position of an adjustment handle on an auto shape.
+    Adjustment handles are the small yellow diamond-shaped handles that appear on certain auto
+    shapes and allow the outline of the shape to be adjusted. For example, a rounded rectangle has
+    an adjustment handle that allows the radius of its corner rounding to be adjusted.
+
+    Values are |float| and generally range from 0.0 to 1.0, although the value can be negative or
+    greater than 1.0 in certain circumstances.
+    """
+
+    def __init__(self, name: str, def_val: int, actual: int | None = None):
+        super(Adjustment, self).__init__()
+        self.name = name
+        self.def_val = def_val
+        self.actual = actual
+
+    @property
+    def effective_value(self) -> float:
+        """Read/write |float| representing normalized adjustment value for this adjustment.
+
+        Actual values are a large-ish integer expressed in shape coordinates, nominally between 0
+        and 100,000. The effective value is normalized to a corresponding value nominally between
+        0.0 and 1.0. Intuitively this represents the proportion of the width or height of the shape
+        at which the adjustment value is located from its starting point. For simple shapes such as
+        a rounded rectangle, this intuitive correspondence holds. For more complicated shapes and
+        at more extreme shape proportions (e.g. width is much greater than height), the value can
+        become negative or greater than 1.0.
+        """
+        raw_value = self.actual if self.actual is not None else self.def_val
+        return self._normalize(raw_value)
+
+    @effective_value.setter
+    def effective_value(self, value: float):
+        if not isinstance(value, Number):
+            raise ValueError(f"adjustment value must be numeric, got {repr(value)}")
+        self.actual = self._denormalize(value)
+
+    @staticmethod
+    def _denormalize(value: float) -> int:
+        """Return integer corresponding to normalized `raw_value` on unit basis of 100,000.
+
+        See Adjustment.normalize for additional details.
+        """
+        return int(value * 100000.0)
+
+    @staticmethod
+    def _normalize(raw_value: int) -> float:
+        """Return normalized value for `raw_value`.
+
+        A normalized value is a |float| between 0.0 and 1.0 for nominal raw values between 0 and
+        100,000. Raw values less than 0 and greater than 100,000 are valid and return values
+        calculated on the same unit basis of 100,000.
+        """
+        return raw_value / 100000.0
+
+    @property
+    def val(self) -> int:
+        """Denormalized effective value.
+
+        Expressed in shape coordinates, this is suitable for using in the XML.
+        """
+        return self.actual if self.actual is not None else self.def_val
+
+
+class AdjustmentCollection:
+    """Sequence of |Adjustment| instances for an auto shape.
+
+    Each represents an available adjustment for a shape of its type. Supports `len()` and indexed
+    access, e.g. `shape.adjustments[1] = 0.15`.
+    """
+
+    def __init__(self, prstGeom: CT_PresetGeometry2D):
+        super(AdjustmentCollection, self).__init__()
+        self._adjustments_ = self._initialized_adjustments(prstGeom)
+        self._prstGeom = prstGeom
+
+    def __getitem__(self, idx: int) -> float:
+        """Provides indexed access, (e.g. 'adjustments[9]')."""
+        return self._adjustments_[idx].effective_value
+
+    def __setitem__(self, idx: int, value: float):
+        """Provides item assignment via an indexed expression, e.g. `adjustments[9] = 999.9`.
+
+        Causes all adjustment values in collection to be written to the XML.
+        """
+        self._adjustments_[idx].effective_value = value
+        self._rewrite_guides()
+
+    def _initialized_adjustments(self, prstGeom: CT_PresetGeometry2D | None) -> list[Adjustment]:
+        """Return an initialized list of adjustment values based on the contents of `prstGeom`."""
+        if prstGeom is None:
+            return []
+        davs = AutoShapeType.default_adjustment_values(prstGeom.prst)
+        adjustments = [Adjustment(name, def_val) for name, def_val in davs]
+        self._update_adjustments_with_actuals(adjustments, prstGeom.gd_lst)
+        return adjustments
+
+    def _rewrite_guides(self):
+        """Write `a:gd` elements to the XML, one for each adjustment value.
+
+        Any existing guide elements are overwritten.
+        """
+        guides = [(adj.name, adj.val) for adj in self._adjustments_]
+        self._prstGeom.rewrite_guides(guides)
+
+    @staticmethod
+    def _update_adjustments_with_actuals(
+        adjustments: Iterable[Adjustment], guides: Iterable[CT_GeomGuide]
+    ):
+        """Update |Adjustment| instances in `adjustments` with actual values held in `guides`.
+
+        `guides` is a list of `a:gd` elements. Guides with a name that does not match an adjustment
+        object are skipped.
+        """
+        adjustments_by_name = dict((adj.name, adj) for adj in adjustments)
+        for gd in guides:
+            name = gd.name
+            actual = int(gd.fmla[4:])
+            try:
+                adjustment = adjustments_by_name[name]
+            except KeyError:
+                continue
+            adjustment.actual = actual
+        return
+
+    @property
+    def _adjustments(self) -> tuple[Adjustment, ...]:
+        """Sequence of |Adjustment| objects contained in collection."""
+        return tuple(self._adjustments_)
+
+    def __len__(self):
+        """Implement built-in function len()"""
+        return len(self._adjustments_)
+
+
+class AutoShapeType:
+    """Provides access to metadata for an auto-shape of type identified by `autoshape_type_id`.
+
+    Instances are cached, so no more than one instance for a particular auto shape type is in
+    memory.
+
+    Instances provide the following attributes:
+
+    .. attribute:: autoshape_type_id
+
+       Integer uniquely identifying this auto shape type. Corresponds to a
+       value in `pptx.constants.MSO` like `MSO_SHAPE.ROUNDED_RECTANGLE`.
+
+    .. attribute:: basename
+
+       Base part of shape name for auto shapes of this type, e.g. `Rounded
+       Rectangle` becomes `Rounded Rectangle 99` when the distinguishing
+       integer is added to the shape name.
+
+    .. attribute:: prst
+
+       String identifier for this auto shape type used in the `a:prstGeom`
+       element.
+
+    """
+
+    _instances: dict[MSO_AUTO_SHAPE_TYPE, AutoShapeType] = {}
+
+    def __new__(cls, autoshape_type_id: MSO_AUTO_SHAPE_TYPE) -> AutoShapeType:
+        """Only create new instance on first call for content_type.
+
+        After that, use cached instance.
+        """
+        # -- if there's not a matching instance in the cache, create one --
+        if autoshape_type_id not in cls._instances:
+            inst = super(AutoShapeType, cls).__new__(cls)
+            cls._instances[autoshape_type_id] = inst
+        # -- return the instance; note that __init__() gets called either way --
+        return cls._instances[autoshape_type_id]
+
+    def __init__(self, autoshape_type_id: MSO_AUTO_SHAPE_TYPE):
+        """Initialize attributes from constant values in `pptx.spec`."""
+        # -- skip loading if this instance is from the cache --
+        if hasattr(self, "_loaded"):
+            return
+        # -- raise on bad autoshape_type_id --
+        if autoshape_type_id not in autoshape_types:
+            raise KeyError(
+                "no autoshape type with id '%s' in pptx.spec.autoshape_types" % autoshape_type_id
+            )
+        # -- otherwise initialize new instance --
+        autoshape_type = autoshape_types[autoshape_type_id]
+        self._autoshape_type_id = autoshape_type_id
+        self._basename = autoshape_type["basename"]
+        self._loaded = True
+
+    @property
+    def autoshape_type_id(self) -> MSO_AUTO_SHAPE_TYPE:
+        """MSO_AUTO_SHAPE_TYPE enumeration member identifying this auto shape type."""
+        return self._autoshape_type_id
+
+    @property
+    def basename(self) -> str:
+        """Base of shape name for this auto shape type.
+
+        A shape name is like "Rounded Rectangle 7" and appears as an XML attribute for example at
+        `p:sp/p:nvSpPr/p:cNvPr{name}`. This basename value is the name less the distinguishing
+        integer. This value is escaped because at least one autoshape-type name includes double
+        quotes ('"No" Symbol').
+        """
+        return saxutils.escape(self._basename, {'"': """})
+
+    @classmethod
+    def default_adjustment_values(cls, prst: MSO_AUTO_SHAPE_TYPE) -> tuple[AdjustmentValue, ...]:
+        """Sequence of (name, value) pair adjustment value defaults for `prst` autoshape-type."""
+        return autoshape_types[prst]["avLst"]
+
+    @classmethod
+    def id_from_prst(cls, prst: str) -> MSO_AUTO_SHAPE_TYPE:
+        """Select auto shape type with matching `prst`.
+
+        e.g. `MSO_SHAPE.RECTANGLE` corresponding to preset geometry keyword `"rect"`.
+        """
+        return MSO_AUTO_SHAPE_TYPE.from_xml(prst)
+
+    @property
+    def prst(self):
+        """
+        Preset geometry identifier string for this auto shape. Used in the
+        `prst` attribute of `a:prstGeom` element to specify the geometry
+        to be used in rendering the shape, for example `'roundRect'`.
+        """
+        return MSO_AUTO_SHAPE_TYPE.to_xml(self._autoshape_type_id)
+
+
+class Shape(BaseShape):
+    """A shape that can appear on a slide.
+
+    Corresponds to the `p:sp` element that can appear in any of the slide-type parts
+    (slide, slideLayout, slideMaster, notesPage, notesMaster, handoutMaster).
+    """
+
+    def __init__(self, sp: CT_Shape, parent: ProvidesPart):
+        super(Shape, self).__init__(sp, parent)
+        self._sp = sp
+
+    @lazyproperty
+    def adjustments(self) -> AdjustmentCollection:
+        """Read-only reference to |AdjustmentCollection| instance for this shape."""
+        return AdjustmentCollection(self._sp.prstGeom)
+
+    @property
+    def auto_shape_type(self):
+        """Enumeration value identifying the type of this auto shape.
+
+        Like `MSO_SHAPE.ROUNDED_RECTANGLE`. Raises |ValueError| if this shape is not an auto shape.
+        """
+        if not self._sp.is_autoshape:
+            raise ValueError("shape is not an auto shape")
+        return self._sp.prst
+
+    @lazyproperty
+    def fill(self):
+        """|FillFormat| instance for this shape.
+
+        Provides access to fill properties such as fill color.
+        """
+        return FillFormat.from_fill_parent(self._sp.spPr)
+
+    def get_or_add_ln(self):
+        """Return the `a:ln` element containing the line format properties XML for this shape."""
+        return self._sp.get_or_add_ln()
+
+    @property
+    def has_text_frame(self) -> bool:
+        """|True| if this shape can contain text. Always |True| for an AutoShape."""
+        return True
+
+    @lazyproperty
+    def line(self):
+        """|LineFormat| instance for this shape.
+
+        Provides access to line properties such as line color.
+        """
+        return LineFormat(self)
+
+    @property
+    def ln(self):
+        """The `a:ln` element containing the line format properties such as line color and width.
+
+        |None| if no `a:ln` element is present.
+        """
+        return self._sp.ln
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Unique integer identifying the type of this shape, like `MSO_SHAPE_TYPE.TEXT_BOX`."""
+        if self.is_placeholder:
+            return MSO_SHAPE_TYPE.PLACEHOLDER
+        if self._sp.has_custom_geometry:
+            return MSO_SHAPE_TYPE.FREEFORM
+        if self._sp.is_autoshape:
+            return MSO_SHAPE_TYPE.AUTO_SHAPE
+        if self._sp.is_textbox:
+            return MSO_SHAPE_TYPE.TEXT_BOX
+        raise NotImplementedError("Shape instance of unrecognized shape type")
+
+    @property
+    def text(self) -> str:
+        """Read/write. Text in shape as a single string.
+
+        The returned string will contain a newline character (`"\\n"`) separating each paragraph
+        and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the
+        shape's text.
+
+        Assignment to `text` replaces any text previously contained in the shape, along with any
+        paragraph or font formatting applied to it. A newline character (`"\\n"`) in the assigned
+        text causes a new paragraph to be started. A vertical-tab (`"\\v"`) character in the
+        assigned text causes a line-break (soft carriage-return) to be inserted. (The vertical-tab
+        character appears in clipboard text copied from PowerPoint as its str encoding of
+        line-breaks.)
+        """
+        return self.text_frame.text
+
+    @text.setter
+    def text(self, text: str):
+        self.text_frame.text = text
+
+    @property
+    def text_frame(self):
+        """|TextFrame| instance for this shape.
+
+        Contains the text of the shape and provides access to text formatting properties.
+        """
+        txBody = self._sp.get_or_add_txBody()
+        return TextFrame(txBody, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/base.py b/.venv/lib/python3.12/site-packages/pptx/shapes/base.py
new file mode 100644
index 00000000..75123502
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/base.py
@@ -0,0 +1,244 @@
+"""Base shape-related objects such as BaseShape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.action import ActionSetting
+from pptx.dml.effect import ShadowFormat
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
+    from pptx.oxml.shapes import ShapeElement
+    from pptx.oxml.shapes.shared import CT_Placeholder
+    from pptx.parts.slide import BaseSlidePart
+    from pptx.types import ProvidesPart
+    from pptx.util import Length
+
+
+class BaseShape(object):
+    """Base class for shape objects.
+
+    Subclasses include |Shape|, |Picture|, and |GraphicFrame|.
+    """
+
+    def __init__(self, shape_elm: ShapeElement, parent: ProvidesPart):
+        super().__init__()
+        self._element = shape_elm
+        self._parent = parent
+
+    def __eq__(self, other: object) -> bool:
+        """|True| if this shape object proxies the same element as *other*.
+
+        Equality for proxy objects is defined as referring to the same XML element, whether or not
+        they are the same proxy object instance.
+        """
+        if not isinstance(other, BaseShape):
+            return False
+        return self._element is other._element
+
+    def __ne__(self, other: object) -> bool:
+        if not isinstance(other, BaseShape):
+            return True
+        return self._element is not other._element
+
+    @lazyproperty
+    def click_action(self) -> ActionSetting:
+        """|ActionSetting| instance providing access to click behaviors.
+
+        Click behaviors are hyperlink-like behaviors including jumping to a hyperlink (web page)
+        or to another slide in the presentation. The click action is that defined on the overall
+        shape, not a run of text within the shape. An |ActionSetting| object is always returned,
+        even when no click behavior is defined on the shape.
+        """
+        cNvPr = self._element._nvXxPr.cNvPr  # pyright: ignore[reportPrivateUsage]
+        return ActionSetting(cNvPr, self)
+
+    @property
+    def element(self) -> ShapeElement:
+        """`lxml` element for this shape, e.g. a CT_Shape instance.
+
+        Note that manipulating this element improperly can produce an invalid presentation file.
+        Make sure you know what you're doing if you use this to change the underlying XML.
+        """
+        return self._element
+
+    @property
+    def has_chart(self) -> bool:
+        """|True| if this shape is a graphic frame containing a chart object.
+
+        |False| otherwise. When |True|, the chart object can be accessed using the ``.chart``
+        property.
+        """
+        # This implementation is unconditionally False, the True version is
+        # on GraphicFrame subclass.
+        return False
+
+    @property
+    def has_table(self) -> bool:
+        """|True| if this shape is a graphic frame containing a table object.
+
+        |False| otherwise. When |True|, the table object can be accessed using the ``.table``
+        property.
+        """
+        # This implementation is unconditionally False, the True version is
+        # on GraphicFrame subclass.
+        return False
+
+    @property
+    def has_text_frame(self) -> bool:
+        """|True| if this shape can contain text."""
+        # overridden on Shape to return True. Only <p:sp> has text frame
+        return False
+
+    @property
+    def height(self) -> Length:
+        """Read/write. Integer distance between top and bottom extents of shape in EMUs."""
+        return self._element.cy
+
+    @height.setter
+    def height(self, value: Length):
+        self._element.cy = value
+
+    @property
+    def is_placeholder(self) -> bool:
+        """True if this shape is a placeholder.
+
+        A shape is a placeholder if it has a <p:ph> element.
+        """
+        return self._element.has_ph_elm
+
+    @property
+    def left(self) -> Length:
+        """Integer distance of the left edge of this shape from the left edge of the slide.
+
+        Read/write. Expressed in English Metric Units (EMU)
+        """
+        return self._element.x
+
+    @left.setter
+    def left(self, value: Length):
+        self._element.x = value
+
+    @property
+    def name(self) -> str:
+        """Name of this shape, e.g. 'Picture 7'."""
+        return self._element.shape_name
+
+    @name.setter
+    def name(self, value: str):
+        self._element._nvXxPr.cNvPr.name = value  # pyright: ignore[reportPrivateUsage]
+
+    @property
+    def part(self) -> BaseSlidePart:
+        """The package part containing this shape.
+
+        A |BaseSlidePart| subclass in this case. Access to a slide part should only be required if
+        you are extending the behavior of |pp| API objects.
+        """
+        return cast("BaseSlidePart", self._parent.part)
+
+    @property
+    def placeholder_format(self) -> _PlaceholderFormat:
+        """Provides access to placeholder-specific properties such as placeholder type.
+
+        Raises |ValueError| on access if the shape is not a placeholder.
+        """
+        ph = self._element.ph
+        if ph is None:
+            raise ValueError("shape is not a placeholder")
+        return _PlaceholderFormat(ph)
+
+    @property
+    def rotation(self) -> float:
+        """Degrees of clockwise rotation.
+
+        Read/write float. Negative values can be assigned to indicate counter-clockwise rotation,
+        e.g. assigning -45.0 will change setting to 315.0.
+        """
+        return self._element.rot
+
+    @rotation.setter
+    def rotation(self, value: float):
+        self._element.rot = value
+
+    @lazyproperty
+    def shadow(self) -> ShadowFormat:
+        """|ShadowFormat| object providing access to shadow for this shape.
+
+        A |ShadowFormat| object is always returned, even when no shadow is
+        explicitly defined on this shape (i.e. it inherits its shadow
+        behavior).
+        """
+        return ShadowFormat(self._element.spPr)
+
+    @property
+    def shape_id(self) -> int:
+        """Read-only positive integer identifying this shape.
+
+        The id of a shape is unique among all shapes on a slide.
+        """
+        return self._element.shape_id
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """A member of MSO_SHAPE_TYPE classifying this shape by type.
+
+        Like ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses.
+        """
+        raise NotImplementedError(f"{type(self).__name__} does not implement `.shape_type`")
+
+    @property
+    def top(self) -> Length:
+        """Distance from the top edge of the slide to the top edge of this shape.
+
+        Read/write. Expressed in English Metric Units (EMU)
+        """
+        return self._element.y
+
+    @top.setter
+    def top(self, value: Length):
+        self._element.y = value
+
+    @property
+    def width(self) -> Length:
+        """Distance between left and right extents of this shape.
+
+        Read/write. Expressed in English Metric Units (EMU).
+        """
+        return self._element.cx
+
+    @width.setter
+    def width(self, value: Length):
+        self._element.cx = value
+
+
+class _PlaceholderFormat(ElementProxy):
+    """Provides properties specific to placeholders, such as the placeholder type.
+
+    Accessed via the :attr:`~.BaseShape.placeholder_format` property of a placeholder shape,
+    """
+
+    def __init__(self, element: CT_Placeholder):
+        super().__init__(element)
+        self._ph = element
+
+    @property
+    def element(self) -> CT_Placeholder:
+        """The `p:ph` element proxied by this object."""
+        return self._ph
+
+    @property
+    def idx(self) -> int:
+        """Integer placeholder 'idx' attribute."""
+        return self._ph.idx
+
+    @property
+    def type(self) -> PP_PLACEHOLDER:
+        """Placeholder type.
+
+        A member of the :ref:`PpPlaceholderType` enumeration, e.g. PP_PLACEHOLDER.CHART
+        """
+        return self._ph.type
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py b/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py
new file mode 100644
index 00000000..070b080d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py
@@ -0,0 +1,297 @@
+"""Connector (line) shape and related objects.
+
+A connector is a line shape having end-points that can be connected to other
+objects (but not to other connectors). A connector can be straight, have
+elbows, or can be curved.
+"""
+
+from __future__ import annotations
+
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.util import Emu, lazyproperty
+
+
+class Connector(BaseShape):
+    """Connector (line) shape.
+
+    A connector is a linear shape having end-points that can be connected to
+    other objects (but not to other connectors). A connector can be straight,
+    have elbows, or can be curved.
+    """
+
+    def begin_connect(self, shape, cxn_pt_idx):
+        """
+        **EXPERIMENTAL** - *The current implementation only works properly
+        with rectangular shapes, such as pictures and rectangles. Use with
+        other shape types may cause unexpected visual alignment of the
+        connected end-point and could lead to a load error if cxn_pt_idx
+        exceeds the connection point count available on the connected shape.
+        That said, a quick test should reveal what to expect when using this
+        method with other shape types.*
+
+        Connect the beginning of this connector to *shape* at the connection
+        point specified by *cxn_pt_idx*. Each shape has zero or more
+        connection points and they are identified by index, starting with 0.
+        Generally, the first connection point of a shape is at the top center
+        of its bounding box and numbering proceeds counter-clockwise from
+        there. However this is only a convention and may vary, especially
+        with non built-in shapes.
+        """
+        self._connect_begin_to(shape, cxn_pt_idx)
+        self._move_begin_to_cxn(shape, cxn_pt_idx)
+
+    @property
+    def begin_x(self):
+        """
+        Return the X-position of the begin point of this connector, in
+        English Metric Units (as a |Length| object).
+        """
+        cxnSp = self._element
+        x, cx, flipH = cxnSp.x, cxnSp.cx, cxnSp.flipH
+        begin_x = x + cx if flipH else x
+        return Emu(begin_x)
+
+    @begin_x.setter
+    def begin_x(self, value):
+        cxnSp = self._element
+        x, cx, flipH, new_x = cxnSp.x, cxnSp.cx, cxnSp.flipH, int(value)
+
+        if flipH:
+            old_x = x + cx
+            dx = abs(new_x - old_x)
+            if new_x >= old_x:
+                cxnSp.cx = cx + dx
+            elif dx <= cx:
+                cxnSp.cx = cx - dx
+            else:
+                cxnSp.flipH = False
+                cxnSp.x = new_x
+                cxnSp.cx = dx - cx
+        else:
+            dx = abs(new_x - x)
+            if new_x <= x:
+                cxnSp.x = new_x
+                cxnSp.cx = cx + dx
+            elif dx <= cx:
+                cxnSp.x = new_x
+                cxnSp.cx = cx - dx
+            else:
+                cxnSp.flipH = True
+                cxnSp.x = x + cx
+                cxnSp.cx = dx - cx
+
+    @property
+    def begin_y(self):
+        """
+        Return the Y-position of the begin point of this connector, in
+        English Metric Units (as a |Length| object).
+        """
+        cxnSp = self._element
+        y, cy, flipV = cxnSp.y, cxnSp.cy, cxnSp.flipV
+        begin_y = y + cy if flipV else y
+        return Emu(begin_y)
+
+    @begin_y.setter
+    def begin_y(self, value):
+        cxnSp = self._element
+        y, cy, flipV, new_y = cxnSp.y, cxnSp.cy, cxnSp.flipV, int(value)
+
+        if flipV:
+            old_y = y + cy
+            dy = abs(new_y - old_y)
+            if new_y >= old_y:
+                cxnSp.cy = cy + dy
+            elif dy <= cy:
+                cxnSp.cy = cy - dy
+            else:
+                cxnSp.flipV = False
+                cxnSp.y = new_y
+                cxnSp.cy = dy - cy
+        else:
+            dy = abs(new_y - y)
+            if new_y <= y:
+                cxnSp.y = new_y
+                cxnSp.cy = cy + dy
+            elif dy <= cy:
+                cxnSp.y = new_y
+                cxnSp.cy = cy - dy
+            else:
+                cxnSp.flipV = True
+                cxnSp.y = y + cy
+                cxnSp.cy = dy - cy
+
+    def end_connect(self, shape, cxn_pt_idx):
+        """
+        **EXPERIMENTAL** - *The current implementation only works properly
+        with rectangular shapes, such as pictures and rectangles. Use with
+        other shape types may cause unexpected visual alignment of the
+        connected end-point and could lead to a load error if cxn_pt_idx
+        exceeds the connection point count available on the connected shape.
+        That said, a quick test should reveal what to expect when using this
+        method with other shape types.*
+
+        Connect the ending of this connector to *shape* at the connection
+        point specified by *cxn_pt_idx*.
+        """
+        self._connect_end_to(shape, cxn_pt_idx)
+        self._move_end_to_cxn(shape, cxn_pt_idx)
+
+    @property
+    def end_x(self):
+        """
+        Return the X-position of the end point of this connector, in English
+        Metric Units (as a |Length| object).
+        """
+        cxnSp = self._element
+        x, cx, flipH = cxnSp.x, cxnSp.cx, cxnSp.flipH
+        end_x = x if flipH else x + cx
+        return Emu(end_x)
+
+    @end_x.setter
+    def end_x(self, value):
+        cxnSp = self._element
+        x, cx, flipH, new_x = cxnSp.x, cxnSp.cx, cxnSp.flipH, int(value)
+
+        if flipH:
+            dx = abs(new_x - x)
+            if new_x <= x:
+                cxnSp.x = new_x
+                cxnSp.cx = cx + dx
+            elif dx <= cx:
+                cxnSp.x = new_x
+                cxnSp.cx = cx - dx
+            else:
+                cxnSp.flipH = False
+                cxnSp.x = x + cx
+                cxnSp.cx = dx - cx
+        else:
+            old_x = x + cx
+            dx = abs(new_x - old_x)
+            if new_x >= old_x:
+                cxnSp.cx = cx + dx
+            elif dx <= cx:
+                cxnSp.cx = cx - dx
+            else:
+                cxnSp.flipH = True
+                cxnSp.x = new_x
+                cxnSp.cx = dx - cx
+
+    @property
+    def end_y(self):
+        """
+        Return the Y-position of the end point of this connector, in English
+        Metric Units (as a |Length| object).
+        """
+        cxnSp = self._element
+        y, cy, flipV = cxnSp.y, cxnSp.cy, cxnSp.flipV
+        end_y = y if flipV else y + cy
+        return Emu(end_y)
+
+    @end_y.setter
+    def end_y(self, value):
+        cxnSp = self._element
+        y, cy, flipV, new_y = cxnSp.y, cxnSp.cy, cxnSp.flipV, int(value)
+
+        if flipV:
+            dy = abs(new_y - y)
+            if new_y <= y:
+                cxnSp.y = new_y
+                cxnSp.cy = cy + dy
+            elif dy <= cy:
+                cxnSp.y = new_y
+                cxnSp.cy = cy - dy
+            else:
+                cxnSp.flipV = False
+                cxnSp.y = y + cy
+                cxnSp.cy = dy - cy
+        else:
+            old_y = y + cy
+            dy = abs(new_y - old_y)
+            if new_y >= old_y:
+                cxnSp.cy = cy + dy
+            elif dy <= cy:
+                cxnSp.cy = cy - dy
+            else:
+                cxnSp.flipV = True
+                cxnSp.y = new_y
+                cxnSp.cy = dy - cy
+
+    def get_or_add_ln(self):
+        """Helper method required by |LineFormat|."""
+        return self._element.spPr.get_or_add_ln()
+
+    @lazyproperty
+    def line(self):
+        """|LineFormat| instance for this connector.
+
+        Provides access to line properties such as line color, width, and
+        line style.
+        """
+        return LineFormat(self)
+
+    @property
+    def ln(self):
+        """Helper method required by |LineFormat|.
+
+        The ``<a:ln>`` element containing the line format properties such as
+        line color and width. |None| if no `<a:ln>` element is present.
+        """
+        return self._element.spPr.ln
+
+    @property
+    def shape_type(self):
+        """Member of `MSO_SHAPE_TYPE` identifying the type of this shape.
+
+        Unconditionally `MSO_SHAPE_TYPE.LINE` for a `Connector` object.
+        """
+        return MSO_SHAPE_TYPE.LINE
+
+    def _connect_begin_to(self, shape, cxn_pt_idx):
+        """
+        Add or update a stCxn element for this connector that connects its
+        begin point to the connection point of *shape* specified by
+        *cxn_pt_idx*.
+        """
+        cNvCxnSpPr = self._element.nvCxnSpPr.cNvCxnSpPr
+        stCxn = cNvCxnSpPr.get_or_add_stCxn()
+        stCxn.id = shape.shape_id
+        stCxn.idx = cxn_pt_idx
+
+    def _connect_end_to(self, shape, cxn_pt_idx):
+        """
+        Add or update an endCxn element for this connector that connects its
+        end point to the connection point of *shape* specified by
+        *cxn_pt_idx*.
+        """
+        cNvCxnSpPr = self._element.nvCxnSpPr.cNvCxnSpPr
+        endCxn = cNvCxnSpPr.get_or_add_endCxn()
+        endCxn.id = shape.shape_id
+        endCxn.idx = cxn_pt_idx
+
+    def _move_begin_to_cxn(self, shape, cxn_pt_idx):
+        """
+        Move the begin point of this connector to coordinates of the
+        connection point of *shape* specified by *cxn_pt_idx*.
+        """
+        x, y, cx, cy = shape.left, shape.top, shape.width, shape.height
+        self.begin_x, self.begin_y = {
+            0: (int(x + cx / 2), y),
+            1: (x, int(y + cy / 2)),
+            2: (int(x + cx / 2), y + cy),
+            3: (x + cx, int(y + cy / 2)),
+        }[cxn_pt_idx]
+
+    def _move_end_to_cxn(self, shape, cxn_pt_idx):
+        """
+        Move the end point of this connector to the coordinates of the
+        connection point of *shape* specified by *cxn_pt_idx*.
+        """
+        x, y, cx, cy = shape.left, shape.top, shape.width, shape.height
+        self.end_x, self.end_y = {
+            0: (int(x + cx / 2), y),
+            1: (x, int(y + cy / 2)),
+            2: (int(x + cx / 2), y + cy),
+            3: (x + cx, int(y + cy / 2)),
+        }[cxn_pt_idx]
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py b/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py
new file mode 100644
index 00000000..afe87385
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py
@@ -0,0 +1,337 @@
+"""Objects related to construction of freeform shapes."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterable, Iterator, Sequence
+
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+
+    from pptx.oxml.shapes.autoshape import (
+        CT_Path2D,
+        CT_Path2DClose,
+        CT_Path2DLineTo,
+        CT_Path2DMoveTo,
+        CT_Shape,
+    )
+    from pptx.shapes.shapetree import _BaseGroupShapes  # pyright: ignore[reportPrivateUsage]
+    from pptx.util import Length
+
+CT_DrawingOperation: TypeAlias = "CT_Path2DClose | CT_Path2DLineTo | CT_Path2DMoveTo"
+DrawingOperation: TypeAlias = "_LineSegment | _MoveTo | _Close"
+
+
+class FreeformBuilder(Sequence[DrawingOperation]):
+    """Allows a freeform shape to be specified and created.
+
+    The initial pen position is provided on construction. From there, drawing proceeds using
+    successive calls to draw line segments. The freeform shape may be closed by calling the
+    :meth:`close` method.
+
+    A shape may have more than one contour, in which case overlapping areas are "subtracted". A
+    contour is a sequence of line segments beginning with a "move-to" operation. A move-to
+    operation is automatically inserted in each new freeform; additional move-to ops can be
+    inserted with the `.move_to()` method.
+    """
+
+    def __init__(
+        self,
+        shapes: _BaseGroupShapes,
+        start_x: Length,
+        start_y: Length,
+        x_scale: float,
+        y_scale: float,
+    ):
+        super(FreeformBuilder, self).__init__()
+        self._shapes = shapes
+        self._start_x = start_x
+        self._start_y = start_y
+        self._x_scale = x_scale
+        self._y_scale = y_scale
+
+    def __getitem__(  # pyright: ignore[reportIncompatibleMethodOverride]
+        self, idx: int
+    ) -> DrawingOperation:
+        return self._drawing_operations.__getitem__(idx)
+
+    def __iter__(self) -> Iterator[DrawingOperation]:
+        return self._drawing_operations.__iter__()
+
+    def __len__(self):
+        return self._drawing_operations.__len__()
+
+    @classmethod
+    def new(
+        cls,
+        shapes: _BaseGroupShapes,
+        start_x: float,
+        start_y: float,
+        x_scale: float,
+        y_scale: float,
+    ):
+        """Return a new |FreeformBuilder| object.
+
+        The initial pen location is specified (in local coordinates) by
+        (`start_x`, `start_y`).
+        """
+        return cls(shapes, Emu(int(round(start_x))), Emu(int(round(start_y))), x_scale, y_scale)
+
+    def add_line_segments(self, vertices: Iterable[tuple[float, float]], close: bool = True):
+        """Add a straight line segment to each point in `vertices`.
+
+        `vertices` must be an iterable of (x, y) pairs (2-tuples). Each x and y value is rounded
+        to the nearest integer before use. The optional `close` parameter determines whether the
+        resulting contour is `closed` or left `open`.
+
+        Returns this |FreeformBuilder| object so it can be used in chained calls.
+        """
+        for x, y in vertices:
+            self._add_line_segment(x, y)
+        if close:
+            self._add_close()
+        return self
+
+    def convert_to_shape(self, origin_x: Length = Emu(0), origin_y: Length = Emu(0)):
+        """Return new freeform shape positioned relative to specified offset.
+
+        `origin_x` and `origin_y` locate the origin of the local coordinate system in slide
+        coordinates (EMU), perhaps most conveniently by use of a |Length| object.
+
+        Note that this method may be called more than once to add multiple shapes of the same
+        geometry in different locations on the slide.
+        """
+        sp = self._add_freeform_sp(origin_x, origin_y)
+        path = self._start_path(sp)
+        for drawing_operation in self:
+            drawing_operation.apply_operation_to(path)
+        return self._shapes._shape_factory(sp)  # pyright: ignore[reportPrivateUsage]
+
+    def move_to(self, x: float, y: float):
+        """Move pen to (x, y) (local coordinates) without drawing line.
+
+        Returns this |FreeformBuilder| object so it can be used in chained calls.
+        """
+        self._drawing_operations.append(_MoveTo.new(self, x, y))
+        return self
+
+    @property
+    def shape_offset_x(self) -> Length:
+        """Return x distance of shape origin from local coordinate origin.
+
+        The returned integer represents the leftmost extent of the freeform shape, in local
+        coordinates. Note that the bounding box of the shape need not start at the local origin.
+        """
+        min_x = self._start_x
+        for drawing_operation in self:
+            if isinstance(drawing_operation, _Close):
+                continue
+            min_x = min(min_x, drawing_operation.x)
+        return Emu(min_x)
+
+    @property
+    def shape_offset_y(self) -> Length:
+        """Return y distance of shape origin from local coordinate origin.
+
+        The returned integer represents the topmost extent of the freeform shape, in local
+        coordinates. Note that the bounding box of the shape need not start at the local origin.
+        """
+        min_y = self._start_y
+        for drawing_operation in self:
+            if isinstance(drawing_operation, _Close):
+                continue
+            min_y = min(min_y, drawing_operation.y)
+        return Emu(min_y)
+
+    def _add_close(self):
+        """Add a close |_Close| operation to the drawing sequence."""
+        self._drawing_operations.append(_Close.new())
+
+    def _add_freeform_sp(self, origin_x: Length, origin_y: Length):
+        """Add a freeform `p:sp` element having no drawing elements.
+
+        `origin_x` and `origin_y` are specified in slide coordinates, and represent the location
+        of the local coordinates origin on the slide.
+        """
+        spTree = self._shapes._spTree  # pyright: ignore[reportPrivateUsage]
+        return spTree.add_freeform_sp(
+            origin_x + self._left, origin_y + self._top, self._width, self._height
+        )
+
+    def _add_line_segment(self, x: float, y: float) -> None:
+        """Add a |_LineSegment| operation to the drawing sequence."""
+        self._drawing_operations.append(_LineSegment.new(self, x, y))
+
+    @lazyproperty
+    def _drawing_operations(self) -> list[DrawingOperation]:
+        """Return the sequence of drawing operation objects for freeform."""
+        return []
+
+    @property
+    def _dx(self) -> Length:
+        """Return width of this shape's path in local units."""
+        min_x = max_x = self._start_x
+        for drawing_operation in self:
+            if isinstance(drawing_operation, _Close):
+                continue
+            min_x = min(min_x, drawing_operation.x)
+            max_x = max(max_x, drawing_operation.x)
+        return Emu(max_x - min_x)
+
+    @property
+    def _dy(self) -> Length:
+        """Return integer height of this shape's path in local units."""
+        min_y = max_y = self._start_y
+        for drawing_operation in self:
+            if isinstance(drawing_operation, _Close):
+                continue
+            min_y = min(min_y, drawing_operation.y)
+            max_y = max(max_y, drawing_operation.y)
+        return Emu(max_y - min_y)
+
+    @property
+    def _height(self):
+        """Return vertical size of this shape's path in slide coordinates.
+
+        This value is based on the actual extents of the shape and does not include any
+        positioning offset.
+        """
+        return int(round(self._dy * self._y_scale))
+
+    @property
+    def _left(self):
+        """Return leftmost extent of this shape's path in slide coordinates.
+
+        Note that this value does not include any positioning offset; it assumes the drawing
+        (local) coordinate origin is at (0, 0) on the slide.
+        """
+        return int(round(self.shape_offset_x * self._x_scale))
+
+    def _local_to_shape(self, local_x: Length, local_y: Length) -> tuple[Length, Length]:
+        """Translate local coordinates point to shape coordinates.
+
+        Shape coordinates have the same unit as local coordinates, but are offset such that the
+        origin of the shape coordinate system (0, 0) is located at the top-left corner of the
+        shape bounding box.
+        """
+        return Emu(local_x - self.shape_offset_x), Emu(local_y - self.shape_offset_y)
+
+    def _start_path(self, sp: CT_Shape) -> CT_Path2D:
+        """Return a newly created `a:path` element added to `sp`.
+
+        The returned `a:path` element has an `a:moveTo` element representing the shape starting
+        point as its only child.
+        """
+        path = sp.add_path(w=self._dx, h=self._dy)
+        path.add_moveTo(*self._local_to_shape(self._start_x, self._start_y))
+        return path
+
+    @property
+    def _top(self):
+        """Return topmost extent of this shape's path in slide coordinates.
+
+        Note that this value does not include any positioning offset; it assumes the drawing
+        (local) coordinate origin is located at slide coordinates (0, 0) (top-left corner of
+        slide).
+        """
+        return int(round(self.shape_offset_y * self._y_scale))
+
+    @property
+    def _width(self):
+        """Return width of this shape's path in slide coordinates.
+
+        This value is based on the actual extents of the shape path and does not include any
+        positioning offset.
+        """
+        return int(round(self._dx * self._x_scale))
+
+
+class _BaseDrawingOperation(object):
+    """Base class for freeform drawing operations.
+
+    A drawing operation has at least one location (x, y) in local coordinates.
+    """
+
+    def __init__(self, freeform_builder: FreeformBuilder, x: Length, y: Length):
+        super(_BaseDrawingOperation, self).__init__()
+        self._freeform_builder = freeform_builder
+        self._x = x
+        self._y = y
+
+    def apply_operation_to(self, path: CT_Path2D) -> CT_DrawingOperation:
+        """Add the XML element(s) implementing this operation to `path`.
+
+        Must be implemented by each subclass.
+        """
+        raise NotImplementedError("must be implemented by each subclass")
+
+    @property
+    def x(self) -> Length:
+        """Return the horizontal (x) target location of this operation.
+
+        The returned value is an integer in local coordinates.
+        """
+        return self._x
+
+    @property
+    def y(self) -> Length:
+        """Return the vertical (y) target location of this operation.
+
+        The returned value is an integer in local coordinates.
+        """
+        return self._y
+
+
+class _Close(object):
+    """Specifies adding a `<a:close/>` element to the current contour."""
+
+    @classmethod
+    def new(cls) -> _Close:
+        """Return a new _Close object."""
+        return cls()
+
+    def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DClose:
+        """Add `a:close` element to `path`."""
+        return path.add_close()
+
+
+class _LineSegment(_BaseDrawingOperation):
+    """Specifies a straight line segment ending at the specified point."""
+
+    @classmethod
+    def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _LineSegment:
+        """Return a new _LineSegment object ending at point *(x, y)*.
+
+        Both `x` and `y` are rounded to the nearest integer before use.
+        """
+        return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y))))
+
+    def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DLineTo:
+        """Add `a:lnTo` element to `path` for this line segment.
+
+        Returns the `a:lnTo` element newly added to the path.
+        """
+        return path.add_lnTo(
+            Emu(self._x - self._freeform_builder.shape_offset_x),
+            Emu(self._y - self._freeform_builder.shape_offset_y),
+        )
+
+
+class _MoveTo(_BaseDrawingOperation):
+    """Specifies a new pen position."""
+
+    @classmethod
+    def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _MoveTo:
+        """Return a new _MoveTo object for move to point `(x, y)`.
+
+        Both `x` and `y` are rounded to the nearest integer before use.
+        """
+        return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y))))
+
+    def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DMoveTo:
+        """Add `a:moveTo` element to `path` for this line segment."""
+        return path.add_moveTo(
+            Emu(self._x - self._freeform_builder.shape_offset_x),
+            Emu(self._y - self._freeform_builder.shape_offset_y),
+        )
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py b/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py
new file mode 100644
index 00000000..c0ed2bba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py
@@ -0,0 +1,166 @@
+"""Graphic Frame shape and related objects.
+
+A graphic frame is a common container for table, chart, smart art, and media
+objects.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.shared import ParentedElementProxy
+from pptx.spec import (
+    GRAPHIC_DATA_URI_CHART,
+    GRAPHIC_DATA_URI_OLEOBJ,
+    GRAPHIC_DATA_URI_TABLE,
+)
+from pptx.table import Table
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.chart.chart import Chart
+    from pptx.dml.effect import ShadowFormat
+    from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectData, CT_GraphicalObjectFrame
+    from pptx.parts.chart import ChartPart
+    from pptx.parts.slide import BaseSlidePart
+    from pptx.types import ProvidesPart
+
+
+class GraphicFrame(BaseShape):
+    """Container shape for table, chart, smart art, and media objects.
+
+    Corresponds to a `p:graphicFrame` element in the shape tree.
+    """
+
+    def __init__(self, graphicFrame: CT_GraphicalObjectFrame, parent: ProvidesPart):
+        super().__init__(graphicFrame, parent)
+        self._graphicFrame = graphicFrame
+
+    @property
+    def chart(self) -> Chart:
+        """The |Chart| object containing the chart in this graphic frame.
+
+        Raises |ValueError| if this graphic frame does not contain a chart.
+        """
+        if not self.has_chart:
+            raise ValueError("shape does not contain a chart")
+        return self.chart_part.chart
+
+    @property
+    def chart_part(self) -> ChartPart:
+        """The |ChartPart| object containing the chart in this graphic frame."""
+        chart_rId = self._graphicFrame.chart_rId
+        if chart_rId is None:
+            raise ValueError("this graphic frame does not contain a chart")
+        return cast("ChartPart", self.part.related_part(chart_rId))
+
+    @property
+    def has_chart(self) -> bool:
+        """|True| if this graphic frame contains a chart object. |False| otherwise.
+
+        When |True|, the chart object can be accessed using the `.chart` property.
+        """
+        return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_CHART
+
+    @property
+    def has_table(self) -> bool:
+        """|True| if this graphic frame contains a table object, |False| otherwise.
+
+        When |True|, the table object can be accessed using the `.table` property.
+        """
+        return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_TABLE
+
+    @property
+    def ole_format(self) -> _OleFormat:
+        """_OleFormat object for this graphic-frame shape.
+
+        Raises `ValueError` on a GraphicFrame instance that does not contain an OLE object.
+
+        An shape that contains an OLE object will have `.shape_type` of either
+        `EMBEDDED_OLE_OBJECT` or `LINKED_OLE_OBJECT`.
+        """
+        if not self._graphicFrame.has_oleobj:
+            raise ValueError("not an OLE-object shape")
+        return _OleFormat(self._graphicFrame.graphicData, self._parent)
+
+    @lazyproperty
+    def shadow(self) -> ShadowFormat:
+        """Unconditionally raises |NotImplementedError|.
+
+        Access to the shadow effect for graphic-frame objects is content-specific (i.e. different
+        for charts, tables, etc.) and has not yet been implemented.
+        """
+        raise NotImplementedError("shadow property on GraphicFrame not yet supported")
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Optional member of `MSO_SHAPE_TYPE` identifying the type of this shape.
+
+        Possible values are `MSO_SHAPE_TYPE.CHART`, `MSO_SHAPE_TYPE.TABLE`,
+        `MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT`, `MSO_SHAPE_TYPE.LINKED_OLE_OBJECT`.
+
+        This value is `None` when none of these four types apply, for example when the shape
+        contains SmartArt.
+        """
+        graphicData_uri = self._graphicFrame.graphicData_uri
+        if graphicData_uri == GRAPHIC_DATA_URI_CHART:
+            return MSO_SHAPE_TYPE.CHART
+        elif graphicData_uri == GRAPHIC_DATA_URI_TABLE:
+            return MSO_SHAPE_TYPE.TABLE
+        elif graphicData_uri == GRAPHIC_DATA_URI_OLEOBJ:
+            return (
+                MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT
+                if self._graphicFrame.is_embedded_ole_obj
+                else MSO_SHAPE_TYPE.LINKED_OLE_OBJECT
+            )
+        else:
+            return None  # pyright: ignore[reportReturnType]
+
+    @property
+    def table(self) -> Table:
+        """The |Table| object contained in this graphic frame.
+
+        Raises |ValueError| if this graphic frame does not contain a table.
+        """
+        if not self.has_table:
+            raise ValueError("shape does not contain a table")
+        tbl = self._graphicFrame.graphic.graphicData.tbl
+        return Table(tbl, self)
+
+
+class _OleFormat(ParentedElementProxy):
+    """Provides attributes on an embedded OLE object."""
+
+    part: BaseSlidePart  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    def __init__(self, graphicData: CT_GraphicalObjectData, parent: ProvidesPart):
+        super().__init__(graphicData, parent)
+        self._graphicData = graphicData
+
+    @property
+    def blob(self) -> bytes | None:
+        """Optional bytes of OLE object, suitable for loading or saving as a file.
+
+        This value is `None` if the embedded object does not represent a "file".
+        """
+        blob_rId = self._graphicData.blob_rId
+        if blob_rId is None:
+            return None
+        return self.part.related_part(blob_rId).blob
+
+    @property
+    def prog_id(self) -> str | None:
+        """str "progId" attribute of this embedded OLE object.
+
+        The progId is a str like "Excel.Sheet.12" that identifies the "file-type" of the embedded
+        object, or perhaps more precisely, the application (aka. "server" in OLE parlance) to be
+        used to open this object.
+        """
+        return self._graphicData.progId
+
+    @property
+    def show_as_icon(self) -> bool | None:
+        """True when OLE object should appear as an icon (rather than preview)."""
+        return self._graphicData.showAsIcon
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/group.py b/.venv/lib/python3.12/site-packages/pptx/shapes/group.py
new file mode 100644
index 00000000..71737585
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/group.py
@@ -0,0 +1,69 @@
+"""GroupShape and related objects."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.dml.effect import ShadowFormat
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.action import ActionSetting
+    from pptx.oxml.shapes.groupshape import CT_GroupShape
+    from pptx.shapes.shapetree import GroupShapes
+    from pptx.types import ProvidesPart
+
+
+class GroupShape(BaseShape):
+    """A shape that acts as a container for other shapes."""
+
+    def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart):
+        super().__init__(grpSp, parent)
+        self._grpSp = grpSp
+
+    @lazyproperty
+    def click_action(self) -> ActionSetting:
+        """Unconditionally raises `TypeError`.
+
+        A group shape cannot have a click action or hover action.
+        """
+        raise TypeError("a group shape cannot have a click action")
+
+    @property
+    def has_text_frame(self) -> bool:
+        """Unconditionally |False|.
+
+        A group shape does not have a textframe and cannot itself contain text. This does not
+        impact the ability of shapes contained by the group to each have their own text.
+        """
+        return False
+
+    @lazyproperty
+    def shadow(self) -> ShadowFormat:
+        """|ShadowFormat| object representing shadow effect for this group.
+
+        A |ShadowFormat| object is always returned, even when no shadow is explicitly defined on
+        this group shape (i.e. when the group inherits its shadow behavior).
+        """
+        return ShadowFormat(self._grpSp.grpSpPr)
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Member of :ref:`MsoShapeType` identifying the type of this shape.
+
+        Unconditionally `MSO_SHAPE_TYPE.GROUP` in this case
+        """
+        return MSO_SHAPE_TYPE.GROUP
+
+    @lazyproperty
+    def shapes(self) -> GroupShapes:
+        """|GroupShapes| object for this group.
+
+        The |GroupShapes| object provides access to the group's member shapes and provides methods
+        for adding new ones.
+        """
+        from pptx.shapes.shapetree import GroupShapes
+
+        return GroupShapes(self._element, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py b/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py
new file mode 100644
index 00000000..59182860
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py
@@ -0,0 +1,203 @@
+"""Shapes based on the `p:pic` element, including Picture and Movie."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.shared import ParentedElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.picture import CT_Picture
+    from pptx.oxml.shapes.shared import CT_LineProperties
+    from pptx.types import ProvidesPart
+
+
+class _BasePicture(BaseShape):
+    """Base class for shapes based on a `p:pic` element."""
+
+    def __init__(self, pic: CT_Picture, parent: ProvidesPart):
+        super(_BasePicture, self).__init__(pic, parent)
+        self._pic = pic
+
+    @property
+    def crop_bottom(self) -> float:
+        """|float| representing relative portion cropped from shape bottom.
+
+        Read/write. 1.0 represents 100%. For example, 25% is represented by 0.25. Negative values
+        are valid as are values greater than 1.0.
+        """
+        return self._pic.srcRect_b
+
+    @crop_bottom.setter
+    def crop_bottom(self, value: float):
+        self._pic.srcRect_b = value
+
+    @property
+    def crop_left(self) -> float:
+        """|float| representing relative portion cropped from left of shape.
+
+        Read/write. 1.0 represents 100%. A negative value extends the side beyond the image
+        boundary.
+        """
+        return self._pic.srcRect_l
+
+    @crop_left.setter
+    def crop_left(self, value: float):
+        self._pic.srcRect_l = value
+
+    @property
+    def crop_right(self) -> float:
+        """|float| representing relative portion cropped from right of shape.
+
+        Read/write. 1.0 represents 100%.
+        """
+        return self._pic.srcRect_r
+
+    @crop_right.setter
+    def crop_right(self, value: float):
+        self._pic.srcRect_r = value
+
+    @property
+    def crop_top(self) -> float:
+        """|float| representing relative portion cropped from shape top.
+
+        Read/write. 1.0 represents 100%.
+        """
+        return self._pic.srcRect_t
+
+    @crop_top.setter
+    def crop_top(self, value: float):
+        self._pic.srcRect_t = value
+
+    def get_or_add_ln(self):
+        """Return the `a:ln` element for this `p:pic`-based image.
+
+        The `a:ln` element contains the line format properties XML.
+        """
+        return self._pic.get_or_add_ln()
+
+    @lazyproperty
+    def line(self) -> LineFormat:
+        """Provides access to properties of the picture outline, such as its color and width."""
+        return LineFormat(self)
+
+    @property
+    def ln(self) -> CT_LineProperties | None:
+        """The `a:ln` element for this `p:pic`.
+
+        Contains the line format properties such as line color and width. |None| if no `a:ln`
+        element is present.
+        """
+        return self._pic.ln
+
+
+class Movie(_BasePicture):
+    """A movie shape, one that places a video on a slide.
+
+    Like |Picture|, a movie shape is based on the `p:pic` element. A movie is composed of a video
+    and a *poster frame*, the placeholder image that represents the video before it is played.
+    """
+
+    @lazyproperty
+    def media_format(self) -> _MediaFormat:
+        """The |_MediaFormat| object for this movie.
+
+        The |_MediaFormat| object provides access to formatting properties for the movie.
+        """
+        return _MediaFormat(self._pic, self)
+
+    @property
+    def media_type(self) -> PP_MEDIA_TYPE:
+        """Member of :ref:`PpMediaType` describing this shape.
+
+        The return value is unconditionally `PP_MEDIA_TYPE.MOVIE` in this case.
+        """
+        return PP_MEDIA_TYPE.MOVIE
+
+    @property
+    def poster_frame(self):
+        """Return |Image| object containing poster frame for this movie.
+
+        Returns |None| if this movie has no poster frame (uncommon).
+        """
+        slide_part, rId = self.part, self._pic.blip_rId
+        if rId is None:
+            return None
+        return slide_part.get_image(rId)
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Return member of :ref:`MsoShapeType` describing this shape.
+
+        The return value is unconditionally `MSO_SHAPE_TYPE.MEDIA` in this
+        case.
+        """
+        return MSO_SHAPE_TYPE.MEDIA
+
+
+class Picture(_BasePicture):
+    """A picture shape, one that places an image on a slide.
+
+    Based on the `p:pic` element.
+    """
+
+    @property
+    def auto_shape_type(self) -> MSO_SHAPE | None:
+        """Member of MSO_SHAPE indicating masking shape.
+
+        A picture can be masked by any of the so-called "auto-shapes" available in PowerPoint,
+        such as an ellipse or triangle. When a picture is masked by a shape, the shape assumes the
+        same dimensions as the picture and the portion of the picture outside the shape boundaries
+        does not appear. Note the default value for a newly-inserted picture is
+        `MSO_AUTO_SHAPE_TYPE.RECTANGLE`, which performs no cropping because the extents of the
+        rectangle exactly correspond to the extents of the picture.
+
+        The available shapes correspond to the members of :ref:`MsoAutoShapeType`.
+
+        The return value can also be |None|, indicating the picture either has no geometry (not
+        expected) or has custom geometry, like a freeform shape. A picture with no geometry will
+        have no visible representation on the slide, although it can be selected. This is because
+        without geometry, there is no "inside-the-shape" for it to appear in.
+        """
+        prstGeom = self._pic.spPr.prstGeom
+        if prstGeom is None:  # ---generally means cropped with freeform---
+            return None
+        return prstGeom.prst
+
+    @auto_shape_type.setter
+    def auto_shape_type(self, member: MSO_SHAPE):
+        MSO_SHAPE.validate(member)
+        spPr = self._pic.spPr
+        prstGeom = spPr.prstGeom
+        if prstGeom is None:
+            spPr._remove_custGeom()  # pyright: ignore[reportPrivateUsage]
+            prstGeom = spPr._add_prstGeom()  # pyright: ignore[reportPrivateUsage]
+        prstGeom.prst = member
+
+    @property
+    def image(self):
+        """The |Image| object for this picture.
+
+        Provides access to the properties and bytes of the image in this picture shape.
+        """
+        slide_part, rId = self.part, self._pic.blip_rId
+        if rId is None:
+            raise ValueError("no embedded image")
+        return slide_part.get_image(rId)
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Unconditionally `MSO_SHAPE_TYPE.PICTURE` in this case."""
+        return MSO_SHAPE_TYPE.PICTURE
+
+
+class _MediaFormat(ParentedElementProxy):
+    """Provides access to formatting properties for a Media object.
+
+    Media format properties are things like start point, volume, and
+    compression type.
+    """
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py b/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py
new file mode 100644
index 00000000..c44837be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py
@@ -0,0 +1,407 @@
+"""Placeholder-related objects.
+
+Specific to shapes having a `p:ph` element. A placeholder has distinct behaviors
+depending on whether it appears on a slide, layout, or master. Hence there is a
+non-trivial class inheritance structure.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.shapes.autoshape import Shape
+from pptx.shapes.graphfrm import GraphicFrame
+from pptx.shapes.picture import Picture
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.autoshape import CT_Shape
+
+
+class _InheritsDimensions(object):
+    """
+    Mixin class that provides inherited dimension behavior. Specifically,
+    left, top, width, and height report the value from the layout placeholder
+    where they would have otherwise reported |None|. This behavior is
+    distinctive to placeholders. :meth:`_base_placeholder` must be overridden
+    by all subclasses to provide lookup of the appropriate base placeholder
+    to inherit from.
+    """
+
+    @property
+    def height(self):
+        """
+        The effective height of this placeholder shape; its directly-applied
+        height if it has one, otherwise the height of its parent layout
+        placeholder.
+        """
+        return self._effective_value("height")
+
+    @height.setter
+    def height(self, value):
+        self._element.cy = value
+
+    @property
+    def left(self):
+        """
+        The effective left of this placeholder shape; its directly-applied
+        left if it has one, otherwise the left of its parent layout
+        placeholder.
+        """
+        return self._effective_value("left")
+
+    @left.setter
+    def left(self, value):
+        self._element.x = value
+
+    @property
+    def shape_type(self):
+        """
+        Member of :ref:`MsoShapeType` specifying the type of this shape.
+        Unconditionally ``MSO_SHAPE_TYPE.PLACEHOLDER`` in this case.
+        Read-only.
+        """
+        return MSO_SHAPE_TYPE.PLACEHOLDER
+
+    @property
+    def top(self):
+        """
+        The effective top of this placeholder shape; its directly-applied
+        top if it has one, otherwise the top of its parent layout
+        placeholder.
+        """
+        return self._effective_value("top")
+
+    @top.setter
+    def top(self, value):
+        self._element.y = value
+
+    @property
+    def width(self):
+        """
+        The effective width of this placeholder shape; its directly-applied
+        width if it has one, otherwise the width of its parent layout
+        placeholder.
+        """
+        return self._effective_value("width")
+
+    @width.setter
+    def width(self, value):
+        self._element.cx = value
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the layout or master placeholder shape this placeholder
+        inherits from. Not to be confused with an instance of
+        |BasePlaceholder| (necessarily).
+        """
+        raise NotImplementedError("Must be implemented by all subclasses.")
+
+    def _effective_value(self, attr_name):
+        """
+        The effective value of *attr_name* on this placeholder shape; its
+        directly-applied value if it has one, otherwise the value on the
+        layout placeholder it inherits from.
+        """
+        directly_applied_value = getattr(super(_InheritsDimensions, self), attr_name)
+        if directly_applied_value is not None:
+            return directly_applied_value
+        return self._inherited_value(attr_name)
+
+    def _inherited_value(self, attr_name):
+        """
+        Return the attribute value, e.g. 'width' of the base placeholder this
+        placeholder inherits from.
+        """
+        base_placeholder = self._base_placeholder
+        if base_placeholder is None:
+            return None
+        inherited_value = getattr(base_placeholder, attr_name)
+        return inherited_value
+
+
+class _BaseSlidePlaceholder(_InheritsDimensions, Shape):
+    """Base class for placeholders on slides.
+
+    Provides common behaviors such as inherited dimensions.
+    """
+
+    @property
+    def is_placeholder(self):
+        """
+        Boolean indicating whether this shape is a placeholder.
+        Unconditionally |True| in this case.
+        """
+        return True
+
+    @property
+    def shape_type(self):
+        """
+        Member of :ref:`MsoShapeType` specifying the type of this shape.
+        Unconditionally ``MSO_SHAPE_TYPE.PLACEHOLDER`` in this case.
+        Read-only.
+        """
+        return MSO_SHAPE_TYPE.PLACEHOLDER
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the layout placeholder this slide placeholder inherits from.
+        Not to be confused with an instance of |BasePlaceholder|
+        (necessarily).
+        """
+        layout, idx = self.part.slide_layout, self._element.ph_idx
+        return layout.placeholders.get(idx=idx)
+
+    def _replace_placeholder_with(self, element):
+        """
+        Substitute *element* for this placeholder element in the shapetree.
+        This placeholder's `._element` attribute is set to |None| and its
+        original element is free for garbage collection. Any attribute access
+        (including a method call) on this placeholder after this call raises
+        |AttributeError|.
+        """
+        element._nvXxPr.nvPr._insert_ph(self._element.ph)
+        self._element.addprevious(element)
+        self._element.getparent().remove(self._element)
+        self._element = None
+
+
+class BasePlaceholder(Shape):
+    """
+    NOTE: This class is deprecated and will be removed from a future release
+    along with the properties *idx*, *orient*, *ph_type*, and *sz*. The *idx*
+    property will be available via the .placeholder_format property. The
+    others will be accessed directly from the oxml layer as they are only
+    used for internal purposes.
+
+    Base class for placeholder subclasses that differentiate the varying
+    behaviors of placeholders on a master, layout, and slide.
+    """
+
+    @property
+    def idx(self):
+        """
+        Integer placeholder 'idx' attribute, e.g. 0
+        """
+        return self._sp.ph_idx
+
+    @property
+    def orient(self):
+        """
+        Placeholder orientation, e.g. ST_Direction.HORZ
+        """
+        return self._sp.ph_orient
+
+    @property
+    def ph_type(self):
+        """
+        Placeholder type, e.g. PP_PLACEHOLDER.CENTER_TITLE
+        """
+        return self._sp.ph_type
+
+    @property
+    def sz(self):
+        """
+        Placeholder 'sz' attribute, e.g. ST_PlaceholderSize.FULL
+        """
+        return self._sp.ph_sz
+
+
+class LayoutPlaceholder(_InheritsDimensions, Shape):
+    """Placeholder shape on a slide layout.
+
+    Provides differentiated behavior for slide layout placeholders, in particular, inheriting
+    shape properties from the master placeholder having the same type, when a matching one exists.
+    """
+
+    element: CT_Shape  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the master placeholder this layout placeholder inherits from.
+        """
+        base_ph_type = {
+            PP_PLACEHOLDER.BODY: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.CHART: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.BITMAP: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.CENTER_TITLE: PP_PLACEHOLDER.TITLE,
+            PP_PLACEHOLDER.ORG_CHART: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.DATE: PP_PLACEHOLDER.DATE,
+            PP_PLACEHOLDER.FOOTER: PP_PLACEHOLDER.FOOTER,
+            PP_PLACEHOLDER.MEDIA_CLIP: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.OBJECT: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.PICTURE: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.SLIDE_NUMBER: PP_PLACEHOLDER.SLIDE_NUMBER,
+            PP_PLACEHOLDER.SUBTITLE: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.TABLE: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.TITLE: PP_PLACEHOLDER.TITLE,
+        }[self._element.ph_type]
+        slide_master = self.part.slide_master
+        return slide_master.placeholders.get(base_ph_type, None)
+
+
+class MasterPlaceholder(BasePlaceholder):
+    """Placeholder shape on a slide master."""
+
+    element: CT_Shape  # pyright: ignore[reportIncompatibleMethodOverride]
+
+
+class NotesSlidePlaceholder(_InheritsDimensions, Shape):
+    """
+    Placeholder shape on a notes slide. Inherits shape properties from the
+    placeholder on the notes master that has the same type (e.g. 'body').
+    """
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the notes master placeholder this notes slide placeholder
+        inherits from, or |None| if no placeholder of the matching type is
+        present.
+        """
+        notes_master = self.part.notes_master
+        ph_type = self.element.ph_type
+        return notes_master.placeholders.get(ph_type=ph_type)
+
+
+class SlidePlaceholder(_BaseSlidePlaceholder):
+    """
+    Placeholder shape on a slide. Inherits shape properties from its
+    corresponding slide layout placeholder.
+    """
+
+
+class ChartPlaceholder(_BaseSlidePlaceholder):
+    """Placeholder shape that can only accept a chart."""
+
+    def insert_chart(self, chart_type, chart_data):
+        """
+        Return a |PlaceholderGraphicFrame| object containing a new chart of
+        *chart_type* depicting *chart_data* and having the same position and
+        size as this placeholder. *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 the new |Chart| object is not returned directly. The chart
+        object may be accessed using the
+        :attr:`~.PlaceholderGraphicFrame.chart` property of the returned
+        |PlaceholderGraphicFrame| object.
+        """
+        rId = self.part.add_chart_part(chart_type, chart_data)
+        graphicFrame = self._new_chart_graphicFrame(
+            rId, self.left, self.top, self.width, self.height
+        )
+        self._replace_placeholder_with(graphicFrame)
+        return PlaceholderGraphicFrame(graphicFrame, self._parent)
+
+    def _new_chart_graphicFrame(self, rId, x, y, cx, cy):
+        """
+        Return a newly created `p:graphicFrame` element having the specified
+        position and size and containing the chart identified by *rId*.
+        """
+        id_, name = self.shape_id, self.name
+        return CT_GraphicalObjectFrame.new_chart_graphicFrame(id_, name, rId, x, y, cx, cy)
+
+
+class PicturePlaceholder(_BaseSlidePlaceholder):
+    """Placeholder shape that can only accept a picture."""
+
+    def insert_picture(self, image_file):
+        """Return a |PlaceholderPicture| object depicting the image in `image_file`.
+
+        `image_file` may be either a path (string) or a file-like object. The image is
+        cropped to fill the entire space of the placeholder. A |PlaceholderPicture|
+        object has all the properties and methods of a |Picture| shape except that the
+        value of its :attr:`~._BaseSlidePlaceholder.shape_type` property is
+        `MSO_SHAPE_TYPE.PLACEHOLDER` instead of `MSO_SHAPE_TYPE.PICTURE`.
+        """
+        pic = self._new_placeholder_pic(image_file)
+        self._replace_placeholder_with(pic)
+        return PlaceholderPicture(pic, self._parent)
+
+    def _new_placeholder_pic(self, image_file):
+        """
+        Return a new `p:pic` element depicting the image in *image_file*,
+        suitable for use as a placeholder. In particular this means not
+        having an `a:xfrm` element, allowing its extents to be inherited from
+        its layout placeholder.
+        """
+        rId, desc, image_size = self._get_or_add_image(image_file)
+        shape_id, name = self.shape_id, self.name
+        pic = CT_Picture.new_ph_pic(shape_id, name, desc, rId)
+        pic.crop_to_fit(image_size, (self.width, self.height))
+        return pic
+
+    def _get_or_add_image(self, image_file):
+        """
+        Return an (rId, description, image_size) 3-tuple identifying the
+        related image part containing *image_file* and describing the image.
+        """
+        image_part, rId = self.part.get_or_add_image_part(image_file)
+        desc, image_size = image_part.desc, image_part._px_size
+        return rId, desc, image_size
+
+
+class PlaceholderGraphicFrame(GraphicFrame):
+    """
+    Placeholder shape populated with a table, chart, or smart art.
+    """
+
+    @property
+    def is_placeholder(self):
+        """
+        Boolean indicating whether this shape is a placeholder.
+        Unconditionally |True| in this case.
+        """
+        return True
+
+
+class PlaceholderPicture(_InheritsDimensions, Picture):
+    """
+    Placeholder shape populated with a picture.
+    """
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the layout placeholder this picture placeholder inherits from.
+        """
+        layout, idx = self.part.slide_layout, self._element.ph_idx
+        return layout.placeholders.get(idx=idx)
+
+
+class TablePlaceholder(_BaseSlidePlaceholder):
+    """Placeholder shape that can only accept a table."""
+
+    def insert_table(self, rows, cols):
+        """Return |PlaceholderGraphicFrame| object containing a `rows` by `cols` table.
+
+        The position and width of the table are those of the placeholder and its height
+        is proportional to the number of rows. A |PlaceholderGraphicFrame| object has
+        all the properties and methods of a |GraphicFrame| shape except that the value
+        of its :attr:`~._BaseSlidePlaceholder.shape_type` property is unconditionally
+        `MSO_SHAPE_TYPE.PLACEHOLDER`. Note that the return value is not the new table
+        but rather *contains* the new table. The table can be accessed using the
+        :attr:`~.PlaceholderGraphicFrame.table` property of the returned
+        |PlaceholderGraphicFrame| object.
+        """
+        graphicFrame = self._new_placeholder_table(rows, cols)
+        self._replace_placeholder_with(graphicFrame)
+        return PlaceholderGraphicFrame(graphicFrame, self._parent)
+
+    def _new_placeholder_table(self, rows, cols):
+        """
+        Return a newly added `p:graphicFrame` element containing an empty
+        table with *rows* rows and *cols* columns, positioned at the location
+        of this placeholder and having its same width. The table's height is
+        determined by the number of rows.
+        """
+        shape_id, name, height = self.shape_id, self.name, Emu(rows * 370840)
+        return CT_GraphicalObjectFrame.new_table_graphicFrame(
+            shape_id, name, rows, cols, self.left, self.top, self.width, height
+        )
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