excel.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. from __future__ import absolute_import
  2. # Copyright (c) 2010-2019 openpyxl
  3. """Write a .xlsx file."""
  4. # Python stdlib imports
  5. import re
  6. from tempfile import TemporaryFile
  7. from zipfile import ZipFile, ZIP_DEFLATED
  8. # package imports
  9. from openpyxl.compat import deprecated
  10. from openpyxl.utils.exceptions import InvalidFileException
  11. from openpyxl.xml.constants import (
  12. ARC_SHARED_STRINGS,
  13. ARC_CONTENT_TYPES,
  14. ARC_ROOT_RELS,
  15. ARC_WORKBOOK_RELS,
  16. ARC_APP, ARC_CORE,
  17. ARC_THEME,
  18. ARC_STYLE,
  19. ARC_WORKBOOK,
  20. PACKAGE_WORKSHEETS,
  21. PACKAGE_CHARTSHEETS,
  22. PACKAGE_DRAWINGS,
  23. PACKAGE_CHARTS,
  24. PACKAGE_IMAGES,
  25. PACKAGE_XL
  26. )
  27. from openpyxl.drawing.spreadsheet_drawing import SpreadsheetDrawing
  28. from openpyxl.xml.functions import tostring, fromstring, Element
  29. from openpyxl.packaging.manifest import Manifest
  30. from openpyxl.packaging.relationship import (
  31. get_rels_path,
  32. RelationshipList,
  33. Relationship,
  34. )
  35. from openpyxl.comments.comment_sheet import CommentSheet
  36. from openpyxl.packaging.extended import ExtendedProperties
  37. from openpyxl.styles.stylesheet import write_stylesheet
  38. from openpyxl.worksheet._writer import WorksheetWriter
  39. from openpyxl.workbook._writer import WorkbookWriter
  40. from .theme import theme_xml
  41. class ExcelWriter(object):
  42. """Write a workbook object to an Excel file."""
  43. def __init__(self, workbook, archive):
  44. self._archive = archive
  45. self.workbook = workbook
  46. self.manifest = Manifest()
  47. self.vba_modified = set()
  48. self._tables = []
  49. self._charts = []
  50. self._images = []
  51. self._drawings = []
  52. self._comments = []
  53. self._pivots = []
  54. def write_data(self):
  55. """Write the various xml files into the zip archive."""
  56. # cleanup all worksheets
  57. archive = self._archive
  58. props = ExtendedProperties()
  59. archive.writestr(ARC_APP, tostring(props.to_tree()))
  60. archive.writestr(ARC_CORE, tostring(self.workbook.properties.to_tree()))
  61. if self.workbook.loaded_theme:
  62. archive.writestr(ARC_THEME, self.workbook.loaded_theme)
  63. else:
  64. archive.writestr(ARC_THEME, theme_xml)
  65. self._write_worksheets()
  66. self._write_chartsheets()
  67. self._write_images()
  68. self._write_charts()
  69. #self._archive.writestr(ARC_SHARED_STRINGS,
  70. #write_string_table(self.workbook.shared_strings))
  71. self._write_external_links()
  72. stylesheet = write_stylesheet(self.workbook)
  73. archive.writestr(ARC_STYLE, tostring(stylesheet))
  74. writer = WorkbookWriter(self.workbook)
  75. archive.writestr(ARC_ROOT_RELS, writer.write_root_rels())
  76. archive.writestr(ARC_WORKBOOK, writer.write())
  77. archive.writestr(ARC_WORKBOOK_RELS, writer.write_rels())
  78. self._merge_vba()
  79. self.manifest._write(archive, self.workbook)
  80. def _merge_vba(self):
  81. """
  82. If workbook contains macros then extract associated files from cache
  83. of old file and add to archive
  84. """
  85. ARC_VBA = re.compile("|".join(
  86. ('xl/vba', r'xl/drawings/.*vmlDrawing\d\.vml',
  87. 'xl/ctrlProps', 'customUI', 'xl/activeX', r'xl/media/.*\.emf')
  88. )
  89. )
  90. if self.workbook.vba_archive:
  91. for name in set(self.workbook.vba_archive.namelist()) - self.vba_modified:
  92. if ARC_VBA.match(name):
  93. self._archive.writestr(name, self.workbook.vba_archive.read(name))
  94. def _write_images(self):
  95. # delegate to object
  96. for img in self._images:
  97. self._archive.writestr(img.path[1:], img._data())
  98. def _write_charts(self):
  99. # delegate to object
  100. if len(self._charts) != len(set(self._charts)):
  101. raise InvalidFileException("The same chart cannot be used in more than one worksheet")
  102. for chart in self._charts:
  103. self._archive.writestr(chart.path[1:], tostring(chart._write()))
  104. self.manifest.append(chart)
  105. def _write_drawing(self, drawing):
  106. """
  107. Write a drawing
  108. """
  109. self._drawings.append(drawing)
  110. drawing._id = len(self._drawings)
  111. for chart in drawing.charts:
  112. self._charts.append(chart)
  113. chart._id = len(self._charts)
  114. for img in drawing.images:
  115. self._images.append(img)
  116. img._id = len(self._images)
  117. rels_path = get_rels_path(drawing.path)[1:]
  118. self._archive.writestr(drawing.path[1:], tostring(drawing._write()))
  119. self._archive.writestr(rels_path, tostring(drawing._write_rels()))
  120. self.manifest.append(drawing)
  121. def _write_chartsheets(self):
  122. for idx, sheet in enumerate(self.workbook.chartsheets, 1):
  123. sheet._id = idx
  124. xml = tostring(sheet.to_tree())
  125. self._archive.writestr(sheet.path[1:], xml)
  126. self.manifest.append(sheet)
  127. if sheet._drawing:
  128. self._write_drawing(sheet._drawing)
  129. rel = Relationship(type="drawing", Target=sheet._drawing.path)
  130. rels = RelationshipList()
  131. rels.append(rel)
  132. tree = rels.to_tree()
  133. rels_path = get_rels_path(sheet.path[1:])
  134. self._archive.writestr(rels_path, tostring(tree))
  135. def _write_comment(self, ws):
  136. cs = CommentSheet.from_comments(ws._comments)
  137. self._comments.append(cs)
  138. cs._id = len(self._comments)
  139. self._archive.writestr(cs.path[1:], tostring(cs.to_tree()))
  140. self.manifest.append(cs)
  141. if ws.legacy_drawing is None or self.workbook.vba_archive is None:
  142. ws.legacy_drawing = 'xl/drawings/commentsDrawing{0}.vml'.format(cs._id)
  143. vml = None
  144. else:
  145. vml = fromstring(self.workbook.vba_archive.read(ws.legacy_drawing))
  146. vml = cs.write_shapes(vml)
  147. self._archive.writestr(ws.legacy_drawing, vml)
  148. self.vba_modified.add(ws.legacy_drawing)
  149. comment_rel = Relationship(Id="comments", type=cs._rel_type, Target=cs.path)
  150. ws._rels.append(comment_rel)
  151. def write_worksheet(self, ws):
  152. ws._drawing = SpreadsheetDrawing()
  153. ws._drawing.charts = ws._charts
  154. ws._drawing.images = ws._images
  155. if self.workbook.write_only:
  156. if not ws.closed:
  157. ws.close()
  158. writer = ws._writer
  159. else:
  160. writer = WorksheetWriter(ws)
  161. writer.write()
  162. ws._rels = writer._rels
  163. self._archive.write(writer.out, ws.path[1:])
  164. self.manifest.append(ws)
  165. writer.cleanup()
  166. def _write_worksheets(self):
  167. pivot_caches = set()
  168. for idx, ws in enumerate(self.workbook.worksheets, 1):
  169. ws._id = idx
  170. self.write_worksheet(ws)
  171. if ws._drawing:
  172. self._write_drawing(ws._drawing)
  173. for r in ws._rels.Relationship:
  174. if "drawing" in r.Type:
  175. r.Target = ws._drawing.path
  176. if ws._comments:
  177. self._write_comment(ws)
  178. if ws.legacy_drawing is not None:
  179. shape_rel = Relationship(type="vmlDrawing", Id="anysvml",
  180. Target="/" + ws.legacy_drawing)
  181. ws._rels.append(shape_rel)
  182. for t in ws._tables:
  183. self._tables.append(t)
  184. t.id = len(self._tables)
  185. t._write(self._archive)
  186. self.manifest.append(t)
  187. ws._rels[t._rel_id].Target = t.path
  188. for p in ws._pivots:
  189. if p.cache not in pivot_caches:
  190. pivot_caches.add(p.cache)
  191. p.cache._id = len(pivot_caches)
  192. self._pivots.append(p)
  193. p._id = len(self._pivots)
  194. p._write(self._archive, self.manifest)
  195. self.workbook._pivots.append(p)
  196. r = Relationship(Type=p.rel_type, Target=p.path)
  197. ws._rels.append(r)
  198. if ws._rels:
  199. tree = ws._rels.to_tree()
  200. rels_path = get_rels_path(ws.path)[1:]
  201. self._archive.writestr(rels_path, tostring(tree))
  202. def _write_external_links(self):
  203. # delegate to object
  204. """Write links to external workbooks"""
  205. wb = self.workbook
  206. for idx, link in enumerate(wb._external_links, 1):
  207. link._id = idx
  208. rels_path = get_rels_path(link.path[1:])
  209. xml = link.to_tree()
  210. self._archive.writestr(link.path[1:], tostring(xml))
  211. rels = RelationshipList()
  212. rels.append(link.file_link)
  213. self._archive.writestr(rels_path, tostring(rels.to_tree()))
  214. self.manifest.append(link)
  215. def save(self):
  216. """Write data into the archive."""
  217. self.write_data()
  218. self._archive.close()
  219. def save_workbook(workbook, filename):
  220. """Save the given workbook on the filesystem under the name filename.
  221. :param workbook: the workbook to save
  222. :type workbook: :class:`openpyxl.workbook.Workbook`
  223. :param filename: the path to which save the workbook
  224. :type filename: string
  225. :rtype: bool
  226. """
  227. archive = ZipFile(filename, 'w', ZIP_DEFLATED, allowZip64=True)
  228. writer = ExcelWriter(workbook, archive)
  229. writer.save()
  230. return True
  231. @deprecated("Use a NamedTemporaryFile")
  232. def save_virtual_workbook(workbook):
  233. """Return an in-memory workbook, suitable for a Django response."""
  234. tmp = TemporaryFile()
  235. archive = ZipFile(tmp, 'w', ZIP_DEFLATED, allowZip64=True)
  236. writer = ExcelWriter(workbook, archive)
  237. writer.save()
  238. tmp.seek(0)
  239. virtual_workbook = tmp.read()
  240. tmp.close()
  241. return virtual_workbook