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#
10# Package imports.
11from . import xmlwriter
14class Styles(xmlwriter.XMLwriter):
15 """
16 A class for writing the Excel XLSX Styles file.
19 """
21 ###########################################################################
22 #
23 # Public API.
24 #
25 ###########################################################################
27 def __init__(self) -> None:
28 """
29 Constructor.
31 """
33 super().__init__()
35 self.xf_formats = []
36 self.palette = []
37 self.font_count = 0
38 self.num_formats = []
39 self.border_count = 0
40 self.fill_count = 0
41 self.custom_colors = []
42 self.dxf_formats = []
43 self.has_hyperlink = False
44 self.hyperlink_font_id = 0
45 self.has_comments = False
47 ###########################################################################
48 #
49 # Private API.
50 #
51 ###########################################################################
53 def _assemble_xml_file(self) -> None:
54 # Assemble and write the XML file.
56 # Write the XML declaration.
57 self._xml_declaration()
59 # Add the style sheet.
60 self._write_style_sheet()
62 # Write the number formats.
63 self._write_num_fmts()
65 # Write the fonts.
66 self._write_fonts()
68 # Write the fills.
69 self._write_fills()
71 # Write the borders element.
72 self._write_borders()
74 # Write the cellStyleXfs element.
75 self._write_cell_style_xfs()
77 # Write the cellXfs element.
78 self._write_cell_xfs()
80 # Write the cellStyles element.
81 self._write_cell_styles()
83 # Write the dxfs element.
84 self._write_dxfs()
86 # Write the tableStyles element.
87 self._write_table_styles()
89 # Write the colors element.
90 self._write_colors()
92 # Close the style sheet tag.
93 self._xml_end_tag("styleSheet")
95 # Close the file.
96 self._xml_close()
98 def _set_style_properties(self, properties) -> None:
99 # Pass in the Format objects and other properties used in the styles.
101 self.xf_formats = properties[0]
102 self.palette = properties[1]
103 self.font_count = properties[2]
104 self.num_formats = properties[3]
105 self.border_count = properties[4]
106 self.fill_count = properties[5]
107 self.custom_colors = properties[6]
108 self.dxf_formats = properties[7]
109 self.has_comments = properties[8]
111 ###########################################################################
112 #
113 # XML methods.
114 #
115 ###########################################################################
117 def _write_style_sheet(self) -> None:
118 # Write the <styleSheet> element.
119 xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
121 attributes = [("xmlns", xmlns)]
122 self._xml_start_tag("styleSheet", attributes)
124 def _write_num_fmts(self) -> None:
125 # Write the <numFmts> element.
126 if not self.num_formats:
127 return
129 attributes = [("count", len(self.num_formats))]
130 self._xml_start_tag("numFmts", attributes)
132 # Write the numFmts elements.
133 for index, num_format in enumerate(self.num_formats, 164):
134 self._write_num_fmt(index, num_format)
136 self._xml_end_tag("numFmts")
138 def _write_num_fmt(self, num_fmt_id, format_code) -> None:
139 # Write the <numFmt> element.
140 format_codes = {
141 0: "General",
142 1: "0",
143 2: "0.00",
144 3: "#,##0",
145 4: "#,##0.00",
146 5: "($#,##0_);($#,##0)",
147 6: "($#,##0_);[Red]($#,##0)",
148 7: "($#,##0.00_);($#,##0.00)",
149 8: "($#,##0.00_);[Red]($#,##0.00)",
150 9: "0%",
151 10: "0.00%",
152 11: "0.00E+00",
153 12: "# ?/?",
154 13: "# ??/??",
155 14: "m/d/yy",
156 15: "d-mmm-yy",
157 16: "d-mmm",
158 17: "mmm-yy",
159 18: "h:mm AM/PM",
160 19: "h:mm:ss AM/PM",
161 20: "h:mm",
162 21: "h:mm:ss",
163 22: "m/d/yy h:mm",
164 37: "(#,##0_);(#,##0)",
165 38: "(#,##0_);[Red](#,##0)",
166 39: "(#,##0.00_);(#,##0.00)",
167 40: "(#,##0.00_);[Red](#,##0.00)",
168 41: '_(* #,##0_);_(* (#,##0);_(* "-"_);_(_)',
169 42: '_($* #,##0_);_($* (#,##0);_($* "-"_);_(_)',
170 43: '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(_)',
171 44: '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(_)',
172 45: "mm:ss",
173 46: "[h]:mm:ss",
174 47: "mm:ss.0",
175 48: "##0.0E+0",
176 49: "@",
177 }
179 # Set the format code for built-in number formats.
180 if num_fmt_id < 164:
181 format_code = format_codes.get(num_fmt_id, "General")
183 attributes = [
184 ("numFmtId", num_fmt_id),
185 ("formatCode", format_code),
186 ]
188 self._xml_empty_tag("numFmt", attributes)
190 def _write_fonts(self) -> None:
191 # Write the <fonts> element.
192 if self.has_comments:
193 # Add extra font for comments.
194 attributes = [("count", self.font_count + 1)]
195 else:
196 attributes = [("count", self.font_count)]
198 self._xml_start_tag("fonts", attributes)
200 # Write the font elements for xf_format objects that have them.
201 for xf_format in self.xf_formats:
202 if xf_format.has_font:
203 self._write_font(xf_format)
205 if self.has_comments:
206 self._write_comment_font()
208 self._xml_end_tag("fonts")
210 def _write_font(self, xf_format, is_dxf_format=False) -> None:
211 # Write the <font> element.
212 self._xml_start_tag("font")
214 # The condense and extend elements are mainly used in dxf formats.
215 if xf_format.font_condense:
216 self._write_condense()
218 if xf_format.font_extend:
219 self._write_extend()
221 if xf_format.bold:
222 self._xml_empty_tag("b")
224 if xf_format.italic:
225 self._xml_empty_tag("i")
227 if xf_format.font_strikeout:
228 self._xml_empty_tag("strike")
230 if xf_format.font_outline:
231 self._xml_empty_tag("outline")
233 if xf_format.font_shadow:
234 self._xml_empty_tag("shadow")
236 # Handle the underline variants.
237 if xf_format.underline:
238 self._write_underline(xf_format.underline)
240 if xf_format.font_script == 1:
241 self._write_vert_align("superscript")
243 if xf_format.font_script == 2:
244 self._write_vert_align("subscript")
246 if not is_dxf_format:
247 self._xml_empty_tag("sz", [("val", xf_format.font_size)])
249 if xf_format.theme == -1:
250 # Ignore for excel2003_style.
251 pass
252 elif xf_format.theme:
253 self._write_color([("theme", xf_format.theme)])
254 elif xf_format.color_indexed:
255 self._write_color([("indexed", xf_format.color_indexed)])
256 elif xf_format.font_color:
257 color = xf_format.font_color
258 if not color._is_automatic:
259 self._write_color(color._attributes())
260 elif not is_dxf_format:
261 self._write_color([("theme", 1)])
263 if not is_dxf_format:
264 self._xml_empty_tag("name", [("val", xf_format.font_name)])
266 if xf_format.font_family:
267 self._xml_empty_tag("family", [("val", xf_format.font_family)])
269 if xf_format.font_charset:
270 self._xml_empty_tag("charset", [("val", xf_format.font_charset)])
272 if xf_format.font_name == "Calibri" and not xf_format.hyperlink:
273 self._xml_empty_tag("scheme", [("val", xf_format.font_scheme)])
275 if xf_format.hyperlink:
276 self.has_hyperlink = True
277 if self.hyperlink_font_id == 0:
278 self.hyperlink_font_id = xf_format.font_index
280 self._xml_end_tag("font")
282 def _write_comment_font(self) -> None:
283 # Write the <font> element for comments.
284 self._xml_start_tag("font")
286 self._xml_empty_tag("sz", [("val", 8)])
287 self._write_color([("indexed", 81)])
288 self._xml_empty_tag("name", [("val", "Tahoma")])
289 self._xml_empty_tag("family", [("val", 2)])
291 self._xml_end_tag("font")
293 def _write_underline(self, underline) -> None:
294 # Write the underline font element.
296 if underline == 2:
297 attributes = [("val", "double")]
298 elif underline == 33:
299 attributes = [("val", "singleAccounting")]
300 elif underline == 34:
301 attributes = [("val", "doubleAccounting")]
302 else:
303 # Default to single underline.
304 attributes = []
306 self._xml_empty_tag("u", attributes)
308 def _write_vert_align(self, val) -> None:
309 # Write the <vertAlign> font sub-element.
310 attributes = [("val", val)]
312 self._xml_empty_tag("vertAlign", attributes)
314 def _write_color(self, attributes) -> None:
315 # Write the <color> element.
316 self._xml_empty_tag("color", attributes)
318 def _write_fills(self) -> None:
319 # Write the <fills> element.
320 attributes = [("count", self.fill_count)]
322 self._xml_start_tag("fills", attributes)
324 # Write the default fill element.
325 self._write_default_fill("none")
326 self._write_default_fill("gray125")
328 # Write the fill elements for xf_format objects that have them.
329 for xf_format in self.xf_formats:
330 if xf_format.has_fill:
331 self._write_fill(xf_format)
333 self._xml_end_tag("fills")
335 def _write_default_fill(self, pattern_type) -> None:
336 # Write the <fill> element for the default fills.
337 self._xml_start_tag("fill")
338 self._xml_empty_tag("patternFill", [("patternType", pattern_type)])
339 self._xml_end_tag("fill")
341 def _write_fill(self, xf_format, is_dxf_format=False) -> None:
342 # Write the <fill> element.
343 pattern = xf_format.pattern
344 bg_color = xf_format.bg_color
345 fg_color = xf_format.fg_color
347 # Colors for dxf formats are handled differently from normal formats
348 # since the normal xf_format reverses the meaning of BG and FG for
349 # solid fills.
350 if is_dxf_format:
351 bg_color = xf_format.dxf_bg_color
352 fg_color = xf_format.dxf_fg_color
354 patterns = (
355 "none",
356 "solid",
357 "mediumGray",
358 "darkGray",
359 "lightGray",
360 "darkHorizontal",
361 "darkVertical",
362 "darkDown",
363 "darkUp",
364 "darkGrid",
365 "darkTrellis",
366 "lightHorizontal",
367 "lightVertical",
368 "lightDown",
369 "lightUp",
370 "lightGrid",
371 "lightTrellis",
372 "gray125",
373 "gray0625",
374 )
376 # Special handling for pattern only case.
377 if not fg_color and not bg_color and patterns[pattern]:
378 self._write_default_fill(patterns[pattern])
379 return
381 self._xml_start_tag("fill")
383 # The "none" pattern is handled differently for dxf formats.
384 if is_dxf_format and pattern <= 1:
385 self._xml_start_tag("patternFill")
386 else:
387 self._xml_start_tag("patternFill", [("patternType", patterns[pattern])])
389 if fg_color:
390 if not fg_color._is_automatic:
391 self._xml_empty_tag("fgColor", fg_color._attributes())
393 if bg_color:
394 if not bg_color._is_automatic:
395 self._xml_empty_tag("bgColor", bg_color._attributes())
396 else:
397 if not is_dxf_format and pattern <= 1:
398 self._xml_empty_tag("bgColor", [("indexed", 64)])
400 self._xml_end_tag("patternFill")
401 self._xml_end_tag("fill")
403 def _write_borders(self) -> None:
404 # Write the <borders> element.
405 attributes = [("count", self.border_count)]
407 self._xml_start_tag("borders", attributes)
409 # Write the border elements for xf_format objects that have them.
410 for xf_format in self.xf_formats:
411 if xf_format.has_border:
412 self._write_border(xf_format)
414 self._xml_end_tag("borders")
416 def _write_border(self, xf_format, is_dxf_format=False) -> None:
417 # Write the <border> element.
418 attributes = []
420 # Diagonal borders add attributes to the <border> element.
421 if xf_format.diag_type == 1:
422 attributes.append(("diagonalUp", 1))
423 elif xf_format.diag_type == 2:
424 attributes.append(("diagonalDown", 1))
425 elif xf_format.diag_type == 3:
426 attributes.append(("diagonalUp", 1))
427 attributes.append(("diagonalDown", 1))
429 # Ensure that a default diag border is set if the diag type is set.
430 if xf_format.diag_type and not xf_format.diag_border:
431 xf_format.diag_border = 1
433 # Write the start border tag.
434 self._xml_start_tag("border", attributes)
436 # Write the <border> sub elements.
437 self._write_sub_border("left", xf_format.left, xf_format.left_color)
439 self._write_sub_border("right", xf_format.right, xf_format.right_color)
441 self._write_sub_border("top", xf_format.top, xf_format.top_color)
443 self._write_sub_border("bottom", xf_format.bottom, xf_format.bottom_color)
445 # Condition DXF formats don't allow diagonal borders.
446 if not is_dxf_format:
447 self._write_sub_border(
448 "diagonal", xf_format.diag_border, xf_format.diag_color
449 )
451 if is_dxf_format:
452 self._write_sub_border("vertical", None, None)
453 self._write_sub_border("horizontal", None, None)
455 self._xml_end_tag("border")
457 def _write_sub_border(self, border_type, style, color) -> None:
458 # Write the <border> sub elements such as <right>, <top>, etc.
459 attributes = []
461 if not style:
462 self._xml_empty_tag(border_type)
463 return
465 border_styles = (
466 "none",
467 "thin",
468 "medium",
469 "dashed",
470 "dotted",
471 "thick",
472 "double",
473 "hair",
474 "mediumDashed",
475 "dashDot",
476 "mediumDashDot",
477 "dashDotDot",
478 "mediumDashDotDot",
479 "slantDashDot",
480 )
482 attributes.append(("style", border_styles[style]))
484 self._xml_start_tag(border_type, attributes)
486 if color and not color._is_automatic:
487 self._xml_empty_tag("color", color._attributes())
488 else:
489 self._xml_empty_tag("color", [("auto", 1)])
491 self._xml_end_tag(border_type)
493 def _write_cell_style_xfs(self) -> None:
494 # Write the <cellStyleXfs> element.
495 count = 1
497 if self.has_hyperlink:
498 count = 2
500 attributes = [("count", count)]
502 self._xml_start_tag("cellStyleXfs", attributes)
503 self._write_style_xf()
505 if self.has_hyperlink:
506 self._write_style_xf(True, self.hyperlink_font_id)
508 self._xml_end_tag("cellStyleXfs")
510 def _write_cell_xfs(self) -> None:
511 # Write the <cellXfs> element.
512 formats = self.xf_formats
514 # Workaround for when the last xf_format is used for the comment font
515 # and shouldn't be used for cellXfs.
516 last_format = formats[-1]
517 if last_format.font_only:
518 formats.pop()
520 attributes = [("count", len(formats))]
521 self._xml_start_tag("cellXfs", attributes)
523 # Write the xf elements.
524 for xf_format in formats:
525 self._write_xf(xf_format)
527 self._xml_end_tag("cellXfs")
529 def _write_style_xf(self, has_hyperlink=False, font_id=0) -> None:
530 # Write the style <xf> element.
531 num_fmt_id = 0
532 fill_id = 0
533 border_id = 0
535 attributes = [
536 ("numFmtId", num_fmt_id),
537 ("fontId", font_id),
538 ("fillId", fill_id),
539 ("borderId", border_id),
540 ]
542 if has_hyperlink:
543 attributes.append(("applyNumberFormat", 0))
544 attributes.append(("applyFill", 0))
545 attributes.append(("applyBorder", 0))
546 attributes.append(("applyAlignment", 0))
547 attributes.append(("applyProtection", 0))
549 self._xml_start_tag("xf", attributes)
550 self._xml_empty_tag("alignment", [("vertical", "top")])
551 self._xml_empty_tag("protection", [("locked", 0)])
552 self._xml_end_tag("xf")
554 else:
555 self._xml_empty_tag("xf", attributes)
557 def _write_xf(self, xf_format) -> None:
558 # Write the <xf> element.
559 xf_id = xf_format.xf_id
560 font_id = xf_format.font_index
561 fill_id = xf_format.fill_index
562 border_id = xf_format.border_index
563 num_fmt_id = xf_format.num_format_index
565 has_checkbox = xf_format.checkbox
566 has_alignment = False
567 has_protection = False
569 attributes = [
570 ("numFmtId", num_fmt_id),
571 ("fontId", font_id),
572 ("fillId", fill_id),
573 ("borderId", border_id),
574 ("xfId", xf_id),
575 ]
577 if xf_format.quote_prefix:
578 attributes.append(("quotePrefix", 1))
580 if xf_format.num_format_index > 0:
581 attributes.append(("applyNumberFormat", 1))
583 # Add applyFont attribute if XF format uses a font element.
584 if xf_format.font_index > 0 and not xf_format.hyperlink:
585 attributes.append(("applyFont", 1))
587 # Add applyFill attribute if XF format uses a fill element.
588 if xf_format.fill_index > 0:
589 attributes.append(("applyFill", 1))
591 # Add applyBorder attribute if XF format uses a border element.
592 if xf_format.border_index > 0:
593 attributes.append(("applyBorder", 1))
595 # Check if XF format has alignment properties set.
596 (apply_align, align) = xf_format._get_align_properties()
598 # Check if an alignment sub-element should be written.
599 if apply_align and align:
600 has_alignment = True
602 # We can also have applyAlignment without a sub-element.
603 if apply_align or xf_format.hyperlink:
604 attributes.append(("applyAlignment", 1))
606 # Check for cell protection properties.
607 protection = xf_format._get_protection_properties()
609 if protection or xf_format.hyperlink:
610 attributes.append(("applyProtection", 1))
612 if not xf_format.hyperlink:
613 has_protection = True
615 # Write XF with sub-elements if required.
616 if has_alignment or has_protection or has_checkbox:
617 self._xml_start_tag("xf", attributes)
619 if has_alignment:
620 self._xml_empty_tag("alignment", align)
622 if has_protection:
623 self._xml_empty_tag("protection", protection)
625 if has_checkbox:
626 self._write_xf_format_extensions()
628 self._xml_end_tag("xf")
629 else:
630 self._xml_empty_tag("xf", attributes)
632 def _write_cell_styles(self) -> None:
633 # Write the <cellStyles> element.
634 count = 1
636 if self.has_hyperlink:
637 count = 2
639 attributes = [("count", count)]
641 self._xml_start_tag("cellStyles", attributes)
643 if self.has_hyperlink:
644 self._write_cell_style("Hyperlink", 1, 8)
646 self._write_cell_style()
648 self._xml_end_tag("cellStyles")
650 def _write_cell_style(self, name="Normal", xf_id=0, builtin_id=0) -> None:
651 # Write the <cellStyle> element.
652 attributes = [
653 ("name", name),
654 ("xfId", xf_id),
655 ("builtinId", builtin_id),
656 ]
658 self._xml_empty_tag("cellStyle", attributes)
660 def _write_dxfs(self) -> None:
661 # Write the <dxfs> element.
662 formats = self.dxf_formats
663 count = len(formats)
665 attributes = [("count", len(formats))]
667 if count:
668 self._xml_start_tag("dxfs", attributes)
670 # Write the font elements for xf_format objects that have them.
671 for dxf_format in self.dxf_formats:
672 self._xml_start_tag("dxf")
673 if dxf_format.has_dxf_font:
674 self._write_font(dxf_format, True)
676 if dxf_format.num_format_index:
677 self._write_num_fmt(
678 dxf_format.num_format_index, dxf_format.num_format
679 )
681 if dxf_format.has_dxf_fill:
682 self._write_fill(dxf_format, True)
684 if dxf_format.has_dxf_border:
685 self._write_border(dxf_format, True)
687 if dxf_format.checkbox:
688 self._write_dxf_format_extensions()
690 self._xml_end_tag("dxf")
692 self._xml_end_tag("dxfs")
693 else:
694 self._xml_empty_tag("dxfs", attributes)
696 def _write_table_styles(self) -> None:
697 # Write the <tableStyles> element.
698 count = 0
699 default_table_style = "TableStyleMedium9"
700 default_pivot_style = "PivotStyleLight16"
702 attributes = [
703 ("count", count),
704 ("defaultTableStyle", default_table_style),
705 ("defaultPivotStyle", default_pivot_style),
706 ]
708 self._xml_empty_tag("tableStyles", attributes)
710 def _write_colors(self) -> None:
711 # Write the <colors> element.
712 custom_colors = self.custom_colors
714 if not custom_colors:
715 return
717 self._xml_start_tag("colors")
718 self._write_mru_colors(custom_colors)
719 self._xml_end_tag("colors")
721 def _write_mru_colors(self, custom_colors) -> None:
722 # Write the <mruColors> element for the most recently used colors.
724 # Write the custom custom_colors in reverse order.
725 custom_colors.reverse()
727 # Limit the mruColors to the last 10.
728 if len(custom_colors) > 10:
729 custom_colors = custom_colors[0:10]
731 self._xml_start_tag("mruColors")
733 # Write the custom custom_colors in reverse order.
734 for color in custom_colors:
735 # For backwards compatibility convert possible
736 self._write_color(color._attributes())
738 self._xml_end_tag("mruColors")
740 def _write_condense(self) -> None:
741 # Write the <condense> element.
742 attributes = [("val", 0)]
744 self._xml_empty_tag("condense", attributes)
746 def _write_extend(self) -> None:
747 # Write the <extend> element.
748 attributes = [("val", 0)]
750 self._xml_empty_tag("extend", attributes)
752 def _write_xf_format_extensions(self) -> None:
753 # Write the xfComplement <extLst> elements.
754 schema = "http://schemas.microsoft.com/office/spreadsheetml"
755 attributes = [
756 ("uri", "{C7286773-470A-42A8-94C5-96B5CB345126}"),
757 (
758 "xmlns:xfpb",
759 schema + "/2022/featurepropertybag",
760 ),
761 ]
763 self._xml_start_tag("extLst")
764 self._xml_start_tag("ext", attributes)
766 self._xml_empty_tag("xfpb:xfComplement", [("i", "0")])
768 self._xml_end_tag("ext")
769 self._xml_end_tag("extLst")
771 def _write_dxf_format_extensions(self) -> None:
772 # Write the DXFComplement <extLst> elements.
773 schema = "http://schemas.microsoft.com/office/spreadsheetml"
774 attributes = [
775 ("uri", "{0417FA29-78FA-4A13-93AC-8FF0FAFDF519}"),
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:DXFComplement", [("i", "0")])
787 self._xml_end_tag("ext")
788 self._xml_end_tag("extLst")