diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/parts')
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) |
