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/dml/fill.py
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
"""DrawingML objects related to fill."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from pptx2.dml.color import ColorFormat, RGBColor
|
|
10
|
+
from pptx2.enum.dml import MSO_FILL, MSO_THEME_COLOR
|
|
11
|
+
from pptx2.oxml.dml.fill import (
|
|
12
|
+
CT_BlipFillProperties,
|
|
13
|
+
CT_GradientFillProperties,
|
|
14
|
+
CT_GroupFillProperties,
|
|
15
|
+
CT_NoFillProperties,
|
|
16
|
+
CT_PatternFillProperties,
|
|
17
|
+
CT_SolidColorFillProperties,
|
|
18
|
+
)
|
|
19
|
+
from pptx2.oxml.xmlchemy import BaseOxmlElement
|
|
20
|
+
from pptx2.shared import ElementProxy
|
|
21
|
+
from pptx2.util import lazyproperty
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from pptx2.enum.dml import MSO_FILL_TYPE
|
|
25
|
+
from pptx2.oxml.xmlchemy import BaseOxmlElement
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _looks_like_color_value(value: Any) -> bool:
|
|
29
|
+
"""True when *value* is plausibly a single colour rather than a stop list."""
|
|
30
|
+
if isinstance(value, (str, RGBColor)):
|
|
31
|
+
return True
|
|
32
|
+
# 3-tuple of ints is an RGB triple, not a (color, position) pair.
|
|
33
|
+
return (
|
|
34
|
+
isinstance(value, tuple)
|
|
35
|
+
and len(value) == 3
|
|
36
|
+
and all(isinstance(v, int) for v in value)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_stop_input(stops: Any):
|
|
41
|
+
"""Yield ``(color, position)`` tuples from a flexible stops sequence.
|
|
42
|
+
|
|
43
|
+
Accepts:
|
|
44
|
+
|
|
45
|
+
* iterable of ``(color, position)`` 2-tuples (positions are taken
|
|
46
|
+
verbatim).
|
|
47
|
+
* iterable of bare colours (positions are spread evenly across
|
|
48
|
+
``[0.0, 1.0]``).
|
|
49
|
+
"""
|
|
50
|
+
seq = list(stops)
|
|
51
|
+
if not seq:
|
|
52
|
+
return
|
|
53
|
+
# Decide whether items are (color, position) pairs or bare colours.
|
|
54
|
+
pair_form = all(
|
|
55
|
+
isinstance(item, tuple)
|
|
56
|
+
and len(item) == 2
|
|
57
|
+
and isinstance(item[1], (int, float))
|
|
58
|
+
and not (isinstance(item, tuple) and all(isinstance(v, int) for v in item))
|
|
59
|
+
for item in seq
|
|
60
|
+
)
|
|
61
|
+
# Special-case: an RGB 3-tuple of ints would otherwise be confused
|
|
62
|
+
# with (color, position). Treat any item that's a 3-int-tuple as a
|
|
63
|
+
# bare RGB triple regardless.
|
|
64
|
+
if any(
|
|
65
|
+
isinstance(item, tuple)
|
|
66
|
+
and len(item) == 3
|
|
67
|
+
and all(isinstance(v, int) for v in item)
|
|
68
|
+
for item in seq
|
|
69
|
+
):
|
|
70
|
+
pair_form = False
|
|
71
|
+
if pair_form:
|
|
72
|
+
for color, position in seq:
|
|
73
|
+
yield color, float(position)
|
|
74
|
+
else:
|
|
75
|
+
n = len(seq)
|
|
76
|
+
if n == 1:
|
|
77
|
+
yield seq[0], 0.0
|
|
78
|
+
return
|
|
79
|
+
for i, color in enumerate(seq):
|
|
80
|
+
yield color, i / (n - 1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FillFormat(object):
|
|
84
|
+
"""Provides access to the current fill properties.
|
|
85
|
+
|
|
86
|
+
Also provides methods to change the fill type.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, eg_fill_properties_parent: BaseOxmlElement, fill_obj: _Fill):
|
|
90
|
+
super(FillFormat, self).__init__()
|
|
91
|
+
self._xPr = eg_fill_properties_parent
|
|
92
|
+
self._fill = fill_obj
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_fill_parent(cls, eg_fillProperties_parent: BaseOxmlElement) -> FillFormat:
|
|
96
|
+
"""
|
|
97
|
+
Return a |FillFormat| instance initialized to the settings contained
|
|
98
|
+
in *eg_fillProperties_parent*, which must be an element having
|
|
99
|
+
EG_FillProperties in its child element sequence in the XML schema.
|
|
100
|
+
"""
|
|
101
|
+
fill_elm = eg_fillProperties_parent.eg_fillProperties
|
|
102
|
+
fill = _Fill(fill_elm)
|
|
103
|
+
fill_format = cls(eg_fillProperties_parent, fill)
|
|
104
|
+
return fill_format
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def back_color(self):
|
|
108
|
+
"""Return a |ColorFormat| object representing background color.
|
|
109
|
+
|
|
110
|
+
This property is only applicable to pattern fills and lines.
|
|
111
|
+
"""
|
|
112
|
+
return self._fill.back_color
|
|
113
|
+
|
|
114
|
+
def background(self):
|
|
115
|
+
"""
|
|
116
|
+
Sets the fill type to noFill, i.e. transparent.
|
|
117
|
+
"""
|
|
118
|
+
noFill = self._xPr.get_or_change_to_noFill()
|
|
119
|
+
self._fill = _NoFill(noFill)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def fore_color(self):
|
|
123
|
+
"""
|
|
124
|
+
Return a |ColorFormat| instance representing the foreground color of
|
|
125
|
+
this fill.
|
|
126
|
+
"""
|
|
127
|
+
return self._fill.fore_color
|
|
128
|
+
|
|
129
|
+
_GRADIENT_KINDS = ("linear", "radial", "rectangular", "shape")
|
|
130
|
+
|
|
131
|
+
def gradient(self, kind: str = "linear", *, angle: float | None = None):
|
|
132
|
+
"""Sets the fill type to gradient.
|
|
133
|
+
|
|
134
|
+
If the fill is not already a gradient, a default gradient is added.
|
|
135
|
+
The default gradient corresponds to the default in the built-in
|
|
136
|
+
PowerPoint "White" template. This gradient is linear at angle
|
|
137
|
+
90-degrees (upward), with two stops. The first stop is Accent-1 with
|
|
138
|
+
tint 100%, shade 100%, and satMod 130%. The second stop is Accent-1
|
|
139
|
+
with tint 50%, shade 100%, and satMod 350%.
|
|
140
|
+
|
|
141
|
+
`kind` selects the gradient shape:
|
|
142
|
+
|
|
143
|
+
* ``"linear"`` (default) — straight-line gradient parameterized by
|
|
144
|
+
angle (the historical behavior).
|
|
145
|
+
* ``"radial"`` — circular gradient (`<a:path path="circle"/>`).
|
|
146
|
+
* ``"rectangular"`` — rectangular gradient (`<a:path path="rect"/>`).
|
|
147
|
+
* ``"shape"`` — gradient that follows the bounding shape of its
|
|
148
|
+
container (`<a:path path="shape"/>`).
|
|
149
|
+
|
|
150
|
+
When called on an existing gradient with a different kind, the
|
|
151
|
+
gradient stops are preserved; only the path/lin shading element is
|
|
152
|
+
swapped out. Invalid `kind` values raise ``ValueError`` *before*
|
|
153
|
+
any fill mutation, leaving the existing fill untouched.
|
|
154
|
+
|
|
155
|
+
The optional ``angle`` keyword sets ``gradient_angle`` after the
|
|
156
|
+
fill has been initialized — symmetric with
|
|
157
|
+
:meth:`linear_gradient`'s ``angle=`` parameter. Only meaningful
|
|
158
|
+
for ``kind="linear"``; passing it with a non-linear ``kind`` is
|
|
159
|
+
a :class:`ValueError`.
|
|
160
|
+
"""
|
|
161
|
+
if kind not in self._GRADIENT_KINDS:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"gradient kind must be one of %r; got %r"
|
|
164
|
+
% (self._GRADIENT_KINDS, kind)
|
|
165
|
+
)
|
|
166
|
+
if angle is not None and kind != "linear":
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"angle= is only valid for kind='linear'; got kind=%r" % kind
|
|
169
|
+
)
|
|
170
|
+
gradFill = self._xPr.get_or_change_to_gradFill()
|
|
171
|
+
if kind != "linear":
|
|
172
|
+
gradFill.change_to_kind(kind)
|
|
173
|
+
elif gradFill.path is not None:
|
|
174
|
+
# convert an existing radial/rectangular/shape gradient back to linear
|
|
175
|
+
gradFill.change_to_kind("linear")
|
|
176
|
+
self._fill = _GradFill(gradFill)
|
|
177
|
+
if angle is not None:
|
|
178
|
+
self.gradient_angle = float(angle)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def gradient_angle(self):
|
|
182
|
+
"""Angle in float degrees of line of a linear gradient.
|
|
183
|
+
|
|
184
|
+
Read/Write. May be |None|, indicating the angle should be inherited
|
|
185
|
+
from the style hierarchy.
|
|
186
|
+
|
|
187
|
+
Angle convention (OOXML)::
|
|
188
|
+
|
|
189
|
+
0 → left-to-right
|
|
190
|
+
90 → top-to-bottom
|
|
191
|
+
180 → right-to-left
|
|
192
|
+
270 → bottom-to-top
|
|
193
|
+
|
|
194
|
+
Raises |TypeError| when the fill type is not
|
|
195
|
+
MSO_FILL_TYPE.GRADIENT. Raises |ValueError| for a non-linear
|
|
196
|
+
gradient (e.g. a radial gradient).
|
|
197
|
+
"""
|
|
198
|
+
if self.type != MSO_FILL.GRADIENT:
|
|
199
|
+
raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
|
|
200
|
+
return self._fill.gradient_angle
|
|
201
|
+
|
|
202
|
+
@gradient_angle.setter
|
|
203
|
+
def gradient_angle(self, value):
|
|
204
|
+
if self.type != MSO_FILL.GRADIENT:
|
|
205
|
+
raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
|
|
206
|
+
self._fill.gradient_angle = value
|
|
207
|
+
|
|
208
|
+
def linear_gradient(
|
|
209
|
+
self,
|
|
210
|
+
stops,
|
|
211
|
+
end=None,
|
|
212
|
+
*,
|
|
213
|
+
angle: float = 0.0,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Apply a linear gradient with two-or-more colour stops in one call.
|
|
216
|
+
|
|
217
|
+
Two equivalent input shapes are accepted:
|
|
218
|
+
|
|
219
|
+
* ``fill.linear_gradient("#06D6FE", "#B14AED", angle=90)`` —
|
|
220
|
+
two-stop short form: pass *start* as ``stops`` and *end* as
|
|
221
|
+
*end*; the stops are placed at positions ``0.0`` and ``1.0``.
|
|
222
|
+
* ``fill.linear_gradient([("#06D6FE", 0.0), ("#FFFFFF", 0.5),
|
|
223
|
+
("#B14AED", 1.0)], angle=45)`` — explicit list of
|
|
224
|
+
``(color, position)`` pairs (or just colors, in which case
|
|
225
|
+
positions are spread evenly across ``[0.0, 1.0]``).
|
|
226
|
+
|
|
227
|
+
*angle* follows the OOXML convention: ``0`` is left-to-right,
|
|
228
|
+
``90`` is top-to-bottom, ``180`` is right-to-left, ``270`` is
|
|
229
|
+
bottom-to-top.
|
|
230
|
+
|
|
231
|
+
Each colour may be an :class:`~pptx2.dml.color.RGBColor`,
|
|
232
|
+
a 6-digit hex string (with or without leading ``#``), or a
|
|
233
|
+
3-tuple of ints. 3-digit hex shorthand (``"#FFF"``) is **not**
|
|
234
|
+
accepted — write the full 6-digit form.
|
|
235
|
+
|
|
236
|
+
This is a convenience wrapper over the lower-level
|
|
237
|
+
:meth:`gradient` + :attr:`gradient_stops` API; reach for that
|
|
238
|
+
for fine-grained stop manipulation.
|
|
239
|
+
"""
|
|
240
|
+
if end is not None:
|
|
241
|
+
# Two-positional-argument form.
|
|
242
|
+
pairs: list[tuple[Any, float]] = [(stops, 0.0), (end, 1.0)]
|
|
243
|
+
elif _looks_like_color_value(stops):
|
|
244
|
+
raise TypeError(
|
|
245
|
+
"linear_gradient takes a list of stops or two positional "
|
|
246
|
+
"colors (start, end); got a single colour. Pass an end colour."
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
pairs = list(_normalize_stop_input(stops))
|
|
250
|
+
|
|
251
|
+
if len(pairs) < 2:
|
|
252
|
+
raise ValueError("linear_gradient requires at least two stops")
|
|
253
|
+
|
|
254
|
+
self.gradient("linear")
|
|
255
|
+
# Defer to the public, atomically-validated GradientStops.replace().
|
|
256
|
+
# ``replace`` expects ``(position, color)`` while we built
|
|
257
|
+
# ``(color, position)`` to match the public API; flip the tuples here.
|
|
258
|
+
# ``replace`` accepts hex strings / RGBColor / 3-tuples natively, so
|
|
259
|
+
# no extra coercion step is needed.
|
|
260
|
+
self.gradient_stops.replace([(position, color) for color, position in pairs])
|
|
261
|
+
self.gradient_angle = float(angle)
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def gradient_kind(self):
|
|
265
|
+
"""One of ``"linear" | "radial" | "rectangular" | "shape" | None``.
|
|
266
|
+
|
|
267
|
+
Raises |TypeError| when fill is not gradient (call `fill.gradient()`
|
|
268
|
+
first). Returns |None| when the gradient inherits its shading
|
|
269
|
+
element from the style hierarchy.
|
|
270
|
+
"""
|
|
271
|
+
if self.type != MSO_FILL.GRADIENT:
|
|
272
|
+
raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
|
|
273
|
+
return self._fill.gradient_kind
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def gradient_stops(self):
|
|
277
|
+
"""|GradientStops| object providing access to stops of this gradient.
|
|
278
|
+
|
|
279
|
+
Raises |TypeError| when fill is not gradient (call `fill.gradient()`
|
|
280
|
+
first). Each stop represents a color between which the gradient
|
|
281
|
+
smoothly transitions.
|
|
282
|
+
"""
|
|
283
|
+
if self.type != MSO_FILL.GRADIENT:
|
|
284
|
+
raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
|
|
285
|
+
return self._fill.gradient_stops
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def pattern(self):
|
|
289
|
+
"""Return member of :ref:`MsoPatternType` indicating fill pattern.
|
|
290
|
+
|
|
291
|
+
Raises |TypeError| when fill is not patterned (call
|
|
292
|
+
`fill.patterned()` first). Returns |None| if no pattern has been set;
|
|
293
|
+
PowerPoint may display the default `PERCENT_5` pattern in this case.
|
|
294
|
+
Assigning |None| will remove any explicit pattern setting, although
|
|
295
|
+
relying on the default behavior is discouraged and may produce
|
|
296
|
+
rendering differences across client applications.
|
|
297
|
+
"""
|
|
298
|
+
return self._fill.pattern
|
|
299
|
+
|
|
300
|
+
@pattern.setter
|
|
301
|
+
def pattern(self, pattern_type):
|
|
302
|
+
self._fill.pattern = pattern_type
|
|
303
|
+
|
|
304
|
+
def patterned(self):
|
|
305
|
+
"""Selects the pattern fill type.
|
|
306
|
+
|
|
307
|
+
Note that calling this method does not by itself set a foreground or
|
|
308
|
+
background color of the pattern. Rather it enables subsequent
|
|
309
|
+
assignments to properties like fore_color to set the pattern and
|
|
310
|
+
colors.
|
|
311
|
+
"""
|
|
312
|
+
pattFill = self._xPr.get_or_change_to_pattFill()
|
|
313
|
+
self._fill = _PattFill(pattFill)
|
|
314
|
+
|
|
315
|
+
def solid(self):
|
|
316
|
+
"""
|
|
317
|
+
Sets the fill type to solid, i.e. a solid color. Note that calling
|
|
318
|
+
this method does not set a color or by itself cause the shape to
|
|
319
|
+
appear with a solid color fill; rather it enables subsequent
|
|
320
|
+
assignments to properties like fore_color to set the color.
|
|
321
|
+
"""
|
|
322
|
+
solidFill = self._xPr.get_or_change_to_solidFill()
|
|
323
|
+
self._fill = _SolidFill(solidFill)
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def type(self) -> MSO_FILL_TYPE:
|
|
327
|
+
"""The type of this fill, e.g. `MSO_FILL_TYPE.SOLID`."""
|
|
328
|
+
return self._fill.type
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class _Fill(object):
|
|
332
|
+
"""
|
|
333
|
+
Object factory for fill object of class matching fill element, such as
|
|
334
|
+
_SolidFill for ``<a:solidFill>``; also serves as the base class for all
|
|
335
|
+
fill classes
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
def __new__(cls, xFill):
|
|
339
|
+
if xFill is None:
|
|
340
|
+
fill_cls = _NoneFill
|
|
341
|
+
elif isinstance(xFill, CT_BlipFillProperties):
|
|
342
|
+
fill_cls = _BlipFill
|
|
343
|
+
elif isinstance(xFill, CT_GradientFillProperties):
|
|
344
|
+
fill_cls = _GradFill
|
|
345
|
+
elif isinstance(xFill, CT_GroupFillProperties):
|
|
346
|
+
fill_cls = _GrpFill
|
|
347
|
+
elif isinstance(xFill, CT_NoFillProperties):
|
|
348
|
+
fill_cls = _NoFill
|
|
349
|
+
elif isinstance(xFill, CT_PatternFillProperties):
|
|
350
|
+
fill_cls = _PattFill
|
|
351
|
+
elif isinstance(xFill, CT_SolidColorFillProperties):
|
|
352
|
+
fill_cls = _SolidFill
|
|
353
|
+
else:
|
|
354
|
+
fill_cls = _Fill
|
|
355
|
+
return super(_Fill, cls).__new__(fill_cls)
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def back_color(self):
|
|
359
|
+
"""Raise TypeError for types that do not override this property."""
|
|
360
|
+
tmpl = "fill type %s has no background color, call .patterned() first"
|
|
361
|
+
raise TypeError(tmpl % self.__class__.__name__)
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def fore_color(self):
|
|
365
|
+
"""Raise TypeError for types that do not override this property."""
|
|
366
|
+
tmpl = "fill type %s has no foreground color, call .solid() or .pattern" "ed() first"
|
|
367
|
+
raise TypeError(tmpl % self.__class__.__name__)
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def pattern(self):
|
|
371
|
+
"""Raise TypeError for fills that do not override this property."""
|
|
372
|
+
tmpl = "fill type %s has no pattern, call .patterned() first"
|
|
373
|
+
raise TypeError(tmpl % self.__class__.__name__)
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def type(self) -> MSO_FILL_TYPE: # pragma: no cover
|
|
377
|
+
raise NotImplementedError(
|
|
378
|
+
f".type property must be implemented on {self.__class__.__name__}"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class _BlipFill(_Fill):
|
|
383
|
+
@property
|
|
384
|
+
def type(self):
|
|
385
|
+
return MSO_FILL.PICTURE
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class _GradFill(_Fill):
|
|
389
|
+
"""Proxies an `a:gradFill` element."""
|
|
390
|
+
|
|
391
|
+
_PATH_KINDS = {"circle": "radial", "rect": "rectangular", "shape": "shape"}
|
|
392
|
+
|
|
393
|
+
def __init__(self, gradFill):
|
|
394
|
+
self._element = self._gradFill = gradFill
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def gradient_kind(self):
|
|
398
|
+
"""One of ``"linear" | "radial" | "rectangular" | "shape" | None``.
|
|
399
|
+
|
|
400
|
+
Returns ``None`` when the gradient inherits its shading element from
|
|
401
|
+
the style hierarchy (no `<a:lin>` or `<a:path>` child is present).
|
|
402
|
+
"""
|
|
403
|
+
path = self._gradFill.path
|
|
404
|
+
if path is not None:
|
|
405
|
+
return self._PATH_KINDS.get(path.path)
|
|
406
|
+
if self._gradFill.lin is not None:
|
|
407
|
+
return "linear"
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def gradient_angle(self):
|
|
412
|
+
"""Angle in float degrees of line of a linear gradient.
|
|
413
|
+
|
|
414
|
+
Read/Write. May be |None|, indicating the angle is inherited from the
|
|
415
|
+
style hierarchy. An angle of 0.0 corresponds to a left-to-right
|
|
416
|
+
gradient. Increasing angles represent clockwise rotation of the line,
|
|
417
|
+
for example 90.0 represents a top-to-bottom gradient. Raises
|
|
418
|
+
|TypeError| when the fill type is not MSO_FILL_TYPE.GRADIENT. Raises
|
|
419
|
+
|ValueError| for a non-linear gradient (e.g. a radial gradient).
|
|
420
|
+
"""
|
|
421
|
+
# ---case 1: gradient path is explicit, but not linear---
|
|
422
|
+
path = self._gradFill.path
|
|
423
|
+
if path is not None:
|
|
424
|
+
raise ValueError("not a linear gradient")
|
|
425
|
+
|
|
426
|
+
# ---case 2: gradient path is inherited (no a:lin OR a:path)---
|
|
427
|
+
lin = self._gradFill.lin
|
|
428
|
+
if lin is None:
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
# ---case 3: gradient path is explicitly linear---
|
|
432
|
+
# angle is stored in XML as a clockwise angle, whereas the UI
|
|
433
|
+
# reports it as counter-clockwise from horizontal-pointing-right.
|
|
434
|
+
# Since the UI is consistent with trigonometry conventions, we
|
|
435
|
+
# respect that in the API. An `a:lin` without an `ang` attribute
|
|
436
|
+
# (e.g. the default `<a:lin scaled="0"/>` gradient) renders at the
|
|
437
|
+
# effective angle 0.
|
|
438
|
+
clockwise_angle = lin.ang
|
|
439
|
+
if clockwise_angle is None or clockwise_angle == 0.0:
|
|
440
|
+
return 0.0
|
|
441
|
+
return 360.0 - clockwise_angle
|
|
442
|
+
|
|
443
|
+
@gradient_angle.setter
|
|
444
|
+
def gradient_angle(self, value):
|
|
445
|
+
lin = self._gradFill.lin
|
|
446
|
+
if lin is None:
|
|
447
|
+
raise ValueError("not a linear gradient")
|
|
448
|
+
lin.ang = 360.0 - value
|
|
449
|
+
|
|
450
|
+
@lazyproperty
|
|
451
|
+
def gradient_stops(self):
|
|
452
|
+
"""|_GradientStops| object providing access to gradient colors.
|
|
453
|
+
|
|
454
|
+
Each stop represents a color between which the gradient smoothly
|
|
455
|
+
transitions.
|
|
456
|
+
"""
|
|
457
|
+
return _GradientStops(self._gradFill.get_or_add_gsLst())
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
def type(self):
|
|
461
|
+
return MSO_FILL.GRADIENT
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class _GrpFill(_Fill):
|
|
465
|
+
@property
|
|
466
|
+
def type(self):
|
|
467
|
+
return MSO_FILL.GROUP
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class _NoFill(_Fill):
|
|
471
|
+
@property
|
|
472
|
+
def type(self):
|
|
473
|
+
return MSO_FILL.BACKGROUND
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class _NoneFill(_Fill):
|
|
477
|
+
@property
|
|
478
|
+
def type(self):
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class _PattFill(_Fill):
|
|
483
|
+
"""Provides access to patterned fill properties."""
|
|
484
|
+
|
|
485
|
+
def __init__(self, pattFill):
|
|
486
|
+
super(_PattFill, self).__init__()
|
|
487
|
+
self._element = self._pattFill = pattFill
|
|
488
|
+
|
|
489
|
+
@lazyproperty
|
|
490
|
+
def back_color(self):
|
|
491
|
+
"""Return |ColorFormat| object that controls background color."""
|
|
492
|
+
bgClr = self._pattFill.get_or_add_bgClr()
|
|
493
|
+
return ColorFormat.from_colorchoice_parent(bgClr)
|
|
494
|
+
|
|
495
|
+
@lazyproperty
|
|
496
|
+
def fore_color(self):
|
|
497
|
+
"""Return |ColorFormat| object that controls foreground color."""
|
|
498
|
+
fgClr = self._pattFill.get_or_add_fgClr()
|
|
499
|
+
return ColorFormat.from_colorchoice_parent(fgClr)
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def pattern(self):
|
|
503
|
+
"""Return member of :ref:`MsoPatternType` indicating fill pattern.
|
|
504
|
+
|
|
505
|
+
Returns |None| if no pattern has been set; PowerPoint may display the
|
|
506
|
+
default `PERCENT_5` pattern in this case. Assigning |None| will
|
|
507
|
+
remove any explicit pattern setting.
|
|
508
|
+
"""
|
|
509
|
+
return self._pattFill.prst
|
|
510
|
+
|
|
511
|
+
@pattern.setter
|
|
512
|
+
def pattern(self, pattern_type):
|
|
513
|
+
self._pattFill.prst = pattern_type
|
|
514
|
+
|
|
515
|
+
@property
|
|
516
|
+
def type(self):
|
|
517
|
+
return MSO_FILL.PATTERNED
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class _SolidFill(_Fill):
|
|
521
|
+
"""Provides access to fill properties such as color for solid fills."""
|
|
522
|
+
|
|
523
|
+
def __init__(self, solidFill):
|
|
524
|
+
super(_SolidFill, self).__init__()
|
|
525
|
+
self._solidFill = solidFill
|
|
526
|
+
|
|
527
|
+
@lazyproperty
|
|
528
|
+
def fore_color(self):
|
|
529
|
+
"""Return |ColorFormat| object controlling fill color."""
|
|
530
|
+
return ColorFormat.from_colorchoice_parent(self._solidFill)
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def type(self):
|
|
534
|
+
return MSO_FILL.SOLID
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class _GradientStops(Sequence):
|
|
538
|
+
"""Collection of |GradientStop| objects defining gradient colors.
|
|
539
|
+
|
|
540
|
+
A gradient must have a minimum of two stops, but can have as many more
|
|
541
|
+
than that as required to achieve the desired effect (three is perhaps
|
|
542
|
+
most common). Stops are sequenced in the order they are transitioned
|
|
543
|
+
through.
|
|
544
|
+
|
|
545
|
+
The collection is mutable: stops can be added with :meth:`append`,
|
|
546
|
+
removed with ``del stops[i]``, and the entire stop sequence can be
|
|
547
|
+
swapped out with :meth:`replace`. The OOXML schema requires at least
|
|
548
|
+
two `<a:gs>` children, so :meth:`__delitem__` raises when removing a
|
|
549
|
+
stop would leave fewer than two.
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
_MIN_STOP_COUNT = 2
|
|
553
|
+
|
|
554
|
+
def __init__(self, gsLst):
|
|
555
|
+
self._gsLst = gsLst
|
|
556
|
+
|
|
557
|
+
def __delitem__(self, idx):
|
|
558
|
+
gs_children = self._gs_children
|
|
559
|
+
if len(gs_children) - 1 < self._MIN_STOP_COUNT:
|
|
560
|
+
raise ValueError(
|
|
561
|
+
"a gradient must have at least %d stops; cannot delete"
|
|
562
|
+
% self._MIN_STOP_COUNT
|
|
563
|
+
)
|
|
564
|
+
target = gs_children[idx]
|
|
565
|
+
self._gsLst.remove(target)
|
|
566
|
+
|
|
567
|
+
def __getitem__(self, idx):
|
|
568
|
+
return _GradientStop(self._gs_children[idx])
|
|
569
|
+
|
|
570
|
+
def __len__(self):
|
|
571
|
+
return len(self._gs_children)
|
|
572
|
+
|
|
573
|
+
def append(self, position, color=None):
|
|
574
|
+
"""Append a new stop at `position` with `color`.
|
|
575
|
+
|
|
576
|
+
`position` is a float in ``[0.0, 1.0]``. `color` may be:
|
|
577
|
+
|
|
578
|
+
* |None| (default): a placeholder ``schemeClr accent1`` color is
|
|
579
|
+
written; mutate ``returned_stop.color`` to refine it.
|
|
580
|
+
* an :class:`~pptx2.dml.color.RGBColor` instance.
|
|
581
|
+
* a 3-tuple of integers in ``[0, 255]``.
|
|
582
|
+
* a hex string like ``"3C2F80"`` (with or without leading ``#``).
|
|
583
|
+
|
|
584
|
+
Returns the newly-added :class:`_GradientStop`.
|
|
585
|
+
"""
|
|
586
|
+
gs = self._gsLst._add_gs()
|
|
587
|
+
gs.pos = float(position)
|
|
588
|
+
stop = _GradientStop(gs)
|
|
589
|
+
if color is None:
|
|
590
|
+
# Default placeholder color so the emitted `<a:gs>` is valid OOXML
|
|
591
|
+
# (the schema requires a color choice child). Callers can mutate
|
|
592
|
+
# the returned stop's `.color` afterwards.
|
|
593
|
+
stop.color.theme_color = MSO_THEME_COLOR.ACCENT_1
|
|
594
|
+
else:
|
|
595
|
+
stop.color.rgb = self._coerce_rgb(color)
|
|
596
|
+
return stop
|
|
597
|
+
|
|
598
|
+
def replace(self, stops):
|
|
599
|
+
"""Replace all stops with the entries in `stops`.
|
|
600
|
+
|
|
601
|
+
Each entry is either a 2-tuple ``(position, color)`` (where `color`
|
|
602
|
+
follows the same rules as :meth:`append`) or an existing
|
|
603
|
+
:class:`_GradientStop` — including stops whose color is a theme,
|
|
604
|
+
scheme, system, or preset color. Existing-stop entries are deep-
|
|
605
|
+
copied as-is so non-RGB color choices round-trip without loss.
|
|
606
|
+
|
|
607
|
+
The new sequence must contain at least 2 entries. The replacement
|
|
608
|
+
is atomic: if any entry is invalid the existing stops are left
|
|
609
|
+
untouched.
|
|
610
|
+
"""
|
|
611
|
+
stops = list(stops)
|
|
612
|
+
if len(stops) < self._MIN_STOP_COUNT:
|
|
613
|
+
raise ValueError(
|
|
614
|
+
"a gradient must have at least %d stops" % self._MIN_STOP_COUNT
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Pre-validate every entry so a failure (bad color, malformed tuple)
|
|
618
|
+
# raises *before* we touch the existing stops.
|
|
619
|
+
validated = []
|
|
620
|
+
for entry in stops:
|
|
621
|
+
if isinstance(entry, _GradientStop):
|
|
622
|
+
validated.append(("copy", entry._gs))
|
|
623
|
+
continue
|
|
624
|
+
try:
|
|
625
|
+
position, color = entry
|
|
626
|
+
except (TypeError, ValueError) as e:
|
|
627
|
+
raise TypeError(
|
|
628
|
+
"replace() entries must be (position, color) tuples or "
|
|
629
|
+
"_GradientStop instances; got %r" % (entry,)
|
|
630
|
+
) from e
|
|
631
|
+
float(position)
|
|
632
|
+
if color is not None:
|
|
633
|
+
self._coerce_rgb(color)
|
|
634
|
+
validated.append(("new", float(position), color))
|
|
635
|
+
|
|
636
|
+
# Mutate only after all entries are validated.
|
|
637
|
+
for gs in self._gs_children:
|
|
638
|
+
self._gsLst.remove(gs)
|
|
639
|
+
for entry in validated:
|
|
640
|
+
if entry[0] == "copy":
|
|
641
|
+
self._gsLst.append(copy.deepcopy(entry[1]))
|
|
642
|
+
else:
|
|
643
|
+
_, position, color = entry
|
|
644
|
+
self.append(position, color)
|
|
645
|
+
|
|
646
|
+
@property
|
|
647
|
+
def _gs_children(self):
|
|
648
|
+
return list(self._gsLst.gs_lst)
|
|
649
|
+
|
|
650
|
+
@staticmethod
|
|
651
|
+
def _coerce_rgb(color):
|
|
652
|
+
if isinstance(color, RGBColor):
|
|
653
|
+
return color
|
|
654
|
+
if isinstance(color, str):
|
|
655
|
+
return RGBColor.from_hex(color)
|
|
656
|
+
if isinstance(color, tuple) and len(color) == 3:
|
|
657
|
+
return RGBColor(*color)
|
|
658
|
+
raise TypeError(
|
|
659
|
+
"color must be RGBColor, hex string, 3-tuple, or None; got %r" % type(color)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class _GradientStop(ElementProxy):
|
|
664
|
+
"""A single gradient stop.
|
|
665
|
+
|
|
666
|
+
A gradient stop defines a color and a position.
|
|
667
|
+
"""
|
|
668
|
+
|
|
669
|
+
def __init__(self, gs):
|
|
670
|
+
super(_GradientStop, self).__init__(gs)
|
|
671
|
+
self._gs = gs
|
|
672
|
+
|
|
673
|
+
@lazyproperty
|
|
674
|
+
def color(self):
|
|
675
|
+
"""Return |ColorFormat| object controlling stop color."""
|
|
676
|
+
return ColorFormat.from_colorchoice_parent(self._gs)
|
|
677
|
+
|
|
678
|
+
@property
|
|
679
|
+
def position(self):
|
|
680
|
+
"""Location of stop in gradient path as float between 0.0 and 1.0.
|
|
681
|
+
|
|
682
|
+
The value represents a percentage, where 0.0 (0%) represents the
|
|
683
|
+
start of the path and 1.0 (100%) represents the end of the path. For
|
|
684
|
+
a linear gradient, these would represent opposing extents of the
|
|
685
|
+
filled area.
|
|
686
|
+
"""
|
|
687
|
+
return self._gs.pos
|
|
688
|
+
|
|
689
|
+
@position.setter
|
|
690
|
+
def position(self, value):
|
|
691
|
+
self._gs.pos = float(value)
|