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/line.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""DrawingML objects related to line formatting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pptx2.dml.color import _LazyColorFormat
|
|
6
|
+
from pptx2.dml.fill import FillFormat
|
|
7
|
+
from pptx2.enum.dml import MSO_LINE_JOIN_STYLE
|
|
8
|
+
from pptx2.oxml.ns import qn
|
|
9
|
+
from pptx2.util import Emu, lazyproperty
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_JOIN_TAG = {
|
|
13
|
+
MSO_LINE_JOIN_STYLE.ROUND: "a:round",
|
|
14
|
+
MSO_LINE_JOIN_STYLE.BEVEL: "a:bevel",
|
|
15
|
+
MSO_LINE_JOIN_STYLE.MITER: "a:miter",
|
|
16
|
+
}
|
|
17
|
+
_JOIN_FROM_TAG = {qn(tag): join for join, tag in _JOIN_TAG.items()}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LineEndFormat(object):
|
|
21
|
+
"""Provides access to one end (head or tail) of a stroked line.
|
|
22
|
+
|
|
23
|
+
Wraps an ``<a:headEnd>`` or ``<a:tailEnd>`` element. The element is
|
|
24
|
+
created lazily; assigning |None| to a property removes the corresponding
|
|
25
|
+
attribute, and removing the last attribute drops the element entirely so
|
|
26
|
+
inheritance is not silently broken.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, line_format, end_tag):
|
|
30
|
+
# `end_tag` is "headEnd" or "tailEnd"
|
|
31
|
+
self._line_format = line_format
|
|
32
|
+
self._end_tag = end_tag
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def type(self):
|
|
36
|
+
"""Arrowhead type as :ref:`MsoLineEndType`, or |None| if not set."""
|
|
37
|
+
end = self._end
|
|
38
|
+
if end is None:
|
|
39
|
+
return None
|
|
40
|
+
return end.type
|
|
41
|
+
|
|
42
|
+
@type.setter
|
|
43
|
+
def type(self, value):
|
|
44
|
+
self._set_attr("type", value)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def width(self):
|
|
48
|
+
"""Arrowhead width as :ref:`MsoLineEndSize`, or |None| if not set."""
|
|
49
|
+
end = self._end
|
|
50
|
+
if end is None:
|
|
51
|
+
return None
|
|
52
|
+
return end.w
|
|
53
|
+
|
|
54
|
+
@width.setter
|
|
55
|
+
def width(self, value):
|
|
56
|
+
self._set_attr("w", value)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def length(self):
|
|
60
|
+
"""Arrowhead length as :ref:`MsoLineEndSize`, or |None| if not set."""
|
|
61
|
+
end = self._end
|
|
62
|
+
if end is None:
|
|
63
|
+
return None
|
|
64
|
+
return end.len
|
|
65
|
+
|
|
66
|
+
@length.setter
|
|
67
|
+
def length(self, value):
|
|
68
|
+
self._set_attr("len", value)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def _end(self):
|
|
72
|
+
ln = self._line_format._ln
|
|
73
|
+
if ln is None:
|
|
74
|
+
return None
|
|
75
|
+
return getattr(ln, self._end_tag)
|
|
76
|
+
|
|
77
|
+
def _set_attr(self, attr_name, value):
|
|
78
|
+
if value is None:
|
|
79
|
+
end = self._end
|
|
80
|
+
if end is None:
|
|
81
|
+
return
|
|
82
|
+
end.attrib.pop(attr_name, None)
|
|
83
|
+
if not end.attrib:
|
|
84
|
+
ln = self._line_format._ln
|
|
85
|
+
if ln is not None:
|
|
86
|
+
getattr(ln, "_remove_%s" % self._end_tag)()
|
|
87
|
+
return
|
|
88
|
+
ln = self._line_format._get_or_add_ln()
|
|
89
|
+
end = getattr(ln, "get_or_add_%s" % self._end_tag)()
|
|
90
|
+
setattr(end, attr_name, value)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LineFormat(object):
|
|
94
|
+
"""Provides access to line properties such as color, style, and width.
|
|
95
|
+
|
|
96
|
+
A LineFormat object is typically accessed via the ``.line`` property of
|
|
97
|
+
a shape such as |Shape| or |Picture|.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, parent):
|
|
101
|
+
super(LineFormat, self).__init__()
|
|
102
|
+
self._parent = parent
|
|
103
|
+
|
|
104
|
+
@lazyproperty
|
|
105
|
+
def color(self):
|
|
106
|
+
"""The color settings for this line; a shortcut for ``line.fill.fore_color``.
|
|
107
|
+
|
|
108
|
+
Reads are non-mutating: when no explicit ``<a:ln>`` element exists or its
|
|
109
|
+
fill is not solid, accessing color properties returns the "no explicit
|
|
110
|
+
color" sentinel (preserving theme inheritance) instead of injecting line
|
|
111
|
+
and fill XML. The line element and a solid fill are only created when
|
|
112
|
+
``rgb`` or ``theme_color`` is assigned.
|
|
113
|
+
"""
|
|
114
|
+
return _LazyColorFormat(peek_fill=self._peek_fill, ensure_fill=lambda: self.fill)
|
|
115
|
+
|
|
116
|
+
def _peek_fill(self):
|
|
117
|
+
"""Return |FillFormat| for the current ``<a:ln>`` element, or |None|.
|
|
118
|
+
|
|
119
|
+
Read-only: never injects an ``<a:ln>`` element if one is not already
|
|
120
|
+
present.
|
|
121
|
+
"""
|
|
122
|
+
ln = self._ln
|
|
123
|
+
if ln is None:
|
|
124
|
+
return None
|
|
125
|
+
return FillFormat.from_fill_parent(ln)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def dash_style(self):
|
|
129
|
+
"""Return value indicating line style.
|
|
130
|
+
|
|
131
|
+
Returns a member of :ref:`MsoLineDashStyle` indicating line style, or
|
|
132
|
+
|None| if no explicit value has been set. When no explicit value has
|
|
133
|
+
been set, the line dash style is inherited from the style hierarchy.
|
|
134
|
+
|
|
135
|
+
Assigning |None| removes any existing explicitly-defined dash style.
|
|
136
|
+
"""
|
|
137
|
+
ln = self._ln
|
|
138
|
+
if ln is None:
|
|
139
|
+
return None
|
|
140
|
+
return ln.prstDash_val
|
|
141
|
+
|
|
142
|
+
@dash_style.setter
|
|
143
|
+
def dash_style(self, dash_style):
|
|
144
|
+
if dash_style is None:
|
|
145
|
+
ln = self._ln
|
|
146
|
+
if ln is None:
|
|
147
|
+
return
|
|
148
|
+
ln._remove_prstDash()
|
|
149
|
+
ln._remove_custDash()
|
|
150
|
+
return
|
|
151
|
+
ln = self._get_or_add_ln()
|
|
152
|
+
ln.prstDash_val = dash_style
|
|
153
|
+
|
|
154
|
+
@lazyproperty
|
|
155
|
+
def fill(self):
|
|
156
|
+
"""
|
|
157
|
+
|FillFormat| instance for this line, providing access to fill
|
|
158
|
+
properties such as foreground color.
|
|
159
|
+
"""
|
|
160
|
+
ln = self._get_or_add_ln()
|
|
161
|
+
return FillFormat.from_fill_parent(ln)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def width(self):
|
|
165
|
+
"""
|
|
166
|
+
The width of the line expressed as an integer number of :ref:`English
|
|
167
|
+
Metric Units <EMU>`. The returned value is an instance of |Length|,
|
|
168
|
+
a value class having properties such as `.inches`, `.cm`, and `.pt`
|
|
169
|
+
for converting the value into convenient units.
|
|
170
|
+
"""
|
|
171
|
+
ln = self._ln
|
|
172
|
+
if ln is None:
|
|
173
|
+
return Emu(0)
|
|
174
|
+
return ln.w
|
|
175
|
+
|
|
176
|
+
@width.setter
|
|
177
|
+
def width(self, emu):
|
|
178
|
+
if emu is None:
|
|
179
|
+
emu = 0
|
|
180
|
+
ln = self._get_or_add_ln()
|
|
181
|
+
ln.w = emu
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def cap(self):
|
|
185
|
+
"""End-cap style as :ref:`MsoLineCapStyle`, or |None| if unset.
|
|
186
|
+
|
|
187
|
+
Maps to the ``cap`` attribute on ``<a:ln>``. Reads are non-mutating:
|
|
188
|
+
no ``<a:ln>`` element is created if one doesn't already exist.
|
|
189
|
+
"""
|
|
190
|
+
ln = self._ln
|
|
191
|
+
if ln is None:
|
|
192
|
+
return None
|
|
193
|
+
return ln.cap
|
|
194
|
+
|
|
195
|
+
@cap.setter
|
|
196
|
+
def cap(self, value):
|
|
197
|
+
if value is None:
|
|
198
|
+
ln = self._ln
|
|
199
|
+
if ln is None:
|
|
200
|
+
return
|
|
201
|
+
ln.cap = None
|
|
202
|
+
return
|
|
203
|
+
ln = self._get_or_add_ln()
|
|
204
|
+
ln.cap = value
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def compound(self):
|
|
208
|
+
"""Compound (multi-stroke) style as :ref:`MsoLineCompoundStyle`, or |None|.
|
|
209
|
+
|
|
210
|
+
Maps to the ``cmpd`` attribute on ``<a:ln>``.
|
|
211
|
+
"""
|
|
212
|
+
ln = self._ln
|
|
213
|
+
if ln is None:
|
|
214
|
+
return None
|
|
215
|
+
return ln.cmpd
|
|
216
|
+
|
|
217
|
+
@compound.setter
|
|
218
|
+
def compound(self, value):
|
|
219
|
+
if value is None:
|
|
220
|
+
ln = self._ln
|
|
221
|
+
if ln is None:
|
|
222
|
+
return
|
|
223
|
+
ln.cmpd = None
|
|
224
|
+
return
|
|
225
|
+
ln = self._get_or_add_ln()
|
|
226
|
+
ln.cmpd = value
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def join(self):
|
|
230
|
+
"""Corner-join style as :ref:`MsoLineJoinStyle`, or |None| if unset.
|
|
231
|
+
|
|
232
|
+
Returns whichever of ``<a:round/>``, ``<a:bevel/>``, or ``<a:miter/>``
|
|
233
|
+
is currently a child of ``<a:ln>``. Assigning |None| removes any
|
|
234
|
+
existing join element; assigning a member writes the matching
|
|
235
|
+
element (replacing any other join element if present).
|
|
236
|
+
"""
|
|
237
|
+
ln = self._ln
|
|
238
|
+
if ln is None:
|
|
239
|
+
return None
|
|
240
|
+
join_elm = ln.eg_lineJoinProperties
|
|
241
|
+
if join_elm is None:
|
|
242
|
+
return None
|
|
243
|
+
return _JOIN_FROM_TAG.get(join_elm.tag)
|
|
244
|
+
|
|
245
|
+
@join.setter
|
|
246
|
+
def join(self, value):
|
|
247
|
+
if value is None:
|
|
248
|
+
ln = self._ln
|
|
249
|
+
if ln is None:
|
|
250
|
+
return
|
|
251
|
+
ln._remove_eg_lineJoinProperties()
|
|
252
|
+
return
|
|
253
|
+
if value not in _JOIN_TAG:
|
|
254
|
+
raise ValueError("invalid line-join style %r" % (value,))
|
|
255
|
+
ln = self._get_or_add_ln()
|
|
256
|
+
ln._remove_eg_lineJoinProperties()
|
|
257
|
+
# -- ZeroOrOneChoice's `_add_x` writes the child in the right slot --
|
|
258
|
+
getattr(ln, "_add_%s" % _JOIN_TAG[value].split(":", 1)[1])()
|
|
259
|
+
|
|
260
|
+
@lazyproperty
|
|
261
|
+
def head_end(self):
|
|
262
|
+
""":class:`LineEndFormat` for the start (head) of this line.
|
|
263
|
+
|
|
264
|
+
Provides access to the ``<a:headEnd>`` element's ``type``, ``width``,
|
|
265
|
+
and ``length`` attributes.
|
|
266
|
+
"""
|
|
267
|
+
return LineEndFormat(self, "headEnd")
|
|
268
|
+
|
|
269
|
+
@lazyproperty
|
|
270
|
+
def tail_end(self):
|
|
271
|
+
""":class:`LineEndFormat` for the end (tail) of this line.
|
|
272
|
+
|
|
273
|
+
Provides access to the ``<a:tailEnd>`` element's ``type``, ``width``,
|
|
274
|
+
and ``length`` attributes.
|
|
275
|
+
"""
|
|
276
|
+
return LineEndFormat(self, "tailEnd")
|
|
277
|
+
|
|
278
|
+
def _get_or_add_ln(self):
|
|
279
|
+
"""
|
|
280
|
+
Return the ``<a:ln>`` element containing the line format properties
|
|
281
|
+
in the XML.
|
|
282
|
+
"""
|
|
283
|
+
return self._parent.get_or_add_ln()
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def _ln(self):
|
|
287
|
+
return self._parent.ln
|
pptx2/dml/picture.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Picture image effects: transparency, brightness, contrast, and recolor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pptx2.dml.color import RGBColor
|
|
9
|
+
from pptx2.oxml.dml.fill import CT_Blip
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Sepia duotone: dark warm brown → light cream, matching PowerPoint's built-in preset.
|
|
13
|
+
_SEPIA_DARK = "532400"
|
|
14
|
+
_SEPIA_LIGHT = "FFEFCA"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PictureEffects:
|
|
18
|
+
"""Provides access to image-level visual effects on a |Picture| shape.
|
|
19
|
+
|
|
20
|
+
Exposes transparency (alpha), brightness, contrast, and recolor. All reads
|
|
21
|
+
are non-mutating — if no explicit effect is present the property returns a
|
|
22
|
+
neutral sentinel value (``0.0`` for numeric adjustments, ``None`` for recolor).
|
|
23
|
+
Writes lazily create the required ``<a:blip>`` child elements.
|
|
24
|
+
|
|
25
|
+
Access via :attr:`pptx2.shapes.picture.Picture.effects`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, blip: CT_Blip):
|
|
29
|
+
self._blip = blip
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# transparency
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def transparency(self) -> float:
|
|
37
|
+
"""Read/write float in ``[0.0, 1.0]``: ``0.0`` = fully opaque (default), ``1.0`` = invisible.
|
|
38
|
+
|
|
39
|
+
Maps to ``<a:alphaModFix amt="N"/>`` where ``amt`` is the *remaining* opacity
|
|
40
|
+
(``100000`` = fully opaque). Setting to ``0.0`` removes the element entirely,
|
|
41
|
+
restoring full opacity.
|
|
42
|
+
"""
|
|
43
|
+
alphaModFix = self._blip.alphaModFix
|
|
44
|
+
if alphaModFix is None:
|
|
45
|
+
return 0.0
|
|
46
|
+
amt = alphaModFix.amt
|
|
47
|
+
return round(1.0 - amt, 10)
|
|
48
|
+
|
|
49
|
+
@transparency.setter
|
|
50
|
+
def transparency(self, value: float) -> None:
|
|
51
|
+
if not 0.0 <= value <= 1.0:
|
|
52
|
+
raise ValueError(f"transparency must be between 0.0 and 1.0, got {value!r}")
|
|
53
|
+
if value == 0.0:
|
|
54
|
+
self._blip._remove_alphaModFix() # pyright: ignore[reportPrivateUsage]
|
|
55
|
+
else:
|
|
56
|
+
self._blip.get_or_add_alphaModFix().amt = 1.0 - value # type: ignore[assignment]
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
# brightness
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def brightness(self) -> float:
|
|
64
|
+
"""Read/write float in ``[-1.0, 1.0]``: ``0.0`` = no change (default).
|
|
65
|
+
|
|
66
|
+
Maps to the ``bright`` attribute of ``<a:lum>``. A value of ``0.4``
|
|
67
|
+
means +40% brightness; ``-0.25`` means 25% darker.
|
|
68
|
+
"""
|
|
69
|
+
lum = self._blip.lum
|
|
70
|
+
return 0.0 if lum is None else lum.bright
|
|
71
|
+
|
|
72
|
+
@brightness.setter
|
|
73
|
+
def brightness(self, value: float) -> None:
|
|
74
|
+
if not -1.0 <= value <= 1.0:
|
|
75
|
+
raise ValueError(f"brightness must be between -1.0 and 1.0, got {value!r}")
|
|
76
|
+
if value == 0.0 and self._blip.lum is None:
|
|
77
|
+
return # -- "no change" on a picture with no `a:lum`: nothing to write
|
|
78
|
+
lum = self._blip.get_or_add_lum()
|
|
79
|
+
lum.bright = value # type: ignore[assignment]
|
|
80
|
+
self._drop_lum_if_neutral()
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# contrast
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def contrast(self) -> float:
|
|
88
|
+
"""Read/write float in ``[-1.0, 1.0]``: ``0.0`` = no change (default).
|
|
89
|
+
|
|
90
|
+
Maps to the ``contrast`` attribute of ``<a:lum>``. Shares the same
|
|
91
|
+
``<a:lum>`` element as :attr:`brightness`.
|
|
92
|
+
"""
|
|
93
|
+
lum = self._blip.lum
|
|
94
|
+
return 0.0 if lum is None else lum.contrast
|
|
95
|
+
|
|
96
|
+
@contrast.setter
|
|
97
|
+
def contrast(self, value: float) -> None:
|
|
98
|
+
if not -1.0 <= value <= 1.0:
|
|
99
|
+
raise ValueError(f"contrast must be between -1.0 and 1.0, got {value!r}")
|
|
100
|
+
if value == 0.0 and self._blip.lum is None:
|
|
101
|
+
return # -- "no change" on a picture with no `a:lum`: nothing to write
|
|
102
|
+
lum = self._blip.get_or_add_lum()
|
|
103
|
+
lum.contrast = value # type: ignore[assignment]
|
|
104
|
+
self._drop_lum_if_neutral()
|
|
105
|
+
|
|
106
|
+
def _drop_lum_if_neutral(self) -> None:
|
|
107
|
+
"""Remove `a:lum` once both adjustments are back at their defaults.
|
|
108
|
+
|
|
109
|
+
Writing ``0.0`` drops the corresponding attribute (the schema
|
|
110
|
+
default), so a lum whose adjustments are both reset would otherwise
|
|
111
|
+
linger as a dead empty ``<a:lum/>``.
|
|
112
|
+
"""
|
|
113
|
+
lum = self._blip.lum
|
|
114
|
+
if lum is not None and lum.get("bright") is None and lum.get("contrast") is None:
|
|
115
|
+
self._blip.remove(lum)
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# recolor
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def recolor(self) -> str | None:
|
|
123
|
+
"""Read/write recolor mode. One of ``"grayscale"``, ``"washout"``,
|
|
124
|
+
``"sepia"``, ``"duotone"``, or ``None`` (no recolor).
|
|
125
|
+
|
|
126
|
+
Assigning a value removes any previously applied recolor before
|
|
127
|
+
applying the new one. Setting to ``None`` clears recolor entirely.
|
|
128
|
+
For custom duotone colors use :meth:`set_duotone`.
|
|
129
|
+
"""
|
|
130
|
+
if self._blip.grayscl is not None:
|
|
131
|
+
return "grayscale"
|
|
132
|
+
biLevel = self._blip.biLevel
|
|
133
|
+
if biLevel is not None:
|
|
134
|
+
return "washout"
|
|
135
|
+
duotone = self._blip.duotone
|
|
136
|
+
if duotone is not None:
|
|
137
|
+
children = list(duotone)
|
|
138
|
+
if len(children) == 2:
|
|
139
|
+
from pptx2.oxml.ns import qn
|
|
140
|
+
|
|
141
|
+
vals = [c.get("val") for c in children if c.tag == qn("a:srgbClr")]
|
|
142
|
+
if vals == [_SEPIA_DARK, _SEPIA_LIGHT]:
|
|
143
|
+
return "sepia"
|
|
144
|
+
return "duotone"
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
@recolor.setter
|
|
148
|
+
def recolor(self, value: str | None) -> None:
|
|
149
|
+
# Validate before mutating so an invalid value leaves the existing effect intact.
|
|
150
|
+
_VALID_RECOLOR = frozenset({"grayscale", "washout", "sepia", "duotone"})
|
|
151
|
+
if value is not None and value not in _VALID_RECOLOR:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"recolor must be 'grayscale', 'washout', 'sepia', 'duotone', or None; got {value!r}"
|
|
154
|
+
)
|
|
155
|
+
self._clear_recolor()
|
|
156
|
+
if value is None:
|
|
157
|
+
return
|
|
158
|
+
if value == "grayscale":
|
|
159
|
+
self._blip.get_or_add_grayscl()
|
|
160
|
+
elif value == "washout":
|
|
161
|
+
self._blip.get_or_add_biLevel().thresh = 0.5 # type: ignore[assignment]
|
|
162
|
+
elif value == "sepia":
|
|
163
|
+
self.set_duotone(_SEPIA_DARK, _SEPIA_LIGHT)
|
|
164
|
+
elif value == "duotone":
|
|
165
|
+
# Default neutral duotone (dark grey → light grey).
|
|
166
|
+
# For custom colors use set_duotone() instead.
|
|
167
|
+
self.set_duotone("333333", "EBEBEB")
|
|
168
|
+
|
|
169
|
+
def set_duotone(self, dark_color: RGBColor | tuple, light_color: RGBColor | tuple) -> None:
|
|
170
|
+
"""Apply a duotone recolor using custom dark and light colors.
|
|
171
|
+
|
|
172
|
+
Each color can be an :class:`~pptx2.dml.color.RGBColor` instance or a
|
|
173
|
+
3-tuple of ``(r, g, b)`` integers. The dark color maps to shadows, the
|
|
174
|
+
light color to highlights.
|
|
175
|
+
"""
|
|
176
|
+
self._clear_recolor()
|
|
177
|
+
from lxml import etree
|
|
178
|
+
|
|
179
|
+
from pptx2.oxml.ns import qn
|
|
180
|
+
|
|
181
|
+
duotone = self._blip.get_or_add_duotone()
|
|
182
|
+
for child in list(duotone):
|
|
183
|
+
duotone.remove(child)
|
|
184
|
+
for hex_val in (_color_to_hex(dark_color), _color_to_hex(light_color)):
|
|
185
|
+
clr = etree.SubElement(duotone, qn("a:srgbClr"))
|
|
186
|
+
clr.set("val", hex_val)
|
|
187
|
+
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
# internals
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def _clear_recolor(self) -> None:
|
|
193
|
+
"""Remove any existing recolor effects from the blip."""
|
|
194
|
+
self._blip._remove_grayscl() # pyright: ignore[reportPrivateUsage]
|
|
195
|
+
self._blip._remove_biLevel() # pyright: ignore[reportPrivateUsage]
|
|
196
|
+
self._blip._remove_duotone() # pyright: ignore[reportPrivateUsage]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _color_to_hex(color: RGBColor | tuple | str) -> str:
|
|
200
|
+
"""Return uppercase 6-char hex string for *color*.
|
|
201
|
+
|
|
202
|
+
Accepts an :class:`~pptx2.dml.color.RGBColor`, a hex string (with or
|
|
203
|
+
without ``#``), or a 3-tuple of ``(r, g, b)`` integers.
|
|
204
|
+
"""
|
|
205
|
+
from pptx2.dml.color import RGBColor as _RGBColor
|
|
206
|
+
|
|
207
|
+
if isinstance(color, _RGBColor):
|
|
208
|
+
return str(color).upper()
|
|
209
|
+
if isinstance(color, str):
|
|
210
|
+
return color.upper().lstrip("#")
|
|
211
|
+
r, g, b = color
|
|
212
|
+
return f"{r:02X}{g:02X}{b:02X}"
|