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