init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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)
|
||||
Reference in New Issue
Block a user