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-2025, 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:
404 if not fg_color._is_automatic:
405 self._xml_empty_tag("fgColor", fg_color._attributes())
407 if bg_color:
408 if not bg_color._is_automatic:
409 self._xml_empty_tag("bgColor", bg_color._attributes())
410 else:
411 if not is_dxf_format and pattern <= 1:
412 self._xml_empty_tag("bgColor", [("indexed", 64)])
414 self._xml_end_tag("patternFill")
415 self._xml_end_tag("fill")
417 def _write_borders(self) -> None:
418 # Write the <borders> element.
419 attributes = [("count", self.border_count)]
421 self._xml_start_tag("borders", attributes)
423 # Write the border elements for xf_format objects that have them.
424 for xf_format in self.xf_formats:
425 if xf_format.has_border:
426 self._write_border(xf_format)
428 self._xml_end_tag("borders")
430 def _write_border(self, xf_format, is_dxf_format=False) -> None:
431 # Write the <border> element.
432 attributes = []
434 # Diagonal borders add attributes to the <border> element.
435 if xf_format.diag_type == 1:
436 attributes.append(("diagonalUp", 1))
437 elif xf_format.diag_type == 2:
438 attributes.append(("diagonalDown", 1))
439 elif xf_format.diag_type == 3:
440 attributes.append(("diagonalUp", 1))
441 attributes.append(("diagonalDown", 1))
443 # Ensure that a default diag border is set if the diag type is set.
444 if xf_format.diag_type and not xf_format.diag_border:
445 xf_format.diag_border = 1
447 # Write the start border tag.
448 self._xml_start_tag("border", attributes)
450 # Write the <border> sub elements.
451 self._write_sub_border("left", xf_format.left, xf_format.left_color)
453 self._write_sub_border("right", xf_format.right, xf_format.right_color)
455 self._write_sub_border("top", xf_format.top, xf_format.top_color)
457 self._write_sub_border("bottom", xf_format.bottom, xf_format.bottom_color)
459 # Condition DXF formats don't allow diagonal borders.
460 if not is_dxf_format:
461 self._write_sub_border(
462 "diagonal", xf_format.diag_border, xf_format.diag_color
463 )
465 if is_dxf_format:
466 self._write_sub_border("vertical", None, None)
467 self._write_sub_border("horizontal", None, None)
469 self._xml_end_tag("border")
471 def _write_sub_border(self, border_type, style, color) -> None:
472 # Write the <border> sub elements such as <right>, <top>, etc.
473 attributes = []
475 if not style:
476 self._xml_empty_tag(border_type)
477 return
479 border_styles = (
480 "none",
481 "thin",
482 "medium",
483 "dashed",
484 "dotted",
485 "thick",
486 "double",
487 "hair",
488 "mediumDashed",
489 "dashDot",
490 "mediumDashDot",
491 "dashDotDot",
492 "mediumDashDotDot",
493 "slantDashDot",
494 )
496 attributes.append(("style", border_styles[style]))
498 self._xml_start_tag(border_type, attributes)
500 if color and not color._is_automatic:
501 self._xml_empty_tag("color", color._attributes())
502 else:
503 self._xml_empty_tag("color", [("auto", 1)])
505 self._xml_end_tag(border_type)
507 def _write_cell_style_xfs(self) -> None:
508 # Write the <cellStyleXfs> element.
509 count = 1
511 if self.has_hyperlink:
512 count = 2
514 attributes = [("count", count)]
516 self._xml_start_tag("cellStyleXfs", attributes)
517 style_format = self.xf_formats[0]
518 self._write_xf(style_format, XFormatType.STYLE)
520 if self.has_hyperlink:
521 self._write_style_xf(True, self.hyperlink_font_id)
523 self._xml_end_tag("cellStyleXfs")
525 def _write_cell_xfs(self) -> None:
526 # Write the <cellXfs> element.
527 formats = self.xf_formats
529 # Workaround for when the last xf_format is used for the comment font
530 # and shouldn't be used for cellXfs.
531 last_format = formats[-1]
532 if last_format.font_only:
533 formats.pop()
535 attributes = [("count", len(formats))]
536 self._xml_start_tag("cellXfs", attributes)
538 # Write the xf elements.
539 cell_type = XFormatType.DEFAULT
540 for xf_format in formats:
541 self._write_xf(xf_format, cell_type)
542 cell_type = XFormatType.USER
544 self._xml_end_tag("cellXfs")
546 def _write_style_xf(self, has_hyperlink=False, font_id=0) -> None:
547 # Write the style <xf> element.
548 num_fmt_id = 0
549 fill_id = 0
550 border_id = 0
552 attributes = [
553 ("numFmtId", num_fmt_id),
554 ("fontId", font_id),
555 ("fillId", fill_id),
556 ("borderId", border_id),
557 ]
559 if has_hyperlink:
560 attributes.append(("applyNumberFormat", 0))
561 attributes.append(("applyFill", 0))
562 attributes.append(("applyBorder", 0))
563 attributes.append(("applyAlignment", 0))
564 attributes.append(("applyProtection", 0))
566 self._xml_start_tag("xf", attributes)
567 self._xml_empty_tag("alignment", [("vertical", "top")])
568 self._xml_empty_tag("protection", [("locked", 0)])
569 self._xml_end_tag("xf")
571 else:
572 self._xml_empty_tag("xf", attributes)
574 def _write_xf(self, xf_format, xf_type: XFormatType) -> None:
575 # Write the <xf> element.
576 xf_id = xf_format.xf_id
577 font_id = xf_format.font_index
578 fill_id = xf_format.fill_index
579 border_id = xf_format.border_index
580 num_fmt_id = xf_format.num_format_index
582 has_checkbox = xf_format.checkbox
583 has_alignment = False
584 has_protection = False
586 attributes = [
587 ("numFmtId", num_fmt_id),
588 ("fontId", font_id),
589 ("fillId", fill_id),
590 ("borderId", border_id),
591 ]
593 if xf_type != XFormatType.STYLE:
594 attributes.append(("xfId", xf_id))
596 if xf_format.quote_prefix:
597 attributes.append(("quotePrefix", 1))
599 if xf_format.num_format_index > 0:
600 attributes.append(("applyNumberFormat", 1))
602 # Add applyFont attribute if XF format uses a font element.
603 if xf_format.font_index > 0 and not xf_format.hyperlink:
604 attributes.append(("applyFont", 1))
606 # Add applyFill attribute if XF format uses a fill element.
607 if xf_format.fill_index > 0:
608 attributes.append(("applyFill", 1))
610 # Add applyBorder attribute if XF format uses a border element.
611 if xf_format.border_index > 0:
612 attributes.append(("applyBorder", 1))
614 # Check if XF format has alignment properties set.
615 (apply_align, align) = xf_format._get_align_properties()
617 # Check if an alignment sub-element should be written.
618 if apply_align and align:
619 has_alignment = True
621 # We can also have applyAlignment without a sub-element.
622 if (apply_align or xf_format.hyperlink) and xf_type == XFormatType.USER:
623 attributes.append(("applyAlignment", 1))
625 # Check for cell protection properties.
626 protection = xf_format._get_protection_properties()
628 if protection or xf_format.hyperlink:
629 attributes.append(("applyProtection", 1))
631 if not xf_format.hyperlink:
632 has_protection = True
634 # Write XF with sub-elements if required.
635 if has_alignment or has_protection or has_checkbox:
636 self._xml_start_tag("xf", attributes)
638 if has_alignment:
639 self._xml_empty_tag("alignment", align)
641 if has_protection:
642 self._xml_empty_tag("protection", protection)
644 if has_checkbox:
645 self._write_xf_format_extensions()
647 self._xml_end_tag("xf")
648 else:
649 self._xml_empty_tag("xf", attributes)
651 def _write_cell_styles(self) -> None:
652 # Write the <cellStyles> element.
653 count = 1
655 if self.has_hyperlink:
656 count = 2
658 attributes = [("count", count)]
660 self._xml_start_tag("cellStyles", attributes)
662 if self.has_hyperlink:
663 self._write_cell_style("Hyperlink", 1, 8)
665 self._write_cell_style()
667 self._xml_end_tag("cellStyles")
669 def _write_cell_style(self, name="Normal", xf_id=0, builtin_id=0) -> None:
670 # Write the <cellStyle> element.
671 attributes = [
672 ("name", name),
673 ("xfId", xf_id),
674 ("builtinId", builtin_id),
675 ]
677 self._xml_empty_tag("cellStyle", attributes)
679 def _write_dxfs(self) -> None:
680 # Write the <dxfs> element.
681 formats = self.dxf_formats
682 count = len(formats)
684 attributes = [("count", len(formats))]
686 if count:
687 self._xml_start_tag("dxfs", attributes)
689 # Write the font elements for xf_format objects that have them.
690 for dxf_format in self.dxf_formats:
691 self._xml_start_tag("dxf")
692 if dxf_format.has_dxf_font:
693 self._write_font(dxf_format, True)
695 if dxf_format.num_format_index:
696 self._write_num_fmt(
697 dxf_format.num_format_index, dxf_format.num_format
698 )
700 if dxf_format.has_dxf_fill:
701 self._write_fill(dxf_format, True)
703 if dxf_format.has_dxf_border:
704 self._write_border(dxf_format, True)
706 if dxf_format.checkbox:
707 self._write_dxf_format_extensions()
709 self._xml_end_tag("dxf")
711 self._xml_end_tag("dxfs")
712 else:
713 self._xml_empty_tag("dxfs", attributes)
715 def _write_table_styles(self) -> None:
716 # Write the <tableStyles> element.
717 count = 0
718 default_table_style = "TableStyleMedium9"
719 default_pivot_style = "PivotStyleLight16"
721 attributes = [
722 ("count", count),
723 ("defaultTableStyle", default_table_style),
724 ("defaultPivotStyle", default_pivot_style),
725 ]
727 self._xml_empty_tag("tableStyles", attributes)
729 def _write_colors(self) -> None:
730 # Write the <colors> element.
731 custom_colors = self.custom_colors
733 if not custom_colors:
734 return
736 self._xml_start_tag("colors")
737 self._write_mru_colors(custom_colors)
738 self._xml_end_tag("colors")
740 def _write_mru_colors(self, custom_colors) -> None:
741 # Write the <mruColors> element for the most recently used colors.
743 # Write the custom custom_colors in reverse order.
744 custom_colors.reverse()
746 # Limit the mruColors to the last 10.
747 if len(custom_colors) > 10:
748 custom_colors = custom_colors[0:10]
750 self._xml_start_tag("mruColors")
752 # Write the custom custom_colors in reverse order.
753 for color in custom_colors:
754 # For backwards compatibility convert possible
755 self._write_color(color._attributes())
757 self._xml_end_tag("mruColors")
759 def _write_condense(self) -> None:
760 # Write the <condense> element.
761 attributes = [("val", 0)]
763 self._xml_empty_tag("condense", attributes)
765 def _write_extend(self) -> None:
766 # Write the <extend> element.
767 attributes = [("val", 0)]
769 self._xml_empty_tag("extend", attributes)
771 def _write_xf_format_extensions(self) -> None:
772 # Write the xfComplement <extLst> elements.
773 schema = "http://schemas.microsoft.com/office/spreadsheetml"
774 attributes = [
775 ("uri", "{C7286773-470A-42A8-94C5-96B5CB345126}"),
776 (
777 "xmlns:xfpb",
778 schema + "/2022/featurepropertybag",
779 ),
780 ]
782 self._xml_start_tag("extLst")
783 self._xml_start_tag("ext", attributes)
785 self._xml_empty_tag("xfpb:xfComplement", [("i", "0")])
787 self._xml_end_tag("ext")
788 self._xml_end_tag("extLst")
790 def _write_dxf_format_extensions(self) -> None:
791 # Write the DXFComplement <extLst> elements.
792 schema = "http://schemas.microsoft.com/office/spreadsheetml"
793 attributes = [
794 ("uri", "{0417FA29-78FA-4A13-93AC-8FF0FAFDF519}"),
795 (
796 "xmlns:xfpb",
797 schema + "/2022/featurepropertybag",
798 ),
799 ]
801 self._xml_start_tag("extLst")
802 self._xml_start_tag("ext", attributes)
804 self._xml_empty_tag("xfpb:DXFComplement", [("i", "0")])
806 self._xml_end_tag("ext")
807 self._xml_end_tag("extLst")