spreadsheet_drawing.py 10 KB

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