python-pptx2 2.13.0__py3-none-any.whl → 2.14.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 CHANGED
@@ -56,7 +56,7 @@ from pptx2.parts.slide import (
56
56
  if TYPE_CHECKING:
57
57
  from pptx2.opc.package import Part
58
58
 
59
- __version__ = "2.13.0"
59
+ __version__ = "2.14.0"
60
60
 
61
61
  sys.modules["pptx2.exceptions"] = exceptions
62
62
  del sys
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: it calls ``latex2mathml`` then ``mathml2omml`` and wraps the
6
- result so PowerPoint will open it.
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 converters with::
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 → OMML converters are not installed."""
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, mathml2omml = _import_math_backends()
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 = mathml2omml.convert(mathml)
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 = f'<a14:m xmlns:a14="{A14_NS}" xmlns:m="{M_NS}">{payload}</a14:m>'
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 run-level size / color / typeface onto each ``m:r`` inside *marker*."""
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
- rPr = run.find(qn("a:rPr"))
119
- if rPr is None:
120
- rPr = etree.Element(qn("a:rPr"))
121
- text = run.find(qn("m:t"))
122
- if text is not None:
123
- text.addprevious(rPr)
124
- else:
125
- run.append(rPr)
126
- if sz is not None:
127
- rPr.set("sz", sz)
128
- if font is not None:
129
- latin = rPr.find(qn("a:latin"))
130
- if latin is None:
131
- latin = etree.SubElement(rPr, qn("a:latin"))
132
- latin.set("typeface", font)
133
- if rgb is not None:
134
- solid = rPr.find(qn("a:solidFill"))
135
- if solid is None:
136
- solid = etree.Element(qn("a:solidFill"))
137
- rPr.insert(0, solid)
138
- srgb = solid.find(qn("a:srgbClr"))
139
- if srgb is None:
140
- srgb = etree.SubElement(solid, qn("a:srgbClr"))
141
- srgb.set("val", rgb)
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 _import_math_backends():
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 the latex2mathml and "
166
- "mathml2omml packages. Install with: pip install 'python-pptx2[math]'"
215
+ "Native PowerPoint equations require latex2mathml. "
216
+ "Install with: pip install 'python-pptx2[math]'"
167
217
  ) from exc
168
- return latex2mathml_converter, mathml2omml
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
  )