_writer.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. # Copyright (c) 2010-2024 openpyxl
  2. """Write the workbook global settings to the archive."""
  3. from openpyxl.utils import quote_sheetname
  4. from openpyxl.xml.constants import (
  5. ARC_APP,
  6. ARC_CORE,
  7. ARC_CUSTOM,
  8. ARC_WORKBOOK,
  9. PKG_REL_NS,
  10. CUSTOMUI_NS,
  11. ARC_ROOT_RELS,
  12. )
  13. from openpyxl.xml.functions import tostring, fromstring
  14. from openpyxl.packaging.relationship import Relationship, RelationshipList
  15. from openpyxl.workbook.defined_name import (
  16. DefinedName,
  17. DefinedNameList,
  18. )
  19. from openpyxl.workbook.external_reference import ExternalReference
  20. from openpyxl.packaging.workbook import ChildSheet, WorkbookPackage, PivotCache
  21. from openpyxl.workbook.properties import WorkbookProperties
  22. from openpyxl.utils.datetime import CALENDAR_MAC_1904
  23. def get_active_sheet(wb):
  24. """
  25. Return the index of the active sheet.
  26. If the sheet set to active is hidden return the next visible sheet or None
  27. """
  28. visible_sheets = [idx for idx, sheet in enumerate(wb._sheets) if sheet.sheet_state == "visible"]
  29. if not visible_sheets:
  30. raise IndexError("At least one sheet must be visible")
  31. idx = wb._active_sheet_index
  32. sheet = wb.active
  33. if sheet and sheet.sheet_state == "visible":
  34. return idx
  35. for idx in visible_sheets[idx:]:
  36. wb.active = idx
  37. return idx
  38. return None
  39. class WorkbookWriter:
  40. def __init__(self, wb):
  41. self.wb = wb
  42. self.rels = RelationshipList()
  43. self.package = WorkbookPackage()
  44. self.package.workbookProtection = wb.security
  45. self.package.calcPr = wb.calculation
  46. def write_properties(self):
  47. props = WorkbookProperties() # needs a mapping to the workbook for preservation
  48. if self.wb.code_name is not None:
  49. props.codeName = self.wb.code_name
  50. if self.wb.excel_base_date == CALENDAR_MAC_1904:
  51. props.date1904 = True
  52. self.package.workbookPr = props
  53. def write_worksheets(self):
  54. for idx, sheet in enumerate(self.wb._sheets, 1):
  55. sheet_node = ChildSheet(name=sheet.title, sheetId=idx, id="rId{0}".format(idx))
  56. rel = Relationship(type=sheet._rel_type, Target=sheet.path)
  57. self.rels.append(rel)
  58. if not sheet.sheet_state == 'visible':
  59. if len(self.wb._sheets) == 1:
  60. raise ValueError("The only worksheet of a workbook cannot be hidden")
  61. sheet_node.state = sheet.sheet_state
  62. self.package.sheets.append(sheet_node)
  63. def write_refs(self):
  64. for link in self.wb._external_links:
  65. # need to match a counter with a workbook's relations
  66. rId = len(self.wb.rels) + 1
  67. rel = Relationship(type=link._rel_type, Target=link.path)
  68. self.rels.append(rel)
  69. ext = ExternalReference(id=rel.id)
  70. self.package.externalReferences.append(ext)
  71. def write_names(self):
  72. defined_names = list(self.wb.defined_names.values())
  73. for idx, sheet in enumerate(self.wb.worksheets):
  74. quoted = quote_sheetname(sheet.title)
  75. # local names
  76. if sheet.defined_names:
  77. names = sheet.defined_names.values()
  78. for n in names:
  79. n.localSheetId = idx
  80. defined_names.extend(names)
  81. if sheet.auto_filter:
  82. name = DefinedName(name='_FilterDatabase', localSheetId=idx, hidden=True)
  83. name.value = f"{quoted}!{sheet.auto_filter}"
  84. defined_names.append(name)
  85. if sheet.print_titles:
  86. name = DefinedName(name="Print_Titles", localSheetId=idx)
  87. name.value = sheet.print_titles
  88. defined_names.append(name)
  89. if sheet.print_area:
  90. name = DefinedName(name="Print_Area", localSheetId=idx)
  91. name.value = sheet.print_area
  92. defined_names.append(name)
  93. self.package.definedNames = DefinedNameList(definedName=defined_names)
  94. def write_pivots(self):
  95. pivot_caches = set()
  96. for pivot in self.wb._pivots:
  97. if pivot.cache not in pivot_caches:
  98. pivot_caches.add(pivot.cache)
  99. c = PivotCache(cacheId=pivot.cacheId)
  100. self.package.pivotCaches.append(c)
  101. rel = Relationship(Type=pivot.cache.rel_type, Target=pivot.cache.path)
  102. self.rels.append(rel)
  103. c.id = rel.id
  104. #self.wb._pivots = [] # reset
  105. def write_views(self):
  106. active = get_active_sheet(self.wb)
  107. if self.wb.views:
  108. self.wb.views[0].activeTab = active
  109. self.package.bookViews = self.wb.views
  110. def write(self):
  111. """Write the core workbook xml."""
  112. self.write_properties()
  113. self.write_worksheets()
  114. self.write_names()
  115. self.write_pivots()
  116. self.write_views()
  117. self.write_refs()
  118. return tostring(self.package.to_tree())
  119. def write_rels(self):
  120. """Write the workbook relationships xml."""
  121. styles = Relationship(type='styles', Target='styles.xml')
  122. self.rels.append(styles)
  123. theme = Relationship(type='theme', Target='theme/theme1.xml')
  124. self.rels.append(theme)
  125. if self.wb.vba_archive:
  126. vba = Relationship(type='', Target='vbaProject.bin')
  127. vba.Type ='http://schemas.microsoft.com/office/2006/relationships/vbaProject'
  128. self.rels.append(vba)
  129. return tostring(self.rels.to_tree())
  130. def write_root_rels(self):
  131. """Write the package relationships"""
  132. rels = RelationshipList()
  133. rel = Relationship(type="officeDocument", Target=ARC_WORKBOOK)
  134. rels.append(rel)
  135. rel = Relationship(Type=f"{PKG_REL_NS}/metadata/core-properties", Target=ARC_CORE)
  136. rels.append(rel)
  137. rel = Relationship(type="extended-properties", Target=ARC_APP)
  138. rels.append(rel)
  139. if len(self.wb.custom_doc_props) >= 1:
  140. rel = Relationship(type="custom-properties", Target=ARC_CUSTOM)
  141. rels.append(rel)
  142. if self.wb.vba_archive is not None:
  143. # See if there was a customUI relation and reuse it
  144. xml = fromstring(self.wb.vba_archive.read(ARC_ROOT_RELS))
  145. root_rels = RelationshipList.from_tree(xml)
  146. for rel in root_rels.find(CUSTOMUI_NS):
  147. rels.append(rel)
  148. return tostring(rels.to_tree())