named_styles.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # Copyright (c) 2010-2024 openpyxl
  2. from openpyxl.compat import safe_string
  3. from openpyxl.descriptors import (
  4. Typed,
  5. Integer,
  6. Bool,
  7. String,
  8. Sequence,
  9. )
  10. from openpyxl.descriptors.excel import ExtensionList
  11. from openpyxl.descriptors.serialisable import Serialisable
  12. from .fills import PatternFill, Fill
  13. from .fonts import Font
  14. from .borders import Border
  15. from .alignment import Alignment
  16. from .protection import Protection
  17. from .numbers import (
  18. NumberFormatDescriptor,
  19. BUILTIN_FORMATS_MAX_SIZE,
  20. BUILTIN_FORMATS_REVERSE,
  21. )
  22. from .cell_style import (
  23. StyleArray,
  24. CellStyle,
  25. )
  26. class NamedStyle(Serialisable):
  27. """
  28. Named and editable styles
  29. """
  30. font = Typed(expected_type=Font)
  31. fill = Typed(expected_type=Fill)
  32. border = Typed(expected_type=Border)
  33. alignment = Typed(expected_type=Alignment)
  34. number_format = NumberFormatDescriptor()
  35. protection = Typed(expected_type=Protection)
  36. builtinId = Integer(allow_none=True)
  37. hidden = Bool(allow_none=True)
  38. name = String()
  39. _wb = None
  40. _style = StyleArray()
  41. def __init__(self,
  42. name="Normal",
  43. font=None,
  44. fill=None,
  45. border=None,
  46. alignment=None,
  47. number_format=None,
  48. protection=None,
  49. builtinId=None,
  50. hidden=False,
  51. ):
  52. self.name = name
  53. self.font = font or Font()
  54. self.fill = fill or PatternFill()
  55. self.border = border or Border()
  56. self.alignment = alignment or Alignment()
  57. self.number_format = number_format
  58. self.protection = protection or Protection()
  59. self.builtinId = builtinId
  60. self.hidden = hidden
  61. self._wb = None
  62. self._style = StyleArray()
  63. def __setattr__(self, attr, value):
  64. super().__setattr__(attr, value)
  65. if getattr(self, '_wb', None) and attr in (
  66. 'font', 'fill', 'border', 'alignment', 'number_format', 'protection',
  67. ):
  68. self._recalculate()
  69. def __iter__(self):
  70. for key in ('name', 'builtinId', 'hidden', 'xfId'):
  71. value = getattr(self, key, None)
  72. if value is not None:
  73. yield key, safe_string(value)
  74. def bind(self, wb):
  75. """
  76. Bind a named style to a workbook
  77. """
  78. self._wb = wb
  79. self._recalculate()
  80. def _recalculate(self):
  81. self._style.fontId = self._wb._fonts.add(self.font)
  82. self._style.borderId = self._wb._borders.add(self.border)
  83. self._style.fillId = self._wb._fills.add(self.fill)
  84. self._style.protectionId = self._wb._protections.add(self.protection)
  85. self._style.alignmentId = self._wb._alignments.add(self.alignment)
  86. fmt = self.number_format
  87. if fmt in BUILTIN_FORMATS_REVERSE:
  88. fmt = BUILTIN_FORMATS_REVERSE[fmt]
  89. else:
  90. fmt = self._wb._number_formats.add(self.number_format) + (
  91. BUILTIN_FORMATS_MAX_SIZE)
  92. self._style.numFmtId = fmt
  93. def as_tuple(self):
  94. """Return a style array representing the current style"""
  95. return self._style
  96. def as_xf(self):
  97. """
  98. Return equivalent XfStyle
  99. """
  100. xf = CellStyle.from_array(self._style)
  101. xf.xfId = None
  102. xf.pivotButton = None
  103. xf.quotePrefix = None
  104. if self.alignment != Alignment():
  105. xf.alignment = self.alignment
  106. if self.protection != Protection():
  107. xf.protection = self.protection
  108. return xf
  109. def as_name(self):
  110. """
  111. Return relevant named style
  112. """
  113. named = _NamedCellStyle(
  114. name=self.name,
  115. builtinId=self.builtinId,
  116. hidden=self.hidden,
  117. xfId=self._style.xfId
  118. )
  119. return named
  120. class NamedStyleList(list):
  121. """
  122. Named styles are editable and can be applied to multiple objects
  123. As only the index is stored in referencing objects the order mus
  124. be preserved.
  125. Returns a list of NamedStyles
  126. """
  127. def __init__(self, iterable=()):
  128. """
  129. Allow a list of named styles to be passed in and index them.
  130. """
  131. for idx, s in enumerate(iterable, len(self)):
  132. s._style.xfId = idx
  133. super().__init__(iterable)
  134. @property
  135. def names(self):
  136. return [s.name for s in self]
  137. def __getitem__(self, key):
  138. if isinstance(key, int):
  139. return super().__getitem__(key)
  140. for idx, name in enumerate(self.names):
  141. if name == key:
  142. return self[idx]
  143. raise KeyError("No named style with the name{0} exists".format(key))
  144. def append(self, style):
  145. if not isinstance(style, NamedStyle):
  146. raise TypeError("""Only NamedStyle instances can be added""")
  147. elif style.name in self.names: # hotspot
  148. raise ValueError("""Style {0} exists already""".format(style.name))
  149. style._style.xfId = (len(self))
  150. super().append(style)
  151. class _NamedCellStyle(Serialisable):
  152. """
  153. Pointer-based representation of named styles in XML
  154. xfId refers to the corresponding CellStyleXfs
  155. Not used in client code.
  156. """
  157. tagname = "cellStyle"
  158. name = String()
  159. xfId = Integer()
  160. builtinId = Integer(allow_none=True)
  161. iLevel = Integer(allow_none=True)
  162. hidden = Bool(allow_none=True)
  163. customBuiltin = Bool(allow_none=True)
  164. extLst = Typed(expected_type=ExtensionList, allow_none=True)
  165. __elements__ = ()
  166. def __init__(self,
  167. name=None,
  168. xfId=None,
  169. builtinId=None,
  170. iLevel=None,
  171. hidden=None,
  172. customBuiltin=None,
  173. extLst=None,
  174. ):
  175. self.name = name
  176. self.xfId = xfId
  177. self.builtinId = builtinId
  178. self.iLevel = iLevel
  179. self.hidden = hidden
  180. self.customBuiltin = customBuiltin
  181. class _NamedCellStyleList(Serialisable):
  182. """
  183. Container for named cell style objects
  184. Not used in client code
  185. """
  186. tagname = "cellStyles"
  187. count = Integer(allow_none=True)
  188. cellStyle = Sequence(expected_type=_NamedCellStyle)
  189. __attrs__ = ("count",)
  190. def __init__(self,
  191. count=None,
  192. cellStyle=(),
  193. ):
  194. self.cellStyle = cellStyle
  195. @property
  196. def count(self):
  197. return len(self.cellStyle)
  198. def remove_duplicates(self):
  199. """
  200. Some applications contain duplicate definitions either by name or
  201. referenced style.
  202. As the references are 0-based indices, styles are sorted by
  203. index.
  204. Returns a list of style references with duplicates removed
  205. """
  206. def sort_fn(v):
  207. return v.xfId
  208. styles = []
  209. names = set()
  210. ids = set()
  211. for ns in sorted(self.cellStyle, key=sort_fn):
  212. if ns.xfId in ids or ns.name in names: # skip duplicates
  213. continue
  214. ids.add(ns.xfId)
  215. names.add(ns.name)
  216. styles.append(ns)
  217. return styles