1# Copyright (c) 2010-2024 openpyxl
2
3from copy import copy
4
5from openpyxl.compat import safe_string
6from openpyxl.utils import (
7 get_column_letter,
8 get_column_interval,
9 column_index_from_string,
10 range_boundaries,
11)
12from openpyxl.utils.units import DEFAULT_COLUMN_WIDTH
13from openpyxl.descriptors import (
14 Integer,
15 Float,
16 Bool,
17 Strict,
18 String,
19 Alias,
20)
21from openpyxl.descriptors.serialisable import Serialisable
22from openpyxl.styles.styleable import StyleableObject
23from openpyxl.utils.bound_dictionary import BoundDictionary
24from openpyxl.xml.functions import Element
25
26
27class Dimension(Strict, StyleableObject):
28 """Information about the display properties of a row or column."""
29 __fields__ = ('hidden',
30 'outlineLevel',
31 'collapsed',)
32
33 index = Integer()
34 hidden = Bool()
35 outlineLevel = Integer(allow_none=True)
36 outline_level = Alias('outlineLevel')
37 collapsed = Bool()
38 style = Alias('style_id')
39
40
41 def __init__(self, index, hidden, outlineLevel,
42 collapsed, worksheet, visible=True, style=None):
43 super().__init__(sheet=worksheet, style_array=style)
44 self.index = index
45 self.hidden = hidden
46 self.outlineLevel = outlineLevel
47 self.collapsed = collapsed
48
49
50 def __iter__(self):
51 for key in self.__fields__:
52 value = getattr(self, key, None)
53 if value:
54 yield key, safe_string(value)
55
56
57 def __copy__(self):
58 cp = self.__new__(self.__class__)
59 attrib = self.__dict__
60 attrib['worksheet'] = self.parent
61 cp.__init__(**attrib)
62 cp._style = copy(self._style)
63 return cp
64
65
66 def __repr__(self):
67 return f"<{self.__class__.__name__} Instance, Attributes={dict(self)}>"
68
69
70class RowDimension(Dimension):
71 """Information about the display properties of a row."""
72
73 __fields__ = Dimension.__fields__ + ('ht', 'customFormat', 'customHeight', 's',
74 'thickBot', 'thickTop')
75 r = Alias('index')
76 s = Alias('style_id')
77 ht = Float(allow_none=True)
78 height = Alias('ht')
79 thickBot = Bool()
80 thickTop = Bool()
81
82 def __init__(self,
83 worksheet,
84 index=0,
85 ht=None,
86 customHeight=None, # do not write
87 s=None,
88 customFormat=None, # do not write
89 hidden=False,
90 outlineLevel=0,
91 outline_level=None,
92 collapsed=False,
93 visible=None,
94 height=None,
95 r=None,
96 spans=None,
97 thickBot=None,
98 thickTop=None,
99 **kw
100 ):
101 if r is not None:
102 index = r
103 if height is not None:
104 ht = height
105 self.ht = ht
106 if visible is not None:
107 hidden = not visible
108 if outline_level is not None:
109 outlineLevel = outline_level
110 self.thickBot = thickBot
111 self.thickTop = thickTop
112 super().__init__(index, hidden, outlineLevel,
113 collapsed, worksheet, style=s)
114
115 @property
116 def customFormat(self):
117 """Always true if there is a style for the row"""
118 return self.has_style
119
120 @property
121 def customHeight(self):
122 """Always true if there is a height for the row"""
123 return self.ht is not None
124
125
126class ColumnDimension(Dimension):
127 """Information about the display properties of a column."""
128
129 width = Float()
130 bestFit = Bool()
131 auto_size = Alias('bestFit')
132 index = String()
133 min = Integer(allow_none=True)
134 max = Integer(allow_none=True)
135 collapsed = Bool()
136
137 __fields__ = Dimension.__fields__ + ('width', 'bestFit', 'customWidth', 'style',
138 'min', 'max')
139
140 def __init__(self,
141 worksheet,
142 index='A',
143 width=DEFAULT_COLUMN_WIDTH,
144 bestFit=False,
145 hidden=False,
146 outlineLevel=0,
147 outline_level=None,
148 collapsed=False,
149 style=None,
150 min=None,
151 max=None,
152 customWidth=False, # do not write
153 visible=None,
154 auto_size=None,):
155 self.width = width
156 self.min = min
157 self.max = max
158 if visible is not None:
159 hidden = not visible
160 if auto_size is not None:
161 bestFit = auto_size
162 self.bestFit = bestFit
163 if outline_level is not None:
164 outlineLevel = outline_level
165 self.collapsed = collapsed
166 super().__init__(index, hidden, outlineLevel,
167 collapsed, worksheet, style=style)
168
169
170 @property
171 def customWidth(self):
172 """Always true if there is a width for the column"""
173 return bool(self.width)
174
175
176 def reindex(self):
177 """
178 Set boundaries for column definition
179 """
180 if not all([self.min, self.max]):
181 self.min = self.max = column_index_from_string(self.index)
182
183 @property
184 def range(self):
185 """Return the range of cells actually covered"""
186 return f"{get_column_letter(self.min)}:{get_column_letter(self.max)}"
187
188
189 def to_tree(self):
190 attrs = dict(self)
191 if attrs.keys() != {'min', 'max'}:
192 return Element("col", **attrs)
193
194
195class DimensionHolder(BoundDictionary):
196 """
197 Allow columns to be grouped
198 """
199
200 def __init__(self, worksheet, reference="index", default_factory=None):
201 self.worksheet = worksheet
202 self.max_outline = None
203 self.default_factory = default_factory
204 super().__init__(reference, default_factory)
205
206
207 def group(self, start, end=None, outline_level=1, hidden=False):
208 """allow grouping a range of consecutive rows or columns together
209
210 :param start: first row or column to be grouped (mandatory)
211 :param end: last row or column to be grouped (optional, default to start)
212 :param outline_level: outline level
213 :param hidden: should the group be hidden on workbook open or not
214 """
215 if end is None:
216 end = start
217
218 if isinstance(self.default_factory(), ColumnDimension):
219 new_dim = self[start]
220 new_dim.outline_level = outline_level
221 new_dim.hidden = hidden
222 work_sequence = get_column_interval(start, end)[1:]
223 for column_letter in work_sequence:
224 if column_letter in self:
225 del self[column_letter]
226 new_dim.min, new_dim.max = map(column_index_from_string, (start, end))
227 elif isinstance(self.default_factory(), RowDimension):
228 for el in range(start, end + 1):
229 new_dim = self.worksheet.row_dimensions[el]
230 new_dim.outline_level = outline_level
231 new_dim.hidden = hidden
232
233
234 def to_tree(self):
235
236 def sorter(value):
237 value.reindex()
238 return value.min
239
240 el = Element('cols')
241 outlines = set()
242
243 for col in sorted(self.values(), key=sorter):
244 obj = col.to_tree()
245 if obj is not None:
246 outlines.add(col.outlineLevel)
247 el.append(obj)
248
249 if outlines:
250 self.max_outline = max(outlines)
251
252 if len(el):
253 return el # must have at least one child
254
255
256class SheetFormatProperties(Serialisable):
257
258 tagname = "sheetFormatPr"
259
260 baseColWidth = Integer(allow_none=True)
261 defaultColWidth = Float(allow_none=True)
262 defaultRowHeight = Float()
263 customHeight = Bool(allow_none=True)
264 zeroHeight = Bool(allow_none=True)
265 thickTop = Bool(allow_none=True)
266 thickBottom = Bool(allow_none=True)
267 outlineLevelRow = Integer(allow_none=True)
268 outlineLevelCol = Integer(allow_none=True)
269
270 def __init__(self,
271 baseColWidth=8, #according to spec
272 defaultColWidth=None,
273 defaultRowHeight=15,
274 customHeight=None,
275 zeroHeight=None,
276 thickTop=None,
277 thickBottom=None,
278 outlineLevelRow=None,
279 outlineLevelCol=None,
280 ):
281 self.baseColWidth = baseColWidth
282 self.defaultColWidth = defaultColWidth
283 self.defaultRowHeight = defaultRowHeight
284 self.customHeight = customHeight
285 self.zeroHeight = zeroHeight
286 self.thickTop = thickTop
287 self.thickBottom = thickBottom
288 self.outlineLevelRow = outlineLevelRow
289 self.outlineLevelCol = outlineLevelCol
290
291
292class SheetDimension(Serialisable):
293
294 tagname = "dimension"
295
296 ref = String()
297
298 def __init__(self,
299 ref=None,
300 ):
301 self.ref = ref
302
303
304 @property
305 def boundaries(self):
306 return range_boundaries(self.ref)