python-pptx2 2.14.0__py3-none-any.whl → 2.16.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 +4 -1
- pptx2/_slide_importer.py +35 -21
- pptx2/api.py +11 -5
- pptx2/audit.py +14 -2
- pptx2/chart/data.py +17 -0
- pptx2/chart/plot.py +19 -0
- pptx2/chart/xmlwriter.py +11 -2
- pptx2/enum/text.py +72 -0
- pptx2/opc/package.py +12 -11
- pptx2/opc/serialized.py +23 -14
- pptx2/oxml/__init__.py +6 -0
- pptx2/oxml/chart/plot.py +13 -0
- pptx2/oxml/customprops.py +170 -0
- pptx2/oxml/ns.py +2 -0
- pptx2/oxml/presentation.py +47 -0
- pptx2/oxml/simpletypes.py +12 -0
- pptx2/oxml/slide.py +66 -0
- pptx2/oxml/text.py +33 -0
- pptx2/package.py +14 -2
- pptx2/parts/customprops.py +129 -0
- pptx2/parts/image.py +7 -5
- pptx2/parts/presentation.py +21 -6
- pptx2/parts/slide.py +68 -1
- pptx2/presentation.py +15 -2
- pptx2/shapes/shapetree.py +45 -4
- pptx2/skill/references/basics.md +30 -0
- pptx2/skill/references/charts.md +1 -0
- pptx2/skill/references/lint.md +9 -0
- pptx2/skill/references/tables.md +10 -0
- pptx2/slide.py +104 -0
- pptx2/table.py +193 -1
- pptx2/text/text.py +134 -3
- pptx2/util.py +3 -5
- {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/METADATA +1 -1
- {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/RECORD +39 -37
- {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/WHEEL +0 -0
- {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/entry_points.txt +0 -0
- {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/licenses/LICENSE +0 -0
- {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/top_level.txt +0 -0
pptx2/__init__.py
CHANGED
|
@@ -35,6 +35,7 @@ from pptx2.opc.constants import CONTENT_TYPE as CT
|
|
|
35
35
|
from pptx2.opc.package import PartFactory
|
|
36
36
|
from pptx2.parts.chart import ChartPart
|
|
37
37
|
from pptx2.parts.coreprops import CorePropertiesPart
|
|
38
|
+
from pptx2.parts.customprops import CustomPropertiesPart
|
|
38
39
|
from pptx2.parts.diagram import (
|
|
39
40
|
DiagramColorsPart,
|
|
40
41
|
DiagramDataPart,
|
|
@@ -56,7 +57,7 @@ from pptx2.parts.slide import (
|
|
|
56
57
|
if TYPE_CHECKING:
|
|
57
58
|
from pptx2.opc.package import Part
|
|
58
59
|
|
|
59
|
-
__version__ = "2.
|
|
60
|
+
__version__ = "2.16.0"
|
|
60
61
|
|
|
61
62
|
sys.modules["pptx2.exceptions"] = exceptions
|
|
62
63
|
del sys
|
|
@@ -98,6 +99,7 @@ content_type_to_part_class_map: dict[str, type[Part]] = {
|
|
|
98
99
|
CT.PML_TEMPLATE_MAIN: PresentationPart,
|
|
99
100
|
CT.PML_SLIDESHOW_MAIN: PresentationPart,
|
|
100
101
|
CT.OPC_CORE_PROPERTIES: CorePropertiesPart,
|
|
102
|
+
CT.OFC_CUSTOM_PROPERTIES: CustomPropertiesPart,
|
|
101
103
|
CT.PML_NOTES_MASTER: NotesMasterPart,
|
|
102
104
|
CT.PML_NOTES_SLIDE: NotesSlidePart,
|
|
103
105
|
CT.PML_SLIDE: SlidePart,
|
|
@@ -136,6 +138,7 @@ PartFactory.part_type_for.update(content_type_to_part_class_map)
|
|
|
136
138
|
del (
|
|
137
139
|
ChartPart,
|
|
138
140
|
CorePropertiesPart,
|
|
141
|
+
CustomPropertiesPart,
|
|
139
142
|
DiagramColorsPart,
|
|
140
143
|
DiagramDataPart,
|
|
141
144
|
DiagramLayoutPart,
|
pptx2/_slide_importer.py
CHANGED
|
@@ -93,6 +93,37 @@ def import_slide(
|
|
|
93
93
|
return importer.run()
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def next_layout_hierarchy_id(prs_part: PresentationPart) -> int:
|
|
97
|
+
"""Return a fresh unique id for a `p:sldMasterId` / `p:sldLayoutId` entry.
|
|
98
|
+
|
|
99
|
+
These share one id space starting at 2147483648 (ST_SlideMasterId /
|
|
100
|
+
ST_SlideLayoutId minimum); duplicates across the presentation are a
|
|
101
|
+
repair trigger, so scan every master's layout-id list plus the
|
|
102
|
+
presentation's master-id list for the current maximum. Used both when
|
|
103
|
+
cloning masters (import_slide) and when adding or cloning slide layouts.
|
|
104
|
+
"""
|
|
105
|
+
used = [2147483647]
|
|
106
|
+
prs_element = prs_part._element # pyright: ignore[reportPrivateUsage]
|
|
107
|
+
used += [int(v) for v in prs_element.xpath("p:sldMasterIdLst/p:sldMasterId/@id")]
|
|
108
|
+
for dst_master_part in _iter_master_parts(prs_part):
|
|
109
|
+
used += [
|
|
110
|
+
int(v)
|
|
111
|
+
for v in dst_master_part._element.xpath( # pyright: ignore[reportPrivateUsage]
|
|
112
|
+
"p:sldLayoutIdLst/p:sldLayoutId/@id"
|
|
113
|
+
)
|
|
114
|
+
]
|
|
115
|
+
return max(used) + 1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _iter_master_parts(prs_part: PresentationPart):
|
|
119
|
+
"""Yield each SlideMasterPart referenced from *prs_part*'s `p:sldMasterIdLst`."""
|
|
120
|
+
prs_element = prs_part._element # pyright: ignore[reportPrivateUsage]
|
|
121
|
+
if prs_element.sldMasterIdLst is None:
|
|
122
|
+
return
|
|
123
|
+
for entry in prs_element.sldMasterIdLst.sldMasterId_lst:
|
|
124
|
+
yield prs_part.related_part(entry.rId)
|
|
125
|
+
|
|
126
|
+
|
|
96
127
|
# ---------------------------------------------------------------------------
|
|
97
128
|
# Internal implementation
|
|
98
129
|
# ---------------------------------------------------------------------------
|
|
@@ -169,13 +200,9 @@ class _SlideImporter:
|
|
|
169
200
|
return dst_master_part
|
|
170
201
|
return None
|
|
171
202
|
|
|
172
|
-
def _iter_dst_masters(self):
|
|
203
|
+
def _iter_dst_masters(self):
|
|
173
204
|
"""Yield each SlideMasterPart already in the destination presentation."""
|
|
174
|
-
|
|
175
|
-
if prs_element.sldMasterIdLst is None:
|
|
176
|
-
return
|
|
177
|
-
for entry in prs_element.sldMasterIdLst.sldMasterId_lst:
|
|
178
|
-
yield self._dst_prs_part.related_part(entry.rId)
|
|
205
|
+
yield from _iter_master_parts(self._dst_prs_part)
|
|
179
206
|
|
|
180
207
|
def _find_or_clone_layout_in_master(
|
|
181
208
|
self, src_layout_part: SlideLayoutPart, dst_master_part: SlideMasterPart
|
|
@@ -325,22 +352,9 @@ class _SlideImporter:
|
|
|
325
352
|
def _next_hierarchy_id(self) -> int:
|
|
326
353
|
"""Return a fresh unique id for a `p:sldMasterId` / `p:sldLayoutId` entry.
|
|
327
354
|
|
|
328
|
-
|
|
329
|
-
ST_SlideLayoutId minimum); duplicates across the presentation are a
|
|
330
|
-
repair trigger, so scan every master's layout-id list plus the
|
|
331
|
-
presentation's master-id list for the current maximum.
|
|
355
|
+
See :func:`next_layout_hierarchy_id`.
|
|
332
356
|
"""
|
|
333
|
-
|
|
334
|
-
prs_element = self._dst_prs_part._element # pyright: ignore[reportPrivateUsage]
|
|
335
|
-
used += [int(v) for v in prs_element.xpath("p:sldMasterIdLst/p:sldMasterId/@id")]
|
|
336
|
-
for dst_master_part in self._iter_dst_masters():
|
|
337
|
-
used += [
|
|
338
|
-
int(v)
|
|
339
|
-
for v in dst_master_part._element.xpath( # pyright: ignore[reportPrivateUsage]
|
|
340
|
-
"p:sldLayoutIdLst/p:sldLayoutId/@id"
|
|
341
|
-
)
|
|
342
|
-
]
|
|
343
|
-
return max(used) + 1
|
|
357
|
+
return next_layout_hierarchy_id(self._dst_prs_part)
|
|
344
358
|
|
|
345
359
|
# ------------------------------------------------------------------
|
|
346
360
|
# Slide copy
|
pptx2/api.py
CHANGED
|
@@ -18,12 +18,14 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from pptx2.parts.presentation import PresentationPart
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def Presentation(
|
|
21
|
+
def Presentation(
|
|
22
|
+
pptx: str | os.PathLike[str] | IO[bytes] | None = None,
|
|
23
|
+
) -> presentation.Presentation:
|
|
22
24
|
"""
|
|
23
25
|
Return a |Presentation| object loaded from *pptx*, where *pptx* can be
|
|
24
|
-
either a path to a ``.pptx`` file (a string) or a
|
|
25
|
-
*pptx* is missing or ``None``, the built-in default
|
|
26
|
-
"template" is loaded.
|
|
26
|
+
either a path to a ``.pptx`` file (a string or ``pathlib.Path``) or a
|
|
27
|
+
file-like object. If *pptx* is missing or ``None``, the built-in default
|
|
28
|
+
presentation "template" is loaded.
|
|
27
29
|
"""
|
|
28
30
|
if pptx is None:
|
|
29
31
|
pptx = _default_pptx_path()
|
|
@@ -45,5 +47,9 @@ def _default_pptx_path() -> str:
|
|
|
45
47
|
|
|
46
48
|
def _is_pptx_package(prs_part: PresentationPart):
|
|
47
49
|
"""Return |True| if *prs_part* is a valid main document part, |False| otherwise."""
|
|
48
|
-
valid_content_types = (
|
|
50
|
+
valid_content_types = (
|
|
51
|
+
CT.PML_PRESENTATION_MAIN,
|
|
52
|
+
CT.PML_PRES_MACRO_MAIN,
|
|
53
|
+
CT.PML_TEMPLATE_MAIN,
|
|
54
|
+
)
|
|
49
55
|
return prs_part.content_type in valid_content_types
|
pptx2/audit.py
CHANGED
|
@@ -30,7 +30,7 @@ The audit is read-only — it never mutates the deck.
|
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
32
|
from dataclasses import dataclass, field
|
|
33
|
-
from typing import TYPE_CHECKING, Any
|
|
33
|
+
from typing import TYPE_CHECKING, Any, Iterable
|
|
34
34
|
|
|
35
35
|
if TYPE_CHECKING:
|
|
36
36
|
from pptx2.api import Presentation
|
|
@@ -174,15 +174,27 @@ def audit(
|
|
|
174
174
|
*,
|
|
175
175
|
size_warn_bytes: int = 2_000_000,
|
|
176
176
|
check_fonts: bool = True,
|
|
177
|
+
extra_safe_fonts: "Iterable[str] | None" = None,
|
|
177
178
|
) -> AuditReport:
|
|
178
179
|
"""Walk the deck and return an :class:`AuditReport` summary.
|
|
179
180
|
|
|
180
181
|
Read-only — never mutates the presentation. Each slide is linted
|
|
181
182
|
in passing-defaults mode; per-slide overrides should be supplied
|
|
182
183
|
via the slide-level ``slide.lint(...)`` API directly.
|
|
184
|
+
|
|
185
|
+
*extra_safe_fonts* names additional fonts to treat as safe for the
|
|
186
|
+
font-warning probe, on top of the common Windows/macOS/Office set
|
|
187
|
+
(matching is case-insensitive). Use it where the rendering
|
|
188
|
+
environment genuinely ships the font — e.g. ``DejaVu Sans`` in a
|
|
189
|
+
sandbox whose font policy standardizes on it, or a corporate font
|
|
190
|
+
you embed in every deck.
|
|
183
191
|
"""
|
|
184
192
|
from pptx2.shapes.picture import Picture
|
|
185
193
|
|
|
194
|
+
safe_fonts = _COMMON_FONTS
|
|
195
|
+
if extra_safe_fonts is not None:
|
|
196
|
+
safe_fonts = _COMMON_FONTS | frozenset(f.lower() for f in extra_safe_fonts)
|
|
197
|
+
|
|
186
198
|
report = AuditReport()
|
|
187
199
|
slides = list(prs.slides)
|
|
188
200
|
report.total_slides = len(slides)
|
|
@@ -249,7 +261,7 @@ def audit(
|
|
|
249
261
|
for para in tf.paragraphs:
|
|
250
262
|
for run in para.runs:
|
|
251
263
|
name = run.font.name
|
|
252
|
-
if name and name.lower() not in
|
|
264
|
+
if name and name.lower() not in safe_fonts:
|
|
253
265
|
report.font_warnings.append((idx, name))
|
|
254
266
|
|
|
255
267
|
if content_shapes == 0:
|
pptx2/chart/data.py
CHANGED
|
@@ -12,6 +12,7 @@ from pptx2.chart.xlsx import (
|
|
|
12
12
|
XyWorkbookWriter,
|
|
13
13
|
)
|
|
14
14
|
from pptx2.chart.xmlwriter import ChartXmlWriter
|
|
15
|
+
from pptx2.oxml.simpletypes import ST_FirstSliceAng
|
|
15
16
|
from pptx2.util import lazyproperty
|
|
16
17
|
|
|
17
18
|
|
|
@@ -268,6 +269,8 @@ class CategoryChartData(_BaseChartData):
|
|
|
268
269
|
The corresponding X value is inferred by its position in the sequence.
|
|
269
270
|
"""
|
|
270
271
|
|
|
272
|
+
_first_slice_angle = 0
|
|
273
|
+
|
|
271
274
|
def add_category(self, label):
|
|
272
275
|
"""
|
|
273
276
|
Return a newly created |data.Category| object having *label* and
|
|
@@ -323,6 +326,20 @@ class CategoryChartData(_BaseChartData):
|
|
|
323
326
|
"""
|
|
324
327
|
return self._workbook_writer.categories_ref
|
|
325
328
|
|
|
329
|
+
@property
|
|
330
|
+
def first_slice_angle(self):
|
|
331
|
+
"""
|
|
332
|
+
Read/write int in range 0..359 specifying the angle in degrees of
|
|
333
|
+
the first slice for a doughnut chart created from this chart data,
|
|
334
|
+
clockwise from 12 o'clock. The default is 0, the PowerPoint default.
|
|
335
|
+
"""
|
|
336
|
+
return self._first_slice_angle
|
|
337
|
+
|
|
338
|
+
@first_slice_angle.setter
|
|
339
|
+
def first_slice_angle(self, value):
|
|
340
|
+
ST_FirstSliceAng.validate(value)
|
|
341
|
+
self._first_slice_angle = int(value)
|
|
342
|
+
|
|
326
343
|
def values_ref(self, series):
|
|
327
344
|
"""
|
|
328
345
|
The Excel worksheet reference to the values for *series* (not
|
pptx2/chart/plot.py
CHANGED
|
@@ -220,6 +220,25 @@ class DoughnutPlot(_BasePlot):
|
|
|
220
220
|
holeSize = self._element.get_or_add_holeSize()
|
|
221
221
|
holeSize.val = value
|
|
222
222
|
|
|
223
|
+
@property
|
|
224
|
+
def first_slice_angle(self):
|
|
225
|
+
"""
|
|
226
|
+
Read/write int in range 0..359 specifying the angle in degrees of
|
|
227
|
+
the first slice of this doughnut, clockwise from 12 o'clock. The
|
|
228
|
+
PowerPoint default for a new doughnut chart is 0, which is what this
|
|
229
|
+
property reports when no explicit ``<c:firstSliceAng>`` element is
|
|
230
|
+
present.
|
|
231
|
+
"""
|
|
232
|
+
firstSliceAng = self._element.firstSliceAng
|
|
233
|
+
if firstSliceAng is None:
|
|
234
|
+
return 0
|
|
235
|
+
return firstSliceAng.val
|
|
236
|
+
|
|
237
|
+
@first_slice_angle.setter
|
|
238
|
+
def first_slice_angle(self, value):
|
|
239
|
+
firstSliceAng = self._element.get_or_add_firstSliceAng()
|
|
240
|
+
firstSliceAng.val = value
|
|
241
|
+
|
|
223
242
|
|
|
224
243
|
class LinePlot(_BasePlot):
|
|
225
244
|
"""
|
pptx2/chart/xmlwriter.py
CHANGED
|
@@ -669,7 +669,7 @@ class _DoughnutChartXmlWriter(_BaseChartXmlWriter):
|
|
|
669
669
|
' <c:showBubbleSize val="0"/>\n'
|
|
670
670
|
' <c:showLeaderLines val="1"/>\n'
|
|
671
671
|
" </c:dLbls>\n"
|
|
672
|
-
' <c:firstSliceAng val="
|
|
672
|
+
' <c:firstSliceAng val="{firstSliceAng}"/>\n'
|
|
673
673
|
' <c:holeSize val="50"/>\n'
|
|
674
674
|
" </c:doughnutChart>\n"
|
|
675
675
|
" </c:plotArea>\n"
|
|
@@ -693,7 +693,7 @@ class _DoughnutChartXmlWriter(_BaseChartXmlWriter):
|
|
|
693
693
|
" </a:p>\n"
|
|
694
694
|
" </c:txPr>\n"
|
|
695
695
|
"</c:chartSpace>\n"
|
|
696
|
-
).format(**{"ser_xml": self._ser_xml})
|
|
696
|
+
).format(**{"ser_xml": self._ser_xml, "firstSliceAng": self._firstSliceAng})
|
|
697
697
|
|
|
698
698
|
@property
|
|
699
699
|
def _explosion_xml(self):
|
|
@@ -701,6 +701,15 @@ class _DoughnutChartXmlWriter(_BaseChartXmlWriter):
|
|
|
701
701
|
return ' <c:explosion val="25"/>\n'
|
|
702
702
|
return ""
|
|
703
703
|
|
|
704
|
+
@property
|
|
705
|
+
def _firstSliceAng(self):
|
|
706
|
+
"""
|
|
707
|
+
The integer angle in degrees for the ``<c:firstSliceAng>`` element,
|
|
708
|
+
taken from the chart data object when it specifies one and 0
|
|
709
|
+
(the PowerPoint default) otherwise.
|
|
710
|
+
"""
|
|
711
|
+
return getattr(self._chart_data, "first_slice_angle", 0)
|
|
712
|
+
|
|
704
713
|
@property
|
|
705
714
|
def _ser_xml(self):
|
|
706
715
|
xml = ""
|
pptx2/enum/text.py
CHANGED
|
@@ -228,3 +228,75 @@ class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum):
|
|
|
228
228
|
|
|
229
229
|
|
|
230
230
|
PP_ALIGN = PP_PARAGRAPH_ALIGNMENT
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class MSO_TEXT_FIELD_TYPE(BaseXmlEnum):
|
|
234
|
+
"""Specifies the kind of value a text field (`a:fld`) displays.
|
|
235
|
+
|
|
236
|
+
Used with :meth:`.Paragraph.add_field` to add a run-like field element to a paragraph.
|
|
237
|
+
The field's displayed text (the actual slide number, the current date, ...) is computed by
|
|
238
|
+
PowerPoint when the slide is rendered; the `a:t` child element holds only the "cached"
|
|
239
|
+
placeholder text readers that do not compute fields will show.
|
|
240
|
+
|
|
241
|
+
Example::
|
|
242
|
+
|
|
243
|
+
from pptx2.enum.text import MSO_TEXT_FIELD_TYPE
|
|
244
|
+
|
|
245
|
+
paragraph.add_field(MSO_TEXT_FIELD_TYPE.SLIDE_NUMBER)
|
|
246
|
+
|
|
247
|
+
The XML tokens are the ST_TextFieldType values DrawingML defines ("slidenum", "datetime",
|
|
248
|
+
"datetime1" through "datetime13") plus "datetimeFigureOut", the token PowerPoint writes for
|
|
249
|
+
an automatically updated date.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
SLIDE_NUMBER = (1, "slidenum", "The number of the slide the field appears on.")
|
|
253
|
+
"""The number of the slide the field appears on."""
|
|
254
|
+
|
|
255
|
+
DATETIME = (2, "datetime", "The date and time in the containing document's default format.")
|
|
256
|
+
"""The date and time in the containing document's default format."""
|
|
257
|
+
|
|
258
|
+
DATETIME_1 = (3, "datetime1", "Predefined date/time format 1 (locale-dependent rendering).")
|
|
259
|
+
"""Predefined date/time format 1 (locale-dependent rendering)."""
|
|
260
|
+
|
|
261
|
+
DATETIME_2 = (4, "datetime2", "Predefined date/time format 2 (locale-dependent rendering).")
|
|
262
|
+
"""Predefined date/time format 2 (locale-dependent rendering)."""
|
|
263
|
+
|
|
264
|
+
DATETIME_3 = (5, "datetime3", "Predefined date/time format 3 (locale-dependent rendering).")
|
|
265
|
+
"""Predefined date/time format 3 (locale-dependent rendering)."""
|
|
266
|
+
|
|
267
|
+
DATETIME_4 = (6, "datetime4", "Predefined date/time format 4 (locale-dependent rendering).")
|
|
268
|
+
"""Predefined date/time format 4 (locale-dependent rendering)."""
|
|
269
|
+
|
|
270
|
+
DATETIME_5 = (7, "datetime5", "Predefined date/time format 5 (locale-dependent rendering).")
|
|
271
|
+
"""Predefined date/time format 5 (locale-dependent rendering)."""
|
|
272
|
+
|
|
273
|
+
DATETIME_6 = (8, "datetime6", "Predefined date/time format 6 (locale-dependent rendering).")
|
|
274
|
+
"""Predefined date/time format 6 (locale-dependent rendering)."""
|
|
275
|
+
|
|
276
|
+
DATETIME_7 = (9, "datetime7", "Predefined date/time format 7 (locale-dependent rendering).")
|
|
277
|
+
"""Predefined date/time format 7 (locale-dependent rendering)."""
|
|
278
|
+
|
|
279
|
+
DATETIME_8 = (10, "datetime8", "Predefined date/time format 8 (locale-dependent rendering).")
|
|
280
|
+
"""Predefined date/time format 8 (locale-dependent rendering)."""
|
|
281
|
+
|
|
282
|
+
DATETIME_9 = (11, "datetime9", "Predefined date/time format 9 (locale-dependent rendering).")
|
|
283
|
+
"""Predefined date/time format 9 (locale-dependent rendering)."""
|
|
284
|
+
|
|
285
|
+
DATETIME_10 = (12, "datetime10", "Predefined date/time format 10 (locale-dependent rendering).")
|
|
286
|
+
"""Predefined date/time format 10 (locale-dependent rendering)."""
|
|
287
|
+
|
|
288
|
+
DATETIME_11 = (13, "datetime11", "Predefined date/time format 11 (locale-dependent rendering).")
|
|
289
|
+
"""Predefined date/time format 11 (locale-dependent rendering)."""
|
|
290
|
+
|
|
291
|
+
DATETIME_12 = (14, "datetime12", "Predefined date/time format 12 (locale-dependent rendering).")
|
|
292
|
+
"""Predefined date/time format 12 (locale-dependent rendering)."""
|
|
293
|
+
|
|
294
|
+
DATETIME_13 = (15, "datetime13", "Predefined date/time format 13 (locale-dependent rendering).")
|
|
295
|
+
"""Predefined date/time format 13 (locale-dependent rendering)."""
|
|
296
|
+
|
|
297
|
+
DATETIME_FIGURE_OUT = (
|
|
298
|
+
16,
|
|
299
|
+
"datetimeFigureOut",
|
|
300
|
+
"The current date, in the format PowerPoint figures out automatically.",
|
|
301
|
+
)
|
|
302
|
+
"""The current date, in the format PowerPoint figures out automatically."""
|
pptx2/opc/package.py
CHANGED
|
@@ -7,6 +7,7 @@ presentations to and from a .pptx file.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import collections
|
|
10
|
+
import os
|
|
10
11
|
from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast
|
|
11
12
|
|
|
12
13
|
from pptx2.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
|
|
@@ -73,11 +74,11 @@ class OpcPackage(_RelatableMixin):
|
|
|
73
74
|
file or file-like object containing a package (.pptx file).
|
|
74
75
|
"""
|
|
75
76
|
|
|
76
|
-
def __init__(self, pkg_file: str | IO[bytes]):
|
|
77
|
+
def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes]):
|
|
77
78
|
self._pkg_file = pkg_file
|
|
78
79
|
|
|
79
80
|
@classmethod
|
|
80
|
-
def open(cls, pkg_file: str | IO[bytes]) -> Self:
|
|
81
|
+
def open(cls, pkg_file: str | os.PathLike[str] | IO[bytes]) -> Self:
|
|
81
82
|
"""Return an |OpcPackage| instance loaded with the contents of `pkg_file`."""
|
|
82
83
|
return cls(pkg_file)._load()
|
|
83
84
|
|
|
@@ -148,10 +149,10 @@ class OpcPackage(_RelatableMixin):
|
|
|
148
149
|
return PackURI(candidate_partname)
|
|
149
150
|
raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover
|
|
150
151
|
|
|
151
|
-
def save(self, pkg_file: str | IO[bytes]) -> None:
|
|
152
|
+
def save(self, pkg_file: str | os.PathLike[str] | IO[bytes]) -> None:
|
|
152
153
|
"""Save this package to `pkg_file`.
|
|
153
154
|
|
|
154
|
-
`file` can be either a path to a file (a string) or a file-like object.
|
|
155
|
+
`file` can be either a path to a file (a string or `pathlib.Path`) or a file-like object.
|
|
155
156
|
"""
|
|
156
157
|
PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts()))
|
|
157
158
|
|
|
@@ -170,13 +171,13 @@ class OpcPackage(_RelatableMixin):
|
|
|
170
171
|
class _PackageLoader:
|
|
171
172
|
"""Function-object that loads a package from disk (or other store)."""
|
|
172
173
|
|
|
173
|
-
def __init__(self, pkg_file: str | IO[bytes], package: Package):
|
|
174
|
+
def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes], package: Package):
|
|
174
175
|
self._pkg_file = pkg_file
|
|
175
176
|
self._package = package
|
|
176
177
|
|
|
177
178
|
@classmethod
|
|
178
179
|
def load(
|
|
179
|
-
cls, pkg_file: str | IO[bytes], package: Package
|
|
180
|
+
cls, pkg_file: str | os.PathLike[str] | IO[bytes], package: Package
|
|
180
181
|
) -> tuple[CT_Relationships, dict[PackURI, Part]]:
|
|
181
182
|
"""Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`.
|
|
182
183
|
|
|
@@ -360,11 +361,11 @@ class Part(_RelatableMixin):
|
|
|
360
361
|
# --- this must be public to allow the part graph to be traversed ---
|
|
361
362
|
return self._rels
|
|
362
363
|
|
|
363
|
-
def _blob_from_file(self, file: str | IO[bytes]) -> bytes:
|
|
364
|
-
"""Return bytes of `file`, which is either a
|
|
365
|
-
# --- a
|
|
366
|
-
if isinstance(file, str):
|
|
367
|
-
with open(file, "rb") as f:
|
|
364
|
+
def _blob_from_file(self, file: str | os.PathLike[str] | IO[bytes]) -> bytes:
|
|
365
|
+
"""Return bytes of `file`, which is either a path or a file-like object."""
|
|
366
|
+
# --- a path-like `file` is assumed to be a path ---
|
|
367
|
+
if isinstance(file, (str, os.PathLike)):
|
|
368
|
+
with open(os.fspath(file), "rb") as f:
|
|
368
369
|
return f.read()
|
|
369
370
|
|
|
370
371
|
# --- otherwise, assume `file` is a file-like object
|
pptx2/opc/serialized.py
CHANGED
|
@@ -26,7 +26,7 @@ class PackageReader(Container[bytes]):
|
|
|
26
26
|
perhaps by unzipping a .pptx file.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
def __init__(self, pkg_file: str | IO[bytes]):
|
|
29
|
+
def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes]):
|
|
30
30
|
self._pkg_file = pkg_file
|
|
31
31
|
|
|
32
32
|
def __contains__(self, pack_uri: object) -> bool:
|
|
@@ -62,14 +62,22 @@ class PackageWriter:
|
|
|
62
62
|
Its single API classmethod is :meth:`write`. This class is not intended to be instantiated.
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
|
-
def __init__(
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
pkg_file: str | os.PathLike[str] | IO[bytes],
|
|
68
|
+
pkg_rels: _Relationships,
|
|
69
|
+
parts: Sequence[Part],
|
|
70
|
+
):
|
|
66
71
|
self._pkg_file = pkg_file
|
|
67
72
|
self._pkg_rels = pkg_rels
|
|
68
73
|
self._parts = parts
|
|
69
74
|
|
|
70
75
|
@classmethod
|
|
71
76
|
def write(
|
|
72
|
-
cls,
|
|
77
|
+
cls,
|
|
78
|
+
pkg_file: str | os.PathLike[str] | IO[bytes],
|
|
79
|
+
pkg_rels: _Relationships,
|
|
80
|
+
parts: Sequence[Part],
|
|
73
81
|
) -> None:
|
|
74
82
|
"""Write a physical package (.pptx file) to `pkg_file`.
|
|
75
83
|
|
|
@@ -127,21 +135,22 @@ class _PhysPkgReader(Container[PackURI]):
|
|
|
127
135
|
)
|
|
128
136
|
|
|
129
137
|
@classmethod
|
|
130
|
-
def factory(cls, pkg_file: str | IO[bytes]) -> _PhysPkgReader:
|
|
138
|
+
def factory(cls, pkg_file: str | os.PathLike[str] | IO[bytes]) -> _PhysPkgReader:
|
|
131
139
|
"""Return |_PhysPkgReader| subtype instance appropriage for `pkg_file`."""
|
|
132
|
-
# --- for pkg_file other than
|
|
140
|
+
# --- for pkg_file other than a path, assume it's a stream and pass it to Zip
|
|
133
141
|
# --- reader to sort out
|
|
134
|
-
if not isinstance(pkg_file, str):
|
|
142
|
+
if not isinstance(pkg_file, (str, os.PathLike)):
|
|
135
143
|
return _ZipPkgReader(pkg_file)
|
|
136
144
|
|
|
137
145
|
# --- otherwise we treat `pkg_file` as a path ---
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
path = os.fspath(pkg_file)
|
|
147
|
+
if os.path.isdir(path):
|
|
148
|
+
return _DirPkgReader(path)
|
|
140
149
|
|
|
141
|
-
if zipfile.is_zipfile(
|
|
142
|
-
return _ZipPkgReader(
|
|
150
|
+
if zipfile.is_zipfile(path):
|
|
151
|
+
return _ZipPkgReader(path)
|
|
143
152
|
|
|
144
|
-
raise PackageNotFoundError("Package not found at '%s'" %
|
|
153
|
+
raise PackageNotFoundError("Package not found at '%s'" % path)
|
|
145
154
|
|
|
146
155
|
|
|
147
156
|
class _DirPkgReader(_PhysPkgReader):
|
|
@@ -172,7 +181,7 @@ class _DirPkgReader(_PhysPkgReader):
|
|
|
172
181
|
class _ZipPkgReader(_PhysPkgReader):
|
|
173
182
|
"""Implements |PhysPkgReader| interface for a zip-file OPC package."""
|
|
174
183
|
|
|
175
|
-
def __init__(self, pkg_file: str | IO[bytes]):
|
|
184
|
+
def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes]):
|
|
176
185
|
self._pkg_file = pkg_file
|
|
177
186
|
|
|
178
187
|
def __contains__(self, pack_uri: object) -> bool:
|
|
@@ -199,7 +208,7 @@ class _PhysPkgWriter:
|
|
|
199
208
|
"""Base class for physical package writer objects."""
|
|
200
209
|
|
|
201
210
|
@classmethod
|
|
202
|
-
def factory(cls, pkg_file: str | IO[bytes]) -> _ZipPkgWriter:
|
|
211
|
+
def factory(cls, pkg_file: str | os.PathLike[str] | IO[bytes]) -> _ZipPkgWriter:
|
|
203
212
|
"""Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`.
|
|
204
213
|
|
|
205
214
|
Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be implemented
|
|
@@ -217,7 +226,7 @@ class _PhysPkgWriter:
|
|
|
217
226
|
class _ZipPkgWriter(_PhysPkgWriter):
|
|
218
227
|
"""Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package."""
|
|
219
228
|
|
|
220
|
-
def __init__(self, pkg_file: str | IO[bytes]):
|
|
229
|
+
def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes]):
|
|
221
230
|
self._pkg_file = pkg_file
|
|
222
231
|
|
|
223
232
|
def __enter__(self) -> _ZipPkgWriter:
|
pptx2/oxml/__init__.py
CHANGED
|
@@ -213,8 +213,10 @@ register_element_cls("c:xMode", CT_LayoutMode)
|
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
from pptx2.oxml.coreprops import CT_CoreProperties # noqa: E402
|
|
216
|
+
from pptx2.oxml.customprops import CT_CustomProperties # noqa: E402
|
|
216
217
|
|
|
217
218
|
register_element_cls("cp:coreProperties", CT_CoreProperties)
|
|
219
|
+
register_element_cls("custom:Properties", CT_CustomProperties)
|
|
218
220
|
|
|
219
221
|
|
|
220
222
|
from pptx2.oxml.dml.color import ( # noqa: E402
|
|
@@ -324,6 +326,8 @@ register_element_cls("a:prstDash", CT_PresetLineDashProperties)
|
|
|
324
326
|
|
|
325
327
|
|
|
326
328
|
from pptx2.oxml.presentation import ( # noqa: E402
|
|
329
|
+
CT_NotesMasterIdList,
|
|
330
|
+
CT_NotesMasterIdListEntry,
|
|
327
331
|
CT_Presentation,
|
|
328
332
|
CT_SlideId,
|
|
329
333
|
CT_SlideIdList,
|
|
@@ -332,6 +336,8 @@ from pptx2.oxml.presentation import ( # noqa: E402
|
|
|
332
336
|
CT_SlideSize,
|
|
333
337
|
)
|
|
334
338
|
|
|
339
|
+
register_element_cls("p:notesMasterId", CT_NotesMasterIdListEntry)
|
|
340
|
+
register_element_cls("p:notesMasterIdLst", CT_NotesMasterIdList)
|
|
335
341
|
register_element_cls("p:presentation", CT_Presentation)
|
|
336
342
|
register_element_cls("p:sldId", CT_SlideId)
|
|
337
343
|
register_element_cls("p:sldIdLst", CT_SlideIdList)
|
pptx2/oxml/chart/plot.py
CHANGED
|
@@ -6,6 +6,7 @@ from pptx2.oxml.chart.datalabel import CT_DLbls
|
|
|
6
6
|
from pptx2.oxml.simpletypes import (
|
|
7
7
|
ST_BarDir,
|
|
8
8
|
ST_BubbleScale,
|
|
9
|
+
ST_FirstSliceAng,
|
|
9
10
|
ST_GapAmount,
|
|
10
11
|
ST_Grouping,
|
|
11
12
|
ST_HoleSize,
|
|
@@ -241,10 +242,21 @@ class CT_DoughnutChart(BaseChartElement):
|
|
|
241
242
|
varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[1:])
|
|
242
243
|
ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
|
|
243
244
|
dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
|
|
245
|
+
firstSliceAng = ZeroOrOne("c:firstSliceAng", successors=_tag_seq[4:])
|
|
244
246
|
holeSize = ZeroOrOne("c:holeSize", successors=_tag_seq[5:])
|
|
245
247
|
del _tag_seq
|
|
246
248
|
|
|
247
249
|
|
|
250
|
+
class CT_FirstSliceAng(BaseOxmlElement):
|
|
251
|
+
"""
|
|
252
|
+
``<c:firstSliceAng>`` child of a ``<c:doughnutChart>`` element,
|
|
253
|
+
specifying the angle in degrees of the first slice, clockwise from 12
|
|
254
|
+
o'clock.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
val = OptionalAttribute("val", ST_FirstSliceAng, default=0)
|
|
258
|
+
|
|
259
|
+
|
|
248
260
|
class CT_GapAmount(BaseOxmlElement):
|
|
249
261
|
"""
|
|
250
262
|
``<c:gapWidth>`` child of ``<c:barChart>`` element, also used for other
|
|
@@ -362,4 +374,5 @@ class CT_ScatterChart(BaseChartElement):
|
|
|
362
374
|
# -- avoid a circular import at package-initialization time.
|
|
363
375
|
from pptx2.oxml import register_element_cls # noqa: E402
|
|
364
376
|
|
|
377
|
+
register_element_cls("c:firstSliceAng", CT_FirstSliceAng)
|
|
365
378
|
register_element_cls("c:holeSize", CT_HoleSize)
|