forked from python-openxml/python-docx
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshape.py
More file actions
337 lines (276 loc) · 11.2 KB
/
shape.py
File metadata and controls
337 lines (276 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
"""Objects related to shapes.
A shape is a visual object that appears on the drawing layer of a document.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Iterator
from docx.enum.shape import WD_INLINE_SHAPE
from docx.oxml.ns import nsmap, qn
from docx.shared import Emu, Parented
if TYPE_CHECKING:
from docx.oxml.document import CT_Body
from docx.oxml.shape import CT_Anchor, CT_Inline
from docx.parts.story import StoryPart
from docx.shared import Length
class InlineShapes(Parented):
"""Sequence of |InlineShape| instances, supporting len(), iteration, and indexed access."""
def __init__(self, body_elm: CT_Body, parent: StoryPart):
super(InlineShapes, self).__init__(parent)
self._body = body_elm
def __getitem__(self, idx: int):
"""Provide indexed access, e.g. 'inline_shapes[idx]'."""
try:
inline = self._inline_lst[idx]
except IndexError:
msg = "inline shape index [%d] out of range" % idx
raise IndexError(msg)
return InlineShape(inline)
def __iter__(self):
return (InlineShape(inline) for inline in self._inline_lst)
def __len__(self):
return len(self._inline_lst)
@property
def _inline_lst(self):
body = self._body
xpath = "//w:p/w:r/w:drawing/wp:inline"
return body.xpath(xpath)
class InlineShape:
"""Proxy for an ``<wp:inline>`` element, representing the container for an inline
graphical object."""
def __init__(self, inline: CT_Inline):
super(InlineShape, self).__init__()
self._inline = inline
@property
def height(self) -> Length:
"""Read/write.
The display height of this inline shape as an |Emu| instance.
"""
return self._inline.extent.cy
@height.setter
def height(self, cy: Length):
self._inline.extent.cy = cy
self._inline.graphic.graphicData.pic.spPr.cy = cy
@property
def type(self):
"""The type of this inline shape as a member of
``docx.enum.shape.WD_INLINE_SHAPE``, e.g. ``LINKED_PICTURE``.
Read-only.
"""
graphicData = self._inline.graphic.graphicData
uri = graphicData.uri
if uri == nsmap["pic"]:
blip = graphicData.pic.blipFill.blip
if blip.link is not None:
return WD_INLINE_SHAPE.LINKED_PICTURE
return WD_INLINE_SHAPE.PICTURE
if uri == nsmap["c"]:
return WD_INLINE_SHAPE.CHART
if uri == nsmap["dgm"]:
return WD_INLINE_SHAPE.SMART_ART
return WD_INLINE_SHAPE.NOT_IMPLEMENTED
@property
def width(self):
"""Read/write.
The display width of this inline shape as an |Emu| instance.
"""
return self._inline.extent.cx
@width.setter
def width(self, cx: Length):
self._inline.extent.cx = cx
self._inline.graphic.graphicData.pic.spPr.cx = cx
class FloatingShapes(Parented):
"""Sequence of |FloatingShape| instances for anchored/floating shapes.
Floating shapes (also called anchored shapes) are positioned independently
of the text flow and can have text wrap around them.
Example::
# Iterate over floating shapes
for shape in document.floating_shapes:
print(f"Shape: {shape.name}, size: {shape.width}x{shape.height}")
# Access by index
first_shape = document.floating_shapes[0]
"""
def __init__(self, body_elm: CT_Body, parent: StoryPart):
super().__init__(parent)
self._body = body_elm
def __getitem__(self, idx: int) -> FloatingShape:
"""Provide indexed access, e.g. 'floating_shapes[idx]'."""
try:
anchor = self._anchor_lst[idx]
except IndexError:
raise IndexError(f"floating shape index [{idx}] out of range")
return FloatingShape(anchor, self.part)
def __iter__(self) -> Iterator[FloatingShape]:
"""Iterate over all floating shapes."""
return (FloatingShape(anchor, self.part) for anchor in self._anchor_lst)
def __len__(self) -> int:
"""Return the number of floating shapes."""
return len(self._anchor_lst)
@property
def _anchor_lst(self):
"""List of all wp:anchor elements in the document body."""
return self._body.xpath("//w:p/w:r/w:drawing/wp:anchor")
class FloatingShape(Parented):
"""Proxy for a ``<wp:anchor>`` element, representing a floating/anchored shape.
Floating shapes are positioned relative to page elements (page, margin,
column, paragraph, line, character) rather than flowing inline with text.
Text can wrap around floating shapes in various ways.
Attributes:
width: Display width in EMUs
height: Display height in EMUs
name: The name of the shape
description: Alt text description
is_behind_text: True if shape is behind document text
type: The shape type (PICTURE, CHART, etc.)
"""
def __init__(self, anchor: CT_Anchor, parent: StoryPart):
super().__init__(parent)
self._anchor = anchor
@property
def height(self) -> Length | None:
"""The display height of this floating shape in EMUs, or None if not set."""
extent = self._anchor.extent
return extent.cy if extent is not None else None
@property
def width(self) -> Length | None:
"""The display width of this floating shape in EMUs, or None if not set."""
extent = self._anchor.extent
return extent.cx if extent is not None else None
@property
def name(self) -> str:
"""The name of this shape, or empty string if not set."""
docPr = self._anchor.docPr
return docPr.name if docPr is not None else ""
@property
def description(self) -> str:
"""The description (alt text) of this shape, or empty string if not set."""
docPr = self._anchor.docPr
return docPr.descr if docPr is not None else ""
@property
def is_behind_text(self) -> bool:
"""True if this shape is positioned behind document text."""
return self._anchor.is_behind_text
@property
def type(self):
"""The type of this floating shape as a member of WD_INLINE_SHAPE enum.
Note: Uses the same enum as inline shapes since the content types are the same.
"""
graphic = self._anchor.graphic
if graphic is None:
return WD_INLINE_SHAPE.NOT_IMPLEMENTED
graphicData = graphic.graphicData
if graphicData is None:
return WD_INLINE_SHAPE.NOT_IMPLEMENTED
uri = graphicData.uri
if uri == nsmap["pic"]:
pic = graphicData.pic
if pic is not None:
blip = pic.blipFill.blip
if blip is not None and blip.link is not None:
return WD_INLINE_SHAPE.LINKED_PICTURE
return WD_INLINE_SHAPE.PICTURE
if uri == nsmap["c"]:
return WD_INLINE_SHAPE.CHART
if uri == nsmap["dgm"]:
return WD_INLINE_SHAPE.SMART_ART
return WD_INLINE_SHAPE.NOT_IMPLEMENTED
# --- Setters for modification ---
@width.setter
def width(self, value: Length) -> None:
"""Set the display width of this floating shape in EMUs."""
extent = self._anchor.extent
if extent is not None:
extent.cx = Emu(value)
# Also update the picture spPr if present
graphic = self._anchor.graphic
if graphic is not None:
graphicData = graphic.graphicData
if graphicData is not None and graphicData.pic is not None:
graphicData.pic.spPr.cx = Emu(value)
@height.setter
def height(self, value: Length) -> None:
"""Set the display height of this floating shape in EMUs."""
extent = self._anchor.extent
if extent is not None:
extent.cy = Emu(value)
# Also update the picture spPr if present
graphic = self._anchor.graphic
if graphic is not None:
graphicData = graphic.graphicData
if graphicData is not None and graphicData.pic is not None:
graphicData.pic.spPr.cy = Emu(value)
@name.setter
def name(self, value: str) -> None:
"""Set the name of this shape."""
docPr = self._anchor.docPr
if docPr is not None:
docPr.name = value
@description.setter
def description(self, value: str) -> None:
"""Set the description (alt text) of this shape."""
docPr = self._anchor.docPr
if docPr is not None:
docPr.set(qn("descr"), value)
@is_behind_text.setter
def is_behind_text(self, value: bool) -> None:
"""Set whether this shape is positioned behind document text."""
self._anchor.set("behindDoc", "1" if value else "0")
@property
def pos_x(self) -> Length | None:
"""The horizontal position offset in EMUs, or None if not set."""
posH = self._anchor.find(qn("wp:positionH"))
if posH is not None:
offset = posH.find(qn("wp:posOffset"))
if offset is not None and offset.text:
return Emu(int(offset.text))
return None
@pos_x.setter
def pos_x(self, value: Length) -> None:
"""Set the horizontal position offset in EMUs."""
posH = self._anchor.find(qn("wp:positionH"))
if posH is not None:
offset = posH.find(qn("wp:posOffset"))
if offset is not None:
offset.text = str(int(value))
@property
def pos_y(self) -> Length | None:
"""The vertical position offset in EMUs, or None if not set."""
posV = self._anchor.find(qn("wp:positionV"))
if posV is not None:
offset = posV.find(qn("wp:posOffset"))
if offset is not None and offset.text:
return Emu(int(offset.text))
return None
@pos_y.setter
def pos_y(self, value: Length) -> None:
"""Set the vertical position offset in EMUs."""
posV = self._anchor.find(qn("wp:positionV"))
if posV is not None:
offset = posV.find(qn("wp:posOffset"))
if offset is not None:
offset.text = str(int(value))
@property
def wrap_type(self) -> str | None:
"""The text wrapping style, or None if not recognized.
Returns one of: 'none', 'square', 'tight', 'through', 'topAndBottom'
"""
wrap_elements = {
qn("wp:wrapNone"): "none",
qn("wp:wrapSquare"): "square",
qn("wp:wrapTight"): "tight",
qn("wp:wrapThrough"): "through",
qn("wp:wrapTopAndBottom"): "topAndBottom",
}
for tag, wrap_name in wrap_elements.items():
if self._anchor.find(tag) is not None:
return wrap_name
return None
def delete(self) -> None:
"""Delete this floating shape from the document.
Removes the entire anchor element and its containing drawing element.
"""
# The anchor is inside w:drawing, which is inside w:r
# We remove the entire w:drawing element
drawing = self._anchor.getparent()
if drawing is not None:
run = drawing.getparent()
if run is not None:
run.remove(drawing)