python-pptx2 2.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pptx2/__init__.py +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/parts/image.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""ImagePart and related objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import io
|
|
7
|
+
import os
|
|
8
|
+
from typing import IO, TYPE_CHECKING, Any, cast
|
|
9
|
+
|
|
10
|
+
from PIL import Image as PIL_Image
|
|
11
|
+
|
|
12
|
+
from pptx2.opc.package import Part
|
|
13
|
+
from pptx2.opc.spec import image_content_types
|
|
14
|
+
from pptx2.util import Emu, lazyproperty
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pptx2.opc.packuri import PackURI
|
|
18
|
+
from pptx2.package import Package
|
|
19
|
+
from pptx2.util import Length
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ImagePart(Part):
|
|
23
|
+
"""An image part.
|
|
24
|
+
|
|
25
|
+
An image part generally has a partname matching the regex `ppt/media/image[1-9][0-9]*.*`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
partname: PackURI,
|
|
31
|
+
content_type: str,
|
|
32
|
+
package: Package,
|
|
33
|
+
blob: bytes,
|
|
34
|
+
filename: str | None = None,
|
|
35
|
+
):
|
|
36
|
+
super(ImagePart, self).__init__(partname, content_type, package, blob)
|
|
37
|
+
self._blob = blob
|
|
38
|
+
self._filename = filename
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def new(cls, package: Package, image: Image) -> ImagePart:
|
|
42
|
+
"""Return new |ImagePart| instance containing `image`.
|
|
43
|
+
|
|
44
|
+
`image` is an |Image| object.
|
|
45
|
+
"""
|
|
46
|
+
return cls(
|
|
47
|
+
package.next_image_partname(image.ext),
|
|
48
|
+
image.content_type,
|
|
49
|
+
package,
|
|
50
|
+
image.blob,
|
|
51
|
+
image.filename,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def desc(self) -> str:
|
|
56
|
+
"""The filename associated with this image.
|
|
57
|
+
|
|
58
|
+
Either the filename of the original image or a generic name of the form `image.ext` where
|
|
59
|
+
`ext` is appropriate to the image file format, e.g. `'jpg'`. An image created using a path
|
|
60
|
+
will have that filename; one created with a file-like object will have a generic name.
|
|
61
|
+
"""
|
|
62
|
+
# -- return generic filename if original filename is unknown --
|
|
63
|
+
if self._filename is None:
|
|
64
|
+
return f"image.{self.ext}"
|
|
65
|
+
return self._filename
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def ext(self) -> str:
|
|
69
|
+
"""File-name extension for this image e.g. `'png'`."""
|
|
70
|
+
return self.partname.ext
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def image(self) -> Image:
|
|
74
|
+
"""An |Image| object containing the image in this image part.
|
|
75
|
+
|
|
76
|
+
Note this is a `pptx2.image.Image` object, not a PIL Image.
|
|
77
|
+
"""
|
|
78
|
+
return Image(self._blob, self.desc)
|
|
79
|
+
|
|
80
|
+
def scale(self, scaled_cx: int | None, scaled_cy: int | None) -> tuple[int, int]:
|
|
81
|
+
"""Return scaled image dimensions in EMU based on the combination of parameters supplied.
|
|
82
|
+
|
|
83
|
+
If `scaled_cx` and `scaled_cy` are both |None|, the native image size is returned. If
|
|
84
|
+
neither `scaled_cx` nor `scaled_cy` is |None|, their values are returned unchanged. If a
|
|
85
|
+
value is provided for either `scaled_cx` or `scaled_cy` and the other is |None|, the
|
|
86
|
+
missing value is calculated such that the image's aspect ratio is preserved.
|
|
87
|
+
"""
|
|
88
|
+
image_cx, image_cy = self._native_size
|
|
89
|
+
|
|
90
|
+
if scaled_cx and scaled_cy:
|
|
91
|
+
return scaled_cx, scaled_cy
|
|
92
|
+
|
|
93
|
+
if scaled_cx and not scaled_cy:
|
|
94
|
+
scaling_factor = float(scaled_cx) / float(image_cx)
|
|
95
|
+
scaled_cy = int(round(image_cy * scaling_factor))
|
|
96
|
+
return scaled_cx, scaled_cy
|
|
97
|
+
|
|
98
|
+
if not scaled_cx and scaled_cy:
|
|
99
|
+
scaling_factor = float(scaled_cy) / float(image_cy)
|
|
100
|
+
scaled_cx = int(round(image_cx * scaling_factor))
|
|
101
|
+
return scaled_cx, scaled_cy
|
|
102
|
+
|
|
103
|
+
# -- only remaining case is both `scaled_cx` and `scaled_cy` are `None` --
|
|
104
|
+
return image_cx, image_cy
|
|
105
|
+
|
|
106
|
+
@lazyproperty
|
|
107
|
+
def sha1(self) -> str:
|
|
108
|
+
"""The 40-character SHA1 hash digest for the image binary of this image part.
|
|
109
|
+
|
|
110
|
+
like: `"1be010ea47803b00e140b852765cdf84f491da47"`.
|
|
111
|
+
"""
|
|
112
|
+
return hashlib.sha1(self._blob).hexdigest()
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def _dpi(self) -> tuple[int, int]:
|
|
116
|
+
"""(horz_dpi, vert_dpi) pair representing the dots-per-inch resolution of this image."""
|
|
117
|
+
image = Image.from_blob(self._blob)
|
|
118
|
+
return image.dpi
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def _native_size(self) -> tuple[Length, Length]:
|
|
122
|
+
"""A (width, height) 2-tuple representing the native dimensions of the image in EMU.
|
|
123
|
+
|
|
124
|
+
Calculated based on the image DPI value, if present, assuming 72 dpi as a default.
|
|
125
|
+
"""
|
|
126
|
+
EMU_PER_INCH = 914400
|
|
127
|
+
horz_dpi, vert_dpi = self._dpi
|
|
128
|
+
width_px, height_px = self._px_size
|
|
129
|
+
|
|
130
|
+
width = EMU_PER_INCH * width_px / horz_dpi
|
|
131
|
+
height = EMU_PER_INCH * height_px / vert_dpi
|
|
132
|
+
|
|
133
|
+
return Emu(int(width)), Emu(int(height))
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def _px_size(self) -> tuple[int, int]:
|
|
137
|
+
"""A (width, height) 2-tuple representing the dimensions of this image in pixels."""
|
|
138
|
+
image = Image.from_blob(self._blob)
|
|
139
|
+
return image.size
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Image(object):
|
|
143
|
+
"""Immutable value object representing an image such as a JPEG, PNG, or GIF."""
|
|
144
|
+
|
|
145
|
+
def __init__(self, blob: bytes, filename: str | None):
|
|
146
|
+
super(Image, self).__init__()
|
|
147
|
+
self._blob = blob
|
|
148
|
+
self._filename = filename
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_blob(cls, blob: bytes, filename: str | None = None) -> Image:
|
|
152
|
+
"""Return a new |Image| object loaded from the image binary in `blob`."""
|
|
153
|
+
return cls(blob, filename)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_file(cls, image_file: str | IO[bytes]) -> Image:
|
|
157
|
+
"""Return a new |Image| object loaded from `image_file`.
|
|
158
|
+
|
|
159
|
+
`image_file` can be either a path (str) or a file-like object.
|
|
160
|
+
"""
|
|
161
|
+
if isinstance(image_file, str):
|
|
162
|
+
# treat image_file as a path
|
|
163
|
+
with open(image_file, "rb") as f:
|
|
164
|
+
blob = f.read()
|
|
165
|
+
filename = os.path.basename(image_file)
|
|
166
|
+
else:
|
|
167
|
+
# assume image_file is a file-like object
|
|
168
|
+
# ---reposition file cursor if it has one---
|
|
169
|
+
if callable(getattr(image_file, "seek")):
|
|
170
|
+
image_file.seek(0)
|
|
171
|
+
blob = image_file.read()
|
|
172
|
+
filename = None
|
|
173
|
+
|
|
174
|
+
return cls.from_blob(blob, filename)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def blob(self) -> bytes:
|
|
178
|
+
"""The binary image bytestream of this image."""
|
|
179
|
+
return self._blob
|
|
180
|
+
|
|
181
|
+
@lazyproperty
|
|
182
|
+
def content_type(self) -> str:
|
|
183
|
+
"""MIME-type of this image, e.g. `"image/jpeg"`."""
|
|
184
|
+
return image_content_types[self.ext]
|
|
185
|
+
|
|
186
|
+
@lazyproperty
|
|
187
|
+
def dpi(self) -> tuple[int, int]:
|
|
188
|
+
"""A (horz_dpi, vert_dpi) 2-tuple specifying the dots-per-inch resolution of this image.
|
|
189
|
+
|
|
190
|
+
A default value of (72, 72) is used if the dpi is not specified in the image file.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def int_dpi(dpi: Any):
|
|
194
|
+
"""Return an integer dots-per-inch value corresponding to `dpi`.
|
|
195
|
+
|
|
196
|
+
If `dpi` is |None|, a non-numeric type, less than 1 or greater than 2048, 72 is
|
|
197
|
+
returned.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
int_dpi = int(round(float(dpi)))
|
|
201
|
+
if int_dpi < 1 or int_dpi > 2048:
|
|
202
|
+
int_dpi = 72
|
|
203
|
+
except (TypeError, ValueError):
|
|
204
|
+
int_dpi = 72
|
|
205
|
+
return int_dpi
|
|
206
|
+
|
|
207
|
+
def normalize_pil_dpi(pil_dpi: tuple[int, int] | None):
|
|
208
|
+
"""Return a (horz_dpi, vert_dpi) 2-tuple corresponding to `pil_dpi`.
|
|
209
|
+
|
|
210
|
+
The value for the 'dpi' key in the `info` dict of a PIL image. If the 'dpi' key is not
|
|
211
|
+
present or contains an invalid value, `(72, 72)` is returned.
|
|
212
|
+
"""
|
|
213
|
+
if isinstance(pil_dpi, tuple):
|
|
214
|
+
return (int_dpi(pil_dpi[0]), int_dpi(pil_dpi[1]))
|
|
215
|
+
return (72, 72)
|
|
216
|
+
|
|
217
|
+
return normalize_pil_dpi(self._pil_props[2])
|
|
218
|
+
|
|
219
|
+
@lazyproperty
|
|
220
|
+
def ext(self) -> str:
|
|
221
|
+
"""Canonical file extension for this image e.g. `'png'`.
|
|
222
|
+
|
|
223
|
+
The returned extension is all lowercase and is the canonical extension for the content type
|
|
224
|
+
of this image, regardless of what extension may have been used in its filename, if any.
|
|
225
|
+
"""
|
|
226
|
+
ext_map = {
|
|
227
|
+
"BMP": "bmp",
|
|
228
|
+
"GIF": "gif",
|
|
229
|
+
"JPEG": "jpg",
|
|
230
|
+
"PNG": "png",
|
|
231
|
+
"TIFF": "tiff",
|
|
232
|
+
"WMF": "wmf",
|
|
233
|
+
}
|
|
234
|
+
format = self._format
|
|
235
|
+
if format not in ext_map:
|
|
236
|
+
tmpl = "unsupported image format, expected one of: %s, got '%s'"
|
|
237
|
+
raise ValueError(tmpl % (ext_map.keys(), format))
|
|
238
|
+
return ext_map[format]
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def filename(self) -> str | None:
|
|
242
|
+
"""Filename from path used to load this image, if loaded from the filesystem.
|
|
243
|
+
|
|
244
|
+
|None| if no filename was used in loading, such as when loaded from an in-memory stream.
|
|
245
|
+
"""
|
|
246
|
+
return self._filename
|
|
247
|
+
|
|
248
|
+
@lazyproperty
|
|
249
|
+
def sha1(self) -> str:
|
|
250
|
+
"""SHA1 hash digest of the image blob."""
|
|
251
|
+
return hashlib.sha1(self._blob).hexdigest()
|
|
252
|
+
|
|
253
|
+
@lazyproperty
|
|
254
|
+
def size(self) -> tuple[int, int]:
|
|
255
|
+
"""A (width, height) 2-tuple specifying the dimensions of this image in pixels."""
|
|
256
|
+
return self._pil_props[1]
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def _format(self) -> str | None:
|
|
260
|
+
"""The PIL Image format of this image, e.g. 'PNG'."""
|
|
261
|
+
return self._pil_props[0]
|
|
262
|
+
|
|
263
|
+
@lazyproperty
|
|
264
|
+
def _pil_props(self) -> tuple[str | None, tuple[int, int], tuple[int, int] | None]:
|
|
265
|
+
"""tuple of image properties extracted from this image using Pillow."""
|
|
266
|
+
stream = io.BytesIO(self._blob)
|
|
267
|
+
pil_image = PIL_Image.open(stream) # pyright: ignore[reportUnknownMemberType]
|
|
268
|
+
format = pil_image.format
|
|
269
|
+
width_px, height_px = pil_image.size
|
|
270
|
+
dpi = cast(
|
|
271
|
+
"tuple[int, int] | None",
|
|
272
|
+
pil_image.info.get("dpi"), # pyright: ignore[reportUnknownMemberType]
|
|
273
|
+
)
|
|
274
|
+
stream.close()
|
|
275
|
+
return (format, (width_px, height_px), dpi)
|
pptx2/parts/media.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""MediaPart and related objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from pptx2.opc.package import Part
|
|
8
|
+
from pptx2.util import lazyproperty
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MediaPart(Part):
|
|
12
|
+
"""A media part, containing an audio or video resource.
|
|
13
|
+
|
|
14
|
+
A media part generally has a partname matching the regex
|
|
15
|
+
`ppt/media/media[1-9][0-9]*.*`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def new(cls, package, media):
|
|
20
|
+
"""Return new |MediaPart| instance containing `media`.
|
|
21
|
+
|
|
22
|
+
`media` must be a |Media| object.
|
|
23
|
+
"""
|
|
24
|
+
return cls(
|
|
25
|
+
package.next_media_partname(media.ext),
|
|
26
|
+
media.content_type,
|
|
27
|
+
package,
|
|
28
|
+
media.blob,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@lazyproperty
|
|
32
|
+
def sha1(self):
|
|
33
|
+
"""The SHA1 hash digest for the media binary of this media part.
|
|
34
|
+
|
|
35
|
+
Example: `'1be010ea47803b00e140b852765cdf84f491da47'`
|
|
36
|
+
"""
|
|
37
|
+
return hashlib.sha1(self._blob).hexdigest()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Presentation part, the main part in a .pptx package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import IO, TYPE_CHECKING, Iterable
|
|
6
|
+
|
|
7
|
+
from pptx2.opc.constants import RELATIONSHIP_TYPE as RT
|
|
8
|
+
from pptx2.opc.package import XmlPart
|
|
9
|
+
from pptx2.opc.packuri import PackURI
|
|
10
|
+
from pptx2.parts.slide import NotesMasterPart, SlidePart
|
|
11
|
+
from pptx2.presentation import Presentation
|
|
12
|
+
from pptx2.util import lazyproperty
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pptx2.parts.coreprops import CorePropertiesPart
|
|
16
|
+
from pptx2.slide import NotesMaster, Slide, SlideLayout, SlideMaster
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PresentationPart(XmlPart):
|
|
20
|
+
"""Top level class in object model.
|
|
21
|
+
|
|
22
|
+
Represents the contents of the /ppt directory of a .pptx file.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def add_slide(self, slide_layout: SlideLayout):
|
|
26
|
+
"""Return (rId, slide) pair of a newly created blank slide.
|
|
27
|
+
|
|
28
|
+
New slide inherits appearance from `slide_layout`.
|
|
29
|
+
"""
|
|
30
|
+
partname = self._next_slide_partname
|
|
31
|
+
slide_layout_part = slide_layout.part
|
|
32
|
+
slide_part = SlidePart.new(partname, self.package, slide_layout_part)
|
|
33
|
+
rId = self.relate_to(slide_part, RT.SLIDE)
|
|
34
|
+
return rId, slide_part.slide
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def core_properties(self) -> CorePropertiesPart:
|
|
38
|
+
"""A |CoreProperties| object for the presentation.
|
|
39
|
+
|
|
40
|
+
Provides read/write access to the Dublin Core properties of this presentation.
|
|
41
|
+
"""
|
|
42
|
+
return self.package.core_properties
|
|
43
|
+
|
|
44
|
+
def get_slide(self, slide_id: int) -> Slide | None:
|
|
45
|
+
"""Return optional related |Slide| object identified by `slide_id`.
|
|
46
|
+
|
|
47
|
+
Returns |None| if no slide with `slide_id` is related to this presentation.
|
|
48
|
+
"""
|
|
49
|
+
for sldId in self._element.sldIdLst:
|
|
50
|
+
if sldId.id == slide_id:
|
|
51
|
+
return self.related_part(sldId.rId).slide
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@lazyproperty
|
|
55
|
+
def notes_master(self) -> NotesMaster:
|
|
56
|
+
"""
|
|
57
|
+
Return the |NotesMaster| object for this presentation. If the
|
|
58
|
+
presentation does not have a notes master, one is created from
|
|
59
|
+
a default template. The same single instance is returned on each
|
|
60
|
+
call.
|
|
61
|
+
"""
|
|
62
|
+
return self.notes_master_part.notes_master
|
|
63
|
+
|
|
64
|
+
@lazyproperty
|
|
65
|
+
def notes_master_part(self) -> NotesMasterPart:
|
|
66
|
+
"""Return the |NotesMasterPart| object for this presentation.
|
|
67
|
+
|
|
68
|
+
If the presentation does not have a notes master, one is created from a default template.
|
|
69
|
+
The same single instance is returned on each call.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
return self.part_related_by(RT.NOTES_MASTER)
|
|
73
|
+
except KeyError:
|
|
74
|
+
notes_master_part = NotesMasterPart.create_default(self.package)
|
|
75
|
+
self.relate_to(notes_master_part, RT.NOTES_MASTER)
|
|
76
|
+
return notes_master_part
|
|
77
|
+
|
|
78
|
+
@lazyproperty
|
|
79
|
+
def presentation(self):
|
|
80
|
+
"""
|
|
81
|
+
A |Presentation| object providing access to the content of this
|
|
82
|
+
presentation.
|
|
83
|
+
"""
|
|
84
|
+
return Presentation(self._element, self)
|
|
85
|
+
|
|
86
|
+
def related_slide(self, rId: str) -> Slide:
|
|
87
|
+
"""Return |Slide| object for related |SlidePart| related by `rId`."""
|
|
88
|
+
return self.related_part(rId).slide
|
|
89
|
+
|
|
90
|
+
def related_slide_master(self, rId: str) -> SlideMaster:
|
|
91
|
+
"""Return |SlideMaster| object for |SlideMasterPart| related by `rId`."""
|
|
92
|
+
return self.related_part(rId).slide_master
|
|
93
|
+
|
|
94
|
+
def rename_slide_parts(self, rIds: Iterable[str]):
|
|
95
|
+
"""Assign incrementing partnames to the slide parts identified by `rIds`.
|
|
96
|
+
|
|
97
|
+
Partnames are like `/ppt/slides/slide9.xml` and are assigned in the order their id appears
|
|
98
|
+
in the `rIds` sequence. The name portion is always `slide`. The number part forms a
|
|
99
|
+
continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). The extension is always
|
|
100
|
+
`.xml`.
|
|
101
|
+
"""
|
|
102
|
+
for idx, rId in enumerate(rIds):
|
|
103
|
+
slide_part = self.related_part(rId)
|
|
104
|
+
slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1))
|
|
105
|
+
|
|
106
|
+
def save(self, path_or_stream: str | IO[bytes]):
|
|
107
|
+
"""Save this presentation package to `path_or_stream`.
|
|
108
|
+
|
|
109
|
+
`path_or_stream` can be either a path to a filesystem location (a string) or a
|
|
110
|
+
file-like object.
|
|
111
|
+
"""
|
|
112
|
+
self.package.save(path_or_stream)
|
|
113
|
+
|
|
114
|
+
def slide_id(self, slide_part):
|
|
115
|
+
"""Return the slide-id associated with `slide_part`."""
|
|
116
|
+
for sldId in self._element.sldIdLst:
|
|
117
|
+
if self.related_part(sldId.rId) is slide_part:
|
|
118
|
+
return sldId.id
|
|
119
|
+
raise ValueError("matching slide_part not found")
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def _next_slide_partname(self):
|
|
123
|
+
"""Return |PackURI| instance containing next available slide partname.
|
|
124
|
+
|
|
125
|
+
Scans the actual partnames in the package rather than counting
|
|
126
|
+
`p:sldIdLst` entries — the package can hold slide parts beyond those
|
|
127
|
+
registered in the id list (e.g. after a cross-deck import), and
|
|
128
|
+
writing two different parts under one partname corrupts the package.
|
|
129
|
+
"""
|
|
130
|
+
existing = {part.partname for part in self.package.iter_parts()}
|
|
131
|
+
n = len(self._element.get_or_add_sldIdLst()) + 1
|
|
132
|
+
while True:
|
|
133
|
+
candidate = PackURI("/ppt/slides/slide%d.xml" % n)
|
|
134
|
+
if candidate not in existing:
|
|
135
|
+
return candidate
|
|
136
|
+
n += 1
|