about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pptx/parts
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/parts')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/chart.py95
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py167
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py93
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/image.py275
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/media.py37
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/presentation.py126
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/slide.py297
8 files changed, 1090 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py b/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/chart.py b/.venv/lib/python3.12/site-packages/pptx/parts/chart.py
new file mode 100644
index 00000000..7208071b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/chart.py
@@ -0,0 +1,95 @@
+"""Chart part objects, including Chart and Charts."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.chart.chart import Chart
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.parts.embeddedpackage import EmbeddedXlsxPart
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.chart.data import ChartData
+    from pptx.enum.chart import XL_CHART_TYPE
+    from pptx.package import Package
+
+
+class ChartPart(XmlPart):
+    """A chart part.
+
+    Corresponds to parts having partnames matching ppt/charts/chart[1-9][0-9]*.xml
+    """
+
+    partname_template = "/ppt/charts/chart%d.xml"
+
+    @classmethod
+    def new(cls, chart_type: XL_CHART_TYPE, chart_data: ChartData, package: Package):
+        """Return new |ChartPart| instance added to `package`.
+
+        Returned chart-part contains a chart of `chart_type` depicting `chart_data`.
+        """
+        chart_part = cls.load(
+            package.next_partname(cls.partname_template),
+            CT.DML_CHART,
+            package,
+            chart_data.xml_bytes(chart_type),
+        )
+        chart_part.chart_workbook.update_from_xlsx_blob(chart_data.xlsx_blob)
+        return chart_part
+
+    @lazyproperty
+    def chart(self):
+        """|Chart| object representing the chart in this part."""
+        return Chart(self._element, self)
+
+    @lazyproperty
+    def chart_workbook(self):
+        """
+        The |ChartWorkbook| object providing access to the external chart
+        data in a linked or embedded Excel workbook.
+        """
+        return ChartWorkbook(self._element, self)
+
+
+class ChartWorkbook(object):
+    """Provides access to external chart data in a linked or embedded Excel workbook."""
+
+    def __init__(self, chartSpace, chart_part):
+        super(ChartWorkbook, self).__init__()
+        self._chartSpace = chartSpace
+        self._chart_part = chart_part
+
+    def update_from_xlsx_blob(self, xlsx_blob):
+        """
+        Replace the Excel spreadsheet in the related |EmbeddedXlsxPart| with
+        the Excel binary in *xlsx_blob*, adding a new |EmbeddedXlsxPart| if
+        there isn't one.
+        """
+        xlsx_part = self.xlsx_part
+        if xlsx_part is None:
+            self.xlsx_part = EmbeddedXlsxPart.new(xlsx_blob, self._chart_part.package)
+            return
+        xlsx_part.blob = xlsx_blob
+
+    @property
+    def xlsx_part(self):
+        """Optional |EmbeddedXlsxPart| object containing data for this chart.
+
+        This related part has its rId at `c:chartSpace/c:externalData/@rId`. This value
+        is |None| if there is no `<c:externalData>` element.
+        """
+        xlsx_part_rId = self._chartSpace.xlsx_part_rId
+        return None if xlsx_part_rId is None else self._chart_part.related_part(xlsx_part_rId)
+
+    @xlsx_part.setter
+    def xlsx_part(self, xlsx_part):
+        """
+        Set the related |EmbeddedXlsxPart| to *xlsx_part*. Assume one does
+        not already exist.
+        """
+        rId = self._chart_part.relate_to(xlsx_part, RT.PACKAGE)
+        externalData = self._chartSpace.get_or_add_externalData()
+        externalData.rId = rId
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py b/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py
new file mode 100644
index 00000000..8471cc8e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py
@@ -0,0 +1,167 @@
+"""Core properties part, corresponds to ``/docProps/core.xml`` part in package."""
+
+from __future__ import annotations
+
+import datetime as dt
+from typing import TYPE_CHECKING
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.oxml.coreprops import CT_CoreProperties
+
+if TYPE_CHECKING:
+    from pptx.package import Package
+
+
+class CorePropertiesPart(XmlPart):
+    """Corresponds to part named `/docProps/core.xml`.
+
+    Contains the core document properties for this document package.
+    """
+
+    _element: CT_CoreProperties
+
+    @classmethod
+    def default(cls, package: Package):
+        """Return default new |CorePropertiesPart| instance suitable as starting point.
+
+        This provides a base for adding core-properties to a package that doesn't yet
+        have any.
+        """
+        core_props = cls._new(package)
+        core_props.title = "PowerPoint Presentation"
+        core_props.last_modified_by = "python-pptx"
+        core_props.revision = 1
+        core_props.modified = dt.datetime.now(dt.timezone.utc).replace(tzinfo=None)
+        return core_props
+
+    @property
+    def author(self) -> str:
+        return self._element.author_text
+
+    @author.setter
+    def author(self, value: str):
+        self._element.author_text = value
+
+    @property
+    def category(self) -> str:
+        return self._element.category_text
+
+    @category.setter
+    def category(self, value: str):
+        self._element.category_text = value
+
+    @property
+    def comments(self) -> str:
+        return self._element.comments_text
+
+    @comments.setter
+    def comments(self, value: str):
+        self._element.comments_text = value
+
+    @property
+    def content_status(self) -> str:
+        return self._element.contentStatus_text
+
+    @content_status.setter
+    def content_status(self, value: str):
+        self._element.contentStatus_text = value
+
+    @property
+    def created(self):
+        return self._element.created_datetime
+
+    @created.setter
+    def created(self, value: dt.datetime):
+        self._element.created_datetime = value
+
+    @property
+    def identifier(self) -> str:
+        return self._element.identifier_text
+
+    @identifier.setter
+    def identifier(self, value: str):
+        self._element.identifier_text = value
+
+    @property
+    def keywords(self) -> str:
+        return self._element.keywords_text
+
+    @keywords.setter
+    def keywords(self, value: str):
+        self._element.keywords_text = value
+
+    @property
+    def language(self) -> str:
+        return self._element.language_text
+
+    @language.setter
+    def language(self, value: str):
+        self._element.language_text = value
+
+    @property
+    def last_modified_by(self) -> str:
+        return self._element.lastModifiedBy_text
+
+    @last_modified_by.setter
+    def last_modified_by(self, value: str):
+        self._element.lastModifiedBy_text = value
+
+    @property
+    def last_printed(self):
+        return self._element.lastPrinted_datetime
+
+    @last_printed.setter
+    def last_printed(self, value: dt.datetime):
+        self._element.lastPrinted_datetime = value
+
+    @property
+    def modified(self):
+        return self._element.modified_datetime
+
+    @modified.setter
+    def modified(self, value: dt.datetime):
+        self._element.modified_datetime = value
+
+    @property
+    def revision(self):
+        return self._element.revision_number
+
+    @revision.setter
+    def revision(self, value: int):
+        self._element.revision_number = value
+
+    @property
+    def subject(self) -> str:
+        return self._element.subject_text
+
+    @subject.setter
+    def subject(self, value: str):
+        self._element.subject_text = value
+
+    @property
+    def title(self) -> str:
+        return self._element.title_text
+
+    @title.setter
+    def title(self, value: str):
+        self._element.title_text = value
+
+    @property
+    def version(self) -> str:
+        return self._element.version_text
+
+    @version.setter
+    def version(self, value: str):
+        self._element.version_text = value
+
+    @classmethod
+    def _new(cls, package: Package) -> CorePropertiesPart:
+        """Return new empty |CorePropertiesPart| instance."""
+        return CorePropertiesPart(
+            PackURI("/docProps/core.xml"),
+            CT.OPC_CORE_PROPERTIES,
+            package,
+            CT_CoreProperties.new_coreProperties(),
+        )
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py b/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py
new file mode 100644
index 00000000..7aa2cf40
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py
@@ -0,0 +1,93 @@
+"""Embedded Package part objects.
+
+"Package" in this context means another OPC package, i.e. a DOCX, PPTX, or XLSX "file".
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.enum.shapes import PROG_ID
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.package import Part
+
+if TYPE_CHECKING:
+    from pptx.package import Package
+
+
+class EmbeddedPackagePart(Part):
+    """A distinct OPC package, e.g. an Excel file, embedded in this PPTX package.
+
+    Has a partname like: `ppt/embeddings/Microsoft_Excel_Sheet1.xlsx`.
+    """
+
+    @classmethod
+    def factory(cls, prog_id: PROG_ID | str, object_blob: bytes, package: Package):
+        """Return a new |EmbeddedPackagePart| subclass instance added to *package*.
+
+        The subclass is determined by `prog_id` which corresponds to the "application"
+        used to open the "file-type" of `object_blob`. The returned part contains the
+        bytes of `object_blob` and has the content-type also determined by `prog_id`.
+        """
+        # --- a generic OLE object has no subclass ---
+        if not isinstance(prog_id, PROG_ID):
+            return cls(
+                package.next_partname("/ppt/embeddings/oleObject%d.bin"),
+                CT.OFC_OLE_OBJECT,
+                package,
+                object_blob,
+            )
+
+        # --- A Microsoft Office file-type is a distinguished package object ---
+        EmbeddedPartCls = {
+            PROG_ID.DOCX: EmbeddedDocxPart,
+            PROG_ID.PPTX: EmbeddedPptxPart,
+            PROG_ID.XLSX: EmbeddedXlsxPart,
+        }[prog_id]
+
+        return EmbeddedPartCls.new(object_blob, package)
+
+    @classmethod
+    def new(cls, blob: bytes, package: Package):
+        """Return new |EmbeddedPackagePart| subclass object.
+
+        The returned part object contains `blob` and is added to `package`.
+        """
+        return cls(
+            package.next_partname(cls.partname_template),
+            cls.content_type,
+            package,
+            blob,
+        )
+
+
+class EmbeddedDocxPart(EmbeddedPackagePart):
+    """A Word .docx file stored in a part.
+
+    This part-type arises when a Word document appears as an embedded OLE-object shape.
+    """
+
+    partname_template = "/ppt/embeddings/Microsoft_Word_Document%d.docx"
+    content_type = CT.WML_DOCUMENT
+
+
+class EmbeddedPptxPart(EmbeddedPackagePart):
+    """A PowerPoint file stored in a part.
+
+    This part-type arises when a PowerPoint presentation (.pptx file) appears as an
+    embedded OLE-object shape.
+    """
+
+    partname_template = "/ppt/embeddings/Microsoft_PowerPoint_Presentation%d.pptx"
+    content_type = CT.PML_PRESENTATION
+
+
+class EmbeddedXlsxPart(EmbeddedPackagePart):
+    """An Excel file stored in a part.
+
+    This part-type arises as the data source for a chart, but may also be the OLE-object
+    for an embedded object shape.
+    """
+
+    partname_template = "/ppt/embeddings/Microsoft_Excel_Sheet%d.xlsx"
+    content_type = CT.SML_SHEET
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/image.py b/.venv/lib/python3.12/site-packages/pptx/parts/image.py
new file mode 100644
index 00000000..9be5d02d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/image.py
@@ -0,0 +1,275 @@
+"""ImagePart and related objects."""
+
+from __future__ import annotations
+
+import hashlib
+import io
+import os
+from typing import IO, TYPE_CHECKING, Any, cast
+
+from PIL import Image as PIL_Image
+
+from pptx.opc.package import Part
+from pptx.opc.spec import image_content_types
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.opc.packuri import PackURI
+    from pptx.package import Package
+    from pptx.util import Length
+
+
+class ImagePart(Part):
+    """An image part.
+
+    An image part generally has a partname matching the regex `ppt/media/image[1-9][0-9]*.*`.
+    """
+
+    def __init__(
+        self,
+        partname: PackURI,
+        content_type: str,
+        package: Package,
+        blob: bytes,
+        filename: str | None = None,
+    ):
+        super(ImagePart, self).__init__(partname, content_type, package, blob)
+        self._blob = blob
+        self._filename = filename
+
+    @classmethod
+    def new(cls, package: Package, image: Image) -> ImagePart:
+        """Return new |ImagePart| instance containing `image`.
+
+        `image` is an |Image| object.
+        """
+        return cls(
+            package.next_image_partname(image.ext),
+            image.content_type,
+            package,
+            image.blob,
+            image.filename,
+        )
+
+    @property
+    def desc(self) -> str:
+        """The filename associated with this image.
+
+        Either the filename of the original image or a generic name of the form `image.ext` where
+        `ext` is appropriate to the image file format, e.g. `'jpg'`. An image created using a path
+        will have that filename; one created with a file-like object will have a generic name.
+        """
+        # -- return generic filename if original filename is unknown --
+        if self._filename is None:
+            return f"image.{self.ext}"
+        return self._filename
+
+    @property
+    def ext(self) -> str:
+        """File-name extension for this image e.g. `'png'`."""
+        return self.partname.ext
+
+    @property
+    def image(self) -> Image:
+        """An |Image| object containing the image in this image part.
+
+        Note this is a `pptx.image.Image` object, not a PIL Image.
+        """
+        return Image(self._blob, self.desc)
+
+    def scale(self, scaled_cx: int | None, scaled_cy: int | None) -> tuple[int, int]:
+        """Return scaled image dimensions in EMU based on the combination of parameters supplied.
+
+        If `scaled_cx` and `scaled_cy` are both |None|, the native image size is returned. If
+        neither `scaled_cx` nor `scaled_cy` is |None|, their values are returned unchanged. If a
+        value is provided for either `scaled_cx` or `scaled_cy` and the other is |None|, the
+        missing value is calculated such that the image's aspect ratio is preserved.
+        """
+        image_cx, image_cy = self._native_size
+
+        if scaled_cx and scaled_cy:
+            return scaled_cx, scaled_cy
+
+        if scaled_cx and not scaled_cy:
+            scaling_factor = float(scaled_cx) / float(image_cx)
+            scaled_cy = int(round(image_cy * scaling_factor))
+            return scaled_cx, scaled_cy
+
+        if not scaled_cx and scaled_cy:
+            scaling_factor = float(scaled_cy) / float(image_cy)
+            scaled_cx = int(round(image_cx * scaling_factor))
+            return scaled_cx, scaled_cy
+
+        # -- only remaining case is both `scaled_cx` and `scaled_cy` are `None` --
+        return image_cx, image_cy
+
+    @lazyproperty
+    def sha1(self) -> str:
+        """The 40-character SHA1 hash digest for the image binary of this image part.
+
+        like: `"1be010ea47803b00e140b852765cdf84f491da47"`.
+        """
+        return hashlib.sha1(self._blob).hexdigest()
+
+    @property
+    def _dpi(self) -> tuple[int, int]:
+        """(horz_dpi, vert_dpi) pair representing the dots-per-inch resolution of this image."""
+        image = Image.from_blob(self._blob)
+        return image.dpi
+
+    @property
+    def _native_size(self) -> tuple[Length, Length]:
+        """A (width, height) 2-tuple representing the native dimensions of the image in EMU.
+
+        Calculated based on the image DPI value, if present, assuming 72 dpi as a default.
+        """
+        EMU_PER_INCH = 914400
+        horz_dpi, vert_dpi = self._dpi
+        width_px, height_px = self._px_size
+
+        width = EMU_PER_INCH * width_px / horz_dpi
+        height = EMU_PER_INCH * height_px / vert_dpi
+
+        return Emu(int(width)), Emu(int(height))
+
+    @property
+    def _px_size(self) -> tuple[int, int]:
+        """A (width, height) 2-tuple representing the dimensions of this image in pixels."""
+        image = Image.from_blob(self._blob)
+        return image.size
+
+
+class Image(object):
+    """Immutable value object representing an image such as a JPEG, PNG, or GIF."""
+
+    def __init__(self, blob: bytes, filename: str | None):
+        super(Image, self).__init__()
+        self._blob = blob
+        self._filename = filename
+
+    @classmethod
+    def from_blob(cls, blob: bytes, filename: str | None = None) -> Image:
+        """Return a new |Image| object loaded from the image binary in `blob`."""
+        return cls(blob, filename)
+
+    @classmethod
+    def from_file(cls, image_file: str | IO[bytes]) -> Image:
+        """Return a new |Image| object loaded from `image_file`.
+
+        `image_file` can be either a path (str) or a file-like object.
+        """
+        if isinstance(image_file, str):
+            # treat image_file as a path
+            with open(image_file, "rb") as f:
+                blob = f.read()
+            filename = os.path.basename(image_file)
+        else:
+            # assume image_file is a file-like object
+            # ---reposition file cursor if it has one---
+            if callable(getattr(image_file, "seek")):
+                image_file.seek(0)
+            blob = image_file.read()
+            filename = None
+
+        return cls.from_blob(blob, filename)
+
+    @property
+    def blob(self) -> bytes:
+        """The binary image bytestream of this image."""
+        return self._blob
+
+    @lazyproperty
+    def content_type(self) -> str:
+        """MIME-type of this image, e.g. `"image/jpeg"`."""
+        return image_content_types[self.ext]
+
+    @lazyproperty
+    def dpi(self) -> tuple[int, int]:
+        """A (horz_dpi, vert_dpi) 2-tuple specifying the dots-per-inch resolution of this image.
+
+        A default value of (72, 72) is used if the dpi is not specified in the image file.
+        """
+
+        def int_dpi(dpi: Any):
+            """Return an integer dots-per-inch value corresponding to `dpi`.
+
+            If `dpi` is |None|, a non-numeric type, less than 1 or greater than 2048, 72 is
+            returned.
+            """
+            try:
+                int_dpi = int(round(float(dpi)))
+                if int_dpi < 1 or int_dpi > 2048:
+                    int_dpi = 72
+            except (TypeError, ValueError):
+                int_dpi = 72
+            return int_dpi
+
+        def normalize_pil_dpi(pil_dpi: tuple[int, int] | None):
+            """Return a (horz_dpi, vert_dpi) 2-tuple corresponding to `pil_dpi`.
+
+            The value for the 'dpi' key in the `info` dict of a PIL image. If the 'dpi' key is not
+            present or contains an invalid value, `(72, 72)` is returned.
+            """
+            if isinstance(pil_dpi, tuple):
+                return (int_dpi(pil_dpi[0]), int_dpi(pil_dpi[1]))
+            return (72, 72)
+
+        return normalize_pil_dpi(self._pil_props[2])
+
+    @lazyproperty
+    def ext(self) -> str:
+        """Canonical file extension for this image e.g. `'png'`.
+
+        The returned extension is all lowercase and is the canonical extension for the content type
+        of this image, regardless of what extension may have been used in its filename, if any.
+        """
+        ext_map = {
+            "BMP": "bmp",
+            "GIF": "gif",
+            "JPEG": "jpg",
+            "PNG": "png",
+            "TIFF": "tiff",
+            "WMF": "wmf",
+        }
+        format = self._format
+        if format not in ext_map:
+            tmpl = "unsupported image format, expected one of: %s, got '%s'"
+            raise ValueError(tmpl % (ext_map.keys(), format))
+        return ext_map[format]
+
+    @property
+    def filename(self) -> str | None:
+        """Filename from path used to load this image, if loaded from the filesystem.
+
+        |None| if no filename was used in loading, such as when loaded from an in-memory stream.
+        """
+        return self._filename
+
+    @lazyproperty
+    def sha1(self) -> str:
+        """SHA1 hash digest of the image blob."""
+        return hashlib.sha1(self._blob).hexdigest()
+
+    @lazyproperty
+    def size(self) -> tuple[int, int]:
+        """A (width, height) 2-tuple specifying the dimensions of this image in pixels."""
+        return self._pil_props[1]
+
+    @property
+    def _format(self) -> str | None:
+        """The PIL Image format of this image, e.g. 'PNG'."""
+        return self._pil_props[0]
+
+    @lazyproperty
+    def _pil_props(self) -> tuple[str | None, tuple[int, int], tuple[int, int] | None]:
+        """tuple of image properties extracted from this image using Pillow."""
+        stream = io.BytesIO(self._blob)
+        pil_image = PIL_Image.open(stream)  # pyright: ignore[reportUnknownMemberType]
+        format = pil_image.format
+        width_px, height_px = pil_image.size
+        dpi = cast(
+            "tuple[int, int] | None",
+            pil_image.info.get("dpi"),  # pyright: ignore[reportUnknownMemberType]
+        )
+        stream.close()
+        return (format, (width_px, height_px), dpi)
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/media.py b/.venv/lib/python3.12/site-packages/pptx/parts/media.py
new file mode 100644
index 00000000..7e8bc2f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/media.py
@@ -0,0 +1,37 @@
+"""MediaPart and related objects."""
+
+from __future__ import annotations
+
+import hashlib
+
+from pptx.opc.package import Part
+from pptx.util import lazyproperty
+
+
+class MediaPart(Part):
+    """A media part, containing an audio or video resource.
+
+    A media part generally has a partname matching the regex
+    `ppt/media/media[1-9][0-9]*.*`.
+    """
+
+    @classmethod
+    def new(cls, package, media):
+        """Return new |MediaPart| instance containing `media`.
+
+        `media` must be a |Media| object.
+        """
+        return cls(
+            package.next_media_partname(media.ext),
+            media.content_type,
+            package,
+            media.blob,
+        )
+
+    @lazyproperty
+    def sha1(self):
+        """The SHA1 hash digest for the media binary of this media part.
+
+        Example: `'1be010ea47803b00e140b852765cdf84f491da47'`
+        """
+        return hashlib.sha1(self._blob).hexdigest()
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py b/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py
new file mode 100644
index 00000000..1413de45
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py
@@ -0,0 +1,126 @@
+"""Presentation part, the main part in a .pptx package."""
+
+from __future__ import annotations
+
+from typing import IO, TYPE_CHECKING, Iterable
+
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.parts.slide import NotesMasterPart, SlidePart
+from pptx.presentation import Presentation
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.parts.coreprops import CorePropertiesPart
+    from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster
+
+
+class PresentationPart(XmlPart):
+    """Top level class in object model.
+
+    Represents the contents of the /ppt directory of a .pptx file.
+    """
+
+    def add_slide(self, slide_layout: SlideLayout):
+        """Return (rId, slide) pair of a newly created blank slide.
+
+        New slide inherits appearance from `slide_layout`.
+        """
+        partname = self._next_slide_partname
+        slide_layout_part = slide_layout.part
+        slide_part = SlidePart.new(partname, self.package, slide_layout_part)
+        rId = self.relate_to(slide_part, RT.SLIDE)
+        return rId, slide_part.slide
+
+    @property
+    def core_properties(self) -> CorePropertiesPart:
+        """A |CoreProperties| object for the presentation.
+
+        Provides read/write access to the Dublin Core properties of this presentation.
+        """
+        return self.package.core_properties
+
+    def get_slide(self, slide_id: int) -> Slide | None:
+        """Return optional related |Slide| object identified by `slide_id`.
+
+        Returns |None| if no slide with `slide_id` is related to this presentation.
+        """
+        for sldId in self._element.sldIdLst:
+            if sldId.id == slide_id:
+                return self.related_part(sldId.rId).slide
+        return None
+
+    @lazyproperty
+    def notes_master(self) -> NotesMaster:
+        """
+        Return the |NotesMaster| object for this presentation. If the
+        presentation does not have a notes master, one is created from
+        a default template. The same single instance is returned on each
+        call.
+        """
+        return self.notes_master_part.notes_master
+
+    @lazyproperty
+    def notes_master_part(self) -> NotesMasterPart:
+        """Return the |NotesMasterPart| object for this presentation.
+
+        If the presentation does not have a notes master, one is created from a default template.
+        The same single instance is returned on each call.
+        """
+        try:
+            return self.part_related_by(RT.NOTES_MASTER)
+        except KeyError:
+            notes_master_part = NotesMasterPart.create_default(self.package)
+            self.relate_to(notes_master_part, RT.NOTES_MASTER)
+            return notes_master_part
+
+    @lazyproperty
+    def presentation(self):
+        """
+        A |Presentation| object providing access to the content of this
+        presentation.
+        """
+        return Presentation(self._element, self)
+
+    def related_slide(self, rId: str) -> Slide:
+        """Return |Slide| object for related |SlidePart| related by `rId`."""
+        return self.related_part(rId).slide
+
+    def related_slide_master(self, rId: str) -> SlideMaster:
+        """Return |SlideMaster| object for |SlideMasterPart| related by `rId`."""
+        return self.related_part(rId).slide_master
+
+    def rename_slide_parts(self, rIds: Iterable[str]):
+        """Assign incrementing partnames to the slide parts identified by `rIds`.
+
+        Partnames are like `/ppt/slides/slide9.xml` and are assigned in the order their id appears
+        in the `rIds` sequence. The name portion is always `slide`. The number part forms a
+        continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). The extension is always
+        `.xml`.
+        """
+        for idx, rId in enumerate(rIds):
+            slide_part = self.related_part(rId)
+            slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1))
+
+    def save(self, path_or_stream: str | IO[bytes]):
+        """Save this presentation package to `path_or_stream`.
+
+        `path_or_stream` can be either a path to a filesystem location (a string) or a
+        file-like object.
+        """
+        self.package.save(path_or_stream)
+
+    def slide_id(self, slide_part):
+        """Return the slide-id associated with `slide_part`."""
+        for sldId in self._element.sldIdLst:
+            if self.related_part(sldId.rId) is slide_part:
+                return sldId.id
+        raise ValueError("matching slide_part not found")
+
+    @property
+    def _next_slide_partname(self):
+        """Return |PackURI| instance containing next available slide partname."""
+        sldIdLst = self._element.get_or_add_sldIdLst()
+        partname_str = "/ppt/slides/slide%d.xml" % (len(sldIdLst) + 1)
+        return PackURI(partname_str)
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/slide.py b/.venv/lib/python3.12/site-packages/pptx/parts/slide.py
new file mode 100644
index 00000000..6650564a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/slide.py
@@ -0,0 +1,297 @@
+"""Slide and related objects."""
+
+from __future__ import annotations
+
+from typing import IO, TYPE_CHECKING, cast
+
+from pptx.enum.shapes import PROG_ID
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide
+from pptx.oxml.theme import CT_OfficeStyleSheet
+from pptx.parts.chart import ChartPart
+from pptx.parts.embeddedpackage import EmbeddedPackagePart
+from pptx.slide import NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.chart.data import ChartData
+    from pptx.enum.chart import XL_CHART_TYPE
+    from pptx.media import Video
+    from pptx.parts.image import Image, ImagePart
+
+
+class BaseSlidePart(XmlPart):
+    """Base class for slide parts.
+
+    This includes slide, slide-layout, and slide-master parts, but also notes-slide,
+    notes-master, and handout-master parts.
+    """
+
+    _element: CT_Slide
+
+    def get_image(self, rId: str) -> Image:
+        """Return an |Image| object containing the image related to this slide by *rId*.
+
+        Raises |KeyError| if no image is related by that id, which would generally indicate a
+        corrupted .pptx file.
+        """
+        return cast("ImagePart", self.related_part(rId)).image
+
+    def get_or_add_image_part(self, image_file: str | IO[bytes]):
+        """Return `(image_part, rId)` pair corresponding to `image_file`.
+
+        The returned |ImagePart| object contains the image in `image_file` and is
+        related to this slide with the key `rId`. If either the image part or
+        relationship already exists, they are reused, otherwise they are newly created.
+        """
+        image_part = self._package.get_or_add_image_part(image_file)
+        rId = self.relate_to(image_part, RT.IMAGE)
+        return image_part, rId
+
+    @property
+    def name(self) -> str:
+        """Internal name of this slide."""
+        return self._element.cSld.name
+
+
+class NotesMasterPart(BaseSlidePart):
+    """Notes master part.
+
+    Corresponds to package file `ppt/notesMasters/notesMaster1.xml`.
+    """
+
+    @classmethod
+    def create_default(cls, package):
+        """
+        Create and return a default notes master part, including creating the
+        new theme it requires.
+        """
+        notes_master_part = cls._new(package)
+        theme_part = cls._new_theme_part(package)
+        notes_master_part.relate_to(theme_part, RT.THEME)
+        return notes_master_part
+
+    @lazyproperty
+    def notes_master(self):
+        """
+        Return the |NotesMaster| object that proxies this notes master part.
+        """
+        return NotesMaster(self._element, self)
+
+    @classmethod
+    def _new(cls, package):
+        """
+        Create and return a standalone, default notes master part based on
+        the built-in template (without any related parts, such as theme).
+        """
+        return NotesMasterPart(
+            PackURI("/ppt/notesMasters/notesMaster1.xml"),
+            CT.PML_NOTES_MASTER,
+            package,
+            CT_NotesMaster.new_default(),
+        )
+
+    @classmethod
+    def _new_theme_part(cls, package):
+        """Return new default theme-part suitable for use with a notes master."""
+        return XmlPart(
+            package.next_partname("/ppt/theme/theme%d.xml"),
+            CT.OFC_THEME,
+            package,
+            CT_OfficeStyleSheet.new_default(),
+        )
+
+
+class NotesSlidePart(BaseSlidePart):
+    """Notes slide part.
+
+    Contains the slide notes content and the layout for the slide handout page.
+    Corresponds to package file `ppt/notesSlides/notesSlide[1-9][0-9]*.xml`.
+    """
+
+    @classmethod
+    def new(cls, package, slide_part):
+        """Return new |NotesSlidePart| for the slide in `slide_part`.
+
+        The new notes-slide part is based on the (singleton) notes master and related to
+        both the notes-master part and `slide_part`. If no notes-master is present,
+        one is created based on the default template.
+        """
+        notes_master_part = package.presentation_part.notes_master_part
+        notes_slide_part = cls._add_notes_slide_part(package, slide_part, notes_master_part)
+        notes_slide = notes_slide_part.notes_slide
+        notes_slide.clone_master_placeholders(notes_master_part.notes_master)
+        return notes_slide_part
+
+    @lazyproperty
+    def notes_master(self):
+        """Return the |NotesMaster| object this notes slide inherits from."""
+        notes_master_part = self.part_related_by(RT.NOTES_MASTER)
+        return notes_master_part.notes_master
+
+    @lazyproperty
+    def notes_slide(self):
+        """Return the |NotesSlide| object that proxies this notes slide part."""
+        return NotesSlide(self._element, self)
+
+    @classmethod
+    def _add_notes_slide_part(cls, package, slide_part, notes_master_part):
+        """Create and return a new notes-slide part.
+
+        The return part is fully related, but has no shape content (i.e. placeholders
+        not cloned).
+        """
+        notes_slide_part = NotesSlidePart(
+            package.next_partname("/ppt/notesSlides/notesSlide%d.xml"),
+            CT.PML_NOTES_SLIDE,
+            package,
+            CT_NotesSlide.new(),
+        )
+        notes_slide_part.relate_to(notes_master_part, RT.NOTES_MASTER)
+        notes_slide_part.relate_to(slide_part, RT.SLIDE)
+        return notes_slide_part
+
+
+class SlidePart(BaseSlidePart):
+    """Slide part. Corresponds to package files ppt/slides/slide[1-9][0-9]*.xml."""
+
+    @classmethod
+    def new(cls, partname, package, slide_layout_part):
+        """Return newly-created blank slide part.
+
+        The new slide-part has `partname` and a relationship to `slide_layout_part`.
+        """
+        slide_part = cls(partname, CT.PML_SLIDE, package, CT_Slide.new())
+        slide_part.relate_to(slide_layout_part, RT.SLIDE_LAYOUT)
+        return slide_part
+
+    def add_chart_part(self, chart_type: XL_CHART_TYPE, chart_data: ChartData):
+        """Return str rId of new |ChartPart| object containing chart of `chart_type`.
+
+        The chart depicts `chart_data` and is related to the slide contained in this
+        part by `rId`.
+        """
+        return self.relate_to(ChartPart.new(chart_type, chart_data, self._package), RT.CHART)
+
+    def add_embedded_ole_object_part(
+        self, prog_id: PROG_ID | str, ole_object_file: str | IO[bytes]
+    ):
+        """Return rId of newly-added OLE-object part formed from `ole_object_file`."""
+        relationship_type = RT.PACKAGE if isinstance(prog_id, PROG_ID) else RT.OLE_OBJECT
+        return self.relate_to(
+            EmbeddedPackagePart.factory(
+                prog_id, self._blob_from_file(ole_object_file), self._package
+            ),
+            relationship_type,
+        )
+
+    def get_or_add_video_media_part(self, video: Video) -> tuple[str, str]:
+        """Return rIds for media and video relationships to media part.
+
+        A new |MediaPart| object is created if it does not already exist
+        (such as would occur if the same video appeared more than once in
+         a presentation). Two relationships to the media part are created,
+        one each with MEDIA and VIDEO relationship types. The need for two
+        appears to be for legacy support for an earlier (pre-Office 2010)
+        PowerPoint media embedding strategy.
+        """
+        media_part = self._package.get_or_add_media_part(video)
+        media_rId = self.relate_to(media_part, RT.MEDIA)
+        video_rId = self.relate_to(media_part, RT.VIDEO)
+        return media_rId, video_rId
+
+    @property
+    def has_notes_slide(self):
+        """
+        Return True if this slide has a notes slide, False otherwise. A notes
+        slide is created by the :attr:`notes_slide` property when one doesn't
+        exist; use this property to test for a notes slide without the
+        possible side-effect of creating one.
+        """
+        try:
+            self.part_related_by(RT.NOTES_SLIDE)
+        except KeyError:
+            return False
+        return True
+
+    @lazyproperty
+    def notes_slide(self) -> NotesSlide:
+        """The |NotesSlide| instance associated with this slide.
+
+        If the slide does not have a notes slide, a new one is created. The same single instance
+        is returned on each call.
+        """
+        try:
+            notes_slide_part = self.part_related_by(RT.NOTES_SLIDE)
+        except KeyError:
+            notes_slide_part = self._add_notes_slide_part()
+        return notes_slide_part.notes_slide
+
+    @lazyproperty
+    def slide(self):
+        """
+        The |Slide| object representing this slide part.
+        """
+        return Slide(self._element, self)
+
+    @property
+    def slide_id(self) -> int:
+        """Return the slide identifier stored in the presentation part for this slide part."""
+        presentation_part = self.package.presentation_part
+        return presentation_part.slide_id(self)
+
+    @property
+    def slide_layout(self) -> SlideLayout:
+        """|SlideLayout| object the slide in this part inherits appearance from."""
+        slide_layout_part = self.part_related_by(RT.SLIDE_LAYOUT)
+        return slide_layout_part.slide_layout
+
+    def _add_notes_slide_part(self):
+        """
+        Return a newly created |NotesSlidePart| object related to this slide
+        part. Caller is responsible for ensuring this slide doesn't already
+        have a notes slide part.
+        """
+        notes_slide_part = NotesSlidePart.new(self.package, self)
+        self.relate_to(notes_slide_part, RT.NOTES_SLIDE)
+        return notes_slide_part
+
+
+class SlideLayoutPart(BaseSlidePart):
+    """Slide layout part.
+
+    Corresponds to package files ``ppt/slideLayouts/slideLayout[1-9][0-9]*.xml``.
+    """
+
+    @lazyproperty
+    def slide_layout(self):
+        """
+        The |SlideLayout| object representing this part.
+        """
+        return SlideLayout(self._element, self)
+
+    @property
+    def slide_master(self) -> SlideMaster:
+        """Slide master from which this slide layout inherits properties."""
+        return self.part_related_by(RT.SLIDE_MASTER).slide_master
+
+
+class SlideMasterPart(BaseSlidePart):
+    """Slide master part.
+
+    Corresponds to package files ppt/slideMasters/slideMaster[1-9][0-9]*.xml.
+    """
+
+    def related_slide_layout(self, rId: str) -> SlideLayout:
+        """Return |SlideLayout| related to this slide-master by key `rId`."""
+        return self.related_part(rId).slide_layout
+
+    @lazyproperty
+    def slide_master(self):
+        """
+        The |SlideMaster| object representing this part.
+        """
+        return SlideMaster(self._element, self)