about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pptx/oxml/shapes
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/oxml/shapes')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py19
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py455
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py107
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py342
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py280
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py270
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py523
7 files changed, 1996 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py
new file mode 100644
index 00000000..37f8ef60
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py
@@ -0,0 +1,19 @@
+"""Base shape-related objects such as BaseShape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+
+    from pptx.oxml.shapes.autoshape import CT_Shape
+    from pptx.oxml.shapes.connector import CT_Connector
+    from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+    from pptx.oxml.shapes.groupshape import CT_GroupShape
+    from pptx.oxml.shapes.picture import CT_Picture
+
+
+ShapeElement: TypeAlias = (
+    "CT_Connector | CT_GraphicalObjectFrame |  CT_GroupShape | CT_Picture | CT_Shape"
+)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py
new file mode 100644
index 00000000..5d78f624
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py
@@ -0,0 +1,455 @@
+# pyright: reportPrivateUsage=false
+
+"""lxml custom element classes for shape-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, PP_PLACEHOLDER
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import (
+    ST_Coordinate,
+    ST_PositiveCoordinate,
+    XsdBoolean,
+    XsdString,
+)
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.shared import (
+        CT_ApplicationNonVisualDrawingProps,
+        CT_NonVisualDrawingProps,
+        CT_ShapeProperties,
+    )
+    from pptx.util import Length
+
+
+class CT_AdjPoint2D(BaseOxmlElement):
+    """`a:pt` custom element class."""
+
+    x: Length = RequiredAttribute("x", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+    y: Length = RequiredAttribute("y", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_CustomGeometry2D(BaseOxmlElement):
+    """`a:custGeom` custom element class."""
+
+    get_or_add_pathLst: Callable[[], CT_Path2DList]
+
+    _tag_seq = ("a:avLst", "a:gdLst", "a:ahLst", "a:cxnLst", "a:rect", "a:pathLst")
+    pathLst: CT_Path2DList | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:pathLst", successors=_tag_seq[6:]
+    )
+
+
+class CT_GeomGuide(BaseOxmlElement):
+    """`a:gd` custom element class.
+
+    Defines a "guide", corresponding to a yellow diamond-shaped handle on an autoshape.
+    """
+
+    name: str = RequiredAttribute("name", XsdString)  # pyright: ignore[reportAssignmentType]
+    fmla: str = RequiredAttribute("fmla", XsdString)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_GeomGuideList(BaseOxmlElement):
+    """`a:avLst` custom element class."""
+
+    _add_gd: Callable[[], CT_GeomGuide]
+
+    gd_lst: list[CT_GeomGuide]
+
+    gd = ZeroOrMore("a:gd")
+
+
+class CT_NonVisualDrawingShapeProps(BaseShapeElement):
+    """`p:cNvSpPr` custom element class."""
+
+    spLocks = ZeroOrOne("a:spLocks")
+    txBox: bool | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "txBox", XsdBoolean
+    )
+
+
+class CT_Path2D(BaseOxmlElement):
+    """`a:path` custom element class."""
+
+    _add_close: Callable[[], CT_Path2DClose]
+    _add_lnTo: Callable[[], CT_Path2DLineTo]
+    _add_moveTo: Callable[[], CT_Path2DMoveTo]
+
+    close = ZeroOrMore("a:close", successors=())
+    lnTo = ZeroOrMore("a:lnTo", successors=())
+    moveTo = ZeroOrMore("a:moveTo", successors=())
+    w: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w", ST_PositiveCoordinate
+    )
+    h: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "h", ST_PositiveCoordinate
+    )
+
+    def add_close(self) -> CT_Path2DClose:
+        """Return a newly created `a:close` element.
+
+        The new `a:close` element is appended to this `a:path` element.
+        """
+        return self._add_close()
+
+    def add_lnTo(self, x: Length, y: Length) -> CT_Path2DLineTo:
+        """Return a newly created `a:lnTo` subtree with end point *(x, y)*.
+
+        The new `a:lnTo` element is appended to this `a:path` element.
+        """
+        lnTo = self._add_lnTo()
+        pt = lnTo._add_pt()
+        pt.x, pt.y = x, y
+        return lnTo
+
+    def add_moveTo(self, x: Length, y: Length):
+        """Return a newly created `a:moveTo` subtree with point `(x, y)`.
+
+        The new `a:moveTo` element is appended to this `a:path` element.
+        """
+        moveTo = self._add_moveTo()
+        pt = moveTo._add_pt()
+        pt.x, pt.y = x, y
+        return moveTo
+
+
+class CT_Path2DClose(BaseOxmlElement):
+    """`a:close` custom element class."""
+
+
+class CT_Path2DLineTo(BaseOxmlElement):
+    """`a:lnTo` custom element class."""
+
+    _add_pt: Callable[[], CT_AdjPoint2D]
+
+    pt = ZeroOrOne("a:pt", successors=())
+
+
+class CT_Path2DList(BaseOxmlElement):
+    """`a:pathLst` custom element class."""
+
+    _add_path: Callable[[], CT_Path2D]
+
+    path = ZeroOrMore("a:path", successors=())
+
+    def add_path(self, w: Length, h: Length):
+        """Return a newly created `a:path` child element."""
+        path = self._add_path()
+        path.w, path.h = w, h
+        return path
+
+
+class CT_Path2DMoveTo(BaseOxmlElement):
+    """`a:moveTo` custom element class."""
+
+    _add_pt: Callable[[], CT_AdjPoint2D]
+
+    pt = ZeroOrOne("a:pt", successors=())
+
+
+class CT_PresetGeometry2D(BaseOxmlElement):
+    """`a:prstGeom` custom element class."""
+
+    _add_avLst: Callable[[], CT_GeomGuideList]
+    _remove_avLst: Callable[[], None]
+
+    avLst: CT_GeomGuideList | None = ZeroOrOne("a:avLst")  # pyright: ignore[reportAssignmentType]
+    prst: MSO_AUTO_SHAPE_TYPE = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "prst", MSO_AUTO_SHAPE_TYPE
+    )
+
+    @property
+    def gd_lst(self) -> list[CT_GeomGuide]:
+        """Sequence of `a:gd` element children of `a:avLst`. Empty if none are present."""
+        avLst = self.avLst
+        if avLst is None:
+            return []
+        return avLst.gd_lst
+
+    def rewrite_guides(self, guides: list[tuple[str, int]]):
+        """Replace any `a:gd` element children of `a:avLst` with ones forme from `guides`."""
+        self._remove_avLst()
+        avLst = self._add_avLst()
+        for name, val in guides:
+            gd = avLst._add_gd()
+            gd.name = name
+            gd.fmla = "val %d" % val
+
+
+class CT_Shape(BaseShapeElement):
+    """`p:sp` custom element class."""
+
+    get_or_add_txBody: Callable[[], CT_TextBody]
+
+    nvSpPr: CT_ShapeNonVisual = OneAndOnlyOne("p:nvSpPr")  # pyright: ignore[reportAssignmentType]
+    spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr")  # pyright: ignore[reportAssignmentType]
+    txBody: CT_TextBody | None = ZeroOrOne("p:txBody", successors=("p:extLst",))  # pyright: ignore
+
+    def add_path(self, w: Length, h: Length) -> CT_Path2D:
+        custGeom = self.spPr.custGeom
+        if custGeom is None:
+            raise ValueError("shape must be freeform")
+        pathLst = custGeom.get_or_add_pathLst()
+        return pathLst.add_path(w=w, h=h)
+
+    def get_or_add_ln(self):
+        """Return the `a:ln` grandchild element, newly added if not present."""
+        return self.spPr.get_or_add_ln()
+
+    @property
+    def has_custom_geometry(self):
+        """True if this shape has custom geometry, i.e. is a freeform shape.
+
+        A shape has custom geometry if it has a `p:spPr/a:custGeom`
+        descendant (instead of `p:spPr/a:prstGeom`).
+        """
+        return self.spPr.custGeom is not None
+
+    @property
+    def is_autoshape(self):
+        """True if this shape is an auto shape.
+
+        A shape is an auto shape if it has a `a:prstGeom` element and does not have a txBox="1"
+        attribute on cNvSpPr.
+        """
+        prstGeom = self.prstGeom
+        if prstGeom is None:
+            return False
+        return self.nvSpPr.cNvSpPr.txBox is not True
+
+    @property
+    def is_textbox(self):
+        """True if this shape is a text box.
+
+        A shape is a text box if it has a `txBox` attribute on cNvSpPr that resolves to |True|.
+        The default when the txBox attribute is missing is |False|.
+        """
+        return self.nvSpPr.cNvSpPr.txBox is True
+
+    @property
+    def ln(self):
+        """`a:ln` grand-child element or |None| if not present."""
+        return self.spPr.ln
+
+    @staticmethod
+    def new_autoshape_sp(
+        id_: int, name: str, prst: str, left: int, top: int, width: int, height: int
+    ) -> CT_Shape:
+        """Return a new `p:sp` element tree configured as a base auto shape."""
+        xml = (
+            "<p:sp %s>\n"
+            "  <p:nvSpPr>\n"
+            '    <p:cNvPr id="%s" name="%s"/>\n'
+            "    <p:cNvSpPr/>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvSpPr>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%s" y="%s"/>\n'
+            '      <a:ext cx="%s" cy="%s"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="%s">\n'
+            "      <a:avLst/>\n"
+            "    </a:prstGeom>\n"
+            "  </p:spPr>\n"
+            "  <p:style>\n"
+            '    <a:lnRef idx="1">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:lnRef>\n"
+            '    <a:fillRef idx="3">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:fillRef>\n"
+            '    <a:effectRef idx="2">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:effectRef>\n"
+            '    <a:fontRef idx="minor">\n'
+            '      <a:schemeClr val="lt1"/>\n'
+            "    </a:fontRef>\n"
+            "  </p:style>\n"
+            "  <p:txBody>\n"
+            '    <a:bodyPr rtlCol="0" anchor="ctr"/>\n'
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            '      <a:pPr algn="ctr"/>\n'
+            "    </a:p>\n"
+            "  </p:txBody>\n"
+            "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d", "%s")
+        ) % (id_, name, left, top, width, height, prst)
+        return cast(CT_Shape, parse_xml(xml))
+
+    @staticmethod
+    def new_freeform_sp(shape_id: int, name: str, x: int, y: int, cx: int, cy: int):
+        """Return new `p:sp` element tree configured as freeform shape.
+
+        The returned shape has a `a:custGeom` subtree but no paths in its
+        path list.
+        """
+        xml = (
+            "<p:sp %s>\n"
+            "  <p:nvSpPr>\n"
+            '    <p:cNvPr id="%s" name="%s"/>\n'
+            "    <p:cNvSpPr/>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvSpPr>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%s" y="%s"/>\n'
+            '      <a:ext cx="%s" cy="%s"/>\n'
+            "    </a:xfrm>\n"
+            "    <a:custGeom>\n"
+            "      <a:avLst/>\n"
+            "      <a:gdLst/>\n"
+            "      <a:ahLst/>\n"
+            "      <a:cxnLst/>\n"
+            '      <a:rect l="l" t="t" r="r" b="b"/>\n'
+            "      <a:pathLst/>\n"
+            "    </a:custGeom>\n"
+            "  </p:spPr>\n"
+            "  <p:style>\n"
+            '    <a:lnRef idx="1">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:lnRef>\n"
+            '    <a:fillRef idx="3">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:fillRef>\n"
+            '    <a:effectRef idx="2">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:effectRef>\n"
+            '    <a:fontRef idx="minor">\n'
+            '      <a:schemeClr val="lt1"/>\n'
+            "    </a:fontRef>\n"
+            "  </p:style>\n"
+            "  <p:txBody>\n"
+            '    <a:bodyPr rtlCol="0" anchor="ctr"/>\n'
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            '      <a:pPr algn="ctr"/>\n'
+            "    </a:p>\n"
+            "  </p:txBody>\n"
+            "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d")
+        ) % (shape_id, name, x, y, cx, cy)
+        return cast(CT_Shape, parse_xml(xml))
+
+    @staticmethod
+    def new_placeholder_sp(
+        id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz, idx
+    ) -> CT_Shape:
+        """Return a new `p:sp` element tree configured as a placeholder shape."""
+        sp = cast(
+            CT_Shape,
+            parse_xml(
+                f"<p:sp {nsdecls('a', 'p')}>\n"
+                f"  <p:nvSpPr>\n"
+                f'    <p:cNvPr id="{id_}" name="{name}"/>\n'
+                f"    <p:cNvSpPr>\n"
+                f'      <a:spLocks noGrp="1"/>\n'
+                f"    </p:cNvSpPr>\n"
+                f"    <p:nvPr/>\n"
+                f"  </p:nvSpPr>\n"
+                f"  <p:spPr/>\n"
+                f"</p:sp>"
+            ),
+        )
+
+        ph = sp.nvSpPr.nvPr.get_or_add_ph()
+        ph.type = ph_type
+        ph.idx = idx
+        ph.orient = orient
+        ph.sz = sz
+
+        placeholder_types_that_have_a_text_frame = (
+            PP_PLACEHOLDER.TITLE,
+            PP_PLACEHOLDER.CENTER_TITLE,
+            PP_PLACEHOLDER.SUBTITLE,
+            PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.OBJECT,
+        )
+
+        if ph_type in placeholder_types_that_have_a_text_frame:
+            sp.append(CT_TextBody.new())
+
+        return sp
+
+    @staticmethod
+    def new_textbox_sp(id_, name, left, top, width, height):
+        """Return a new `p:sp` element tree configured as a base textbox shape."""
+        tmpl = CT_Shape._textbox_sp_tmpl()
+        xml = tmpl % (id_, name, left, top, width, height)
+        sp = parse_xml(xml)
+        return sp
+
+    @property
+    def prst(self):
+        """Value of `prst` attribute of `a:prstGeom` element or |None| if not present."""
+        prstGeom = self.prstGeom
+        if prstGeom is None:
+            return None
+        return prstGeom.prst
+
+    @property
+    def prstGeom(self) -> CT_PresetGeometry2D:
+        """Reference to `a:prstGeom` child element.
+
+        |None| if this shape doesn't have one, for example, if it's a placeholder shape.
+        """
+        return self.spPr.prstGeom
+
+    def _new_txBody(self):
+        return CT_TextBody.new_p_txBody()
+
+    @staticmethod
+    def _textbox_sp_tmpl():
+        return (
+            "<p:sp %s>\n"
+            "  <p:nvSpPr>\n"
+            '    <p:cNvPr id="%s" name="%s"/>\n'
+            '    <p:cNvSpPr txBox="1"/>\n'
+            "    <p:nvPr/>\n"
+            "  </p:nvSpPr>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%s" y="%s"/>\n'
+            '      <a:ext cx="%s" cy="%s"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="rect">\n'
+            "      <a:avLst/>\n"
+            "    </a:prstGeom>\n"
+            "    <a:noFill/>\n"
+            "  </p:spPr>\n"
+            "  <p:txBody>\n"
+            '    <a:bodyPr wrap="none">\n'
+            "      <a:spAutoFit/>\n"
+            "    </a:bodyPr>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p/>\n"
+            "  </p:txBody>\n"
+            "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d")
+        )
+
+
+class CT_ShapeNonVisual(BaseShapeElement):
+    """`p:nvSpPr` custom element class."""
+
+    cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:cNvPr"
+    )
+    cNvSpPr: CT_NonVisualDrawingShapeProps = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:cNvSpPr"
+    )
+    nvPr: CT_ApplicationNonVisualDrawingProps = (  # pyright: ignore[reportAssignmentType]
+        OneAndOnlyOne("p:nvPr")
+    )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py
new file mode 100644
index 00000000..91261f78
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py
@@ -0,0 +1,107 @@
+"""lxml custom element classes for XML elements related to the Connector shape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import ST_DrawingElementId, XsdUnsignedInt
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.shared import CT_ShapeProperties
+
+
+class CT_Connection(BaseShapeElement):
+    """A `a:stCxn` or `a:endCxn` element.
+
+    Specifies a connection between an end-point of a connector and a shape connection point.
+    """
+
+    id = RequiredAttribute("id", ST_DrawingElementId)
+    idx = RequiredAttribute("idx", XsdUnsignedInt)
+
+
+class CT_Connector(BaseShapeElement):
+    """A line/connector shape `p:cxnSp` element"""
+
+    _tag_seq = ("p:nvCxnSpPr", "p:spPr", "p:style", "p:extLst")
+    nvCxnSpPr = OneAndOnlyOne("p:nvCxnSpPr")
+    spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr")  # pyright: ignore[reportAssignmentType]
+    del _tag_seq
+
+    @classmethod
+    def new_cxnSp(
+        cls,
+        id_: int,
+        name: str,
+        prst: str,
+        x: int,
+        y: int,
+        cx: int,
+        cy: int,
+        flipH: bool,
+        flipV: bool,
+    ) -> CT_Connector:
+        """Return a new `p:cxnSp` element tree configured as a base connector."""
+        flip = (' flipH="1"' if flipH else "") + (' flipV="1"' if flipV else "")
+        return cast(
+            CT_Connector,
+            parse_xml(
+                f"<p:cxnSp {nsdecls('a', 'p')}>\n"
+                f"  <p:nvCxnSpPr>\n"
+                f'    <p:cNvPr id="{id_}" name="{name}"/>\n'
+                f"    <p:cNvCxnSpPr/>\n"
+                f"    <p:nvPr/>\n"
+                f"  </p:nvCxnSpPr>\n"
+                f"  <p:spPr>\n"
+                f"    <a:xfrm{flip}>\n"
+                f'      <a:off x="{x}" y="{y}"/>\n'
+                f'      <a:ext cx="{cx}" cy="{cy}"/>\n'
+                f"    </a:xfrm>\n"
+                f'    <a:prstGeom prst="{prst}">\n'
+                f"      <a:avLst/>\n"
+                f"    </a:prstGeom>\n"
+                f"  </p:spPr>\n"
+                f"  <p:style>\n"
+                f'    <a:lnRef idx="2">\n'
+                f'      <a:schemeClr val="accent1"/>\n'
+                f"    </a:lnRef>\n"
+                f'    <a:fillRef idx="0">\n'
+                f'      <a:schemeClr val="accent1"/>\n'
+                f"    </a:fillRef>\n"
+                f'    <a:effectRef idx="1">\n'
+                f'      <a:schemeClr val="accent1"/>\n'
+                f"    </a:effectRef>\n"
+                f'    <a:fontRef idx="minor">\n'
+                f'      <a:schemeClr val="tx1"/>\n'
+                f"    </a:fontRef>\n"
+                f"  </p:style>\n"
+                f"</p:cxnSp>"
+            ),
+        )
+
+
+class CT_ConnectorNonVisual(BaseOxmlElement):
+    """
+    `p:nvCxnSpPr` element, container for the non-visual properties of
+    a connector, such as name, id, etc.
+    """
+
+    cNvPr = OneAndOnlyOne("p:cNvPr")
+    cNvCxnSpPr = OneAndOnlyOne("p:cNvCxnSpPr")
+    nvPr = OneAndOnlyOne("p:nvPr")
+
+
+class CT_NonVisualConnectorProperties(BaseOxmlElement):
+    """
+    `p:cNvCxnSpPr` element, container for the non-visual properties specific
+    to a connector shape, such as connections and connector locking.
+    """
+
+    _tag_seq = ("a:cxnSpLocks", "a:stCxn", "a:endCxn", "a:extLst")
+    stCxn = ZeroOrOne("a:stCxn", successors=_tag_seq[2:])
+    endCxn = ZeroOrOne("a:endCxn", successors=_tag_seq[3:])
+    del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py
new file mode 100644
index 00000000..efa0b363
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py
@@ -0,0 +1,342 @@
+"""lxml custom element class for CT_GraphicalObjectFrame XML element."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.oxml import parse_xml
+from pptx.oxml.chart.chart import CT_Chart
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import XsdBoolean, XsdString
+from pptx.oxml.table import CT_Table
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrOne,
+)
+from pptx.spec import (
+    GRAPHIC_DATA_URI_CHART,
+    GRAPHIC_DATA_URI_OLEOBJ,
+    GRAPHIC_DATA_URI_TABLE,
+)
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.shared import (
+        CT_ApplicationNonVisualDrawingProps,
+        CT_NonVisualDrawingProps,
+        CT_Transform2D,
+    )
+
+
+class CT_GraphicalObject(BaseOxmlElement):
+    """`a:graphic` element.
+
+    The container for the reference to or definition of the framed graphical object (table, chart,
+    etc.).
+    """
+
+    graphicData: CT_GraphicalObjectData = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "a:graphicData"
+    )
+
+    @property
+    def chart(self) -> CT_Chart | None:
+        """The `c:chart` grandchild element, or |None| if not present."""
+        return self.graphicData.chart
+
+
+class CT_GraphicalObjectData(BaseShapeElement):
+    """`p:graphicData` element.
+
+    The direct container for a table, a chart, or another graphical object.
+    """
+
+    chart: CT_Chart | None = ZeroOrOne("c:chart")  # pyright: ignore[reportAssignmentType]
+    tbl: CT_Table | None = ZeroOrOne("a:tbl")  # pyright: ignore[reportAssignmentType]
+    uri: str = RequiredAttribute("uri", XsdString)  # pyright: ignore[reportAssignmentType]
+
+    @property
+    def blob_rId(self) -> str | None:
+        """Optional `r:id` attribute value of `p:oleObj` descendent element.
+
+        This value is `None` when this `p:graphicData` element does not enclose an OLE object.
+        This value could also be `None` if an enclosed OLE object does not specify this attribute
+        (it is specified optional in the schema) but so far, all OLE objects we've encountered
+        specify this value.
+        """
+        return None if self._oleObj is None else self._oleObj.rId
+
+    @property
+    def is_embedded_ole_obj(self) -> bool | None:
+        """Optional boolean indicating an embedded OLE object.
+
+        Returns `None` when this `p:graphicData` element does not enclose an OLE object. `True`
+        indicates an embedded OLE object and `False` indicates a linked OLE object.
+        """
+        return None if self._oleObj is None else self._oleObj.is_embedded
+
+    @property
+    def progId(self) -> str | None:
+        """Optional str value of "progId" attribute of `p:oleObj` descendent.
+
+        This value identifies the "type" of the embedded object in terms of the application used
+        to open it.
+
+        This value is `None` when this `p:graphicData` element does not enclose an OLE object.
+        This could also be `None` if an enclosed OLE object does not specify this attribute (it is
+        specified optional in the schema) but so far, all OLE objects we've encountered specify
+        this value.
+        """
+        return None if self._oleObj is None else self._oleObj.progId
+
+    @property
+    def showAsIcon(self) -> bool | None:
+        """Optional value of "showAsIcon" attribute value of `p:oleObj` descendent.
+
+        This value is `None` when this `p:graphicData` element does not enclose an OLE object. It
+        is False when the `showAsIcon` attribute is omitted on the `p:oleObj` element.
+        """
+        return None if self._oleObj is None else self._oleObj.showAsIcon
+
+    @property
+    def _oleObj(self) -> CT_OleObject | None:
+        """Optional `p:oleObj` element contained in this `p:graphicData' element.
+
+        Returns `None` when this graphic-data element does not enclose an OLE object. Note that
+        this returns the last `p:oleObj` element found. There can be more than one `p:oleObj`
+        element because an `mc.AlternateContent` element may appear as the child of
+        `p:graphicData` and that alternate-content subtree can contain multiple compatibility
+        choices. The last one should suit best for reading purposes because it contains the lowest
+        common denominator.
+        """
+        oleObjs = cast("list[CT_OleObject]", self.xpath(".//p:oleObj"))
+        return oleObjs[-1] if oleObjs else None
+
+
+class CT_GraphicalObjectFrame(BaseShapeElement):
+    """`p:graphicFrame` element.
+
+    A container for a table, a chart, or another graphical object.
+    """
+
+    nvGraphicFramePr: CT_GraphicalObjectFrameNonVisual = (  # pyright: ignore[reportAssignmentType]
+        OneAndOnlyOne("p:nvGraphicFramePr")
+    )
+    xfrm: CT_Transform2D = OneAndOnlyOne("p:xfrm")  # pyright: ignore
+    graphic: CT_GraphicalObject = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "a:graphic"
+    )
+
+    @property
+    def chart(self) -> CT_Chart | None:
+        """The `c:chart` great-grandchild element, or |None| if not present."""
+        return self.graphic.chart
+
+    @property
+    def chart_rId(self) -> str | None:
+        """The `rId` attribute of the `c:chart` great-grandchild element.
+
+        |None| if not present.
+        """
+        chart = self.chart
+        if chart is None:
+            return None
+        return chart.rId
+
+    def get_or_add_xfrm(self) -> CT_Transform2D:
+        """Return the required `p:xfrm` child element.
+
+        Overrides version on BaseShapeElement.
+        """
+        return self.xfrm
+
+    @property
+    def graphicData(self) -> CT_GraphicalObjectData:
+        """`a:graphicData` grandchild of this graphic-frame element."""
+        return self.graphic.graphicData
+
+    @property
+    def graphicData_uri(self) -> str:
+        """str value of `uri` attribute of `a:graphicData` grandchild."""
+        return self.graphic.graphicData.uri
+
+    @property
+    def has_oleobj(self) -> bool:
+        """`True` for graphicFrame containing an OLE object, `False` otherwise."""
+        return self.graphicData.uri == GRAPHIC_DATA_URI_OLEOBJ
+
+    @property
+    def is_embedded_ole_obj(self) -> bool | None:
+        """Optional boolean indicating an embedded OLE object.
+
+        Returns `None` when this `p:graphicFrame` element does not enclose an OLE object. `True`
+        indicates an embedded OLE object and `False` indicates a linked OLE object.
+        """
+        return self.graphicData.is_embedded_ole_obj
+
+    @classmethod
+    def new_chart_graphicFrame(
+        cls, id_: int, name: str, rId: str, x: int, y: int, cx: int, cy: int
+    ) -> CT_GraphicalObjectFrame:
+        """Return a `p:graphicFrame` element tree populated with a chart element."""
+        graphicFrame = CT_GraphicalObjectFrame.new_graphicFrame(id_, name, x, y, cx, cy)
+        graphicData = graphicFrame.graphic.graphicData
+        graphicData.uri = GRAPHIC_DATA_URI_CHART
+        graphicData.append(CT_Chart.new_chart(rId))
+        return graphicFrame
+
+    @classmethod
+    def new_graphicFrame(
+        cls, id_: int, name: str, x: int, y: int, cx: int, cy: int
+    ) -> CT_GraphicalObjectFrame:
+        """Return a new `p:graphicFrame` element tree suitable for containing a table or chart.
+
+        Note that a graphicFrame element is not a valid shape until it contains a graphical object
+        such as a table.
+        """
+        return cast(
+            CT_GraphicalObjectFrame,
+            parse_xml(
+                f"<p:graphicFrame {nsdecls('a', 'p')}>\n"
+                f"  <p:nvGraphicFramePr>\n"
+                f'    <p:cNvPr id="{id_}" name="{name}"/>\n'
+                f"    <p:cNvGraphicFramePr>\n"
+                f'      <a:graphicFrameLocks noGrp="1"/>\n'
+                f"    </p:cNvGraphicFramePr>\n"
+                f"    <p:nvPr/>\n"
+                f"  </p:nvGraphicFramePr>\n"
+                f"  <p:xfrm>\n"
+                f'    <a:off x="{x}" y="{y}"/>\n'
+                f'    <a:ext cx="{cx}" cy="{cy}"/>\n'
+                f"  </p:xfrm>\n"
+                f"  <a:graphic>\n"
+                f"    <a:graphicData/>\n"
+                f"  </a:graphic>\n"
+                f"</p:graphicFrame>"
+            ),
+        )
+
+    @classmethod
+    def new_ole_object_graphicFrame(
+        cls,
+        id_: int,
+        name: str,
+        ole_object_rId: str,
+        progId: str,
+        icon_rId: str,
+        x: int,
+        y: int,
+        cx: int,
+        cy: int,
+        imgW: int,
+        imgH: int,
+    ) -> CT_GraphicalObjectFrame:
+        """Return newly-created `p:graphicFrame` for embedded OLE-object.
+
+        `ole_object_rId` identifies the relationship to the OLE-object part.
+
+        `progId` is a str identifying the object-type in terms of the application (program) used
+        to open it. This becomes an attribute of the same name in the `p:oleObj` element.
+
+        `icon_rId` identifies the relationship to an image part used to display the OLE-object as
+        an icon (vs. a preview).
+        """
+        return cast(
+            CT_GraphicalObjectFrame,
+            parse_xml(
+                f"<p:graphicFrame {nsdecls('a', 'p', 'r')}>\n"
+                f"  <p:nvGraphicFramePr>\n"
+                f'    <p:cNvPr id="{id_}" name="{name}"/>\n'
+                f"    <p:cNvGraphicFramePr>\n"
+                f'      <a:graphicFrameLocks noGrp="1"/>\n'
+                f"    </p:cNvGraphicFramePr>\n"
+                f"    <p:nvPr/>\n"
+                f"  </p:nvGraphicFramePr>\n"
+                f"  <p:xfrm>\n"
+                f'    <a:off x="{x}" y="{y}"/>\n'
+                f'    <a:ext cx="{cx}" cy="{cy}"/>\n'
+                f"  </p:xfrm>\n"
+                f"  <a:graphic>\n"
+                f"    <a:graphicData"
+                f'        uri="http://schemas.openxmlformats.org/presentationml/2006/ole">\n'
+                f'      <p:oleObj showAsIcon="1"'
+                f'                r:id="{ole_object_rId}"'
+                f'                imgW="{imgW}"'
+                f'                imgH="{imgH}"'
+                f'                progId="{progId}">\n'
+                f"        <p:embed/>\n"
+                f"        <p:pic>\n"
+                f"          <p:nvPicPr>\n"
+                f'            <p:cNvPr id="0" name=""/>\n'
+                f"            <p:cNvPicPr/>\n"
+                f"            <p:nvPr/>\n"
+                f"          </p:nvPicPr>\n"
+                f"          <p:blipFill>\n"
+                f'            <a:blip r:embed="{icon_rId}"/>\n'
+                f"            <a:stretch>\n"
+                f"              <a:fillRect/>\n"
+                f"            </a:stretch>\n"
+                f"          </p:blipFill>\n"
+                f"          <p:spPr>\n"
+                f"            <a:xfrm>\n"
+                f'              <a:off x="{x}" y="{y}"/>\n'
+                f'              <a:ext cx="{cx}" cy="{cy}"/>\n'
+                f"            </a:xfrm>\n"
+                f'            <a:prstGeom prst="rect">\n'
+                f"              <a:avLst/>\n"
+                f"            </a:prstGeom>\n"
+                f"          </p:spPr>\n"
+                f"        </p:pic>\n"
+                f"      </p:oleObj>\n"
+                f"    </a:graphicData>\n"
+                f"  </a:graphic>\n"
+                f"</p:graphicFrame>"
+            ),
+        )
+
+    @classmethod
+    def new_table_graphicFrame(
+        cls, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int
+    ) -> CT_GraphicalObjectFrame:
+        """Return a `p:graphicFrame` element tree populated with a table element."""
+        graphicFrame = cls.new_graphicFrame(id_, name, x, y, cx, cy)
+        graphicFrame.graphic.graphicData.uri = GRAPHIC_DATA_URI_TABLE
+        graphicFrame.graphic.graphicData.append(CT_Table.new_tbl(rows, cols, cx, cy))
+        return graphicFrame
+
+
+class CT_GraphicalObjectFrameNonVisual(BaseOxmlElement):
+    """`p:nvGraphicFramePr` element.
+
+    This contains the non-visual properties of a graphic frame, such as name, id, etc.
+    """
+
+    cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:cNvPr"
+    )
+    nvPr: CT_ApplicationNonVisualDrawingProps = (  # pyright: ignore[reportAssignmentType]
+        OneAndOnlyOne("p:nvPr")
+    )
+
+
+class CT_OleObject(BaseOxmlElement):
+    """`p:oleObj` element, container for an OLE object (e.g. Excel file).
+
+    An OLE object can be either linked or embedded (hence the name).
+    """
+
+    progId: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "progId", XsdString
+    )
+    rId: str | None = OptionalAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+    showAsIcon: bool = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "showAsIcon", XsdBoolean, default=False
+    )
+
+    @property
+    def is_embedded(self) -> bool:
+        """True when this OLE object is embedded, False when it is linked."""
+        return len(self.xpath("./p:embed")) > 0
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py
new file mode 100644
index 00000000..f62bc666
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py
@@ -0,0 +1,280 @@
+"""lxml custom element classes for shape-tree-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, Iterator
+
+from pptx.enum.shapes import MSO_CONNECTOR_TYPE
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls, qn
+from pptx.oxml.shapes.autoshape import CT_Shape
+from pptx.oxml.shapes.connector import CT_Connector
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, ZeroOrOne
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+    from pptx.enum.shapes import PP_PLACEHOLDER
+    from pptx.oxml.shapes import ShapeElement
+    from pptx.oxml.shapes.shared import CT_Transform2D
+
+
+class CT_GroupShape(BaseShapeElement):
+    """Used for shape tree (`p:spTree`) as well as the group shape (`p:grpSp`) elements."""
+
+    nvGrpSpPr: CT_GroupShapeNonVisual = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:nvGrpSpPr"
+    )
+    grpSpPr: CT_GroupShapeProperties = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:grpSpPr"
+    )
+
+    _shape_tags = (
+        qn("p:sp"),
+        qn("p:grpSp"),
+        qn("p:graphicFrame"),
+        qn("p:cxnSp"),
+        qn("p:pic"),
+        qn("p:contentPart"),
+    )
+
+    def add_autoshape(
+        self, id_: int, name: str, prst: str, x: int, y: int, cx: int, cy: int
+    ) -> CT_Shape:
+        """Return new `p:sp` appended to the group/shapetree with specified attributes."""
+        sp = CT_Shape.new_autoshape_sp(id_, name, prst, x, y, cx, cy)
+        self.insert_element_before(sp, "p:extLst")
+        return sp
+
+    def add_cxnSp(
+        self,
+        id_: int,
+        name: str,
+        type_member: MSO_CONNECTOR_TYPE,
+        x: int,
+        y: int,
+        cx: int,
+        cy: int,
+        flipH: bool,
+        flipV: bool,
+    ) -> CT_Connector:
+        """Return new `p:cxnSp` appended to the group/shapetree with the specified attribues."""
+        prst = MSO_CONNECTOR_TYPE.to_xml(type_member)
+        cxnSp = CT_Connector.new_cxnSp(id_, name, prst, x, y, cx, cy, flipH, flipV)
+        self.insert_element_before(cxnSp, "p:extLst")
+        return cxnSp
+
+    def add_freeform_sp(self, x: int, y: int, cx: int, cy: int) -> CT_Shape:
+        """Append a new freeform `p:sp` with specified position and size."""
+        shape_id = self._next_shape_id
+        name = "Freeform %d" % (shape_id - 1,)
+        sp = CT_Shape.new_freeform_sp(shape_id, name, x, y, cx, cy)
+        self.insert_element_before(sp, "p:extLst")
+        return sp
+
+    def add_grpSp(self) -> CT_GroupShape:
+        """Return `p:grpSp` element newly appended to this shape tree.
+
+        The element contains no sub-shapes, is positioned at (0, 0), and has
+        width and height of zero.
+        """
+        shape_id = self._next_shape_id
+        name = "Group %d" % (shape_id - 1,)
+        grpSp = CT_GroupShape.new_grpSp(shape_id, name)
+        self.insert_element_before(grpSp, "p:extLst")
+        return grpSp
+
+    def add_pic(
+        self, id_: int, name: str, desc: str, rId: str, x: int, y: int, cx: int, cy: int
+    ) -> CT_Picture:
+        """Append a `p:pic` shape to the group/shapetree having properties as specified in call."""
+        pic = CT_Picture.new_pic(id_, name, desc, rId, x, y, cx, cy)
+        self.insert_element_before(pic, "p:extLst")
+        return pic
+
+    def add_placeholder(
+        self, id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz: str, idx: int
+    ) -> CT_Shape:
+        """Append a newly-created placeholder `p:sp` shape having the specified properties."""
+        sp = CT_Shape.new_placeholder_sp(id_, name, ph_type, orient, sz, idx)
+        self.insert_element_before(sp, "p:extLst")
+        return sp
+
+    def add_table(
+        self, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int
+    ) -> CT_GraphicalObjectFrame:
+        """Append a `p:graphicFrame` shape containing a table as specified in call."""
+        graphicFrame = CT_GraphicalObjectFrame.new_table_graphicFrame(
+            id_, name, rows, cols, x, y, cx, cy
+        )
+        self.insert_element_before(graphicFrame, "p:extLst")
+        return graphicFrame
+
+    def add_textbox(self, id_: int, name: str, x: int, y: int, cx: int, cy: int) -> CT_Shape:
+        """Append a newly-created textbox `p:sp` shape having the specified position and size."""
+        sp = CT_Shape.new_textbox_sp(id_, name, x, y, cx, cy)
+        self.insert_element_before(sp, "p:extLst")
+        return sp
+
+    @property
+    def chExt(self):
+        """Descendent `p:grpSpPr/a:xfrm/a:chExt` element."""
+        return self.grpSpPr.get_or_add_xfrm().get_or_add_chExt()
+
+    @property
+    def chOff(self):
+        """Descendent `p:grpSpPr/a:xfrm/a:chOff` element."""
+        return self.grpSpPr.get_or_add_xfrm().get_or_add_chOff()
+
+    def get_or_add_xfrm(self) -> CT_Transform2D:
+        """Return the `a:xfrm` grandchild element, newly-added if not present."""
+        return self.grpSpPr.get_or_add_xfrm()
+
+    def iter_ph_elms(self):
+        """Generate each placeholder shape child element in document order."""
+        for e in self.iter_shape_elms():
+            if e.has_ph_elm:
+                yield e
+
+    def iter_shape_elms(self) -> Iterator[ShapeElement]:
+        """Generate each child of this `p:spTree` element that corresponds to a shape.
+
+        Items appear in XML document order.
+        """
+        for elm in self.iterchildren():
+            if elm.tag in self._shape_tags:
+                yield elm
+
+    @property
+    def max_shape_id(self) -> int:
+        """Maximum int value assigned as @id in this slide.
+
+        This is generally a shape-id, but ids can be assigned to other
+        objects so we just check all @id values anywhere in the document
+        (XML id-values have document scope).
+
+        In practice, its minimum value is 1 because the spTree element itself
+        is always assigned id="1".
+        """
+        id_str_lst = self.xpath("//@id")
+        used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()]
+        return max(used_ids) if used_ids else 0
+
+    @classmethod
+    def new_grpSp(cls, id_: int, name: str) -> CT_GroupShape:
+        """Return new "loose" `p:grpSp` element having `id_` and `name`."""
+        xml = (
+            "<p:grpSp %s>\n"
+            "  <p:nvGrpSpPr>\n"
+            '    <p:cNvPr id="%%d" name="%%s"/>\n'
+            "    <p:cNvGrpSpPr/>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvGrpSpPr>\n"
+            "  <p:grpSpPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="0" y="0"/>\n'
+            '      <a:ext cx="0" cy="0"/>\n'
+            '      <a:chOff x="0" y="0"/>\n'
+            '      <a:chExt cx="0" cy="0"/>\n'
+            "    </a:xfrm>\n"
+            "  </p:grpSpPr>\n"
+            "</p:grpSp>" % nsdecls("a", "p", "r")
+        ) % (id_, name)
+        grpSp = parse_xml(xml)
+        return grpSp
+
+    def recalculate_extents(self) -> None:
+        """Adjust x, y, cx, and cy to incorporate all contained shapes.
+
+        This would typically be called when a contained shape is added,
+        removed, or its position or size updated.
+
+        This method is recursive "upwards" since a change in a group shape
+        can change the position and size of its containing group.
+        """
+        if not self.tag == qn("p:grpSp"):
+            return
+
+        x, y, cx, cy = self._child_extents
+
+        self.chOff.x = self.x = x
+        self.chOff.y = self.y = y
+        self.chExt.cx = self.cx = cx
+        self.chExt.cy = self.cy = cy
+        self.getparent().recalculate_extents()
+
+    @property
+    def xfrm(self) -> CT_Transform2D | None:
+        """The `a:xfrm` grandchild element or |None| if not found."""
+        return self.grpSpPr.xfrm
+
+    @property
+    def _child_extents(self) -> tuple[int, int, int, int]:
+        """(x, y, cx, cy) tuple representing net position and size.
+
+        The values are formed as a composite of the contained child shapes.
+        """
+        child_shape_elms = list(self.iter_shape_elms())
+
+        if not child_shape_elms:
+            return Emu(0), Emu(0), Emu(0), Emu(0)
+
+        min_x = min([xSp.x for xSp in child_shape_elms])
+        min_y = min([xSp.y for xSp in child_shape_elms])
+        max_x = max([(xSp.x + xSp.cx) for xSp in child_shape_elms])
+        max_y = max([(xSp.y + xSp.cy) for xSp in child_shape_elms])
+
+        x = min_x
+        y = min_y
+        cx = max_x - min_x
+        cy = max_y - min_y
+
+        return x, y, cx, cy
+
+    @property
+    def _next_shape_id(self) -> int:
+        """Return unique shape id suitable for use with a new shape element.
+
+        The returned id is the next available positive integer drawing object
+        id in shape tree, starting from 1 and making use of any gaps in
+        numbering. In practice, the minimum id is 2 because the spTree
+        element itself is always assigned id="1".
+        """
+        id_str_lst = self.xpath("//@id")
+        used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()]
+        for n in range(1, len(used_ids) + 2):
+            if n not in used_ids:
+                return n
+
+
+class CT_GroupShapeNonVisual(BaseShapeElement):
+    """`p:nvGrpSpPr` element."""
+
+    cNvPr = OneAndOnlyOne("p:cNvPr")
+
+
+class CT_GroupShapeProperties(BaseOxmlElement):
+    """p:grpSpPr element"""
+
+    get_or_add_xfrm: Callable[[], CT_Transform2D]
+
+    _tag_seq = (
+        "a:xfrm",
+        "a:noFill",
+        "a:solidFill",
+        "a:gradFill",
+        "a:blipFill",
+        "a:pattFill",
+        "a:grpFill",
+        "a:effectLst",
+        "a:effectDag",
+        "a:scene3d",
+        "a:extLst",
+    )
+    xfrm: CT_Transform2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:xfrm", successors=_tag_seq[1:]
+    )
+    effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[8:])
+    del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py
new file mode 100644
index 00000000..bacc9719
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py
@@ -0,0 +1,270 @@
+"""lxml custom element classes for picture-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+from xml.sax.saxutils import escape
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.shared import CT_ShapeProperties
+    from pptx.util import Length
+
+
+class CT_Picture(BaseShapeElement):
+    """`p:pic` element.
+
+    Represents a picture shape (an image placement on a slide).
+    """
+
+    nvPicPr = OneAndOnlyOne("p:nvPicPr")
+    blipFill = OneAndOnlyOne("p:blipFill")
+    spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr")  # pyright: ignore[reportAssignmentType]
+
+    @property
+    def blip_rId(self) -> str | None:
+        """Value of `p:blipFill/a:blip/@r:embed`.
+
+        Returns |None| if not present.
+        """
+        blip = self.blipFill.blip
+        if blip is not None and blip.rEmbed is not None:
+            return blip.rEmbed
+        return None
+
+    def crop_to_fit(self, image_size, view_size):
+        """
+        Set cropping values in `p:blipFill/a:srcRect` such that an image of
+        *image_size* will stretch to exactly fit *view_size* when its aspect
+        ratio is preserved.
+        """
+        self.blipFill.crop(self._fill_cropping(image_size, view_size))
+
+    def get_or_add_ln(self):
+        """
+        Return the <a:ln> grandchild element, newly added if not present.
+        """
+        return self.spPr.get_or_add_ln()
+
+    @property
+    def ln(self):
+        """
+        ``<a:ln>`` grand-child element or |None| if not present
+        """
+        return self.spPr.ln
+
+    @classmethod
+    def new_ph_pic(cls, id_, name, desc, rId):
+        """
+        Return a new `p:pic` placeholder element populated with the supplied
+        parameters.
+        """
+        return parse_xml(cls._pic_ph_tmpl() % (id_, name, desc, rId))
+
+    @classmethod
+    def new_pic(cls, shape_id, name, desc, rId, x, y, cx, cy):
+        """Return new `<p:pic>` element tree configured with supplied parameters."""
+        return parse_xml(cls._pic_tmpl() % (shape_id, name, escape(desc), rId, x, y, cx, cy))
+
+    @classmethod
+    def new_video_pic(
+        cls,
+        shape_id: int,
+        shape_name: str,
+        video_rId: str,
+        media_rId: str,
+        poster_frame_rId: str,
+        x: Length,
+        y: Length,
+        cx: Length,
+        cy: Length,
+    ) -> CT_Picture:
+        """Return a new `p:pic` populated with the specified video."""
+        return cast(
+            CT_Picture,
+            parse_xml(
+                cls._pic_video_tmpl()
+                % (
+                    shape_id,
+                    shape_name,
+                    video_rId,
+                    media_rId,
+                    poster_frame_rId,
+                    x,
+                    y,
+                    cx,
+                    cy,
+                )
+            ),
+        )
+
+    @property
+    def srcRect_b(self):
+        """Value of `p:blipFill/a:srcRect/@b` or 0.0 if not present."""
+        return self._srcRect_x("b")
+
+    @srcRect_b.setter
+    def srcRect_b(self, value):
+        self.blipFill.get_or_add_srcRect().b = value
+
+    @property
+    def srcRect_l(self):
+        """Value of `p:blipFill/a:srcRect/@l` or 0.0 if not present."""
+        return self._srcRect_x("l")
+
+    @srcRect_l.setter
+    def srcRect_l(self, value):
+        self.blipFill.get_or_add_srcRect().l = value  # noqa
+
+    @property
+    def srcRect_r(self):
+        """Value of `p:blipFill/a:srcRect/@r` or 0.0 if not present."""
+        return self._srcRect_x("r")
+
+    @srcRect_r.setter
+    def srcRect_r(self, value):
+        self.blipFill.get_or_add_srcRect().r = value
+
+    @property
+    def srcRect_t(self):
+        """Value of `p:blipFill/a:srcRect/@t` or 0.0 if not present."""
+        return self._srcRect_x("t")
+
+    @srcRect_t.setter
+    def srcRect_t(self, value):
+        self.blipFill.get_or_add_srcRect().t = value
+
+    def _fill_cropping(self, image_size, view_size):
+        """
+        Return a (left, top, right, bottom) 4-tuple containing the cropping
+        values required to display an image of *image_size* in *view_size*
+        when stretched proportionately. Each value is a percentage expressed
+        as a fraction of 1.0, e.g. 0.425 represents 42.5%. *image_size* and
+        *view_size* are each (width, height) pairs.
+        """
+
+        def aspect_ratio(width, height):
+            return width / height
+
+        ar_view = aspect_ratio(*view_size)
+        ar_image = aspect_ratio(*image_size)
+
+        if ar_view < ar_image:  # image too wide
+            crop = (1.0 - (ar_view / ar_image)) / 2.0
+            return (crop, 0.0, crop, 0.0)
+        if ar_view > ar_image:  # image too tall
+            crop = (1.0 - (ar_image / ar_view)) / 2.0
+            return (0.0, crop, 0.0, crop)
+        return (0.0, 0.0, 0.0, 0.0)
+
+    @classmethod
+    def _pic_ph_tmpl(cls):
+        return (
+            "<p:pic %s>\n"
+            "  <p:nvPicPr>\n"
+            '    <p:cNvPr id="%%d" name="%%s" descr="%%s"/>\n'
+            "    <p:cNvPicPr>\n"
+            '      <a:picLocks noGrp="1" noChangeAspect="1"/>\n'
+            "    </p:cNvPicPr>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvPicPr>\n"
+            "  <p:blipFill>\n"
+            '    <a:blip r:embed="%%s"/>\n'
+            "    <a:stretch>\n"
+            "      <a:fillRect/>\n"
+            "    </a:stretch>\n"
+            "  </p:blipFill>\n"
+            "  <p:spPr/>\n"
+            "</p:pic>" % nsdecls("p", "a", "r")
+        )
+
+    @classmethod
+    def _pic_tmpl(cls):
+        return (
+            "<p:pic %s>\n"
+            "  <p:nvPicPr>\n"
+            '    <p:cNvPr id="%%d" name="%%s" descr="%%s"/>\n'
+            "    <p:cNvPicPr>\n"
+            '      <a:picLocks noChangeAspect="1"/>\n'
+            "    </p:cNvPicPr>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvPicPr>\n"
+            "  <p:blipFill>\n"
+            '    <a:blip r:embed="%%s"/>\n'
+            "    <a:stretch>\n"
+            "      <a:fillRect/>\n"
+            "    </a:stretch>\n"
+            "  </p:blipFill>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%%d" y="%%d"/>\n'
+            '      <a:ext cx="%%d" cy="%%d"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="rect">\n'
+            "      <a:avLst/>\n"
+            "    </a:prstGeom>\n"
+            "  </p:spPr>\n"
+            "</p:pic>" % nsdecls("a", "p", "r")
+        )
+
+    @classmethod
+    def _pic_video_tmpl(cls):
+        return (
+            "<p:pic %s>\n"
+            "  <p:nvPicPr>\n"
+            '    <p:cNvPr id="%%d" name="%%s">\n'
+            '      <a:hlinkClick r:id="" action="ppaction://media"/>\n'
+            "    </p:cNvPr>\n"
+            "    <p:cNvPicPr>\n"
+            '      <a:picLocks noChangeAspect="1"/>\n'
+            "    </p:cNvPicPr>\n"
+            "    <p:nvPr>\n"
+            '      <a:videoFile r:link="%%s"/>\n'
+            "      <p:extLst>\n"
+            '        <p:ext uri="{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}">\n'
+            '          <p14:media xmlns:p14="http://schemas.microsoft.com/of'
+            'fice/powerpoint/2010/main" r:embed="%%s"/>\n'
+            "        </p:ext>\n"
+            "      </p:extLst>\n"
+            "    </p:nvPr>\n"
+            "  </p:nvPicPr>\n"
+            "  <p:blipFill>\n"
+            '    <a:blip r:embed="%%s"/>\n'
+            "    <a:stretch>\n"
+            "      <a:fillRect/>\n"
+            "    </a:stretch>\n"
+            "  </p:blipFill>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%%d" y="%%d"/>\n'
+            '      <a:ext cx="%%d" cy="%%d"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="rect">\n'
+            "      <a:avLst/>\n"
+            "    </a:prstGeom>\n"
+            "  </p:spPr>\n"
+            "</p:pic>" % nsdecls("a", "p", "r")
+        )
+
+    def _srcRect_x(self, attr_name):
+        """
+        Value of `p:blipFill/a:srcRect/@{attr_name}` or 0.0 if not present.
+        """
+        srcRect = self.blipFill.srcRect
+        if srcRect is None:
+            return 0.0
+        return getattr(srcRect, attr_name)
+
+
+class CT_PictureNonVisual(BaseOxmlElement):
+    """
+    ``<p:nvPicPr>`` element, containing non-visual properties for a picture
+    shape.
+    """
+
+    cNvPr = OneAndOnlyOne("p:cNvPr")
+    nvPr = OneAndOnlyOne("p:nvPr")
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py
new file mode 100644
index 00000000..d9f94569
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py
@@ -0,0 +1,523 @@
+"""Common shape-related oxml objects."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable
+
+from pptx.dml.fill import CT_GradientFillProperties
+from pptx.enum.shapes import PP_PLACEHOLDER
+from pptx.oxml.ns import qn
+from pptx.oxml.simpletypes import (
+    ST_Angle,
+    ST_Coordinate,
+    ST_Direction,
+    ST_DrawingElementId,
+    ST_LineWidth,
+    ST_PlaceholderSize,
+    ST_PositiveCoordinate,
+    XsdBoolean,
+    XsdString,
+    XsdUnsignedInt,
+)
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    Choice,
+    OptionalAttribute,
+    OxmlElement,
+    RequiredAttribute,
+    ZeroOrOne,
+    ZeroOrOneChoice,
+)
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+    from pptx.oxml.action import CT_Hyperlink
+    from pptx.oxml.shapes.autoshape import CT_CustomGeometry2D, CT_PresetGeometry2D
+    from pptx.util import Length
+
+
+class BaseShapeElement(BaseOxmlElement):
+    """Provides common behavior for shape element classes like CT_Shape, CT_Picture, etc."""
+
+    spPr: CT_ShapeProperties
+
+    @property
+    def cx(self) -> Length:
+        return self._get_xfrm_attr("cx")
+
+    @cx.setter
+    def cx(self, value):
+        self._set_xfrm_attr("cx", value)
+
+    @property
+    def cy(self) -> Length:
+        return self._get_xfrm_attr("cy")
+
+    @cy.setter
+    def cy(self, value):
+        self._set_xfrm_attr("cy", value)
+
+    @property
+    def flipH(self):
+        return bool(self._get_xfrm_attr("flipH"))
+
+    @flipH.setter
+    def flipH(self, value):
+        self._set_xfrm_attr("flipH", value)
+
+    @property
+    def flipV(self):
+        return bool(self._get_xfrm_attr("flipV"))
+
+    @flipV.setter
+    def flipV(self, value):
+        self._set_xfrm_attr("flipV", value)
+
+    def get_or_add_xfrm(self):
+        """Return the `a:xfrm` grandchild element, newly-added if not present.
+
+        This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to
+        override.
+        """
+        return self.spPr.get_or_add_xfrm()
+
+    @property
+    def has_ph_elm(self):
+        """
+        True if this shape element has a `p:ph` descendant, indicating it
+        is a placeholder shape. False otherwise.
+        """
+        return self.ph is not None
+
+    @property
+    def ph(self) -> CT_Placeholder | None:
+        """The `p:ph` descendant element if there is one, None otherwise."""
+        ph_elms = self.xpath("./*[1]/p:nvPr/p:ph")
+        if len(ph_elms) == 0:
+            return None
+        return ph_elms[0]
+
+    @property
+    def ph_idx(self) -> int:
+        """Integer value of placeholder idx attribute.
+
+        Raises |ValueError| if shape is not a placeholder.
+        """
+        ph = self.ph
+        if ph is None:
+            raise ValueError("not a placeholder shape")
+        return ph.idx
+
+    @property
+    def ph_orient(self) -> str:
+        """Placeholder orientation, e.g. 'vert'.
+
+        Raises |ValueError| if shape is not a placeholder.
+        """
+        ph = self.ph
+        if ph is None:
+            raise ValueError("not a placeholder shape")
+        return ph.orient
+
+    @property
+    def ph_sz(self) -> str:
+        """Placeholder size, e.g. ST_PlaceholderSize.HALF.
+
+        Raises `ValueError` if shape is not a placeholder.
+        """
+        ph = self.ph
+        if ph is None:
+            raise ValueError("not a placeholder shape")
+        return ph.sz
+
+    @property
+    def ph_type(self):
+        """Placeholder type, e.g. ST_PlaceholderType.TITLE ('title').
+
+        Raises `ValueError` if shape is not a placeholder.
+        """
+        ph = self.ph
+        if ph is None:
+            raise ValueError("not a placeholder shape")
+        return ph.type
+
+    @property
+    def rot(self) -> float:
+        """Float representing degrees this shape is rotated clockwise."""
+        xfrm = self.xfrm
+        if xfrm is None or xfrm.rot is None:
+            return 0.0
+        return xfrm.rot
+
+    @rot.setter
+    def rot(self, value: float):
+        self.get_or_add_xfrm().rot = value
+
+    @property
+    def shape_id(self):
+        """
+        Integer id of this shape
+        """
+        return self._nvXxPr.cNvPr.id
+
+    @property
+    def shape_name(self):
+        """
+        Name of this shape
+        """
+        return self._nvXxPr.cNvPr.name
+
+    @property
+    def txBody(self):
+        """Child `p:txBody` element, None if not present."""
+        return self.find(qn("p:txBody"))
+
+    @property
+    def x(self) -> Length:
+        return self._get_xfrm_attr("x")
+
+    @x.setter
+    def x(self, value):
+        self._set_xfrm_attr("x", value)
+
+    @property
+    def xfrm(self):
+        """The `a:xfrm` grandchild element or |None| if not found.
+
+        This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to
+        override.
+        """
+        return self.spPr.xfrm
+
+    @property
+    def y(self) -> Length:
+        return self._get_xfrm_attr("y")
+
+    @y.setter
+    def y(self, value):
+        self._set_xfrm_attr("y", value)
+
+    @property
+    def _nvXxPr(self):
+        """
+        Required non-visual shape properties element for this shape. Actual
+        name depends on the shape type, e.g. `p:nvPicPr` for picture
+        shape.
+        """
+        return self.xpath("./*[1]")[0]
+
+    def _get_xfrm_attr(self, name: str) -> Length | None:
+        xfrm = self.xfrm
+        if xfrm is None:
+            return None
+        return getattr(xfrm, name)
+
+    def _set_xfrm_attr(self, name, value):
+        xfrm = self.get_or_add_xfrm()
+        setattr(xfrm, name, value)
+
+
+class CT_ApplicationNonVisualDrawingProps(BaseOxmlElement):
+    """`p:nvPr` element."""
+
+    get_or_add_ph: Callable[[], CT_Placeholder]
+
+    ph = ZeroOrOne(
+        "p:ph",
+        successors=(
+            "a:audioCd",
+            "a:wavAudioFile",
+            "a:audioFile",
+            "a:videoFile",
+            "a:quickTimeFile",
+            "p:custDataLst",
+            "p:extLst",
+        ),
+    )
+
+
+class CT_LineProperties(BaseOxmlElement):
+    """Custom element class for <a:ln> element"""
+
+    _tag_seq = (
+        "a:noFill",
+        "a:solidFill",
+        "a:gradFill",
+        "a:pattFill",
+        "a:prstDash",
+        "a:custDash",
+        "a:round",
+        "a:bevel",
+        "a:miter",
+        "a:headEnd",
+        "a:tailEnd",
+        "a:extLst",
+    )
+    eg_lineFillProperties = ZeroOrOneChoice(
+        (
+            Choice("a:noFill"),
+            Choice("a:solidFill"),
+            Choice("a:gradFill"),
+            Choice("a:pattFill"),
+        ),
+        successors=_tag_seq[4:],
+    )
+    prstDash = ZeroOrOne("a:prstDash", successors=_tag_seq[5:])
+    custDash = ZeroOrOne("a:custDash", successors=_tag_seq[6:])
+    del _tag_seq
+    w = OptionalAttribute("w", ST_LineWidth, default=Emu(0))
+
+    @property
+    def eg_fillProperties(self):
+        """
+        Required to fulfill the interface used by dml.fill.
+        """
+        return self.eg_lineFillProperties
+
+    @property
+    def prstDash_val(self):
+        """Return value of `val` attribute of `a:prstDash` child.
+
+        Return |None| if not present.
+        """
+        prstDash = self.prstDash
+        if prstDash is None:
+            return None
+        return prstDash.val
+
+    @prstDash_val.setter
+    def prstDash_val(self, val):
+        self._remove_custDash()
+        prstDash = self.get_or_add_prstDash()
+        prstDash.val = val
+
+
+class CT_NonVisualDrawingProps(BaseOxmlElement):
+    """`p:cNvPr` custom element class."""
+
+    get_or_add_hlinkClick: Callable[[], CT_Hyperlink]
+    get_or_add_hlinkHover: Callable[[], CT_Hyperlink]
+
+    _tag_seq = ("a:hlinkClick", "a:hlinkHover", "a:extLst")
+    hlinkClick: CT_Hyperlink | None = ZeroOrOne("a:hlinkClick", successors=_tag_seq[1:])
+    hlinkHover: CT_Hyperlink | None = ZeroOrOne("a:hlinkHover", successors=_tag_seq[2:])
+    id = RequiredAttribute("id", ST_DrawingElementId)
+    name = RequiredAttribute("name", XsdString)
+    del _tag_seq
+
+
+class CT_Placeholder(BaseOxmlElement):
+    """`p:ph` custom element class."""
+
+    type: PP_PLACEHOLDER = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "type", PP_PLACEHOLDER, default=PP_PLACEHOLDER.OBJECT
+    )
+    orient: str = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "orient", ST_Direction, default=ST_Direction.HORZ
+    )
+    sz: str = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "sz", ST_PlaceholderSize, default=ST_PlaceholderSize.FULL
+    )
+    idx: int = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "idx", XsdUnsignedInt, default=0
+    )
+
+
+class CT_Point2D(BaseOxmlElement):
+    """
+    Custom element class for <a:off> element.
+    """
+
+    x: Length = RequiredAttribute("x", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+    y: Length = RequiredAttribute("y", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_PositiveSize2D(BaseOxmlElement):
+    """
+    Custom element class for <a:ext> element.
+    """
+
+    cx = RequiredAttribute("cx", ST_PositiveCoordinate)
+    cy = RequiredAttribute("cy", ST_PositiveCoordinate)
+
+
+class CT_ShapeProperties(BaseOxmlElement):
+    """Custom element class for `p:spPr` element.
+
+    Shared by `p:sp`, `p:cxnSp`,  and `p:pic` elements as well as a few more obscure ones.
+    """
+
+    get_or_add_xfrm: Callable[[], CT_Transform2D]
+    get_or_add_ln: Callable[[], CT_LineProperties]
+    _add_prstGeom: Callable[[], CT_PresetGeometry2D]
+    _remove_custGeom: Callable[[], None]
+
+    _tag_seq = (
+        "a:xfrm",
+        "a:custGeom",
+        "a:prstGeom",
+        "a:noFill",
+        "a:solidFill",
+        "a:gradFill",
+        "a:blipFill",
+        "a:pattFill",
+        "a:grpFill",
+        "a:ln",
+        "a:effectLst",
+        "a:effectDag",
+        "a:scene3d",
+        "a:sp3d",
+        "a:extLst",
+    )
+    xfrm: CT_Transform2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:xfrm", successors=_tag_seq[1:]
+    )
+    custGeom: CT_CustomGeometry2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:custGeom", successors=_tag_seq[2:]
+    )
+    prstGeom: CT_PresetGeometry2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:prstGeom", successors=_tag_seq[3:]
+    )
+    eg_fillProperties = ZeroOrOneChoice(
+        (
+            Choice("a:noFill"),
+            Choice("a:solidFill"),
+            Choice("a:gradFill"),
+            Choice("a:blipFill"),
+            Choice("a:pattFill"),
+            Choice("a:grpFill"),
+        ),
+        successors=_tag_seq[9:],
+    )
+    ln: CT_LineProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:ln", successors=_tag_seq[10:]
+    )
+    effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[11:])
+    del _tag_seq
+
+    @property
+    def cx(self):
+        """
+        Shape width as an instance of Emu, or None if not present.
+        """
+        cx_str_lst = self.xpath("./a:xfrm/a:ext/@cx")
+        if not cx_str_lst:
+            return None
+        return Emu(cx_str_lst[0])
+
+    @property
+    def cy(self):
+        """
+        Shape height as an instance of Emu, or None if not present.
+        """
+        cy_str_lst = self.xpath("./a:xfrm/a:ext/@cy")
+        if not cy_str_lst:
+            return None
+        return Emu(cy_str_lst[0])
+
+    @property
+    def x(self) -> Length | None:
+        """Distance between the left edge of the slide and left edge of the shape.
+
+        0 if not present.
+        """
+        x_str_lst = self.xpath("./a:xfrm/a:off/@x")
+        if not x_str_lst:
+            return None
+        return Emu(x_str_lst[0])
+
+    @property
+    def y(self):
+        """
+        The offset of the top of the shape from the top of the slide, as an
+        instance of Emu. None if not present.
+        """
+        y_str_lst = self.xpath("./a:xfrm/a:off/@y")
+        if not y_str_lst:
+            return None
+        return Emu(y_str_lst[0])
+
+    def _new_gradFill(self):
+        return CT_GradientFillProperties.new_gradFill()
+
+
+class CT_Transform2D(BaseOxmlElement):
+    """`a:xfrm` custom element class.
+
+    NOTE: this is a composite including CT_GroupTransform2D, which appears
+    with the `a:xfrm` tag in a group shape (including a slide `p:spTree`).
+    """
+
+    _tag_seq = ("a:off", "a:ext", "a:chOff", "a:chExt")
+    off: CT_Point2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:off", successors=_tag_seq[1:]
+    )
+    ext = ZeroOrOne("a:ext", successors=_tag_seq[2:])
+    chOff = ZeroOrOne("a:chOff", successors=_tag_seq[3:])
+    chExt = ZeroOrOne("a:chExt", successors=_tag_seq[4:])
+    del _tag_seq
+    rot: float | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "rot", ST_Angle, default=0.0
+    )
+    flipH = OptionalAttribute("flipH", XsdBoolean, default=False)
+    flipV = OptionalAttribute("flipV", XsdBoolean, default=False)
+
+    @property
+    def x(self):
+        off = self.off
+        if off is None:
+            return None
+        return off.x
+
+    @x.setter
+    def x(self, value):
+        off = self.get_or_add_off()
+        off.x = value
+
+    @property
+    def y(self):
+        off = self.off
+        if off is None:
+            return None
+        return off.y
+
+    @y.setter
+    def y(self, value):
+        off = self.get_or_add_off()
+        off.y = value
+
+    @property
+    def cx(self):
+        ext = self.ext
+        if ext is None:
+            return None
+        return ext.cx
+
+    @cx.setter
+    def cx(self, value):
+        ext = self.get_or_add_ext()
+        ext.cx = value
+
+    @property
+    def cy(self):
+        ext = self.ext
+        if ext is None:
+            return None
+        return ext.cy
+
+    @cy.setter
+    def cy(self, value):
+        ext = self.get_or_add_ext()
+        ext.cy = value
+
+    def _new_ext(self):
+        ext = OxmlElement("a:ext")
+        ext.cx = 0
+        ext.cy = 0
+        return ext
+
+    def _new_off(self):
+        off = OxmlElement("a:off")
+        off.x = 0
+        off.y = 0
+        return off