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#
9
10# Package imports.
11from . import xmlwriter
12
13
14class Styles(xmlwriter.XMLwriter):
15 """
16 A class for writing the Excel XLSX Styles file.
17
18
19 """
20
21 ###########################################################################
22 #
23 # Public API.
24 #
25 ###########################################################################
26
27 def __init__(self):
28 """
29 Constructor.
30
31 """
32
33 super().__init__()
34
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
46
47 ###########################################################################
48 #
49 # Private API.
50 #
51 ###########################################################################
52
53 def _assemble_xml_file(self):
54 # Assemble and write the XML file.
55
56 # Write the XML declaration.
57 self._xml_declaration()
58
59 # Add the style sheet.
60 self._write_style_sheet()
61
62 # Write the number formats.
63 self._write_num_fmts()
64
65 # Write the fonts.
66 self._write_fonts()
67
68 # Write the fills.
69 self._write_fills()
70
71 # Write the borders element.
72 self._write_borders()
73
74 # Write the cellStyleXfs element.
75 self._write_cell_style_xfs()
76
77 # Write the cellXfs element.
78 self._write_cell_xfs()
79
80 # Write the cellStyles element.
81 self._write_cell_styles()
82
83 # Write the dxfs element.
84 self._write_dxfs()
85
86 # Write the tableStyles element.
87 self._write_table_styles()
88
89 # Write the colors element.
90 self._write_colors()
91
92 # Close the style sheet tag.
93 self._xml_end_tag("styleSheet")
94
95 # Close the file.
96 self._xml_close()
97
98 def _set_style_properties(self, properties):
99 # Pass in the Format objects and other properties used in the styles.
100
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]
110
111 ###########################################################################
112 #
113 # XML methods.
114 #
115 ###########################################################################
116
117 def _write_style_sheet(self):
118 # Write the <styleSheet> element.
119 xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
120
121 attributes = [("xmlns", xmlns)]
122 self._xml_start_tag("styleSheet", attributes)
123
124 def _write_num_fmts(self):
125 # Write the <numFmts> element.
126 if not self.num_formats:
127 return
128
129 attributes = [("count", len(self.num_formats))]
130 self._xml_start_tag("numFmts", attributes)
131
132 # Write the numFmts elements.
133 for index, num_format in enumerate(self.num_formats, 164):
134 self._write_num_fmt(index, num_format)
135
136 self._xml_end_tag("numFmts")
137
138 def _write_num_fmt(self, num_fmt_id, format_code):
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 }
178
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")
182
183 attributes = [
184 ("numFmtId", num_fmt_id),
185 ("formatCode", format_code),
186 ]
187
188 self._xml_empty_tag("numFmt", attributes)
189
190 def _write_fonts(self):
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)]
197
198 self._xml_start_tag("fonts", attributes)
199
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)
204
205 if self.has_comments:
206 self._write_comment_font()
207
208 self._xml_end_tag("fonts")
209
210 def _write_font(self, xf_format, is_dxf_format=False):
211 # Write the <font> element.
212 self._xml_start_tag("font")
213
214 # The condense and extend elements are mainly used in dxf formats.
215 if xf_format.font_condense:
216 self._write_condense()
217
218 if xf_format.font_extend:
219 self._write_extend()
220
221 if xf_format.bold:
222 self._xml_empty_tag("b")
223
224 if xf_format.italic:
225 self._xml_empty_tag("i")
226
227 if xf_format.font_strikeout:
228 self._xml_empty_tag("strike")
229
230 if xf_format.font_outline:
231 self._xml_empty_tag("outline")
232
233 if xf_format.font_shadow:
234 self._xml_empty_tag("shadow")
235
236 # Handle the underline variants.
237 if xf_format.underline:
238 self._write_underline(xf_format.underline)
239
240 if xf_format.font_script == 1:
241 self._write_vert_align("superscript")
242
243 if xf_format.font_script == 2:
244 self._write_vert_align("subscript")
245
246 if not is_dxf_format:
247 self._xml_empty_tag("sz", [("val", xf_format.font_size)])
248
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)])
262
263 if not is_dxf_format:
264 self._xml_empty_tag("name", [("val", xf_format.font_name)])
265
266 if xf_format.font_family:
267 self._xml_empty_tag("family", [("val", xf_format.font_family)])
268
269 if xf_format.font_charset:
270 self._xml_empty_tag("charset", [("val", xf_format.font_charset)])
271
272 if xf_format.font_name == "Calibri" and not xf_format.hyperlink:
273 self._xml_empty_tag("scheme", [("val", xf_format.font_scheme)])
274
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
279
280 self._xml_end_tag("font")
281
282 def _write_comment_font(self):
283 # Write the <font> element for comments.
284 self._xml_start_tag("font")
285
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)])
290
291 self._xml_end_tag("font")
292
293 def _write_underline(self, underline):
294 # Write the underline font element.
295
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 = []
305
306 self._xml_empty_tag("u", attributes)
307
308 def _write_vert_align(self, val):
309 # Write the <vertAlign> font sub-element.
310 attributes = [("val", val)]
311
312 self._xml_empty_tag("vertAlign", attributes)
313
314 def _write_color(self, attributes):
315 # Write the <color> element.
316 self._xml_empty_tag("color", attributes)
317
318 def _write_fills(self):
319 # Write the <fills> element.
320 attributes = [("count", self.fill_count)]
321
322 self._xml_start_tag("fills", attributes)
323
324 # Write the default fill element.
325 self._write_default_fill("none")
326 self._write_default_fill("gray125")
327
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)
332
333 self._xml_end_tag("fills")
334
335 def _write_default_fill(self, pattern_type):
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")
340
341 def _write_fill(self, xf_format, is_dxf_format=False):
342 # Write the <fill> element.
343 pattern = xf_format.pattern
344 bg_color = xf_format.bg_color
345 fg_color = xf_format.fg_color
346
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
353
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 )
375
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
380
381 self._xml_start_tag("fill")
382
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])])
388
389 if fg_color:
390 if not fg_color._is_automatic:
391 self._xml_empty_tag("fgColor", fg_color._attributes())
392
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)])
399
400 self._xml_end_tag("patternFill")
401 self._xml_end_tag("fill")
402
403 def _write_borders(self):
404 # Write the <borders> element.
405 attributes = [("count", self.border_count)]
406
407 self._xml_start_tag("borders", attributes)
408
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)
413
414 self._xml_end_tag("borders")
415
416 def _write_border(self, xf_format, is_dxf_format=False):
417 # Write the <border> element.
418 attributes = []
419
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))
428
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
432
433 # Write the start border tag.
434 self._xml_start_tag("border", attributes)
435
436 # Write the <border> sub elements.
437 self._write_sub_border("left", xf_format.left, xf_format.left_color)
438
439 self._write_sub_border("right", xf_format.right, xf_format.right_color)
440
441 self._write_sub_border("top", xf_format.top, xf_format.top_color)
442
443 self._write_sub_border("bottom", xf_format.bottom, xf_format.bottom_color)
444
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 )
450
451 if is_dxf_format:
452 self._write_sub_border("vertical", None, None)
453 self._write_sub_border("horizontal", None, None)
454
455 self._xml_end_tag("border")
456
457 def _write_sub_border(self, border_type, style, color):
458 # Write the <border> sub elements such as <right>, <top>, etc.
459 attributes = []
460
461 if not style:
462 self._xml_empty_tag(border_type)
463 return
464
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 )
481
482 attributes.append(("style", border_styles[style]))
483
484 self._xml_start_tag(border_type, attributes)
485
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)])
490
491 self._xml_end_tag(border_type)
492
493 def _write_cell_style_xfs(self):
494 # Write the <cellStyleXfs> element.
495 count = 1
496
497 if self.has_hyperlink:
498 count = 2
499
500 attributes = [("count", count)]
501
502 self._xml_start_tag("cellStyleXfs", attributes)
503 self._write_style_xf()
504
505 if self.has_hyperlink:
506 self._write_style_xf(True, self.hyperlink_font_id)
507
508 self._xml_end_tag("cellStyleXfs")
509
510 def _write_cell_xfs(self):
511 # Write the <cellXfs> element.
512 formats = self.xf_formats
513
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()
519
520 attributes = [("count", len(formats))]
521 self._xml_start_tag("cellXfs", attributes)
522
523 # Write the xf elements.
524 for xf_format in formats:
525 self._write_xf(xf_format)
526
527 self._xml_end_tag("cellXfs")
528
529 def _write_style_xf(self, has_hyperlink=False, font_id=0):
530 # Write the style <xf> element.
531 num_fmt_id = 0
532 fill_id = 0
533 border_id = 0
534
535 attributes = [
536 ("numFmtId", num_fmt_id),
537 ("fontId", font_id),
538 ("fillId", fill_id),
539 ("borderId", border_id),
540 ]
541
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))
548
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")
553
554 else:
555 self._xml_empty_tag("xf", attributes)
556
557 def _write_xf(self, xf_format):
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
564
565 has_checkbox = xf_format.checkbox
566 has_alignment = False
567 has_protection = False
568
569 attributes = [
570 ("numFmtId", num_fmt_id),
571 ("fontId", font_id),
572 ("fillId", fill_id),
573 ("borderId", border_id),
574 ("xfId", xf_id),
575 ]
576
577 if xf_format.quote_prefix:
578 attributes.append(("quotePrefix", 1))
579
580 if xf_format.num_format_index > 0:
581 attributes.append(("applyNumberFormat", 1))
582
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))
586
587 # Add applyFill attribute if XF format uses a fill element.
588 if xf_format.fill_index > 0:
589 attributes.append(("applyFill", 1))
590
591 # Add applyBorder attribute if XF format uses a border element.
592 if xf_format.border_index > 0:
593 attributes.append(("applyBorder", 1))
594
595 # Check if XF format has alignment properties set.
596 (apply_align, align) = xf_format._get_align_properties()
597
598 # Check if an alignment sub-element should be written.
599 if apply_align and align:
600 has_alignment = True
601
602 # We can also have applyAlignment without a sub-element.
603 if apply_align or xf_format.hyperlink:
604 attributes.append(("applyAlignment", 1))
605
606 # Check for cell protection properties.
607 protection = xf_format._get_protection_properties()
608
609 if protection or xf_format.hyperlink:
610 attributes.append(("applyProtection", 1))
611
612 if not xf_format.hyperlink:
613 has_protection = True
614
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)
618
619 if has_alignment:
620 self._xml_empty_tag("alignment", align)
621
622 if has_protection:
623 self._xml_empty_tag("protection", protection)
624
625 if has_checkbox:
626 self._write_xf_format_extensions()
627
628 self._xml_end_tag("xf")
629 else:
630 self._xml_empty_tag("xf", attributes)
631
632 def _write_cell_styles(self):
633 # Write the <cellStyles> element.
634 count = 1
635
636 if self.has_hyperlink:
637 count = 2
638
639 attributes = [("count", count)]
640
641 self._xml_start_tag("cellStyles", attributes)
642
643 if self.has_hyperlink:
644 self._write_cell_style("Hyperlink", 1, 8)
645
646 self._write_cell_style()
647
648 self._xml_end_tag("cellStyles")
649
650 def _write_cell_style(self, name="Normal", xf_id=0, builtin_id=0):
651 # Write the <cellStyle> element.
652 attributes = [
653 ("name", name),
654 ("xfId", xf_id),
655 ("builtinId", builtin_id),
656 ]
657
658 self._xml_empty_tag("cellStyle", attributes)
659
660 def _write_dxfs(self):
661 # Write the <dxfs> element.
662 formats = self.dxf_formats
663 count = len(formats)
664
665 attributes = [("count", len(formats))]
666
667 if count:
668 self._xml_start_tag("dxfs", attributes)
669
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)
675
676 if dxf_format.num_format_index:
677 self._write_num_fmt(
678 dxf_format.num_format_index, dxf_format.num_format
679 )
680
681 if dxf_format.has_dxf_fill:
682 self._write_fill(dxf_format, True)
683
684 if dxf_format.has_dxf_border:
685 self._write_border(dxf_format, True)
686
687 if dxf_format.checkbox:
688 self._write_dxf_format_extensions()
689
690 self._xml_end_tag("dxf")
691
692 self._xml_end_tag("dxfs")
693 else:
694 self._xml_empty_tag("dxfs", attributes)
695
696 def _write_table_styles(self):
697 # Write the <tableStyles> element.
698 count = 0
699 default_table_style = "TableStyleMedium9"
700 default_pivot_style = "PivotStyleLight16"
701
702 attributes = [
703 ("count", count),
704 ("defaultTableStyle", default_table_style),
705 ("defaultPivotStyle", default_pivot_style),
706 ]
707
708 self._xml_empty_tag("tableStyles", attributes)
709
710 def _write_colors(self):
711 # Write the <colors> element.
712 custom_colors = self.custom_colors
713
714 if not custom_colors:
715 return
716
717 self._xml_start_tag("colors")
718 self._write_mru_colors(custom_colors)
719 self._xml_end_tag("colors")
720
721 def _write_mru_colors(self, custom_colors):
722 # Write the <mruColors> element for the most recently used colors.
723
724 # Write the custom custom_colors in reverse order.
725 custom_colors.reverse()
726
727 # Limit the mruColors to the last 10.
728 if len(custom_colors) > 10:
729 custom_colors = custom_colors[0:10]
730
731 self._xml_start_tag("mruColors")
732
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())
737
738 self._xml_end_tag("mruColors")
739
740 def _write_condense(self):
741 # Write the <condense> element.
742 attributes = [("val", 0)]
743
744 self._xml_empty_tag("condense", attributes)
745
746 def _write_extend(self):
747 # Write the <extend> element.
748 attributes = [("val", 0)]
749
750 self._xml_empty_tag("extend", attributes)
751
752 def _write_xf_format_extensions(self):
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 ]
762
763 self._xml_start_tag("extLst")
764 self._xml_start_tag("ext", attributes)
765
766 self._xml_empty_tag("xfpb:xfComplement", [("i", "0")])
767
768 self._xml_end_tag("ext")
769 self._xml_end_tag("extLst")
770
771 def _write_dxf_format_extensions(self):
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 ]
781
782 self._xml_start_tag("extLst")
783 self._xml_start_tag("ext", attributes)
784
785 self._xml_empty_tag("xfpb:DXFComplement", [("i", "0")])
786
787 self._xml_end_tag("ext")
788 self._xml_end_tag("extLst")