1# Copyright (c) 2010-2024 openpyxl
2
3from openpyxl.descriptors.serialisable import Serialisable
4from openpyxl.descriptors import (
5 Typed,
6 Bool,
7 NoneSet,
8 Integer,
9 Sequence,
10 Alias,
11)
12from openpyxl.descriptors.nested import (
13 NestedText,
14 NestedNoneSet,
15)
16from openpyxl.descriptors.excel import Relation
17
18from openpyxl.packaging.relationship import (
19 Relationship,
20 RelationshipList,
21)
22from openpyxl.utils import coordinate_to_tuple
23from openpyxl.utils.units import (
24 cm_to_EMU,
25 pixels_to_EMU,
26)
27from openpyxl.drawing.image import Image
28
29from openpyxl.xml.constants import SHEET_DRAWING_NS
30
31from openpyxl.chart._chart import ChartBase
32from .xdr import (
33 XDRPoint2D,
34 XDRPositiveSize2D,
35)
36from .fill import Blip
37from .connector import Shape
38from .graphic import (
39 GroupShape,
40 GraphicFrame,
41 )
42from .geometry import PresetGeometry2D
43from .picture import PictureFrame
44from .relation import ChartRelation
45
46
47class AnchorClientData(Serialisable):
48
49 fLocksWithSheet = Bool(allow_none=True)
50 fPrintsWithSheet = Bool(allow_none=True)
51
52 def __init__(self,
53 fLocksWithSheet=None,
54 fPrintsWithSheet=None,
55 ):
56 self.fLocksWithSheet = fLocksWithSheet
57 self.fPrintsWithSheet = fPrintsWithSheet
58
59
60class AnchorMarker(Serialisable):
61
62 tagname = "marker"
63
64 col = NestedText(expected_type=int)
65 colOff = NestedText(expected_type=int)
66 row = NestedText(expected_type=int)
67 rowOff = NestedText(expected_type=int)
68
69 def __init__(self,
70 col=0,
71 colOff=0,
72 row=0,
73 rowOff=0,
74 ):
75 self.col = col
76 self.colOff = colOff
77 self.row = row
78 self.rowOff = rowOff
79
80
81class _AnchorBase(Serialisable):
82
83 #one of
84 sp = Typed(expected_type=Shape, allow_none=True)
85 shape = Alias("sp")
86 grpSp = Typed(expected_type=GroupShape, allow_none=True)
87 groupShape = Alias("grpSp")
88 graphicFrame = Typed(expected_type=GraphicFrame, allow_none=True)
89 cxnSp = Typed(expected_type=Shape, allow_none=True)
90 connectionShape = Alias("cxnSp")
91 pic = Typed(expected_type=PictureFrame, allow_none=True)
92 contentPart = Relation()
93
94 clientData = Typed(expected_type=AnchorClientData)
95
96 __elements__ = ('sp', 'grpSp', 'graphicFrame',
97 'cxnSp', 'pic', 'contentPart', 'clientData')
98
99 def __init__(self,
100 clientData=None,
101 sp=None,
102 grpSp=None,
103 graphicFrame=None,
104 cxnSp=None,
105 pic=None,
106 contentPart=None
107 ):
108 if clientData is None:
109 clientData = AnchorClientData()
110 self.clientData = clientData
111 self.sp = sp
112 self.grpSp = grpSp
113 self.graphicFrame = graphicFrame
114 self.cxnSp = cxnSp
115 self.pic = pic
116 self.contentPart = contentPart
117
118
119class AbsoluteAnchor(_AnchorBase):
120
121 tagname = "absoluteAnchor"
122
123 pos = Typed(expected_type=XDRPoint2D)
124 ext = Typed(expected_type=XDRPositiveSize2D)
125
126 sp = _AnchorBase.sp
127 grpSp = _AnchorBase.grpSp
128 graphicFrame = _AnchorBase.graphicFrame
129 cxnSp = _AnchorBase.cxnSp
130 pic = _AnchorBase.pic
131 contentPart = _AnchorBase.contentPart
132 clientData = _AnchorBase.clientData
133
134 __elements__ = ('pos', 'ext') + _AnchorBase.__elements__
135
136 def __init__(self,
137 pos=None,
138 ext=None,
139 **kw
140 ):
141 if pos is None:
142 pos = XDRPoint2D(0, 0)
143 self.pos = pos
144 if ext is None:
145 ext = XDRPositiveSize2D(0, 0)
146 self.ext = ext
147 super().__init__(**kw)
148
149
150class OneCellAnchor(_AnchorBase):
151
152 tagname = "oneCellAnchor"
153
154 _from = Typed(expected_type=AnchorMarker)
155 ext = Typed(expected_type=XDRPositiveSize2D)
156
157 sp = _AnchorBase.sp
158 grpSp = _AnchorBase.grpSp
159 graphicFrame = _AnchorBase.graphicFrame
160 cxnSp = _AnchorBase.cxnSp
161 pic = _AnchorBase.pic
162 contentPart = _AnchorBase.contentPart
163 clientData = _AnchorBase.clientData
164
165 __elements__ = ('_from', 'ext') + _AnchorBase.__elements__
166
167
168 def __init__(self,
169 _from=None,
170 ext=None,
171 **kw
172 ):
173 if _from is None:
174 _from = AnchorMarker()
175 self._from = _from
176 if ext is None:
177 ext = XDRPositiveSize2D(0, 0)
178 self.ext = ext
179 super().__init__(**kw)
180
181
182class TwoCellAnchor(_AnchorBase):
183
184 tagname = "twoCellAnchor"
185
186 editAs = NoneSet(values=(['twoCell', 'oneCell', 'absolute']))
187 _from = Typed(expected_type=AnchorMarker)
188 to = Typed(expected_type=AnchorMarker)
189
190 sp = _AnchorBase.sp
191 grpSp = _AnchorBase.grpSp
192 graphicFrame = _AnchorBase.graphicFrame
193 cxnSp = _AnchorBase.cxnSp
194 pic = _AnchorBase.pic
195 contentPart = _AnchorBase.contentPart
196 clientData = _AnchorBase.clientData
197
198 __elements__ = ('_from', 'to') + _AnchorBase.__elements__
199
200 def __init__(self,
201 editAs=None,
202 _from=None,
203 to=None,
204 **kw
205 ):
206 self.editAs = editAs
207 if _from is None:
208 _from = AnchorMarker()
209 self._from = _from
210 if to is None:
211 to = AnchorMarker()
212 self.to = to
213 super().__init__(**kw)
214
215
216def _check_anchor(obj):
217 """
218 Check whether an object has an existing Anchor object
219 If not create a OneCellAnchor using the provided coordinate
220 """
221 anchor = obj.anchor
222 if not isinstance(anchor, _AnchorBase):
223 row, col = coordinate_to_tuple(anchor.upper())
224 anchor = OneCellAnchor()
225 anchor._from.row = row -1
226 anchor._from.col = col -1
227 if isinstance(obj, ChartBase):
228 anchor.ext.width = cm_to_EMU(obj.width)
229 anchor.ext.height = cm_to_EMU(obj.height)
230 elif isinstance(obj, Image):
231 anchor.ext.width = pixels_to_EMU(obj.width)
232 anchor.ext.height = pixels_to_EMU(obj.height)
233 return anchor
234
235
236class SpreadsheetDrawing(Serialisable):
237
238 tagname = "wsDr"
239 mime_type = "application/vnd.openxmlformats-officedocument.drawing+xml"
240 _rel_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
241 _path = PartName="/xl/drawings/drawing{0}.xml"
242 _id = None
243
244 twoCellAnchor = Sequence(expected_type=TwoCellAnchor, allow_none=True)
245 oneCellAnchor = Sequence(expected_type=OneCellAnchor, allow_none=True)
246 absoluteAnchor = Sequence(expected_type=AbsoluteAnchor, allow_none=True)
247
248 __elements__ = ("twoCellAnchor", "oneCellAnchor", "absoluteAnchor")
249
250 def __init__(self,
251 twoCellAnchor=(),
252 oneCellAnchor=(),
253 absoluteAnchor=(),
254 ):
255 self.twoCellAnchor = twoCellAnchor
256 self.oneCellAnchor = oneCellAnchor
257 self.absoluteAnchor = absoluteAnchor
258 self.charts = []
259 self.images = []
260 self._rels = []
261
262
263 def __hash__(self):
264 """
265 Just need to check for identity
266 """
267 return id(self)
268
269
270 def __bool__(self):
271 return bool(self.charts) or bool(self.images)
272
273
274
275 def _write(self):
276 """
277 create required structure and the serialise
278 """
279 anchors = []
280 for idx, obj in enumerate(self.charts + self.images, 1):
281 anchor = _check_anchor(obj)
282 if isinstance(obj, ChartBase):
283 rel = Relationship(type="chart", Target=obj.path)
284 anchor.graphicFrame = self._chart_frame(idx)
285 elif isinstance(obj, Image):
286 rel = Relationship(type="image", Target=obj.path)
287 child = anchor.pic or anchor.groupShape and anchor.groupShape.pic
288 if not child:
289 anchor.pic = self._picture_frame(idx)
290 else:
291 child.blipFill.blip.embed = "rId{0}".format(idx)
292
293 anchors.append(anchor)
294 self._rels.append(rel)
295
296 for a in anchors:
297 if isinstance(a, OneCellAnchor):
298 self.oneCellAnchor.append(a)
299 elif isinstance(a, TwoCellAnchor):
300 self.twoCellAnchor.append(a)
301 else:
302 self.absoluteAnchor.append(a)
303
304 tree = self.to_tree()
305 tree.set('xmlns', SHEET_DRAWING_NS)
306 return tree
307
308
309 def _chart_frame(self, idx):
310 chart_rel = ChartRelation(f"rId{idx}")
311 frame = GraphicFrame()
312 nv = frame.nvGraphicFramePr.cNvPr
313 nv.id = idx
314 nv.name = "Chart {0}".format(idx)
315 frame.graphic.graphicData.chart = chart_rel
316 return frame
317
318
319 def _picture_frame(self, idx):
320 pic = PictureFrame()
321 pic.nvPicPr.cNvPr.descr = "Picture"
322 pic.nvPicPr.cNvPr.id = idx
323 pic.nvPicPr.cNvPr.name = "Image {0}".format(idx)
324
325 pic.blipFill.blip = Blip()
326 pic.blipFill.blip.embed = "rId{0}".format(idx)
327 pic.blipFill.blip.cstate = "print"
328
329 pic.spPr.prstGeom = PresetGeometry2D(prst="rect")
330 pic.spPr.ln = None
331 return pic
332
333
334 def _write_rels(self):
335 rels = RelationshipList()
336 for r in self._rels:
337 rels.append(r)
338 return rels.to_tree()
339
340
341 @property
342 def path(self):
343 return self._path.format(self._id)
344
345
346 @property
347 def _chart_rels(self):
348 """
349 Get relationship information for each chart and bind anchor to it
350 """
351 rels = []
352 anchors = self.absoluteAnchor + self.oneCellAnchor + self.twoCellAnchor
353 for anchor in anchors:
354 if anchor.graphicFrame is not None:
355 graphic = anchor.graphicFrame.graphic
356 rel = graphic.graphicData.chart
357 if rel is not None:
358 rel.anchor = anchor
359 rel.anchor.graphicFrame = None
360 rels.append(rel)
361 return rels
362
363
364 @property
365 def _blip_rels(self):
366 """
367 Get relationship information for each blip and bind anchor to it
368
369 Images that are not part of the XLSX package will be ignored.
370 """
371 rels = []
372 anchors = self.absoluteAnchor + self.oneCellAnchor + self.twoCellAnchor
373
374 for anchor in anchors:
375 child = anchor.pic or anchor.groupShape and anchor.groupShape.pic
376 if child and child.blipFill:
377 rel = child.blipFill.blip
378 if rel is not None and rel.embed:
379 rel.anchor = anchor
380 rels.append(rel)
381
382 return rels