_writer.py 10 KB

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