_writer.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. from __future__ import absolute_import
  2. # Copyright (c) 2010-2019 openpyxl
  3. import atexit
  4. from collections import defaultdict
  5. from io import BytesIO
  6. import os
  7. from tempfile import NamedTemporaryFile
  8. from warnings import warn
  9. from openpyxl.xml.functions import xmlfile
  10. from openpyxl.xml.constants import SHEET_MAIN_NS
  11. from openpyxl.compat import unicode
  12. from openpyxl.comments.comment_sheet import CommentRecord
  13. from openpyxl.packaging.relationship import Relationship, RelationshipList
  14. from openpyxl.styles.differential import DifferentialStyle
  15. from .dimensions import SheetDimension
  16. from .hyperlink import HyperlinkList
  17. from .merge import MergeCell, MergeCells
  18. from .related import Related
  19. from .table import TablePartList
  20. from openpyxl.cell._writer import write_cell
  21. ALL_TEMP_FILES = []
  22. @atexit.register
  23. def _openpyxl_shutdown():
  24. for path in ALL_TEMP_FILES:
  25. if os.path.exists(path):
  26. os.remove(path)
  27. def create_temporary_file(suffix=''):
  28. fobj = NamedTemporaryFile(mode='w+', suffix=suffix,
  29. prefix='openpyxl.', delete=False)
  30. filename = fobj.name
  31. fobj.close()
  32. ALL_TEMP_FILES.append(filename)
  33. return filename
  34. class WorksheetWriter:
  35. def __init__(self, ws, out=None):
  36. self.ws = ws
  37. self.ws._comments = []
  38. if out is None:
  39. out = create_temporary_file()
  40. self.out = out
  41. self._rels = RelationshipList()
  42. self.xf = self.get_stream()
  43. next(self.xf) # start generator
  44. def write_properties(self):
  45. props = self.ws.sheet_properties
  46. self.xf.send(props.to_tree())
  47. def write_dimensions(self):
  48. """
  49. Write worksheet size if known
  50. """
  51. ref = getattr(self.ws, 'calculate_dimension', None)
  52. if ref:
  53. dim = SheetDimension(ref())
  54. self.xf.send(dim.to_tree())
  55. def write_format(self):
  56. self.ws.sheet_format.outlineLevelCol = self.ws.column_dimensions.max_outline
  57. fmt = self.ws.sheet_format
  58. self.xf.send(fmt.to_tree())
  59. def write_views(self):
  60. views = self.ws.views
  61. self.xf.send(views.to_tree())
  62. def write_cols(self):
  63. cols = self.ws.column_dimensions
  64. self.xf.send(cols.to_tree())
  65. def write_top(self):
  66. """
  67. Write all elements up to rows:
  68. properties
  69. dimensions
  70. views
  71. format
  72. cols
  73. """
  74. self.write_properties()
  75. self.write_dimensions()
  76. self.write_views()
  77. self.write_format()
  78. self.write_cols()
  79. def rows(self):
  80. """Return all rows, and any cells that they contain"""
  81. # order cells by row
  82. rows = defaultdict(list)
  83. for (row, col), cell in sorted(self.ws._cells.items()):
  84. rows[row].append(cell)
  85. # add empty rows if styling has been applied
  86. for row in set(self.ws.row_dimensions.keys()) - set(rows.keys()):
  87. rows[row] = []
  88. return sorted(rows.items())
  89. def write_rows(self):
  90. xf = self.xf.send(True)
  91. with xf.element("sheetData"):
  92. for row_idx, row in self.rows():
  93. self.write_row(xf, row, row_idx)
  94. self.xf.send(None) # return control to generator
  95. def write_row(self, xf, row, row_idx):
  96. attrs = {'r': '%d' % row_idx}
  97. dims = self.ws.row_dimensions
  98. attrs.update(dims.get(row_idx, {}))
  99. with xf.element("row", attrs):
  100. for cell in row:
  101. if cell._comment is not None:
  102. comment = CommentRecord.from_cell(cell)
  103. self.ws._comments.append(comment)
  104. if (
  105. cell._value is None
  106. and not cell.has_style
  107. and not cell._comment
  108. ):
  109. continue
  110. write_cell(xf, self.ws, cell, cell.has_style)
  111. def write_protection(self):
  112. prot = self.ws.protection
  113. if prot:
  114. self.xf.send(prot.to_tree())
  115. def write_scenarios(self):
  116. scenarios = self.ws.scenarios
  117. if scenarios:
  118. self.xf.send(scenarios.to_tree())
  119. def write_filter(self):
  120. flt = self.ws.auto_filter
  121. if flt:
  122. self.xf.send(flt.to_tree())
  123. def write_sort(self):
  124. """
  125. As per discusion with the OOXML Working Group global sort state is not required.
  126. openpyxl never reads it from existing files
  127. """
  128. pass
  129. def write_merged_cells(self):
  130. merged = self.ws.merged_cells
  131. if merged:
  132. cells = [MergeCell(str(ref)) for ref in self.ws.merged_cells]
  133. self.xf.send(MergeCells(mergeCell=cells).to_tree())
  134. def write_formatting(self):
  135. df = DifferentialStyle()
  136. wb = self.ws.parent
  137. for cf in self.ws.conditional_formatting:
  138. for rule in cf.rules:
  139. if rule.dxf and rule.dxf != df:
  140. rule.dxfId = wb._differential_styles.add(rule.dxf)
  141. self.xf.send(cf.to_tree())
  142. def write_validations(self):
  143. dv = self.ws.data_validations
  144. if dv:
  145. self.xf.send(dv.to_tree())
  146. def write_hyperlinks(self):
  147. links = HyperlinkList()
  148. for link in self.ws._hyperlinks:
  149. if link.target:
  150. rel = Relationship(type="hyperlink", TargetMode="External", Target=link.target)
  151. self._rels.append(rel)
  152. link.id = rel.id
  153. links.hyperlink.append(link)
  154. if links:
  155. self.xf.send(links.to_tree())
  156. def write_print(self):
  157. print_options = self.ws.print_options
  158. if print_options:
  159. self.xf.send(print_options.to_tree())
  160. def write_margins(self):
  161. margins = self.ws.page_margins
  162. if margins:
  163. self.xf.send(margins.to_tree())
  164. def write_page(self):
  165. setup = self.ws.page_setup
  166. if setup:
  167. self.xf.send(setup.to_tree())
  168. def write_header(self):
  169. hf = self.ws.HeaderFooter
  170. if hf:
  171. self.xf.send(hf.to_tree())
  172. def write_breaks(self):
  173. brks = self.ws.page_breaks
  174. for brk in brks:
  175. if brk:
  176. self.xf.send(brk.to_tree())
  177. def write_drawings(self):
  178. if self.ws._charts or self.ws._images:
  179. rel = Relationship(type="drawing", Target="")
  180. self._rels.append(rel)
  181. drawing = Related()
  182. drawing.id = rel.id
  183. self.xf.send(drawing.to_tree("drawing"))
  184. def write_legacy(self):
  185. """
  186. Comments & VBA controls use VML and require an additional element
  187. that is no longer in the specification.
  188. """
  189. if (self.ws.legacy_drawing is not None or self.ws._comments):
  190. legacy = Related(id="anysvml")
  191. self.xf.send(legacy.to_tree("legacyDrawing"))
  192. def write_tables(self):
  193. tables = TablePartList()
  194. for table in self.ws._tables:
  195. if not table.tableColumns:
  196. table._initialise_columns()
  197. if table.headerRowCount:
  198. row = self.ws[table.ref][0]
  199. for cell, col in zip(row, table.tableColumns):
  200. if cell.data_type != "s":
  201. warn("File may not be readable: column headings must be strings.")
  202. col.name = unicode(cell.value)
  203. rel = Relationship(Type=table._rel_type, Target="")
  204. self._rels.append(rel)
  205. table._rel_id = rel.Id
  206. tables.append(Related(id=rel.Id))
  207. if tables:
  208. self.xf.send(tables.to_tree())
  209. def get_stream(self):
  210. with xmlfile(self.out) as xf:
  211. with xf.element("worksheet", xmlns=SHEET_MAIN_NS):
  212. try:
  213. while True:
  214. el = (yield)
  215. if el is True:
  216. yield xf
  217. elif el is None: # et_xmlfile chokes
  218. continue
  219. else:
  220. xf.write(el)
  221. except GeneratorExit:
  222. pass
  223. def write_tail(self):
  224. """
  225. Write all elements after the rows
  226. calc properties
  227. protection
  228. protected ranges #
  229. scenarios
  230. filters
  231. sorts # always ignored
  232. data consolidation #
  233. custom views #
  234. merged cells
  235. phonetic properties #
  236. conditional formatting
  237. data validation
  238. hyperlinks
  239. print options
  240. page margins
  241. page setup
  242. header
  243. row breaks
  244. col breaks
  245. custom properties #
  246. cell watches #
  247. ignored errors #
  248. smart tags #
  249. drawing
  250. drawingHF #
  251. background #
  252. OLE objects #
  253. controls #
  254. web publishing #
  255. tables
  256. """
  257. self.write_protection()
  258. self.write_scenarios()
  259. self.write_filter()
  260. self.write_merged_cells()
  261. self.write_formatting()
  262. self.write_validations()
  263. self.write_hyperlinks()
  264. self.write_print()
  265. self.write_margins()
  266. self.write_page()
  267. self.write_header()
  268. self.write_breaks()
  269. self.write_drawings()
  270. self.write_legacy()
  271. self.write_tables()
  272. def write(self):
  273. """
  274. High level
  275. """
  276. self.write_top()
  277. self.write_rows()
  278. self.write_tail()
  279. self.close()
  280. def close(self):
  281. """
  282. Close the context manager
  283. """
  284. if self.xf:
  285. self.xf.close()
  286. def read(self):
  287. """
  288. Close the context manager and return serialised XML
  289. """
  290. self.close()
  291. if isinstance(self.out, BytesIO):
  292. return self.out.getvalue()
  293. with open(self.out, "rb") as src:
  294. out = src.read()
  295. return out
  296. def cleanup(self):
  297. """
  298. Remove tempfile
  299. """
  300. os.remove(self.out)
  301. ALL_TEMP_FILES.remove(self.out)