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/design/figures.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Figure / plot embedding for slides — Plotly, Matplotlib, SVG, HTML.
|
|
2
|
+
|
|
3
|
+
PowerPoint can't render Plotly traces, Matplotlib axes, or HTML
|
|
4
|
+
documents directly: every external visualisation has to land on a
|
|
5
|
+
slide as a static image (PNG / SVG via Office's SVG extension) or, for
|
|
6
|
+
HTML, as a screenshot. This module owns those conversion paths so
|
|
7
|
+
recipes and authoring code don't have to re-derive them.
|
|
8
|
+
|
|
9
|
+
Every adapter is **optional**: the third-party libraries the adapters
|
|
10
|
+
need are imported lazily, and missing dependencies surface a clear
|
|
11
|
+
``ImportError`` naming the right install command rather than a deep
|
|
12
|
+
stack trace from inside the integration.
|
|
13
|
+
|
|
14
|
+
Public functions
|
|
15
|
+
----------------
|
|
16
|
+
|
|
17
|
+
* :func:`add_plotly_figure` — render a Plotly ``Figure`` and embed it.
|
|
18
|
+
* :func:`add_matplotlib_figure` — render a Matplotlib ``Figure`` and embed it.
|
|
19
|
+
* :func:`add_svg_figure` — embed any SVG markup (path, file-like, or bytes).
|
|
20
|
+
* :func:`add_html_figure` — render an HTML snippet via headless Playwright
|
|
21
|
+
and embed the screenshot.
|
|
22
|
+
|
|
23
|
+
Each returns the embedded :class:`~pptx2.shapes.picture.Picture` shape.
|
|
24
|
+
|
|
25
|
+
Quick install matrix
|
|
26
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
27
|
+
|
|
28
|
+
* Plotly → ``pip install plotly kaleido``
|
|
29
|
+
* Plotly with SVG output → also ``pip install cairosvg``
|
|
30
|
+
* Matplotlib → ``pip install matplotlib``
|
|
31
|
+
* HTML → ``pip install playwright && playwright install chromium``
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import io
|
|
37
|
+
import os
|
|
38
|
+
from typing import IO, TYPE_CHECKING, Any, Optional, Tuple, Union
|
|
39
|
+
|
|
40
|
+
from pptx2.util import Length
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from pptx2.shapes.picture import Picture
|
|
44
|
+
from pptx2.slide import Slide
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
PathOrFile = Union[str, "os.PathLike[str]", IO[bytes], bytes]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = (
|
|
51
|
+
"add_plotly_figure",
|
|
52
|
+
"add_matplotlib_figure",
|
|
53
|
+
"add_svg_figure",
|
|
54
|
+
"add_html_figure",
|
|
55
|
+
"FigureBackendUnavailable",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FigureBackendUnavailable(ImportError):
|
|
60
|
+
"""Raised when an optional figure-rendering dependency is missing.
|
|
61
|
+
|
|
62
|
+
Subclasses :class:`ImportError` so callers can still catch it via
|
|
63
|
+
``except ImportError`` while letting code that knows about the
|
|
64
|
+
figure pipeline distinguish a missing-dep failure from any other
|
|
65
|
+
import error.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Plotly
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def add_plotly_figure(
|
|
75
|
+
slide: "Slide",
|
|
76
|
+
figure: Any,
|
|
77
|
+
left: Length,
|
|
78
|
+
top: Length,
|
|
79
|
+
width: Optional[Length] = None,
|
|
80
|
+
height: Optional[Length] = None,
|
|
81
|
+
*,
|
|
82
|
+
format: str = "auto",
|
|
83
|
+
scale: float = 2.0,
|
|
84
|
+
width_px: Optional[int] = None,
|
|
85
|
+
height_px: Optional[int] = None,
|
|
86
|
+
) -> "Picture":
|
|
87
|
+
"""Render a Plotly ``Figure`` and embed the result on *slide*.
|
|
88
|
+
|
|
89
|
+
*figure* is a ``plotly.graph_objects.Figure`` (or anything that
|
|
90
|
+
exposes a ``.to_image(format=...)`` method, which is the public
|
|
91
|
+
plotly contract). *left* / *top* / *width* / *height* are slide
|
|
92
|
+
coordinates; if *width* / *height* are omitted the picture's
|
|
93
|
+
rendered size determines them, exactly like
|
|
94
|
+
:meth:`ShapeTree.add_picture`.
|
|
95
|
+
|
|
96
|
+
*format* picks the embedded image format:
|
|
97
|
+
|
|
98
|
+
* ``"svg"`` — vector, sharpest at any zoom; embedded via Office's
|
|
99
|
+
SVG extension with a PNG fallback (requires ``cairosvg``).
|
|
100
|
+
* ``"png"`` — raster, no extra dependencies beyond ``kaleido``.
|
|
101
|
+
* ``"auto"`` (default) — SVG when ``cairosvg`` is available, PNG
|
|
102
|
+
otherwise.
|
|
103
|
+
|
|
104
|
+
*scale* scales raster output to higher pixel density (2.0 ≈ retina).
|
|
105
|
+
*width_px* / *height_px* override the renderer's pixel canvas; by
|
|
106
|
+
default Plotly's own layout sizing wins.
|
|
107
|
+
|
|
108
|
+
Requires ``plotly`` and ``kaleido``::
|
|
109
|
+
|
|
110
|
+
pip install plotly kaleido
|
|
111
|
+
"""
|
|
112
|
+
blob, fmt = _plotly_to_blob(
|
|
113
|
+
figure, format=format, scale=scale,
|
|
114
|
+
width_px=width_px, height_px=height_px,
|
|
115
|
+
)
|
|
116
|
+
return _embed_blob(slide, blob, fmt, left, top, width, height)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _plotly_to_blob(
|
|
120
|
+
figure: Any,
|
|
121
|
+
*,
|
|
122
|
+
format: str,
|
|
123
|
+
scale: float,
|
|
124
|
+
width_px: Optional[int],
|
|
125
|
+
height_px: Optional[int],
|
|
126
|
+
) -> Tuple[bytes, str]:
|
|
127
|
+
if not hasattr(figure, "to_image"):
|
|
128
|
+
raise TypeError(
|
|
129
|
+
"add_plotly_figure expects a Plotly Figure (or an object "
|
|
130
|
+
"exposing .to_image(format=...)); got "
|
|
131
|
+
f"{type(figure).__name__!r}. If you have a dict spec, wrap "
|
|
132
|
+
"it with plotly.graph_objects.Figure(spec) first."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
fmt = _resolve_format(format)
|
|
136
|
+
if fmt == "svg":
|
|
137
|
+
try:
|
|
138
|
+
blob = figure.to_image(
|
|
139
|
+
format="svg",
|
|
140
|
+
scale=scale,
|
|
141
|
+
width=width_px,
|
|
142
|
+
height=height_px,
|
|
143
|
+
)
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
raise FigureBackendUnavailable(
|
|
146
|
+
"Plotly figure → SVG export failed. Ensure both "
|
|
147
|
+
"`plotly` and `kaleido` are installed (`pip install "
|
|
148
|
+
"plotly kaleido`); the underlying error was: %s"
|
|
149
|
+
% (exc,)
|
|
150
|
+
) from exc
|
|
151
|
+
return blob, "svg"
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
blob = figure.to_image(
|
|
155
|
+
format="png",
|
|
156
|
+
scale=scale,
|
|
157
|
+
width=width_px,
|
|
158
|
+
height=height_px,
|
|
159
|
+
)
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
raise FigureBackendUnavailable(
|
|
162
|
+
"Plotly figure → PNG export failed. Ensure both `plotly` "
|
|
163
|
+
"and `kaleido` are installed (`pip install plotly kaleido`); "
|
|
164
|
+
"the underlying error was: %s" % (exc,)
|
|
165
|
+
) from exc
|
|
166
|
+
return blob, "png"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# Matplotlib
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def add_matplotlib_figure(
|
|
175
|
+
slide: "Slide",
|
|
176
|
+
figure: Any,
|
|
177
|
+
left: Length,
|
|
178
|
+
top: Length,
|
|
179
|
+
width: Optional[Length] = None,
|
|
180
|
+
height: Optional[Length] = None,
|
|
181
|
+
*,
|
|
182
|
+
format: str = "auto",
|
|
183
|
+
dpi: int = 200,
|
|
184
|
+
) -> "Picture":
|
|
185
|
+
"""Render a Matplotlib ``Figure`` and embed the result on *slide*.
|
|
186
|
+
|
|
187
|
+
*figure* is a ``matplotlib.figure.Figure`` (or anything with a
|
|
188
|
+
``.savefig(buf, format=...)`` method). Geometry args mirror
|
|
189
|
+
:func:`add_plotly_figure`.
|
|
190
|
+
|
|
191
|
+
*format* picks the embedded image format:
|
|
192
|
+
|
|
193
|
+
* ``"svg"`` — vector, sharpest at zoom; needs ``cairosvg`` for the
|
|
194
|
+
PNG fallback (Office's SVG extension requires both).
|
|
195
|
+
* ``"png"`` — raster only.
|
|
196
|
+
* ``"auto"`` (default) — SVG when ``cairosvg`` is available, PNG
|
|
197
|
+
otherwise.
|
|
198
|
+
|
|
199
|
+
*dpi* applies to PNG output only; SVG is resolution-independent.
|
|
200
|
+
|
|
201
|
+
Requires ``matplotlib``::
|
|
202
|
+
|
|
203
|
+
pip install matplotlib
|
|
204
|
+
"""
|
|
205
|
+
if not hasattr(figure, "savefig"):
|
|
206
|
+
raise TypeError(
|
|
207
|
+
"add_matplotlib_figure expects a Matplotlib Figure (or an "
|
|
208
|
+
"object exposing .savefig); got "
|
|
209
|
+
f"{type(figure).__name__!r}."
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
fmt = _resolve_format(format)
|
|
213
|
+
buf = io.BytesIO()
|
|
214
|
+
try:
|
|
215
|
+
if fmt == "svg":
|
|
216
|
+
figure.savefig(buf, format="svg", bbox_inches="tight")
|
|
217
|
+
else:
|
|
218
|
+
figure.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
|
|
219
|
+
except ImportError as exc: # pragma: no cover - defensive
|
|
220
|
+
raise FigureBackendUnavailable(
|
|
221
|
+
"matplotlib not installed; `pip install matplotlib`."
|
|
222
|
+
) from exc
|
|
223
|
+
return _embed_blob(slide, buf.getvalue(), fmt, left, top, width, height)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# SVG (any source)
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def add_svg_figure(
|
|
232
|
+
slide: "Slide",
|
|
233
|
+
svg: PathOrFile,
|
|
234
|
+
left: Length,
|
|
235
|
+
top: Length,
|
|
236
|
+
width: Optional[Length] = None,
|
|
237
|
+
height: Optional[Length] = None,
|
|
238
|
+
*,
|
|
239
|
+
png_fallback: Optional[PathOrFile] = None,
|
|
240
|
+
) -> "Picture":
|
|
241
|
+
"""Embed an arbitrary SVG (path, file-like, or bytes) on *slide*.
|
|
242
|
+
|
|
243
|
+
Thin wrapper over :meth:`ShapeTree.add_svg_picture` — useful as the
|
|
244
|
+
same-shape entry point alongside :func:`add_plotly_figure` /
|
|
245
|
+
:func:`add_matplotlib_figure` so authoring code can route every
|
|
246
|
+
figure kind through the ``pptx2.design.figures`` module.
|
|
247
|
+
|
|
248
|
+
When *png_fallback* is omitted the SVG is rasterised via
|
|
249
|
+
``cairosvg`` for the Office SVG-extension PNG companion.
|
|
250
|
+
"""
|
|
251
|
+
return slide.shapes.add_svg_picture(
|
|
252
|
+
svg, left, top, width, height, png_fallback=png_fallback
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------------------------
|
|
257
|
+
# HTML (headless browser proxy)
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def add_html_figure(
|
|
262
|
+
slide: "Slide",
|
|
263
|
+
html: Union[str, bytes],
|
|
264
|
+
left: Length,
|
|
265
|
+
top: Length,
|
|
266
|
+
width: Optional[Length] = None,
|
|
267
|
+
height: Optional[Length] = None,
|
|
268
|
+
*,
|
|
269
|
+
viewport: Tuple[int, int] = (1280, 720),
|
|
270
|
+
device_scale_factor: float = 2.0,
|
|
271
|
+
wait_until: str = "networkidle",
|
|
272
|
+
full_page: bool = False,
|
|
273
|
+
timeout_ms: int = 15000,
|
|
274
|
+
) -> "Picture":
|
|
275
|
+
"""Render an HTML snippet to a screenshot via Playwright and embed it.
|
|
276
|
+
|
|
277
|
+
PowerPoint has no HTML-rendering surface. This adapter screenshots
|
|
278
|
+
the rendered DOM in a headless Chromium and embeds the PNG —
|
|
279
|
+
suitable for diagrams from web-based tools (Mermaid, D3, Vega-Lite
|
|
280
|
+
via mini-HTML wrappers, branded layouts) that don't have a direct
|
|
281
|
+
image export.
|
|
282
|
+
|
|
283
|
+
*html* may be raw markup or already-encoded ``bytes``. External
|
|
284
|
+
URLs (``<img src="https://...">``) are loaded as long as the
|
|
285
|
+
process has network access.
|
|
286
|
+
|
|
287
|
+
*viewport* sizes the headless browser canvas in CSS pixels;
|
|
288
|
+
*device_scale_factor* renders at the equivalent of a retina
|
|
289
|
+
display by default for crisper text. *full_page=True* captures
|
|
290
|
+
the entire scroll height instead of just the viewport — useful
|
|
291
|
+
when the rendered content overflows.
|
|
292
|
+
|
|
293
|
+
Requires ``playwright`` and a browser install::
|
|
294
|
+
|
|
295
|
+
pip install playwright
|
|
296
|
+
playwright install chromium
|
|
297
|
+
"""
|
|
298
|
+
blob = _html_to_png(
|
|
299
|
+
html,
|
|
300
|
+
viewport=viewport,
|
|
301
|
+
device_scale_factor=device_scale_factor,
|
|
302
|
+
wait_until=wait_until,
|
|
303
|
+
full_page=full_page,
|
|
304
|
+
timeout_ms=timeout_ms,
|
|
305
|
+
)
|
|
306
|
+
return _embed_blob(slide, blob, "png", left, top, width, height)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _html_to_png(
|
|
310
|
+
html: Union[str, bytes],
|
|
311
|
+
*,
|
|
312
|
+
viewport: Tuple[int, int],
|
|
313
|
+
device_scale_factor: float,
|
|
314
|
+
wait_until: str,
|
|
315
|
+
full_page: bool,
|
|
316
|
+
timeout_ms: int,
|
|
317
|
+
) -> bytes:
|
|
318
|
+
try:
|
|
319
|
+
from playwright.sync_api import sync_playwright # type: ignore[import-not-found]
|
|
320
|
+
except ImportError as exc:
|
|
321
|
+
raise FigureBackendUnavailable(
|
|
322
|
+
"HTML rendering needs Playwright; install with "
|
|
323
|
+
"`pip install playwright && playwright install chromium`."
|
|
324
|
+
) from exc
|
|
325
|
+
|
|
326
|
+
if isinstance(html, bytes):
|
|
327
|
+
html = html.decode("utf-8", "replace")
|
|
328
|
+
|
|
329
|
+
with sync_playwright() as pw:
|
|
330
|
+
try:
|
|
331
|
+
browser = pw.chromium.launch()
|
|
332
|
+
except Exception as exc:
|
|
333
|
+
raise FigureBackendUnavailable(
|
|
334
|
+
"Playwright failed to launch Chromium. Did you run "
|
|
335
|
+
"`playwright install chromium`? Original error: %s"
|
|
336
|
+
% (exc,)
|
|
337
|
+
) from exc
|
|
338
|
+
try:
|
|
339
|
+
page = browser.new_page(
|
|
340
|
+
viewport={"width": viewport[0], "height": viewport[1]},
|
|
341
|
+
device_scale_factor=device_scale_factor,
|
|
342
|
+
)
|
|
343
|
+
page.set_default_timeout(timeout_ms)
|
|
344
|
+
page.set_content(html, wait_until=wait_until)
|
|
345
|
+
return page.screenshot(full_page=full_page, type="png")
|
|
346
|
+
finally:
|
|
347
|
+
browser.close()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
# Internal: embed a blob (PNG or SVG) using the right shapetree path.
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _embed_blob(
|
|
356
|
+
slide: "Slide",
|
|
357
|
+
blob: bytes,
|
|
358
|
+
fmt: str,
|
|
359
|
+
left: Length,
|
|
360
|
+
top: Length,
|
|
361
|
+
width: Optional[Length],
|
|
362
|
+
height: Optional[Length],
|
|
363
|
+
) -> "Picture":
|
|
364
|
+
if fmt == "svg":
|
|
365
|
+
return slide.shapes.add_svg_picture(
|
|
366
|
+
blob, left, top, width, height
|
|
367
|
+
)
|
|
368
|
+
return slide.shapes.add_picture(
|
|
369
|
+
io.BytesIO(blob), left, top, width=width, height=height
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _resolve_format(format: str) -> str:
|
|
374
|
+
"""Return ``"svg"`` or ``"png"`` based on *format* and cairosvg availability."""
|
|
375
|
+
if format not in ("auto", "svg", "png"):
|
|
376
|
+
raise ValueError(
|
|
377
|
+
f"format must be 'auto', 'svg', or 'png'; got {format!r}"
|
|
378
|
+
)
|
|
379
|
+
if format == "auto":
|
|
380
|
+
# SVG is the visual win when both halves of the Office SVG
|
|
381
|
+
# extension can be produced — i.e. when cairosvg is installed
|
|
382
|
+
# to rasterise the PNG fallback. Fall back to PNG otherwise
|
|
383
|
+
# so callers without cairosvg still get a working picture.
|
|
384
|
+
try:
|
|
385
|
+
import cairosvg # type: ignore[import-not-found] # noqa: F401
|
|
386
|
+
except ImportError:
|
|
387
|
+
return "png"
|
|
388
|
+
return "svg"
|
|
389
|
+
return format
|