python-pptx2 2.13.0__py3-none-any.whl → 2.15.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/chart/data.py +17 -0
- pptx2/chart/plot.py +19 -0
- pptx2/chart/xmlwriter.py +11 -2
- pptx2/enum/text.py +72 -0
- pptx2/math.py +89 -37
- pptx2/mathml2omml.py +1163 -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 +3 -0
- pptx2/oxml/presentation.py +47 -0
- pptx2/oxml/simpletypes.py +12 -0
- pptx2/oxml/slide.py +68 -2
- pptx2/oxml/text.py +34 -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 +77 -6
- pptx2/presentation.py +15 -2
- pptx2/shapes/shapetree.py +48 -7
- pptx2/skill/SKILL.md +1 -1
- pptx2/skill/references/basics.md +30 -0
- pptx2/skill/references/charts.md +1 -0
- pptx2/skill/references/math.md +6 -4
- pptx2/skill/references/tables.md +10 -0
- pptx2/slide.py +104 -0
- pptx2/table.py +193 -1
- pptx2/text/text.py +137 -7
- pptx2/util.py +3 -5
- {python_pptx2-2.13.0.dist-info → python_pptx2-2.15.0.dist-info}/METADATA +3 -3
- {python_pptx2-2.13.0.dist-info → python_pptx2-2.15.0.dist-info}/RECORD +41 -38
- {python_pptx2-2.13.0.dist-info → python_pptx2-2.15.0.dist-info}/WHEEL +0 -0
- {python_pptx2-2.13.0.dist-info → python_pptx2-2.15.0.dist-info}/entry_points.txt +0 -0
- {python_pptx2-2.13.0.dist-info → python_pptx2-2.15.0.dist-info}/licenses/LICENSE +0 -0
- {python_pptx2-2.13.0.dist-info → python_pptx2-2.15.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.15.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/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/math.py
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
PowerPoint stores editable equations as Office Math (OMML) wrapped in an
|
|
4
4
|
``a14:m`` marker inside a DrawingML paragraph. This module does not compile
|
|
5
|
-
LaTeX itself:
|
|
6
|
-
|
|
5
|
+
LaTeX itself: ``latex2mathml`` turns the fragment into MathML and the
|
|
6
|
+
bundled ``pptx2.mathml2omml`` port (from mathml2omml-plus / ECMA-376 §7.1)
|
|
7
|
+
turns that into Office Math.
|
|
7
8
|
|
|
8
|
-
Install the
|
|
9
|
+
Install the LaTeX front-end with::
|
|
9
10
|
|
|
10
11
|
pip install "python-pptx2[math]"
|
|
11
12
|
"""
|
|
@@ -17,6 +18,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
17
18
|
|
|
18
19
|
from lxml import etree
|
|
19
20
|
|
|
21
|
+
from pptx2.mathml2omml import convert as mathml_to_omml
|
|
20
22
|
from pptx2.oxml import parse_xml
|
|
21
23
|
from pptx2.oxml.ns import nsuri, qn
|
|
22
24
|
|
|
@@ -26,6 +28,8 @@ if TYPE_CHECKING:
|
|
|
26
28
|
|
|
27
29
|
A14_NS = nsuri("a14")
|
|
28
30
|
M_NS = nsuri("m")
|
|
31
|
+
W_NS = nsuri("w")
|
|
32
|
+
MC_NS = nsuri("mc")
|
|
29
33
|
|
|
30
34
|
_WRAPPER_PATTERNS = (
|
|
31
35
|
re.compile(r"^\s*\$\$(.*)\$\$\s*$", re.DOTALL),
|
|
@@ -47,9 +51,15 @@ _DISPLAY_JC = {
|
|
|
47
51
|
"justify": "centerGroup",
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
_SLD_OPEN_RE = re.compile(
|
|
55
|
+
r"^(<\?xml[^>]*\?>\s*)?(<p:sld\b)([^>]*)(>)",
|
|
56
|
+
re.DOTALL,
|
|
57
|
+
)
|
|
58
|
+
_IGNORABLE_RE = re.compile(r'\smc:Ignorable="([^"]*)"')
|
|
59
|
+
|
|
50
60
|
|
|
51
61
|
class MathBackendUnavailable(ImportError):
|
|
52
|
-
"""Raised when the optional LaTeX →
|
|
62
|
+
"""Raised when the optional LaTeX → MathML converter is not installed."""
|
|
53
63
|
|
|
54
64
|
|
|
55
65
|
def strip_math_delimiters(source: str) -> str:
|
|
@@ -72,13 +82,13 @@ def latex_to_omml(latex: str) -> str:
|
|
|
72
82
|
if not fragment:
|
|
73
83
|
raise ValueError("latex must be a non-empty math fragment")
|
|
74
84
|
|
|
75
|
-
converter
|
|
85
|
+
converter = _import_latex2mathml()
|
|
76
86
|
try:
|
|
77
87
|
mathml = converter.convert(fragment)
|
|
78
88
|
except Exception as exc:
|
|
79
89
|
raise ValueError(f"LaTeX could not be converted to MathML: {exc}") from exc
|
|
80
90
|
try:
|
|
81
|
-
omml =
|
|
91
|
+
omml = mathml_to_omml(mathml)
|
|
82
92
|
except Exception as exc:
|
|
83
93
|
raise ValueError(f"MathML could not be converted to Office Math: {exc}") from exc
|
|
84
94
|
if not isinstance(omml, str) or not omml.strip():
|
|
@@ -94,7 +104,10 @@ def office_math_element(
|
|
|
94
104
|
) -> CT_OfficeMathMarker:
|
|
95
105
|
"""Return an ``a14:m`` element ready to append to a DrawingML paragraph."""
|
|
96
106
|
payload = _omml_payload(latex_to_omml(latex), display=display, align=align)
|
|
97
|
-
xml =
|
|
107
|
+
xml = (
|
|
108
|
+
f'<a14:m xmlns:a14="{A14_NS}" xmlns:m="{M_NS}" xmlns:w="{W_NS}">'
|
|
109
|
+
f"{payload}</a14:m>"
|
|
110
|
+
)
|
|
98
111
|
return parse_xml(xml)
|
|
99
112
|
|
|
100
113
|
|
|
@@ -105,7 +118,7 @@ def style_office_math(
|
|
|
105
118
|
color: Any = None,
|
|
106
119
|
font: str | None = None,
|
|
107
120
|
) -> None:
|
|
108
|
-
"""Write
|
|
121
|
+
"""Write size / color / typeface onto every ``m:r`` and ``m:ctrlPr``."""
|
|
109
122
|
if size_pt is None and color is None and font is None:
|
|
110
123
|
return
|
|
111
124
|
|
|
@@ -113,32 +126,70 @@ def style_office_math(
|
|
|
113
126
|
|
|
114
127
|
sz = None if size_pt is None else str(int(round(float(size_pt) * 100)))
|
|
115
128
|
rgb = None if color is None else str(coerce_color(color))
|
|
129
|
+
r_pr_xml = _drawing_rpr(sz, rgb, font)
|
|
116
130
|
|
|
117
131
|
for run in marker.iter(qn("m:r")):
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
_stamp_arpr(run, r_pr_xml, before=qn("m:t"))
|
|
133
|
+
for ctrl in marker.iter(qn("m:ctrlPr")):
|
|
134
|
+
_stamp_arpr(ctrl, r_pr_xml, before=None)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def prepare_slide_xml_for_math(xml: bytes) -> bytes:
|
|
138
|
+
"""Ensure the ``p:sld`` root declares math host namespaces.
|
|
139
|
+
|
|
140
|
+
PowerPoint requires ``xmlns:m``, ``xmlns:a14``, ``xmlns:mc`` and
|
|
141
|
+
``mc:Ignorable="a14"`` on the slide (MS-PPTX §2.2.8). Bare ``m:oMath``
|
|
142
|
+
without ``a14:m`` is stripped on open.
|
|
143
|
+
"""
|
|
144
|
+
match = _SLD_OPEN_RE.match(xml.decode("utf-8"))
|
|
145
|
+
if match is None:
|
|
146
|
+
return xml
|
|
147
|
+
decl, start, attrs, close = match.group(1) or "", match.group(2), match.group(3), match.group(4)
|
|
148
|
+
for prefix, uri in (
|
|
149
|
+
("xmlns:m", M_NS),
|
|
150
|
+
("xmlns:a14", A14_NS),
|
|
151
|
+
("xmlns:mc", MC_NS),
|
|
152
|
+
("xmlns:w", W_NS),
|
|
153
|
+
):
|
|
154
|
+
if f'{prefix}="' not in attrs:
|
|
155
|
+
attrs += f' {prefix}="{uri}"'
|
|
156
|
+
ignorable = _IGNORABLE_RE.search(attrs)
|
|
157
|
+
if ignorable is None:
|
|
158
|
+
attrs += ' mc:Ignorable="a14"'
|
|
159
|
+
else:
|
|
160
|
+
tokens = ignorable.group(1).split()
|
|
161
|
+
if "a14" not in tokens:
|
|
162
|
+
tokens.append("a14")
|
|
163
|
+
attrs = _IGNORABLE_RE.sub(f' mc:Ignorable="{" ".join(tokens)}"', attrs, count=1)
|
|
164
|
+
rewritten = decl + start + attrs + close + xml.decode("utf-8")[match.end() :]
|
|
165
|
+
return rewritten.encode("utf-8")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _drawing_rpr(sz: str | None, rgb: str | None, font: str | None) -> etree._Element:
|
|
169
|
+
attrs = {"dirty": "0"}
|
|
170
|
+
if sz is not None:
|
|
171
|
+
attrs["sz"] = sz
|
|
172
|
+
r_pr = etree.Element(qn("a:rPr"), attrs)
|
|
173
|
+
if rgb is not None:
|
|
174
|
+
solid = etree.SubElement(r_pr, qn("a:solidFill"))
|
|
175
|
+
etree.SubElement(solid, qn("a:srgbClr")).set("val", rgb)
|
|
176
|
+
if font is not None:
|
|
177
|
+
etree.SubElement(r_pr, qn("a:latin")).set("typeface", font)
|
|
178
|
+
return r_pr
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _stamp_arpr(parent: Any, template: etree._Element, before: str | None) -> None:
|
|
182
|
+
existing = parent.find(qn("a:rPr"))
|
|
183
|
+
stamped = etree.fromstring(etree.tostring(template))
|
|
184
|
+
if existing is not None:
|
|
185
|
+
parent.replace(existing, stamped)
|
|
186
|
+
return
|
|
187
|
+
if before is not None:
|
|
188
|
+
sibling = parent.find(before)
|
|
189
|
+
if sibling is not None:
|
|
190
|
+
sibling.addprevious(stamped)
|
|
191
|
+
return
|
|
192
|
+
parent.append(stamped)
|
|
142
193
|
|
|
143
194
|
|
|
144
195
|
def _omml_payload(omml: str, *, display: bool, align: str | None) -> str:
|
|
@@ -156,22 +207,23 @@ def _omml_payload(omml: str, *, display: bool, align: str | None) -> str:
|
|
|
156
207
|
)
|
|
157
208
|
|
|
158
209
|
|
|
159
|
-
def
|
|
210
|
+
def _import_latex2mathml():
|
|
160
211
|
try:
|
|
161
212
|
import latex2mathml.converter as latex2mathml_converter
|
|
162
|
-
import mathml2omml
|
|
163
213
|
except ImportError as exc:
|
|
164
214
|
raise MathBackendUnavailable(
|
|
165
|
-
"Native PowerPoint equations require
|
|
166
|
-
"
|
|
215
|
+
"Native PowerPoint equations require latex2mathml. "
|
|
216
|
+
"Install with: pip install 'python-pptx2[math]'"
|
|
167
217
|
) from exc
|
|
168
|
-
return latex2mathml_converter
|
|
218
|
+
return latex2mathml_converter
|
|
169
219
|
|
|
170
220
|
|
|
171
221
|
__all__ = (
|
|
172
222
|
"MathBackendUnavailable",
|
|
173
223
|
"latex_to_omml",
|
|
224
|
+
"mathml_to_omml",
|
|
174
225
|
"office_math_element",
|
|
226
|
+
"prepare_slide_xml_for_math",
|
|
175
227
|
"strip_math_delimiters",
|
|
176
228
|
"style_office_math",
|
|
177
229
|
)
|