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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Connector (line) shape and related objects.
|
|
2
|
+
|
|
3
|
+
A connector is a line shape having end-points that can be connected to other
|
|
4
|
+
objects (but not to other connectors). A connector can be straight, have
|
|
5
|
+
elbows, or can be curved.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pptx2.dml.line import LineFormat
|
|
11
|
+
from pptx2.enum.shapes import MSO_SHAPE_TYPE
|
|
12
|
+
from pptx2.shapes.base import BaseShape
|
|
13
|
+
from pptx2.util import Emu, lazyproperty
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Connector(BaseShape):
|
|
17
|
+
"""Connector (line) shape.
|
|
18
|
+
|
|
19
|
+
A connector is a linear shape having end-points that can be connected to
|
|
20
|
+
other objects (but not to other connectors). A connector can be straight,
|
|
21
|
+
have elbows, or can be curved.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def begin_connect(self, shape, cxn_pt_idx):
|
|
25
|
+
"""
|
|
26
|
+
**EXPERIMENTAL** - *The current implementation only works properly
|
|
27
|
+
with rectangular shapes, such as pictures and rectangles. Use with
|
|
28
|
+
other shape types may cause unexpected visual alignment of the
|
|
29
|
+
connected end-point and could lead to a load error if cxn_pt_idx
|
|
30
|
+
exceeds the connection point count available on the connected shape.
|
|
31
|
+
That said, a quick test should reveal what to expect when using this
|
|
32
|
+
method with other shape types.*
|
|
33
|
+
|
|
34
|
+
Connect the beginning of this connector to *shape* at the connection
|
|
35
|
+
point specified by *cxn_pt_idx*. Each shape has zero or more
|
|
36
|
+
connection points and they are identified by index, starting with 0.
|
|
37
|
+
Generally, the first connection point of a shape is at the top center
|
|
38
|
+
of its bounding box and numbering proceeds counter-clockwise from
|
|
39
|
+
there. However this is only a convention and may vary, especially
|
|
40
|
+
with non built-in shapes.
|
|
41
|
+
"""
|
|
42
|
+
self._connect_begin_to(shape, cxn_pt_idx)
|
|
43
|
+
self._move_begin_to_cxn(shape, cxn_pt_idx)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def begin_x(self):
|
|
47
|
+
"""
|
|
48
|
+
Return the X-position of the begin point of this connector, in
|
|
49
|
+
English Metric Units (as a |Length| object).
|
|
50
|
+
"""
|
|
51
|
+
cxnSp = self._element
|
|
52
|
+
x, cx, flipH = cxnSp.x, cxnSp.cx, cxnSp.flipH
|
|
53
|
+
begin_x = x + cx if flipH else x
|
|
54
|
+
return Emu(begin_x)
|
|
55
|
+
|
|
56
|
+
@begin_x.setter
|
|
57
|
+
def begin_x(self, value):
|
|
58
|
+
cxnSp = self._element
|
|
59
|
+
x, cx, flipH, new_x = cxnSp.x, cxnSp.cx, cxnSp.flipH, int(value)
|
|
60
|
+
|
|
61
|
+
if flipH:
|
|
62
|
+
old_x = x + cx
|
|
63
|
+
dx = abs(new_x - old_x)
|
|
64
|
+
if new_x >= old_x:
|
|
65
|
+
cxnSp.cx = cx + dx
|
|
66
|
+
elif dx <= cx:
|
|
67
|
+
cxnSp.cx = cx - dx
|
|
68
|
+
else:
|
|
69
|
+
cxnSp.flipH = False
|
|
70
|
+
cxnSp.x = new_x
|
|
71
|
+
cxnSp.cx = dx - cx
|
|
72
|
+
else:
|
|
73
|
+
dx = abs(new_x - x)
|
|
74
|
+
if new_x <= x:
|
|
75
|
+
cxnSp.x = new_x
|
|
76
|
+
cxnSp.cx = cx + dx
|
|
77
|
+
elif dx <= cx:
|
|
78
|
+
cxnSp.x = new_x
|
|
79
|
+
cxnSp.cx = cx - dx
|
|
80
|
+
else:
|
|
81
|
+
cxnSp.flipH = True
|
|
82
|
+
cxnSp.x = x + cx
|
|
83
|
+
cxnSp.cx = dx - cx
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def begin_y(self):
|
|
87
|
+
"""
|
|
88
|
+
Return the Y-position of the begin point of this connector, in
|
|
89
|
+
English Metric Units (as a |Length| object).
|
|
90
|
+
"""
|
|
91
|
+
cxnSp = self._element
|
|
92
|
+
y, cy, flipV = cxnSp.y, cxnSp.cy, cxnSp.flipV
|
|
93
|
+
begin_y = y + cy if flipV else y
|
|
94
|
+
return Emu(begin_y)
|
|
95
|
+
|
|
96
|
+
@begin_y.setter
|
|
97
|
+
def begin_y(self, value):
|
|
98
|
+
cxnSp = self._element
|
|
99
|
+
y, cy, flipV, new_y = cxnSp.y, cxnSp.cy, cxnSp.flipV, int(value)
|
|
100
|
+
|
|
101
|
+
if flipV:
|
|
102
|
+
old_y = y + cy
|
|
103
|
+
dy = abs(new_y - old_y)
|
|
104
|
+
if new_y >= old_y:
|
|
105
|
+
cxnSp.cy = cy + dy
|
|
106
|
+
elif dy <= cy:
|
|
107
|
+
cxnSp.cy = cy - dy
|
|
108
|
+
else:
|
|
109
|
+
cxnSp.flipV = False
|
|
110
|
+
cxnSp.y = new_y
|
|
111
|
+
cxnSp.cy = dy - cy
|
|
112
|
+
else:
|
|
113
|
+
dy = abs(new_y - y)
|
|
114
|
+
if new_y <= y:
|
|
115
|
+
cxnSp.y = new_y
|
|
116
|
+
cxnSp.cy = cy + dy
|
|
117
|
+
elif dy <= cy:
|
|
118
|
+
cxnSp.y = new_y
|
|
119
|
+
cxnSp.cy = cy - dy
|
|
120
|
+
else:
|
|
121
|
+
cxnSp.flipV = True
|
|
122
|
+
cxnSp.y = y + cy
|
|
123
|
+
cxnSp.cy = dy - cy
|
|
124
|
+
|
|
125
|
+
def end_connect(self, shape, cxn_pt_idx):
|
|
126
|
+
"""
|
|
127
|
+
**EXPERIMENTAL** - *The current implementation only works properly
|
|
128
|
+
with rectangular shapes, such as pictures and rectangles. Use with
|
|
129
|
+
other shape types may cause unexpected visual alignment of the
|
|
130
|
+
connected end-point and could lead to a load error if cxn_pt_idx
|
|
131
|
+
exceeds the connection point count available on the connected shape.
|
|
132
|
+
That said, a quick test should reveal what to expect when using this
|
|
133
|
+
method with other shape types.*
|
|
134
|
+
|
|
135
|
+
Connect the ending of this connector to *shape* at the connection
|
|
136
|
+
point specified by *cxn_pt_idx*.
|
|
137
|
+
"""
|
|
138
|
+
self._connect_end_to(shape, cxn_pt_idx)
|
|
139
|
+
self._move_end_to_cxn(shape, cxn_pt_idx)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def end_x(self):
|
|
143
|
+
"""
|
|
144
|
+
Return the X-position of the end point of this connector, in English
|
|
145
|
+
Metric Units (as a |Length| object).
|
|
146
|
+
"""
|
|
147
|
+
cxnSp = self._element
|
|
148
|
+
x, cx, flipH = cxnSp.x, cxnSp.cx, cxnSp.flipH
|
|
149
|
+
end_x = x if flipH else x + cx
|
|
150
|
+
return Emu(end_x)
|
|
151
|
+
|
|
152
|
+
@end_x.setter
|
|
153
|
+
def end_x(self, value):
|
|
154
|
+
cxnSp = self._element
|
|
155
|
+
x, cx, flipH, new_x = cxnSp.x, cxnSp.cx, cxnSp.flipH, int(value)
|
|
156
|
+
|
|
157
|
+
if flipH:
|
|
158
|
+
dx = abs(new_x - x)
|
|
159
|
+
if new_x <= x:
|
|
160
|
+
cxnSp.x = new_x
|
|
161
|
+
cxnSp.cx = cx + dx
|
|
162
|
+
elif dx <= cx:
|
|
163
|
+
cxnSp.x = new_x
|
|
164
|
+
cxnSp.cx = cx - dx
|
|
165
|
+
else:
|
|
166
|
+
cxnSp.flipH = False
|
|
167
|
+
cxnSp.x = x + cx
|
|
168
|
+
cxnSp.cx = dx - cx
|
|
169
|
+
else:
|
|
170
|
+
old_x = x + cx
|
|
171
|
+
dx = abs(new_x - old_x)
|
|
172
|
+
if new_x >= old_x:
|
|
173
|
+
cxnSp.cx = cx + dx
|
|
174
|
+
elif dx <= cx:
|
|
175
|
+
cxnSp.cx = cx - dx
|
|
176
|
+
else:
|
|
177
|
+
cxnSp.flipH = True
|
|
178
|
+
cxnSp.x = new_x
|
|
179
|
+
cxnSp.cx = dx - cx
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def end_y(self):
|
|
183
|
+
"""
|
|
184
|
+
Return the Y-position of the end point of this connector, in English
|
|
185
|
+
Metric Units (as a |Length| object).
|
|
186
|
+
"""
|
|
187
|
+
cxnSp = self._element
|
|
188
|
+
y, cy, flipV = cxnSp.y, cxnSp.cy, cxnSp.flipV
|
|
189
|
+
end_y = y if flipV else y + cy
|
|
190
|
+
return Emu(end_y)
|
|
191
|
+
|
|
192
|
+
@end_y.setter
|
|
193
|
+
def end_y(self, value):
|
|
194
|
+
cxnSp = self._element
|
|
195
|
+
y, cy, flipV, new_y = cxnSp.y, cxnSp.cy, cxnSp.flipV, int(value)
|
|
196
|
+
|
|
197
|
+
if flipV:
|
|
198
|
+
dy = abs(new_y - y)
|
|
199
|
+
if new_y <= y:
|
|
200
|
+
cxnSp.y = new_y
|
|
201
|
+
cxnSp.cy = cy + dy
|
|
202
|
+
elif dy <= cy:
|
|
203
|
+
cxnSp.y = new_y
|
|
204
|
+
cxnSp.cy = cy - dy
|
|
205
|
+
else:
|
|
206
|
+
cxnSp.flipV = False
|
|
207
|
+
cxnSp.y = y + cy
|
|
208
|
+
cxnSp.cy = dy - cy
|
|
209
|
+
else:
|
|
210
|
+
old_y = y + cy
|
|
211
|
+
dy = abs(new_y - old_y)
|
|
212
|
+
if new_y >= old_y:
|
|
213
|
+
cxnSp.cy = cy + dy
|
|
214
|
+
elif dy <= cy:
|
|
215
|
+
cxnSp.cy = cy - dy
|
|
216
|
+
else:
|
|
217
|
+
cxnSp.flipV = True
|
|
218
|
+
cxnSp.y = new_y
|
|
219
|
+
cxnSp.cy = dy - cy
|
|
220
|
+
|
|
221
|
+
def get_or_add_ln(self):
|
|
222
|
+
"""Helper method required by |LineFormat|."""
|
|
223
|
+
return self._element.spPr.get_or_add_ln()
|
|
224
|
+
|
|
225
|
+
@lazyproperty
|
|
226
|
+
def line(self):
|
|
227
|
+
"""|LineFormat| instance for this connector.
|
|
228
|
+
|
|
229
|
+
Provides access to line properties such as line color, width, and
|
|
230
|
+
line style.
|
|
231
|
+
"""
|
|
232
|
+
return LineFormat(self)
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def ln(self):
|
|
236
|
+
"""Helper method required by |LineFormat|.
|
|
237
|
+
|
|
238
|
+
The ``<a:ln>`` element containing the line format properties such as
|
|
239
|
+
line color and width. |None| if no `<a:ln>` element is present.
|
|
240
|
+
"""
|
|
241
|
+
return self._element.spPr.ln
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def shape_type(self):
|
|
245
|
+
"""Member of `MSO_SHAPE_TYPE` identifying the type of this shape.
|
|
246
|
+
|
|
247
|
+
Unconditionally `MSO_SHAPE_TYPE.LINE` for a `Connector` object.
|
|
248
|
+
"""
|
|
249
|
+
return MSO_SHAPE_TYPE.LINE
|
|
250
|
+
|
|
251
|
+
def _connect_begin_to(self, shape, cxn_pt_idx):
|
|
252
|
+
"""
|
|
253
|
+
Add or update a stCxn element for this connector that connects its
|
|
254
|
+
begin point to the connection point of *shape* specified by
|
|
255
|
+
*cxn_pt_idx*.
|
|
256
|
+
"""
|
|
257
|
+
cNvCxnSpPr = self._element.nvCxnSpPr.cNvCxnSpPr
|
|
258
|
+
stCxn = cNvCxnSpPr.get_or_add_stCxn()
|
|
259
|
+
stCxn.id = shape.shape_id
|
|
260
|
+
stCxn.idx = cxn_pt_idx
|
|
261
|
+
|
|
262
|
+
def _connect_end_to(self, shape, cxn_pt_idx):
|
|
263
|
+
"""
|
|
264
|
+
Add or update an endCxn element for this connector that connects its
|
|
265
|
+
end point to the connection point of *shape* specified by
|
|
266
|
+
*cxn_pt_idx*.
|
|
267
|
+
"""
|
|
268
|
+
cNvCxnSpPr = self._element.nvCxnSpPr.cNvCxnSpPr
|
|
269
|
+
endCxn = cNvCxnSpPr.get_or_add_endCxn()
|
|
270
|
+
endCxn.id = shape.shape_id
|
|
271
|
+
endCxn.idx = cxn_pt_idx
|
|
272
|
+
|
|
273
|
+
def _move_begin_to_cxn(self, shape, cxn_pt_idx):
|
|
274
|
+
"""
|
|
275
|
+
Move the begin point of this connector to coordinates of the
|
|
276
|
+
connection point of *shape* specified by *cxn_pt_idx*.
|
|
277
|
+
"""
|
|
278
|
+
x, y, cx, cy = shape.left, shape.top, shape.width, shape.height
|
|
279
|
+
self.begin_x, self.begin_y = {
|
|
280
|
+
0: (int(x + cx / 2), y),
|
|
281
|
+
1: (x, int(y + cy / 2)),
|
|
282
|
+
2: (int(x + cx / 2), y + cy),
|
|
283
|
+
3: (x + cx, int(y + cy / 2)),
|
|
284
|
+
}[cxn_pt_idx]
|
|
285
|
+
|
|
286
|
+
def _move_end_to_cxn(self, shape, cxn_pt_idx):
|
|
287
|
+
"""
|
|
288
|
+
Move the end point of this connector to the coordinates of the
|
|
289
|
+
connection point of *shape* specified by *cxn_pt_idx*.
|
|
290
|
+
"""
|
|
291
|
+
x, y, cx, cy = shape.left, shape.top, shape.width, shape.height
|
|
292
|
+
self.end_x, self.end_y = {
|
|
293
|
+
0: (int(x + cx / 2), y),
|
|
294
|
+
1: (x, int(y + cy / 2)),
|
|
295
|
+
2: (int(x + cx / 2), y + cy),
|
|
296
|
+
3: (x + cx, int(y + cy / 2)),
|
|
297
|
+
}[cxn_pt_idx]
|
pptx2/shapes/freeform.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Objects related to construction of freeform shapes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Iterable, Iterator, Sequence
|
|
6
|
+
|
|
7
|
+
from pptx2.util import Emu, lazyproperty
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from typing_extensions import TypeAlias
|
|
11
|
+
|
|
12
|
+
from pptx2.oxml.shapes.autoshape import (
|
|
13
|
+
CT_Path2D,
|
|
14
|
+
CT_Path2DClose,
|
|
15
|
+
CT_Path2DLineTo,
|
|
16
|
+
CT_Path2DMoveTo,
|
|
17
|
+
CT_Shape,
|
|
18
|
+
)
|
|
19
|
+
from pptx2.shapes.shapetree import _BaseGroupShapes # pyright: ignore[reportPrivateUsage]
|
|
20
|
+
from pptx2.util import Length
|
|
21
|
+
|
|
22
|
+
CT_DrawingOperation: TypeAlias = "CT_Path2DClose | CT_Path2DLineTo | CT_Path2DMoveTo"
|
|
23
|
+
DrawingOperation: TypeAlias = "_LineSegment | _MoveTo | _Close"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FreeformBuilder(Sequence[DrawingOperation]):
|
|
27
|
+
"""Allows a freeform shape to be specified and created.
|
|
28
|
+
|
|
29
|
+
The initial pen position is provided on construction. From there, drawing proceeds using
|
|
30
|
+
successive calls to draw line segments. The freeform shape may be closed by calling the
|
|
31
|
+
:meth:`close` method.
|
|
32
|
+
|
|
33
|
+
A shape may have more than one contour, in which case overlapping areas are "subtracted". A
|
|
34
|
+
contour is a sequence of line segments beginning with a "move-to" operation. A move-to
|
|
35
|
+
operation is automatically inserted in each new freeform; additional move-to ops can be
|
|
36
|
+
inserted with the `.move_to()` method.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
shapes: _BaseGroupShapes,
|
|
42
|
+
start_x: Length,
|
|
43
|
+
start_y: Length,
|
|
44
|
+
x_scale: float,
|
|
45
|
+
y_scale: float,
|
|
46
|
+
):
|
|
47
|
+
super(FreeformBuilder, self).__init__()
|
|
48
|
+
self._shapes = shapes
|
|
49
|
+
self._start_x = start_x
|
|
50
|
+
self._start_y = start_y
|
|
51
|
+
self._x_scale = x_scale
|
|
52
|
+
self._y_scale = y_scale
|
|
53
|
+
|
|
54
|
+
def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride]
|
|
55
|
+
self, idx: int
|
|
56
|
+
) -> DrawingOperation:
|
|
57
|
+
return self._drawing_operations.__getitem__(idx)
|
|
58
|
+
|
|
59
|
+
def __iter__(self) -> Iterator[DrawingOperation]:
|
|
60
|
+
return self._drawing_operations.__iter__()
|
|
61
|
+
|
|
62
|
+
def __len__(self):
|
|
63
|
+
return self._drawing_operations.__len__()
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def new(
|
|
67
|
+
cls,
|
|
68
|
+
shapes: _BaseGroupShapes,
|
|
69
|
+
start_x: float,
|
|
70
|
+
start_y: float,
|
|
71
|
+
x_scale: float,
|
|
72
|
+
y_scale: float,
|
|
73
|
+
):
|
|
74
|
+
"""Return a new |FreeformBuilder| object.
|
|
75
|
+
|
|
76
|
+
The initial pen location is specified (in local coordinates) by
|
|
77
|
+
(`start_x`, `start_y`).
|
|
78
|
+
"""
|
|
79
|
+
return cls(shapes, Emu(int(round(start_x))), Emu(int(round(start_y))), x_scale, y_scale)
|
|
80
|
+
|
|
81
|
+
def add_line_segments(self, vertices: Iterable[tuple[float, float]], close: bool = True):
|
|
82
|
+
"""Add a straight line segment to each point in `vertices`.
|
|
83
|
+
|
|
84
|
+
`vertices` must be an iterable of (x, y) pairs (2-tuples). Each x and y value is rounded
|
|
85
|
+
to the nearest integer before use. The optional `close` parameter determines whether the
|
|
86
|
+
resulting contour is `closed` or left `open`.
|
|
87
|
+
|
|
88
|
+
Returns this |FreeformBuilder| object so it can be used in chained calls.
|
|
89
|
+
"""
|
|
90
|
+
for x, y in vertices:
|
|
91
|
+
self._add_line_segment(x, y)
|
|
92
|
+
if close:
|
|
93
|
+
self._add_close()
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def convert_to_shape(self, origin_x: Length = Emu(0), origin_y: Length = Emu(0)):
|
|
97
|
+
"""Return new freeform shape positioned relative to specified offset.
|
|
98
|
+
|
|
99
|
+
`origin_x` and `origin_y` locate the origin of the local coordinate system in slide
|
|
100
|
+
coordinates (EMU), perhaps most conveniently by use of a |Length| object.
|
|
101
|
+
|
|
102
|
+
Note that this method may be called more than once to add multiple shapes of the same
|
|
103
|
+
geometry in different locations on the slide.
|
|
104
|
+
"""
|
|
105
|
+
sp = self._add_freeform_sp(origin_x, origin_y)
|
|
106
|
+
path = self._start_path(sp)
|
|
107
|
+
for drawing_operation in self:
|
|
108
|
+
drawing_operation.apply_operation_to(path)
|
|
109
|
+
return self._shapes._shape_factory(sp) # pyright: ignore[reportPrivateUsage]
|
|
110
|
+
|
|
111
|
+
def move_to(self, x: float, y: float):
|
|
112
|
+
"""Move pen to (x, y) (local coordinates) without drawing line.
|
|
113
|
+
|
|
114
|
+
Returns this |FreeformBuilder| object so it can be used in chained calls.
|
|
115
|
+
"""
|
|
116
|
+
self._drawing_operations.append(_MoveTo.new(self, x, y))
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def shape_offset_x(self) -> Length:
|
|
121
|
+
"""Return x distance of shape origin from local coordinate origin.
|
|
122
|
+
|
|
123
|
+
The returned integer represents the leftmost extent of the freeform shape, in local
|
|
124
|
+
coordinates. Note that the bounding box of the shape need not start at the local origin.
|
|
125
|
+
"""
|
|
126
|
+
min_x = self._start_x
|
|
127
|
+
for drawing_operation in self:
|
|
128
|
+
if isinstance(drawing_operation, _Close):
|
|
129
|
+
continue
|
|
130
|
+
min_x = min(min_x, drawing_operation.x)
|
|
131
|
+
return Emu(min_x)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def shape_offset_y(self) -> Length:
|
|
135
|
+
"""Return y distance of shape origin from local coordinate origin.
|
|
136
|
+
|
|
137
|
+
The returned integer represents the topmost extent of the freeform shape, in local
|
|
138
|
+
coordinates. Note that the bounding box of the shape need not start at the local origin.
|
|
139
|
+
"""
|
|
140
|
+
min_y = self._start_y
|
|
141
|
+
for drawing_operation in self:
|
|
142
|
+
if isinstance(drawing_operation, _Close):
|
|
143
|
+
continue
|
|
144
|
+
min_y = min(min_y, drawing_operation.y)
|
|
145
|
+
return Emu(min_y)
|
|
146
|
+
|
|
147
|
+
def _add_close(self):
|
|
148
|
+
"""Add a close |_Close| operation to the drawing sequence."""
|
|
149
|
+
self._drawing_operations.append(_Close.new())
|
|
150
|
+
|
|
151
|
+
def _add_freeform_sp(self, origin_x: Length, origin_y: Length):
|
|
152
|
+
"""Add a freeform `p:sp` element having no drawing elements.
|
|
153
|
+
|
|
154
|
+
`origin_x` and `origin_y` are specified in slide coordinates, and represent the location
|
|
155
|
+
of the local coordinates origin on the slide.
|
|
156
|
+
"""
|
|
157
|
+
spTree = self._shapes._spTree # pyright: ignore[reportPrivateUsage]
|
|
158
|
+
return spTree.add_freeform_sp(
|
|
159
|
+
origin_x + self._left, origin_y + self._top, self._width, self._height
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _add_line_segment(self, x: float, y: float) -> None:
|
|
163
|
+
"""Add a |_LineSegment| operation to the drawing sequence."""
|
|
164
|
+
self._drawing_operations.append(_LineSegment.new(self, x, y))
|
|
165
|
+
|
|
166
|
+
@lazyproperty
|
|
167
|
+
def _drawing_operations(self) -> list[DrawingOperation]:
|
|
168
|
+
"""Return the sequence of drawing operation objects for freeform."""
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def _dx(self) -> Length:
|
|
173
|
+
"""Return width of this shape's path in local units."""
|
|
174
|
+
min_x = max_x = self._start_x
|
|
175
|
+
for drawing_operation in self:
|
|
176
|
+
if isinstance(drawing_operation, _Close):
|
|
177
|
+
continue
|
|
178
|
+
min_x = min(min_x, drawing_operation.x)
|
|
179
|
+
max_x = max(max_x, drawing_operation.x)
|
|
180
|
+
return Emu(max_x - min_x)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def _dy(self) -> Length:
|
|
184
|
+
"""Return integer height of this shape's path in local units."""
|
|
185
|
+
min_y = max_y = self._start_y
|
|
186
|
+
for drawing_operation in self:
|
|
187
|
+
if isinstance(drawing_operation, _Close):
|
|
188
|
+
continue
|
|
189
|
+
min_y = min(min_y, drawing_operation.y)
|
|
190
|
+
max_y = max(max_y, drawing_operation.y)
|
|
191
|
+
return Emu(max_y - min_y)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def _height(self):
|
|
195
|
+
"""Return vertical size of this shape's path in slide coordinates.
|
|
196
|
+
|
|
197
|
+
This value is based on the actual extents of the shape and does not include any
|
|
198
|
+
positioning offset.
|
|
199
|
+
"""
|
|
200
|
+
return int(round(self._dy * self._y_scale))
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def _left(self):
|
|
204
|
+
"""Return leftmost extent of this shape's path in slide coordinates.
|
|
205
|
+
|
|
206
|
+
Note that this value does not include any positioning offset; it assumes the drawing
|
|
207
|
+
(local) coordinate origin is at (0, 0) on the slide.
|
|
208
|
+
"""
|
|
209
|
+
return int(round(self.shape_offset_x * self._x_scale))
|
|
210
|
+
|
|
211
|
+
def _local_to_shape(self, local_x: Length, local_y: Length) -> tuple[Length, Length]:
|
|
212
|
+
"""Translate local coordinates point to shape coordinates.
|
|
213
|
+
|
|
214
|
+
Shape coordinates have the same unit as local coordinates, but are offset such that the
|
|
215
|
+
origin of the shape coordinate system (0, 0) is located at the top-left corner of the
|
|
216
|
+
shape bounding box.
|
|
217
|
+
"""
|
|
218
|
+
return Emu(local_x - self.shape_offset_x), Emu(local_y - self.shape_offset_y)
|
|
219
|
+
|
|
220
|
+
def _start_path(self, sp: CT_Shape) -> CT_Path2D:
|
|
221
|
+
"""Return a newly created `a:path` element added to `sp`.
|
|
222
|
+
|
|
223
|
+
The returned `a:path` element has an `a:moveTo` element representing the shape starting
|
|
224
|
+
point as its only child.
|
|
225
|
+
"""
|
|
226
|
+
path = sp.add_path(w=self._dx, h=self._dy)
|
|
227
|
+
path.add_moveTo(*self._local_to_shape(self._start_x, self._start_y))
|
|
228
|
+
return path
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def _top(self):
|
|
232
|
+
"""Return topmost extent of this shape's path in slide coordinates.
|
|
233
|
+
|
|
234
|
+
Note that this value does not include any positioning offset; it assumes the drawing
|
|
235
|
+
(local) coordinate origin is located at slide coordinates (0, 0) (top-left corner of
|
|
236
|
+
slide).
|
|
237
|
+
"""
|
|
238
|
+
return int(round(self.shape_offset_y * self._y_scale))
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def _width(self):
|
|
242
|
+
"""Return width of this shape's path in slide coordinates.
|
|
243
|
+
|
|
244
|
+
This value is based on the actual extents of the shape path and does not include any
|
|
245
|
+
positioning offset.
|
|
246
|
+
"""
|
|
247
|
+
return int(round(self._dx * self._x_scale))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class _BaseDrawingOperation(object):
|
|
251
|
+
"""Base class for freeform drawing operations.
|
|
252
|
+
|
|
253
|
+
A drawing operation has at least one location (x, y) in local coordinates.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def __init__(self, freeform_builder: FreeformBuilder, x: Length, y: Length):
|
|
257
|
+
super(_BaseDrawingOperation, self).__init__()
|
|
258
|
+
self._freeform_builder = freeform_builder
|
|
259
|
+
self._x = x
|
|
260
|
+
self._y = y
|
|
261
|
+
|
|
262
|
+
def apply_operation_to(self, path: CT_Path2D) -> CT_DrawingOperation:
|
|
263
|
+
"""Add the XML element(s) implementing this operation to `path`.
|
|
264
|
+
|
|
265
|
+
Must be implemented by each subclass.
|
|
266
|
+
"""
|
|
267
|
+
raise NotImplementedError("must be implemented by each subclass")
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def x(self) -> Length:
|
|
271
|
+
"""Return the horizontal (x) target location of this operation.
|
|
272
|
+
|
|
273
|
+
The returned value is an integer in local coordinates.
|
|
274
|
+
"""
|
|
275
|
+
return self._x
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def y(self) -> Length:
|
|
279
|
+
"""Return the vertical (y) target location of this operation.
|
|
280
|
+
|
|
281
|
+
The returned value is an integer in local coordinates.
|
|
282
|
+
"""
|
|
283
|
+
return self._y
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class _Close(object):
|
|
287
|
+
"""Specifies adding a `<a:close/>` element to the current contour."""
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def new(cls) -> _Close:
|
|
291
|
+
"""Return a new _Close object."""
|
|
292
|
+
return cls()
|
|
293
|
+
|
|
294
|
+
def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DClose:
|
|
295
|
+
"""Add `a:close` element to `path`."""
|
|
296
|
+
return path.add_close()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class _LineSegment(_BaseDrawingOperation):
|
|
300
|
+
"""Specifies a straight line segment ending at the specified point."""
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _LineSegment:
|
|
304
|
+
"""Return a new _LineSegment object ending at point *(x, y)*.
|
|
305
|
+
|
|
306
|
+
Both `x` and `y` are rounded to the nearest integer before use.
|
|
307
|
+
"""
|
|
308
|
+
return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y))))
|
|
309
|
+
|
|
310
|
+
def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DLineTo:
|
|
311
|
+
"""Add `a:lnTo` element to `path` for this line segment.
|
|
312
|
+
|
|
313
|
+
Returns the `a:lnTo` element newly added to the path.
|
|
314
|
+
"""
|
|
315
|
+
return path.add_lnTo(
|
|
316
|
+
Emu(self._x - self._freeform_builder.shape_offset_x),
|
|
317
|
+
Emu(self._y - self._freeform_builder.shape_offset_y),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class _MoveTo(_BaseDrawingOperation):
|
|
322
|
+
"""Specifies a new pen position."""
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _MoveTo:
|
|
326
|
+
"""Return a new _MoveTo object for move to point `(x, y)`.
|
|
327
|
+
|
|
328
|
+
Both `x` and `y` are rounded to the nearest integer before use.
|
|
329
|
+
"""
|
|
330
|
+
return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y))))
|
|
331
|
+
|
|
332
|
+
def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DMoveTo:
|
|
333
|
+
"""Add `a:moveTo` element to `path` for this line segment."""
|
|
334
|
+
return path.add_moveTo(
|
|
335
|
+
Emu(self._x - self._freeform_builder.shape_offset_x),
|
|
336
|
+
Emu(self._y - self._freeform_builder.shape_offset_y),
|
|
337
|
+
)
|