Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xlsxwriter/styles.py: 56%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1###############################################################################
2#
3# Styles - A class for writing the Excel XLSX Worksheet file.
4#
5# SPDX-License-Identifier: BSD-2-Clause
6#
7# Copyright (c) 2013-2026, John McNamara, jmcnamara@cpan.org
8#
10from enum import Enum
12# Package imports.
13from xlsxwriter import xmlwriter
16class XFormatType(Enum):
17 """
18 Enum to distinguish the type of cell xf format since style and the default
19 (the first format) are handled slightly differently.
21 """
23 USER = 1
24 STYLE = 2
25 DEFAULT = 3
28class Styles(xmlwriter.XMLwriter):
29 """
30 A class for writing the Excel XLSX Styles file.
33 """
35 ###########################################################################
36 #
37 # Public API.
38 #
39 ###########################################################################
41 def __init__(self) -> None:
42 """
43 Constructor.
45 """
47 super().__init__()
49 self.xf_formats = []
50 self.palette = []
51 self.font_count = 0
52 self.num_formats = []
53 self.border_count = 0
54 self.fill_count = 0
55 self.custom_colors = []
56 self.dxf_formats = []
57 self.has_hyperlink = False
58 self.hyperlink_font_id = 0
59 self.has_comments = False
61 ###########################################################################
62 #
63 # Private API.
64 #
65 ###########################################################################
67 def _assemble_xml_file(self) -> None:
68 # Assemble and write the XML file.
70 # Write the XML declaration.
71 self._xml_declaration()
73 # Add the style sheet.
74 self._write_style_sheet()
76 # Write the number formats.
77 self._write_num_fmts()
79 # Write the fonts.
80 self._write_fonts()
82 # Write the fills.
83 self._write_fills()
85 # Write the borders element.
86 self._write_borders()
88 # Write the cellStyleXfs element.
89 self._write_cell_style_xfs()
91 # Write the cellXfs element.
92 self._write_cell_xfs()
94 # Write the cellStyles element.
95 self._write_cell_styles()
97 # Write the dxfs element.
98 self._write_dxfs()
100 # Write the tableStyles element.
101 self._write_table_styles()
103 # Write the colors element.
104 self._write_colors()
106 # Close the style sheet tag.
107 self._xml_end_tag("styleSheet")
109 # Close the file.
110 self._xml_close()
112 def _set_style_properties(self, properties) -> None:
113 # Pass in the Format objects and other properties used in the styles.
115 self.xf_formats = properties[0]
116 self.palette = properties[1]
117 self.font_count = properties[2]
118 self.num_formats = properties[3]
119 self.border_count = properties[4]
120 self.fill_count = properties[5]
121 self.custom_colors = properties[6]
122 self.dxf_formats = properties[7]
123 self.has_comments = properties[8]
125 ###########################################################################
126 #
127 # XML methods.
128 #
129 ###########################################################################
131 def _write_style_sheet(self) -> None:
132 # Write the <styleSheet> element.
133 xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
135 attributes = [("xmlns", xmlns)]
136 self._xml_start_tag("styleSheet", attributes)
138 def _write_num_fmts(self) -> None:
139 # Write the <numFmts> element.
140 if not self.num_formats:
141 return
143 attributes = [("count", len(self.num_formats))]
144 self._xml_start_tag("numFmts", attributes)
146 # Write the numFmts elements.
147 for index, num_format in enumerate(self.num_formats, 164):
148 self._write_num_fmt(index, num_format)
150 self._xml_end_tag("numFmts")
152 def _write_num_fmt(self, num_fmt_id, format_code) -> None:
153 # Write the <numFmt> element.
154 format_codes = {
155 0: "General",
156 1: "0",
157 2: "0.00",
158 3: "#,##0",
159 4: "#,##0.00",
160 5: "($#,##0_);($#,##0)",
161 6: "($#,##0_);[Red]($#,##0)",
162 7: "($#,##0.00_);($#,##0.00)",
163 8: "($#,##0.00_);[Red]($#,##0.00)",
164 9: "0%",
165 10: "0.00%",
166 11: "0.00E+00",
167 12: "# ?/?",
168 13: "# ??/??",
169 14: "m/d/yy",
170 15: "d-mmm-yy",
171 16: "d-mmm",
172 17: "mmm-yy",
173 18: "h:mm AM/PM",
174 19: "h:mm:ss AM/PM",
175 20: "h:mm",
176 21: "h:mm:ss",
177 22: "m/d/yy h:mm",
178 37: "(#,##0_);(#,##0)",
179 38: "(#,##0_);[Red](#,##0)",
180 39: "(#,##0.00_);(#,##0.00)",
181 40: "(#,##0.00_);[Red](#,##0.00)",
182 41: '_(* #,##0_);_(* (#,##0);_(* "-"_);_(_)',
183 42: '_($* #,##0_);_($* (#,##0);_($* "-"_);_(_)',
184 43: '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(_)',
185 44: '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(_)',
186 45: "mm:ss",
187 46: "[h]:mm:ss",
188 47: "mm:ss.0",
189 48: "##0.0E+0",
190 49: "@",
191 }
193 # Set the format code for built-in number formats.
194 if num_fmt_id < 164:
195 format_code = format_codes.get(num_fmt_id, "General")
197 attributes = [
198 ("numFmtId", num_fmt_id),
199 ("formatCode", format_code),
200 ]
202 self._xml_empty_tag("numFmt", attributes)
204 def _write_fonts(self) -> None:
205 # Write the <fonts> element.
206 if self.has_comments:
207 # Add extra font for comments.
208 attributes = [("count", self.font_count + 1)]
209 else:
210 attributes = [("count", self.font_count)]
212 self._xml_start_tag("fonts", attributes)
214 # Write the font elements for xf_format objects that have them.
215 for xf_format in self.xf_formats:
216 if xf_format.has_font:
217 self._write_font(xf_format)
219 if self.has_comments:
220 self._write_comment_font()
222 self._xml_end_tag("fonts")
224 def _write_font(self, xf_format, is_dxf_format=False) -> None:
225 # Write the <font> element.
226 self._xml_start_tag("font")
228 # The condense and extend elements are mainly used in dxf formats.
229 if xf_format.font_condense:
230 self._write_condense()
232 if xf_format.font_extend:
233 self._write_extend()
235 if xf_format.bold:
236 self._xml_empty_tag("b")
238 if xf_format.italic:
239 self._xml_empty_tag("i")
241 if xf_format.font_strikeout:
242 self._xml_empty_tag("strike")
244 if xf_format.font_outline:
245 self._xml_empty_tag("outline")
247 if xf_format.font_shadow:
248 self._xml_empty_tag("shadow")
250 # Handle the underline variants.
251 if xf_format.underline:
252 self._write_underline(xf_format.underline)
254 if xf_format.font_script == 1:
255 self._write_vert_align("superscript")
257 if xf_format.font_script == 2:
258 self._write_vert_align("subscript")
260 if not is_dxf_format:
261 self._xml_empty_tag("sz", [("val", xf_format.font_size)])
263 if xf_format.theme == -1:
264 # Ignore for excel2003_style.
265 pass
266 elif xf_format.theme:
267 self._write_color([("theme", xf_format.theme)])
268 elif xf_format.color_indexed:
269 self._write_color([("indexed", xf_format.color_indexed)])
270 elif xf_format.font_color:
271 color = xf_format.font_color
272 if not color._is_automatic:
273 self._write_color(color._attributes())
274 elif not is_dxf_format:
275 self._write_color([("theme", 1)])
277 if not is_dxf_format:
278 self._xml_empty_tag("name", [("val", xf_format.font_name)])
280 if xf_format.font_family:
281 self._xml_empty_tag("family", [("val", xf_format.font_family)])
283 if xf_format.font_charset:
284 self._xml_empty_tag("charset", [("val", xf_format.font_charset)])
286 if xf_format.font_scheme in ("major", "minor"):
287 self._xml_empty_tag("scheme", [("val", xf_format.font_scheme)])
289 if xf_format.hyperlink:
290 self.has_hyperlink = True
291 if self.hyperlink_font_id == 0:
292 self.hyperlink_font_id = xf_format.font_index
294 self._xml_end_tag("font")
296 def _write_comment_font(self) -> None:
297 # Write the <font> element for comments.
298 self._xml_start_tag("font")
300 self._xml_empty_tag("sz", [("val", 8)])
301 self._write_color([("indexed", 81)])
302 self._xml_empty_tag("name", [("val", "Tahoma")])
303 self._xml_empty_tag("family", [("val", 2)])
305 self._xml_end_tag("font")
307 def _write_underline(self, underline) -> None:
308 # Write the underline font element.
310 if underline == 2:
311 attributes = [("val", "double")]
312 elif underline == 33:
313 attributes = [("val", "singleAccounting")]
314 elif underline == 34:
315 attributes = [("val", "doubleAccounting")]
316 else:
317 # Default to single underline.
318 attributes = []
320 self._xml_empty_tag("u", attributes)
322 def _write_vert_align(self, val) -> None:
323 # Write the <vertAlign> font sub-element.
324 attributes = [("val", val)]
326 self._xml_empty_tag("vertAlign", attributes)
328 def _write_color(self, attributes) -> None:
329 # Write the <color> element.
330 self._xml_empty_tag("color", attributes)
332 def _write_fills(self) -> None:
333 # Write the <fills> element.
334 attributes = [("count", self.fill_count)]
336 self._xml_start_tag("fills", attributes)
338 # Write the default fill element.
339 self._write_default_fill("none")
340 self._write_default_fill("gray125")
342 # Write the fill elements for xf_format objects that have them.
343 for xf_format in self.xf_formats:
344 if xf_format.has_fill:
345 self._write_fill(xf_format)
347 self._xml_end_tag("fills")
349 def _write_default_fill(self, pattern_type) -> None:
350 # Write the <fill> element for the default fills.
351 self._xml_start_tag("fill")
352 self._xml_empty_tag("patternFill", [("patternType", pattern_type)])
353 self._xml_end_tag("fill")
355 def _write_fill(self, xf_format, is_dxf_format=False) -> None:
356 # Write the <fill> element.
357 pattern = xf_format.pattern
358 bg_color = xf_format.bg_color
359 fg_color = xf_format.fg_color
361 # Colors for dxf formats are handled differently from normal formats
362 # since the normal xf_format reverses the meaning of BG and FG for
363 # solid fills.
364 if is_dxf_format:
365 bg_color = xf_format.dxf_bg_color
366 fg_color = xf_format.dxf_fg_color
368 patterns = (
369 "none",
370 "solid",
371 "mediumGray",
372 "darkGray",
373 "lightGray",
374 "darkHorizontal",
375 "darkVertical",
376 "darkDown",
377 "darkUp",
378 "darkGrid",
379 "darkTrellis",
380 "lightHorizontal",
381 "lightVertical",
382 "lightDown",
383 "lightUp",
384 "lightGrid",
385 "lightTrellis",
386 "gray125",
387 "gray0625",
388 )
390 # Special handling for pattern only case.
391 if not fg_color and not bg_color and patterns[pattern]:
392 self._write_default_fill(patterns[pattern])
393 return
395 self._xml_start_tag("fill")
397 # The "none" pattern is handled differently for dxf formats.
398 if is_dxf_format and pattern <= 1:
399 self._xml_start_tag("patternFill")
400 else:
401 self._xml_start_tag("patternFill", [("patternType", patterns[pattern])])
403 if fg_color and not fg_color._is_automatic:
404 self._xml_empty_tag("fgColor", fg_color._attributes())
406 if bg_color:
407 if not bg_color._is_automatic:
408 self._xml_empty_tag("bgColor", bg_color._attributes())
409 else:
410 if not is_dxf_format and pattern <= 1:
411 self._xml_empty_tag("bgColor", [("indexed", 64)])
413 self._xml_end_tag("patternFill")
414 self._xml_end_tag("fill")
416 def _write_borders(self) -> None:
417 # Write the <borders> element.
418 attributes = [("count", self.border_count)]
420 self._xml_start_tag("borders", attributes)
422 # Write the border elements for xf_format objects that have them.
423 for xf_format in self.xf_formats:
424 if xf_format.has_border:
425 self._write_border(xf_format)
427 self._xml_end_tag("borders")
429 def _write_border(self, xf_format, is_dxf_format=False) -> None:
430 # Write the <border> element.
431 attributes = []
433 # Diagonal borders add attributes to the <border> element.
434 if xf_format.diag_type == 1:
435 attributes.append(("diagonalUp", 1))
436 elif xf_format.diag_type == 2:
437 attributes.append(("diagonalDown", 1))
438 elif xf_format.diag_type == 3:
439 attributes.append(("diagonalUp", 1))
440 attributes.append(("diagonalDown", 1))
442 # Ensure that a default diag border is set if the diag type is set.
443 if xf_format.diag_type and not xf_format.diag_border:
444 xf_format.diag_border = 1
446 # Write the start border tag.
447 self._xml_start_tag("border", attributes)
449 # Write the <border> sub elements.
450 self._write_sub_border("left", xf_format.left, xf_format.left_color)
452 self._write_sub_border("right", xf_format.right, xf_format.right_color)
454 self._write_sub_border("top", xf_format.top, xf_format.top_color)
456 self._write_sub_border("bottom", xf_format.bottom, xf_format.bottom_color)
458 # Condition DXF formats don't allow diagonal borders.
459 if not is_dxf_format:
460 self._write_sub_border(
461 "diagonal", xf_format.diag_border, xf_format.diag_color
462 )
464 if is_dxf_format:
465 self._write_sub_border("vertical", None, None)
466 self._write_sub_border("horizontal", None, None)
468 self._xml_end_tag("border")
470 def _write_sub_border(self, border_type, style, color) -> None:
471 # Write the <border> sub elements such as <right>, <top>, etc.
472 attributes = []
474 if not style:
475 self._xml_empty_tag(border_type)
476 return
478 border_styles = (
479 "none",
480 "thin",
481 "medium",
482 "dashed",
483 "dotted",
484 "thick",
485 "double",
486 "hair",
487 "mediumDashed",
488 "dashDot",
489 "mediumDashDot",
490 "dashDotDot",
491 "mediumDashDotDot",
492 "slantDashDot",
493 )
495 attributes.append(("style", border_styles[style]))
497 self._xml_start_tag(border_type, attributes)
499 if color and not color._is_automatic:
500 self._xml_empty_tag("color", color._attributes())
501 else:
502 self._xml_empty_tag("color", [("auto", 1)])
504 self._xml_end_tag(border_type)
506 def _write_cell_style_xfs(self) -> None:
507 # Write the <cellStyleXfs> element.
508 count = 1
510 if self.has_hyperlink:
511 count = 2
513 attributes = [("count", count)]
515 self._xml_start_tag("cellStyleXfs", attributes)
516 style_format = self.xf_formats[0]
517 self._write_xf(style_format, XFormatType.STYLE)
519 if self.has_hyperlink:
520 self._write_style_xf(True, self.hyperlink_font_id)
522 self._xml_end_tag("cellStyleXfs")
524 def _write_cell_xfs(self) -> None:
525 # Write the <cellXfs> element.
526 formats = self.xf_formats
528 # Workaround for when the last xf_format is used for the comment font
529 # and shouldn't be used for cellXfs.
530 last_format = formats[-1]
531 if last_format.font_only:
532 formats.pop()
534 attributes = [("count", len(formats))]
535 self._xml_start_tag("cellXfs", attributes)
537 # Write the xf elements.
538 cell_type = XFormatType.DEFAULT
539 for xf_format in formats:
540 self._write_xf(xf_format, cell_type)
541 cell_type = XFormatType.USER
543 self._xml_end_tag("cellXfs")
545 def _write_style_xf(self, has_hyperlink=False, font_id=0) -> None:
546 # Write the style <xf> element.
547 num_fmt_id = 0
548 fill_id = 0
549 border_id = 0
551 attributes = [
552 ("numFmtId", num_fmt_id),
553 ("fontId", font_id),
554 ("fillId", fill_id),
555 ("borderId", border_id),
556 ]
558 if has_hyperlink:
559 attributes.append(("applyNumberFormat", 0))
560 attributes.append(("applyFill", 0))
561 attributes.append(("applyBorder", 0))
562 attributes.append(("applyAlignment", 0))
563 attributes.append(("applyProtection", 0))
565 self._xml_start_tag("xf", attributes)
566 self._xml_empty_tag("alignment", [("vertical", "top")])
567 self._xml_empty_tag("protection", [("locked", 0)])
568 self._xml_end_tag("xf")
570 else:
571 self._xml_empty_tag("xf", attributes)
573 def _write_xf(self, xf_format, xf_type: XFormatType) -> None:
574 # Write the <xf> element.
575 xf_id = xf_format.xf_id
576 font_id = xf_format.font_index
577 fill_id = xf_format.fill_index
578 border_id = xf_format.border_index
579 num_fmt_id = xf_format.num_format_index
581 has_checkbox = xf_format.checkbox
582 has_alignment = False
583 has_protection = False
585 attributes = [
586 ("numFmtId", num_fmt_id),
587 ("fontId", font_id),
588 ("fillId", fill_id),
589 ("borderId", border_id),
590 ]
592 if xf_type != XFormatType.STYLE:
593 attributes.append(("xfId", xf_id))
595 if xf_format.quote_prefix:
596 attributes.append(("quotePrefix", 1))
598 if xf_format.num_format_index > 0:
599 attributes.append(("applyNumberFormat", 1))
601 # Add applyFont attribute if XF format uses a font element.
602 if xf_format.font_index > 0 and not xf_format.hyperlink:
603 attributes.append(("applyFont", 1))
605 # Add applyFill attribute if XF format uses a fill element.
606 if xf_format.fill_index > 0:
607 attributes.append(("applyFill", 1))
609 # Add applyBorder attribute if XF format uses a border element.
610 if xf_format.border_index > 0:
611 attributes.append(("applyBorder", 1))
613 # Check if XF format has alignment properties set.
614 apply_align, align = xf_format._get_align_properties()
616 # Check if an alignment sub-element should be written.
617 if apply_align and align:
618 has_alignment = True
620 # We can also have applyAlignment without a sub-element.
621 if (apply_align or xf_format.hyperlink) and xf_type == XFormatType.USER:
622 attributes.append(("applyAlignment", 1))
624 # Check for cell protection properties.
625 protection = xf_format._get_protection_properties()
627 if protection or xf_format.hyperlink:
628 attributes.append(("applyProtection", 1))
630 if not xf_format.hyperlink:
631 has_protection = True
633 # Write XF with sub-elements if required.
634 if has_alignment or has_protection or has_checkbox:
635 self._xml_start_tag("xf", attributes)
637 if has_alignment:
638 self._xml_empty_tag("alignment", align)
640 if has_protection:
641 self._xml_empty_tag("protection", protection)
643 if has_checkbox:
644 self._write_xf_format_extensions()
646 self._xml_end_tag("xf")
647 else:
648 self._xml_empty_tag("xf", attributes)
650 def _write_cell_styles(self) -> None:
651 # Write the <cellStyles> element.
652 count = 1
654 if self.has_hyperlink:
655 count = 2
657 attributes = [("count", count)]
659 self._xml_start_tag("cellStyles", attributes)
661 if self.has_hyperlink:
662 self._write_cell_style("Hyperlink", 1, 8)
664 self._write_cell_style()
666 self._xml_end_tag("cellStyles")
668 def _write_cell_style(self, name="Normal", xf_id=0, builtin_id=0) -> None:
669 # Write the <cellStyle> element.
670 attributes = [
671 ("name", name),
672 ("xfId", xf_id),
673 ("builtinId", builtin_id),
674 ]
676 self._xml_empty_tag("cellStyle", attributes)
678 def _write_dxfs(self) -> None:
679 # Write the <dxfs> element.
680 formats = self.dxf_formats
681 count = len(formats)
683 attributes = [("count", len(formats))]
685 if count:
686 self._xml_start_tag("dxfs", attributes)
688 # Write the font elements for xf_format objects that have them.
689 for dxf_format in self.dxf_formats:
690 self._xml_start_tag("dxf")
691 if dxf_format.has_dxf_font:
692 self._write_font(dxf_format, True)
694 if dxf_format.num_format_index:
695 self._write_num_fmt(
696 dxf_format.num_format_index, dxf_format.num_format
697 )
699 if dxf_format.has_dxf_fill:
700 self._write_fill(dxf_format, True)
702 if dxf_format.has_dxf_border:
703 self._write_border(dxf_format, True)
705 if dxf_format.checkbox:
706 self._write_dxf_format_extensions()
708 self._xml_end_tag("dxf")
710 self._xml_end_tag("dxfs")
711 else:
712 self._xml_empty_tag("dxfs", attributes)
714 def _write_table_styles(self) -> None:
715 # Write the <tableStyles> element.
716 count = 0
717 default_table_style = "TableStyleMedium9"
718 default_pivot_style = "PivotStyleLight16"
720 attributes = [
721 ("count", count),
722 ("defaultTableStyle", default_table_style),
723 ("defaultPivotStyle", default_pivot_style),
724 ]
726 self._xml_empty_tag("tableStyles", attributes)
728 def _write_colors(self) -> None:
729 # Write the <colors> element.
730 custom_colors = self.custom_colors
732 if not custom_colors:
733 return
735 self._xml_start_tag("colors")
736 self._write_mru_colors(custom_colors)
737 self._xml_end_tag("colors")
739 def _write_mru_colors(self, custom_colors) -> None:
740 # Write the <mruColors> element for the most recently used colors.
742 # Write the custom custom_colors in reverse order.
743 custom_colors.reverse()
745 # Limit the mruColors to the last 10.
746 if len(custom_colors) > 10:
747 custom_colors = custom_colors[0:10]
749 self._xml_start_tag("mruColors")
751 # Write the custom custom_colors in reverse order.
752 for color in custom_colors:
753 # For backwards compatibility convert possible
754 self._write_color(color._attributes())
756 self._xml_end_tag("mruColors")
758 def _write_condense(self) -> None:
759 # Write the <condense> element.
760 attributes = [("val", 0)]
762 self._xml_empty_tag("condense", attributes)
764 def _write_extend(self) -> None:
765 # Write the <extend> element.
766 attributes = [("val", 0)]
768 self._xml_empty_tag("extend", attributes)
770 def _write_xf_format_extensions(self) -> None:
771 # Write the xfComplement <extLst> elements.
772 schema = "http://schemas.microsoft.com/office/spreadsheetml"
773 attributes = [
774 ("uri", "{C7286773-470A-42A8-94C5-96B5CB345126}"),
775 (
776 "xmlns:xfpb",
777 schema + "/2022/featurepropertybag",
778 ),
779 ]
781 self._xml_start_tag("extLst")
782 self._xml_start_tag("ext", attributes)
784 self._xml_empty_tag("xfpb:xfComplement", [("i", "0")])
786 self._xml_end_tag("ext")
787 self._xml_end_tag("extLst")
789 def _write_dxf_format_extensions(self) -> None:
790 # Write the DXFComplement <extLst> elements.
791 schema = "http://schemas.microsoft.com/office/spreadsheetml"
792 attributes = [
793 ("uri", "{0417FA29-78FA-4A13-93AC-8FF0FAFDF519}"),
794 (
795 "xmlns:xfpb",
796 schema + "/2022/featurepropertybag",
797 ),
798 ]
800 self._xml_start_tag("extLst")
801 self._xml_start_tag("ext", attributes)
803 self._xml_empty_tag("xfpb:DXFComplement", [("i", "0")])
805 self._xml_end_tag("ext")
806 self._xml_end_tag("extLst")