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/shapes/graphfrm.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Graphic Frame shape and related objects.
|
|
2
|
+
|
|
3
|
+
A graphic frame is a common container for table, chart, smart art, and media
|
|
4
|
+
objects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, cast
|
|
10
|
+
|
|
11
|
+
from pptx2.dml.effect import (
|
|
12
|
+
BlurFormat,
|
|
13
|
+
GlowFormat,
|
|
14
|
+
ReflectionFormat,
|
|
15
|
+
ShadowFormat,
|
|
16
|
+
SoftEdgeFormat,
|
|
17
|
+
)
|
|
18
|
+
from pptx2.enum.shapes import MSO_SHAPE_TYPE
|
|
19
|
+
from pptx2.shapes.base import BaseShape
|
|
20
|
+
from pptx2.shared import ParentedElementProxy
|
|
21
|
+
from pptx2.spec import (
|
|
22
|
+
GRAPHIC_DATA_URI_CHART,
|
|
23
|
+
GRAPHIC_DATA_URI_OLEOBJ,
|
|
24
|
+
GRAPHIC_DATA_URI_TABLE,
|
|
25
|
+
)
|
|
26
|
+
from pptx2.table import Table
|
|
27
|
+
from pptx2.util import lazyproperty
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from pptx2.chart.chart import Chart
|
|
31
|
+
from pptx2.dml.effect import InnerShadowFormat, PresetShadowFormat, ShadowFormat
|
|
32
|
+
from pptx2.oxml.shapes.graphfrm import CT_GraphicalObjectData, CT_GraphicalObjectFrame
|
|
33
|
+
from pptx2.parts.chart import ChartPart
|
|
34
|
+
from pptx2.parts.slide import BaseSlidePart
|
|
35
|
+
from pptx2.types import ProvidesPart
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GraphicFrame(BaseShape):
|
|
39
|
+
"""Container shape for table, chart, smart art, and media objects.
|
|
40
|
+
|
|
41
|
+
Corresponds to a `p:graphicFrame` element in the shape tree.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, graphicFrame: CT_GraphicalObjectFrame, parent: ProvidesPart):
|
|
45
|
+
super().__init__(graphicFrame, parent)
|
|
46
|
+
self._graphicFrame = graphicFrame
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def chart(self) -> Chart:
|
|
50
|
+
"""The |Chart| object containing the chart in this graphic frame.
|
|
51
|
+
|
|
52
|
+
Raises |ValueError| if this graphic frame does not contain a chart.
|
|
53
|
+
"""
|
|
54
|
+
if not self.has_chart:
|
|
55
|
+
raise ValueError("shape does not contain a chart")
|
|
56
|
+
chart = self.chart_part.chart
|
|
57
|
+
# Cache the parent shape ref on the chart so callers can reach
|
|
58
|
+
# back to the GraphicFrame without keeping the ``add_chart``
|
|
59
|
+
# return value around. ``Chart.shape`` reads this attribute.
|
|
60
|
+
chart._parent_shape = self # type: ignore[attr-defined]
|
|
61
|
+
return chart
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def chart_part(self) -> ChartPart:
|
|
65
|
+
"""The |ChartPart| object containing the chart in this graphic frame."""
|
|
66
|
+
chart_rId = self._graphicFrame.chart_rId
|
|
67
|
+
if chart_rId is None:
|
|
68
|
+
raise ValueError("this graphic frame does not contain a chart")
|
|
69
|
+
return cast("ChartPart", self.part.related_part(chart_rId))
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def has_chart(self) -> bool:
|
|
73
|
+
"""|True| if this graphic frame contains a chart object. |False| otherwise.
|
|
74
|
+
|
|
75
|
+
When |True|, the chart object can be accessed using the `.chart` property.
|
|
76
|
+
"""
|
|
77
|
+
return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_CHART
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def has_table(self) -> bool:
|
|
81
|
+
"""|True| if this graphic frame contains a table object, |False| otherwise.
|
|
82
|
+
|
|
83
|
+
When |True|, the table object can be accessed using the `.table` property.
|
|
84
|
+
"""
|
|
85
|
+
return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_TABLE
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def ole_format(self) -> _OleFormat:
|
|
89
|
+
"""_OleFormat object for this graphic-frame shape.
|
|
90
|
+
|
|
91
|
+
Raises `ValueError` on a GraphicFrame instance that does not contain an OLE object.
|
|
92
|
+
|
|
93
|
+
An shape that contains an OLE object will have `.shape_type` of either
|
|
94
|
+
`EMBEDDED_OLE_OBJECT` or `LINKED_OLE_OBJECT`.
|
|
95
|
+
"""
|
|
96
|
+
if not self._graphicFrame.has_oleobj:
|
|
97
|
+
raise ValueError("not an OLE-object shape")
|
|
98
|
+
return _OleFormat(self._graphicFrame.graphicData, self._parent)
|
|
99
|
+
|
|
100
|
+
@lazyproperty
|
|
101
|
+
def blur(self) -> BlurFormat:
|
|
102
|
+
"""Unconditionally raises |NotImplementedError|.
|
|
103
|
+
|
|
104
|
+
Gaussian blur access for graphic-frame objects is content-specific
|
|
105
|
+
(i.e. different for charts, tables, etc.) and has not yet been
|
|
106
|
+
implemented.
|
|
107
|
+
"""
|
|
108
|
+
raise NotImplementedError("blur property on GraphicFrame not yet supported")
|
|
109
|
+
|
|
110
|
+
@lazyproperty
|
|
111
|
+
def glow(self) -> GlowFormat:
|
|
112
|
+
"""Unconditionally raises |NotImplementedError|.
|
|
113
|
+
|
|
114
|
+
Glow effect access for graphic-frame objects is not yet implemented.
|
|
115
|
+
"""
|
|
116
|
+
raise NotImplementedError("glow property on GraphicFrame not yet supported")
|
|
117
|
+
|
|
118
|
+
@lazyproperty
|
|
119
|
+
def reflection(self) -> ReflectionFormat:
|
|
120
|
+
"""Unconditionally raises |NotImplementedError|.
|
|
121
|
+
|
|
122
|
+
Reflection effect access for graphic-frame objects is not yet
|
|
123
|
+
implemented.
|
|
124
|
+
"""
|
|
125
|
+
raise NotImplementedError("reflection property on GraphicFrame not yet supported")
|
|
126
|
+
|
|
127
|
+
@lazyproperty
|
|
128
|
+
def shadow(self) -> ShadowFormat | None:
|
|
129
|
+
"""Returns ``None``: shadow access on a |GraphicFrame| is unsupported.
|
|
130
|
+
|
|
131
|
+
Charts and tables expose their effect tree at content-specific
|
|
132
|
+
locations (e.g. ``c:spPr/a:effectLst`` on a chart) and the unified
|
|
133
|
+
:class:`~pptx2.dml.effect.ShadowFormat` facade doesn't apply.
|
|
134
|
+
Returning ``None`` keeps callers that probe ``shape.shadow`` across
|
|
135
|
+
every shape on a slide free from per-type ``try/except`` guards;
|
|
136
|
+
``if shape.shadow is None`` is the supported "no facade available"
|
|
137
|
+
check.
|
|
138
|
+
"""
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
@lazyproperty
|
|
142
|
+
def inner_shadow(self) -> InnerShadowFormat | None:
|
|
143
|
+
"""Returns ``None``: inner-shadow access on a |GraphicFrame| is unsupported.
|
|
144
|
+
|
|
145
|
+
A graphic frame has no ``spPr``, so (like :attr:`shadow`) the unified
|
|
146
|
+
effect facade doesn't apply; ``None`` lets callers probe every shape
|
|
147
|
+
without a per-type ``try/except``.
|
|
148
|
+
"""
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
@lazyproperty
|
|
152
|
+
def preset_shadow(self) -> PresetShadowFormat | None:
|
|
153
|
+
"""Returns ``None``: preset-shadow access on a |GraphicFrame| is unsupported."""
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
@lazyproperty
|
|
157
|
+
def soft_edges(self) -> SoftEdgeFormat:
|
|
158
|
+
"""Unconditionally raises |NotImplementedError|.
|
|
159
|
+
|
|
160
|
+
Soft-edge effect access for graphic-frame objects is not yet implemented.
|
|
161
|
+
"""
|
|
162
|
+
raise NotImplementedError("soft_edges property on GraphicFrame not yet supported")
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def three_d(self):
|
|
166
|
+
"""Unconditionally raises |NotImplementedError|.
|
|
167
|
+
|
|
168
|
+
A graphic frame has no ``p:spPr``, so the |ThreeDFormat| facade does
|
|
169
|
+
not apply (mirrors :attr:`blur` / :attr:`glow` behaviour).
|
|
170
|
+
"""
|
|
171
|
+
raise NotImplementedError("three_d property on GraphicFrame not yet supported")
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def shape_type(self) -> MSO_SHAPE_TYPE:
|
|
175
|
+
"""Optional member of `MSO_SHAPE_TYPE` identifying the type of this shape.
|
|
176
|
+
|
|
177
|
+
Possible values are `MSO_SHAPE_TYPE.CHART`, `MSO_SHAPE_TYPE.TABLE`,
|
|
178
|
+
`MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT`, `MSO_SHAPE_TYPE.LINKED_OLE_OBJECT`.
|
|
179
|
+
|
|
180
|
+
This value is `None` when none of these four types apply, for example when the shape
|
|
181
|
+
contains SmartArt.
|
|
182
|
+
"""
|
|
183
|
+
graphicData_uri = self._graphicFrame.graphicData_uri
|
|
184
|
+
if graphicData_uri == GRAPHIC_DATA_URI_CHART:
|
|
185
|
+
return MSO_SHAPE_TYPE.CHART
|
|
186
|
+
elif graphicData_uri == GRAPHIC_DATA_URI_TABLE:
|
|
187
|
+
return MSO_SHAPE_TYPE.TABLE
|
|
188
|
+
elif graphicData_uri == GRAPHIC_DATA_URI_OLEOBJ:
|
|
189
|
+
return (
|
|
190
|
+
MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT
|
|
191
|
+
if self._graphicFrame.is_embedded_ole_obj
|
|
192
|
+
else MSO_SHAPE_TYPE.LINKED_OLE_OBJECT
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
return None # pyright: ignore[reportReturnType]
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def table(self) -> Table:
|
|
199
|
+
"""The |Table| object contained in this graphic frame.
|
|
200
|
+
|
|
201
|
+
Raises |ValueError| if this graphic frame does not contain a table.
|
|
202
|
+
"""
|
|
203
|
+
if not self.has_table:
|
|
204
|
+
raise ValueError("shape does not contain a table")
|
|
205
|
+
tbl = self._graphicFrame.graphic.graphicData.tbl
|
|
206
|
+
return Table(tbl, self)
|
|
207
|
+
|
|
208
|
+
def render_to_png(self, **kwargs):
|
|
209
|
+
"""Render this graphic frame's region to a PNG.
|
|
210
|
+
|
|
211
|
+
Renders the parent slide via headless LibreOffice (the same path
|
|
212
|
+
used by :meth:`Slide.render_thumbnail`), then crops the result to
|
|
213
|
+
this frame's bounding box. Useful for getting a standalone
|
|
214
|
+
chart / table image out of a deck.
|
|
215
|
+
|
|
216
|
+
Forwards keyword arguments (``out_path``, ``soffice_bin``,
|
|
217
|
+
``timeout``) to :meth:`Slide.render_thumbnail`. ``return_bytes``
|
|
218
|
+
is also supported and returns the cropped PNG bytes directly.
|
|
219
|
+
|
|
220
|
+
Requires :pypi:`Pillow` for cropping (already a python-pptx2
|
|
221
|
+
dependency) and ``soffice`` on PATH.
|
|
222
|
+
"""
|
|
223
|
+
from io import BytesIO
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
from PIL import Image
|
|
227
|
+
except ImportError as e: # pragma: no cover - Pillow is a dep
|
|
228
|
+
raise RuntimeError(
|
|
229
|
+
"render_to_png requires Pillow; install python-pptx2[render]"
|
|
230
|
+
) from e
|
|
231
|
+
|
|
232
|
+
return_bytes = kwargs.pop("return_bytes", False)
|
|
233
|
+
out_path = kwargs.pop("out_path", None)
|
|
234
|
+
|
|
235
|
+
slide = self.part.slide
|
|
236
|
+
png_bytes = slide.render_thumbnail(return_bytes=True, **kwargs)
|
|
237
|
+
|
|
238
|
+
with Image.open(BytesIO(png_bytes)) as img:
|
|
239
|
+
# Compute pixel bounds of this graphic frame from the EMU-vs-pixel
|
|
240
|
+
# scale of the rendered slide.
|
|
241
|
+
prs = self.part.package.presentation_part.presentation
|
|
242
|
+
slide_w_emu = int(prs.slide_width or 9144000)
|
|
243
|
+
slide_h_emu = int(prs.slide_height or 6858000)
|
|
244
|
+
scale_x = img.width / slide_w_emu
|
|
245
|
+
scale_y = img.height / slide_h_emu
|
|
246
|
+
left = int(int(self.left) * scale_x)
|
|
247
|
+
top = int(int(self.top) * scale_y)
|
|
248
|
+
right = int((int(self.left) + int(self.width)) * scale_x)
|
|
249
|
+
bottom = int((int(self.top) + int(self.height)) * scale_y)
|
|
250
|
+
cropped = img.crop((left, top, right, bottom))
|
|
251
|
+
buf = BytesIO()
|
|
252
|
+
cropped.save(buf, format="PNG")
|
|
253
|
+
data = buf.getvalue()
|
|
254
|
+
|
|
255
|
+
if return_bytes:
|
|
256
|
+
return data
|
|
257
|
+
if out_path is not None:
|
|
258
|
+
from pathlib import Path
|
|
259
|
+
|
|
260
|
+
target = Path(out_path)
|
|
261
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
262
|
+
target.write_bytes(data)
|
|
263
|
+
return target
|
|
264
|
+
# No destination given: persist to a stable temp file.
|
|
265
|
+
import os
|
|
266
|
+
import tempfile
|
|
267
|
+
from pathlib import Path
|
|
268
|
+
|
|
269
|
+
fd, persistent = tempfile.mkstemp(prefix="pptx-frame-", suffix=".png")
|
|
270
|
+
os.close(fd)
|
|
271
|
+
try:
|
|
272
|
+
Path(persistent).write_bytes(data)
|
|
273
|
+
except Exception:
|
|
274
|
+
# Don't leak the empty temp file when the write fails.
|
|
275
|
+
try:
|
|
276
|
+
os.remove(persistent)
|
|
277
|
+
except OSError:
|
|
278
|
+
pass
|
|
279
|
+
raise
|
|
280
|
+
return Path(persistent)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class _OleFormat(ParentedElementProxy):
|
|
284
|
+
"""Provides attributes on an embedded OLE object."""
|
|
285
|
+
|
|
286
|
+
part: BaseSlidePart # pyright: ignore[reportIncompatibleMethodOverride]
|
|
287
|
+
|
|
288
|
+
def __init__(self, graphicData: CT_GraphicalObjectData, parent: ProvidesPart):
|
|
289
|
+
super().__init__(graphicData, parent)
|
|
290
|
+
self._graphicData = graphicData
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def blob(self) -> bytes | None:
|
|
294
|
+
"""Optional bytes of OLE object, suitable for loading or saving as a file.
|
|
295
|
+
|
|
296
|
+
This value is `None` if the embedded object does not represent a "file".
|
|
297
|
+
"""
|
|
298
|
+
blob_rId = self._graphicData.blob_rId
|
|
299
|
+
if blob_rId is None:
|
|
300
|
+
return None
|
|
301
|
+
return self.part.related_part(blob_rId).blob
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def prog_id(self) -> str | None:
|
|
305
|
+
"""str "progId" attribute of this embedded OLE object.
|
|
306
|
+
|
|
307
|
+
The progId is a str like "Excel.Sheet.12" that identifies the "file-type" of the embedded
|
|
308
|
+
object, or perhaps more precisely, the application (aka. "server" in OLE parlance) to be
|
|
309
|
+
used to open this object.
|
|
310
|
+
"""
|
|
311
|
+
return self._graphicData.progId
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def show_as_icon(self) -> bool | None:
|
|
315
|
+
"""True when OLE object should appear as an icon (rather than preview)."""
|
|
316
|
+
return self._graphicData.showAsIcon
|
pptx2/shapes/group.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""GroupShape and related objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Iterator
|
|
6
|
+
|
|
7
|
+
from pptx2.dml.effect import (
|
|
8
|
+
BlurFormat,
|
|
9
|
+
GlowFormat,
|
|
10
|
+
InnerShadowFormat,
|
|
11
|
+
PresetShadowFormat,
|
|
12
|
+
ReflectionFormat,
|
|
13
|
+
ShadowFormat,
|
|
14
|
+
SoftEdgeFormat,
|
|
15
|
+
)
|
|
16
|
+
from pptx2.dml.fill import FillFormat
|
|
17
|
+
from pptx2.enum.shapes import MSO_SHAPE_TYPE
|
|
18
|
+
from pptx2.shapes.base import BaseShape
|
|
19
|
+
from pptx2.util import Emu, _coerce_emu, lazyproperty
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pptx2.action import ActionSetting
|
|
23
|
+
from pptx2.oxml.shapes import ShapeElement
|
|
24
|
+
from pptx2.oxml.shapes.groupshape import CT_GroupShape
|
|
25
|
+
from pptx2.shapes.shapetree import GroupShapes
|
|
26
|
+
from pptx2.types import ProvidesPart
|
|
27
|
+
from pptx2.util import Length
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GroupShape(BaseShape):
|
|
31
|
+
"""A shape that acts as a container for other shapes."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart):
|
|
34
|
+
super().__init__(grpSp, parent)
|
|
35
|
+
self._grpSp = grpSp
|
|
36
|
+
|
|
37
|
+
@lazyproperty
|
|
38
|
+
def click_action(self) -> ActionSetting:
|
|
39
|
+
"""Unconditionally raises `TypeError`.
|
|
40
|
+
|
|
41
|
+
A group shape cannot have a click action or hover action.
|
|
42
|
+
"""
|
|
43
|
+
raise TypeError("a group shape cannot have a click action")
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def has_text_frame(self) -> bool:
|
|
47
|
+
"""Unconditionally |False|.
|
|
48
|
+
|
|
49
|
+
A group shape does not have a textframe and cannot itself contain text. This does not
|
|
50
|
+
impact the ability of shapes contained by the group to each have their own text.
|
|
51
|
+
"""
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
@lazyproperty
|
|
55
|
+
def blur(self) -> BlurFormat:
|
|
56
|
+
"""|BlurFormat| object representing the Gaussian blur on this group."""
|
|
57
|
+
return BlurFormat(self._grpSp.grpSpPr)
|
|
58
|
+
|
|
59
|
+
@lazyproperty
|
|
60
|
+
def glow(self) -> GlowFormat:
|
|
61
|
+
"""|GlowFormat| object representing glow effect for this group."""
|
|
62
|
+
return GlowFormat(self._grpSp.grpSpPr)
|
|
63
|
+
|
|
64
|
+
@lazyproperty
|
|
65
|
+
def reflection(self) -> ReflectionFormat:
|
|
66
|
+
"""|ReflectionFormat| object representing the reflection on this group."""
|
|
67
|
+
return ReflectionFormat(self._grpSp.grpSpPr)
|
|
68
|
+
|
|
69
|
+
@lazyproperty
|
|
70
|
+
def shadow(self) -> ShadowFormat:
|
|
71
|
+
"""|ShadowFormat| object representing shadow effect for this group.
|
|
72
|
+
|
|
73
|
+
A |ShadowFormat| object is always returned, even when no shadow is explicitly defined on
|
|
74
|
+
this group shape (i.e. when the group inherits its shadow behavior).
|
|
75
|
+
"""
|
|
76
|
+
return ShadowFormat(self._grpSp.grpSpPr)
|
|
77
|
+
|
|
78
|
+
@lazyproperty
|
|
79
|
+
def soft_edges(self) -> SoftEdgeFormat:
|
|
80
|
+
"""|SoftEdgeFormat| object representing soft-edge effect for this group."""
|
|
81
|
+
return SoftEdgeFormat(self._grpSp.grpSpPr)
|
|
82
|
+
|
|
83
|
+
@lazyproperty
|
|
84
|
+
def inner_shadow(self) -> InnerShadowFormat:
|
|
85
|
+
"""|InnerShadowFormat| for this group (over ``p:grpSpPr``).
|
|
86
|
+
|
|
87
|
+
Overrides the |BaseShape| version, which targets ``spPr`` — a group
|
|
88
|
+
exposes ``grpSpPr`` instead, so the inherited accessor would raise.
|
|
89
|
+
"""
|
|
90
|
+
return InnerShadowFormat(self._grpSp.grpSpPr)
|
|
91
|
+
|
|
92
|
+
@lazyproperty
|
|
93
|
+
def preset_shadow(self) -> PresetShadowFormat:
|
|
94
|
+
"""|PresetShadowFormat| for this group (over ``p:grpSpPr``)."""
|
|
95
|
+
return PresetShadowFormat(self._grpSp.grpSpPr)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def three_d(self):
|
|
99
|
+
"""Unconditionally raises |NotImplementedError|.
|
|
100
|
+
|
|
101
|
+
A group shape's ``p:grpSpPr`` legally carries ``a:scene3d`` but not
|
|
102
|
+
``a:sp3d`` (bevel / extrusion), so the |ThreeDFormat| facade — which
|
|
103
|
+
writes both — cannot target it without emitting schema-invalid XML.
|
|
104
|
+
Apply 3-D formatting to the member shapes instead.
|
|
105
|
+
"""
|
|
106
|
+
raise NotImplementedError("three_d property on GroupShape not supported")
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def fill(self) -> FillFormat:
|
|
110
|
+
"""|FillFormat| instance for this group, providing access to fill properties.
|
|
111
|
+
|
|
112
|
+
A group's ``p:grpSpPr`` admits a fill (but, unlike a regular shape, *not* a
|
|
113
|
+
line — the OOXML schema does not allow ``a:ln`` on a group). Setting a fill
|
|
114
|
+
tints the whole group; member shapes that declare their own fill are
|
|
115
|
+
unaffected and paint on top. Use ``fill.solid()``, ``fill.gradient()``,
|
|
116
|
+
``fill.background()`` (transparent), etc., exactly as on an autoshape::
|
|
117
|
+
|
|
118
|
+
group.fill.solid()
|
|
119
|
+
group.fill.fore_color.rgb = "1F4E79"
|
|
120
|
+
"""
|
|
121
|
+
return FillFormat.from_fill_parent(self._grpSp.grpSpPr)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def shape_type(self) -> MSO_SHAPE_TYPE:
|
|
125
|
+
"""Member of :ref:`MsoShapeType` identifying the type of this shape.
|
|
126
|
+
|
|
127
|
+
Unconditionally `MSO_SHAPE_TYPE.GROUP` in this case
|
|
128
|
+
"""
|
|
129
|
+
return MSO_SHAPE_TYPE.GROUP
|
|
130
|
+
|
|
131
|
+
@lazyproperty
|
|
132
|
+
def shapes(self) -> GroupShapes:
|
|
133
|
+
"""|GroupShapes| object for this group.
|
|
134
|
+
|
|
135
|
+
The |GroupShapes| object provides access to the group's member shapes and provides methods
|
|
136
|
+
for adding new ones.
|
|
137
|
+
"""
|
|
138
|
+
from pptx2.shapes.shapetree import GroupShapes
|
|
139
|
+
|
|
140
|
+
return GroupShapes(self._element, self)
|
|
141
|
+
|
|
142
|
+
def move(self, dx: Length | int | float, dy: Length | int | float) -> GroupShape:
|
|
143
|
+
"""Translate the entire group by (*dx*, *dy*) and return self.
|
|
144
|
+
|
|
145
|
+
*dx* and *dy* are lengths (e.g. ``Inches(1)``, ``Emu(...)``, or a bare
|
|
146
|
+
EMU ``int``)::
|
|
147
|
+
|
|
148
|
+
group.move(Inches(0.5), Inches(-0.25))
|
|
149
|
+
|
|
150
|
+
The group's offset, its child-coordinate origin, and every member are
|
|
151
|
+
shifted by the same translation. Shifting the members + ``chOff`` (not
|
|
152
|
+
just the group's own ``off``) is what makes the move *durable*: a later
|
|
153
|
+
``recalculate_extents()`` — triggered by ``group.shapes.add_*`` or
|
|
154
|
+
``fit_to_children()`` — recomputes ``off`` from the member geometry, so
|
|
155
|
+
a move that only touched ``off`` would silently snap back.
|
|
156
|
+
"""
|
|
157
|
+
grpSp = self._grpSp
|
|
158
|
+
edx = int(_coerce_emu(dx))
|
|
159
|
+
edy = int(_coerce_emu(dy))
|
|
160
|
+
# Express the translation in the group's child-coordinate space too, in
|
|
161
|
+
# case the group is scaled (ext != chExt); for the common unscaled
|
|
162
|
+
# group this is just (edx, edy).
|
|
163
|
+
ext_cx = int(self.width) if self.width is not None else 0
|
|
164
|
+
ext_cy = int(self.height) if self.height is not None else 0
|
|
165
|
+
chExt = grpSp.chExt
|
|
166
|
+
ch_cx, ch_cy = int(chExt.cx or 0), int(chExt.cy or 0)
|
|
167
|
+
cdx = round(edx * ch_cx / ext_cx) if ext_cx else edx
|
|
168
|
+
cdy = round(edy * ch_cy / ext_cy) if ext_cy else edy
|
|
169
|
+
|
|
170
|
+
self.left = Emu((int(self.left) if self.left is not None else 0) + edx)
|
|
171
|
+
self.top = Emu((int(self.top) if self.top is not None else 0) + edy)
|
|
172
|
+
chOff = grpSp.chOff
|
|
173
|
+
chOff.x = Emu(int(chOff.x or 0) + cdx)
|
|
174
|
+
chOff.y = Emu(int(chOff.y or 0) + cdy)
|
|
175
|
+
for elm in grpSp.iter_shape_elms():
|
|
176
|
+
elm.x = Emu(int(elm.x or 0) + cdx)
|
|
177
|
+
elm.y = Emu(int(elm.y or 0) + cdy)
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def walk(self) -> Iterator[BaseShape]:
|
|
181
|
+
"""Generate every descendant shape, recursing into nested groups.
|
|
182
|
+
|
|
183
|
+
Yields shapes depth-first in document (z-order) order. Nested
|
|
184
|
+
|GroupShape| objects are themselves yielded *before* their children, so
|
|
185
|
+
callers that only want leaf shapes can filter with
|
|
186
|
+
``s.shape_type != MSO_SHAPE_TYPE.GROUP``. This makes whole-tree layout,
|
|
187
|
+
measurement, and lint passes possible without hand-rolled recursion::
|
|
188
|
+
|
|
189
|
+
for shape in group.walk():
|
|
190
|
+
...
|
|
191
|
+
"""
|
|
192
|
+
for shape in self.shapes:
|
|
193
|
+
yield shape
|
|
194
|
+
if isinstance(shape, GroupShape):
|
|
195
|
+
yield from shape.walk()
|
|
196
|
+
|
|
197
|
+
def fit_to_children(self) -> GroupShape:
|
|
198
|
+
"""Shrink-wrap the group's offset/extent to tightly bound its children.
|
|
199
|
+
|
|
200
|
+
Recalculates the group's position and size (``a:off`` / ``a:ext``) from
|
|
201
|
+
the current geometry of its member shapes — the same recalculation that
|
|
202
|
+
runs automatically when a shape is added through ``group.shapes.add_*``.
|
|
203
|
+
Call it after moving or resizing member shapes directly so the group's
|
|
204
|
+
``bbox`` (and any lint that relies on it) stays accurate. Returns self.
|
|
205
|
+
"""
|
|
206
|
+
self._grpSp.recalculate_extents()
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
def ungroup(self) -> list[BaseShape]:
|
|
210
|
+
"""Dissolve the group, promoting its member shapes to the parent and return them.
|
|
211
|
+
|
|
212
|
+
Each child is re-parented to the group's container (the slide shape tree
|
|
213
|
+
or an enclosing group) with its position and size transformed from the
|
|
214
|
+
group's child coordinate space into the container's space, so shapes do
|
|
215
|
+
not move or resize visually. Z-order is preserved: the promoted shapes
|
|
216
|
+
occupy the group's former slot. The (now empty) group element is removed.
|
|
217
|
+
|
|
218
|
+
Raises ``ValueError`` if the group is rotated or flipped — baking such a
|
|
219
|
+
transform into each child is ambiguous and unsupported; reset rotation
|
|
220
|
+
and flip to zero before ungrouping.
|
|
221
|
+
"""
|
|
222
|
+
grpSp = self._grpSp
|
|
223
|
+
container = grpSp.getparent()
|
|
224
|
+
if container is None:
|
|
225
|
+
raise ValueError("cannot ungroup a group that is not in a shape tree")
|
|
226
|
+
if self.rotation or grpSp.flipH or grpSp.flipV:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
"ungroup() does not support a rotated or flipped group; reset "
|
|
229
|
+
"rotation and flip to 0 before ungrouping"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Group offset/extent (in the container's coordinate space) and the
|
|
233
|
+
# group's own child coordinate space (chOff/chExt).
|
|
234
|
+
off_x = int(self.left) if self.left is not None else 0
|
|
235
|
+
off_y = int(self.top) if self.top is not None else 0
|
|
236
|
+
ext_cx = int(self.width) if self.width is not None else 0
|
|
237
|
+
ext_cy = int(self.height) if self.height is not None else 0
|
|
238
|
+
chOff, chExt = grpSp.chOff, grpSp.chExt
|
|
239
|
+
ch_x, ch_y = int(chOff.x or 0), int(chOff.y or 0)
|
|
240
|
+
ch_cx, ch_cy = int(chExt.cx or 0), int(chExt.cy or 0)
|
|
241
|
+
sx = (ext_cx / ch_cx) if ch_cx else 1.0
|
|
242
|
+
sy = (ext_cy / ch_cy) if ch_cy else 1.0
|
|
243
|
+
|
|
244
|
+
# Pre-compute each child's container-space geometry from its child-space
|
|
245
|
+
# coords before re-parenting (so the read isn't affected by the moves).
|
|
246
|
+
placements: list[tuple[ShapeElement, int, int, int, int]] = []
|
|
247
|
+
for elm in grpSp.iter_shape_elms():
|
|
248
|
+
cx0 = int(elm.x or 0)
|
|
249
|
+
cy0 = int(elm.y or 0)
|
|
250
|
+
cw = int(elm.cx or 0)
|
|
251
|
+
chgt = int(elm.cy or 0)
|
|
252
|
+
new_x = off_x + round((cx0 - ch_x) * sx)
|
|
253
|
+
new_y = off_y + round((cy0 - ch_y) * sy)
|
|
254
|
+
placements.append((elm, new_x, new_y, round(cw * sx), round(chgt * sy)))
|
|
255
|
+
|
|
256
|
+
promoted: list[BaseShape] = []
|
|
257
|
+
for elm, nx, ny, nw, nh in placements:
|
|
258
|
+
grpSp.remove(elm)
|
|
259
|
+
container.insert(container.index(grpSp), elm)
|
|
260
|
+
elm.x, elm.y, elm.cx, elm.cy = Emu(nx), Emu(ny), Emu(nw), Emu(nh)
|
|
261
|
+
promoted.append(self._parent._shape_factory(elm)) # pyright: ignore[reportAttributeAccessIssue]
|
|
262
|
+
|
|
263
|
+
container.remove(grpSp)
|
|
264
|
+
return promoted
|