init: 폴더구조 설계 및 인프라 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:31:53 +09:00
parent 34ad612281
commit f9c463f87a
1839 changed files with 323096 additions and 1 deletions
@@ -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
@@ -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(),
)
@@ -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
@@ -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)
@@ -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()
@@ -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)
@@ -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)