1###############################################################################
2#
3# Styles - A class for writing the Excel XLSX Worksheet file.
4#
5# SPDX-License-Identifier: BSD-2-Clause
6# Copyright 2013-2024, John McNamara, jmcnamara@cpan.org
7#
8
9# Package imports.
10from . import xmlwriter
11
12
13class Styles(xmlwriter.XMLwriter):
14 """
15 A class for writing the Excel XLSX Styles file.
16
17
18 """
19
20 ###########################################################################
21 #
22 # Public API.
23 #
24 ###########################################################################
25
26 def __init__(self):
27 """
28 Constructor.
29
30 """
31
32 super(Styles, self).__init__()
33
34 self.xf_formats = []
35 self.palette = []
36 self.font_count = 0
37 self.num_formats = []
38 self.border_count = 0
39 self.fill_count = 0
40 self.custom_colors = []
41 self.dxf_formats = []
42 self.has_hyperlink = False
43 self.hyperlink_font_id = 0
44 self.has_comments = False
45
46 ###########################################################################
47 #
48 # Private API.
49 #
50 ###########################################################################
51
52 def _assemble_xml_file(self):
53 # Assemble and write the XML file.
54
55 # Write the XML declaration.
56 self._xml_declaration()
57
58 # Add the style sheet.
59 self._write_style_sheet()
60
61 # Write the number formats.
62 self._write_num_fmts()
63
64 # Write the fonts.
65 self._write_fonts()
66
67 # Write the fills.
68 self._write_fills()
69
70 # Write the borders element.
71 self._write_borders()
72
73 # Write the cellStyleXfs element.
74 self._write_cell_style_xfs()
75
76 # Write the cellXfs element.
77 self._write_cell_xfs()
78
79 # Write the cellStyles element.
80 self._write_cell_styles()
81
82 # Write the dxfs element.
83 self._write_dxfs()
84
85 # Write the tableStyles element.
86 self._write_table_styles()
87
88 # Write the colors element.
89 self._write_colors()
90
91 # Close the style sheet tag.
92 self._xml_end_tag("styleSheet")
93
94 # Close the file.
95 self._xml_close()
96
97 def _set_style_properties(self, properties):
98 # Pass in the Format objects and other properties used in the styles.
99
100 self.xf_formats = properties[0]
101 self.palette = properties[1]
102 self.font_count = properties[2]
103 self.num_formats = properties[3]
104 self.border_count = properties[4]
105 self.fill_count = properties[5]
106 self.custom_colors = properties[6]
107 self.dxf_formats = properties[7]
108 self.has_comments = properties[8]
109
110 def _get_palette_color(self, color):
111 # Special handling for automatic color.
112 if color == "Automatic":
113 return color
114
115 # Convert the RGB color.
116 if color[0] == "#":
117 color = color[1:]
118
119 return "FF" + color.upper()
120
121 ###########################################################################
122 #
123 # XML methods.
124 #
125 ###########################################################################
126
127 def _write_style_sheet(self):
128 # Write the <styleSheet> element.
129 xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
130
131 attributes = [("xmlns", xmlns)]
132 self._xml_start_tag("styleSheet", attributes)
133
134 def _write_num_fmts(self):
135 # Write the <numFmts> element.
136 if not self.num_formats:
137 return
138
139 attributes = [("count", len(self.num_formats))]
140 self._xml_start_tag("numFmts", attributes)
141
142 # Write the numFmts elements.
143 for index, num_format in enumerate(self.num_formats, 164):
144 self._write_num_fmt(index, num_format)
145
146 self._xml_end_tag("numFmts")
147
148 def _write_num_fmt(self, num_fmt_id, format_code):
149 # Write the <numFmt> element.
150 format_codes = {
151 0: "General",
152 1: "0",
153 2: "0.00",
154 3: "#,##0",
155 4: "#,##0.00",
156 5: "($#,##0_);($#,##0)",
157 6: "($#,##0_);[Red]($#,##0)",
158 7: "($#,##0.00_);($#,##0.00)",
159 8: "($#,##0.00_);[Red]($#,##0.00)",
160 9: "0%",
161 10: "0.00%",
162 11: "0.00E+00",
163 12: "# ?/?",
164 13: "# ??/??",
165 14: "m/d/yy",
166 15: "d-mmm-yy",
167 16: "d-mmm",
168 17: "mmm-yy",
169 18: "h:mm AM/PM",
170 19: "h:mm:ss AM/PM",
171 20: "h:mm",
172 21: "h:mm:ss",
173 22: "m/d/yy h:mm",
174 37: "(#,##0_);(#,##0)",
175 38: "(#,##0_);[Red](#,##0)",
176 39: "(#,##0.00_);(#,##0.00)",
177 40: "(#,##0.00_);[Red](#,##0.00)",
178 41: '_(* #,##0_);_(* (#,##0);_(* "-"_);_(_)',
179 42: '_($* #,##0_);_($* (#,##0);_($* "-"_);_(_)',
180 43: '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(_)',
181 44: '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(_)',
182 45: "mm:ss",
183 46: "[h]:mm:ss",
184 47: "mm:ss.0",
185 48: "##0.0E+0",
186 49: "@",
187 }
188
189 # Set the format code for built-in number formats.
190 if num_fmt_id < 164:
191 if num_fmt_id in format_codes:
192 format_code = format_codes[num_fmt_id]
193 else:
194 format_code = "General"
195
196 attributes = [
197 ("numFmtId", num_fmt_id),
198 ("formatCode", format_code),
199 ]
200
201 self._xml_empty_tag("numFmt", attributes)
202
203 def _write_fonts(self):
204 # Write the <fonts> element.
205 if self.has_comments:
206 # Add extra font for comments.
207 attributes = [("count", self.font_count + 1)]
208 else:
209 attributes = [("count", self.font_count)]
210
211 self._xml_start_tag("fonts", attributes)
212
213 # Write the font elements for xf_format objects that have them.
214 for xf_format in self.xf_formats:
215 if xf_format.has_font:
216 self._write_font(xf_format)
217
218 if self.has_comments:
219 self._write_comment_font()
220
221 self._xml_end_tag("fonts")
222
223 def _write_font(self, xf_format, is_dxf_format=False):
224 # Write the <font> element.
225 self._xml_start_tag("font")
226
227 # The condense and extend elements are mainly used in dxf formats.
228 if xf_format.font_condense:
229 self._write_condense()
230
231 if xf_format.font_extend:
232 self._write_extend()
233
234 if xf_format.bold:
235 self._xml_empty_tag("b")
236
237 if xf_format.italic:
238 self._xml_empty_tag("i")
239
240 if xf_format.font_strikeout:
241 self._xml_empty_tag("strike")
242
243 if xf_format.font_outline:
244 self._xml_empty_tag("outline")
245
246 if xf_format.font_shadow:
247 self._xml_empty_tag("shadow")
248
249 # Handle the underline variants.
250 if xf_format.underline:
251 self._write_underline(xf_format.underline)
252
253 if xf_format.font_script == 1:
254 self._write_vert_align("superscript")
255
256 if xf_format.font_script == 2:
257 self._write_vert_align("subscript")
258
259 if not is_dxf_format:
260 self._xml_empty_tag("sz", [("val", xf_format.font_size)])
261
262 if xf_format.theme == -1:
263 # Ignore for excel2003_style.
264 pass
265 elif xf_format.theme:
266 self._write_color("theme", xf_format.theme)
267 elif xf_format.color_indexed:
268 self._write_color("indexed", xf_format.color_indexed)
269 elif xf_format.font_color:
270 color = self._get_palette_color(xf_format.font_color)
271 if color != "Automatic":
272 self._write_color("rgb", color)
273 elif not is_dxf_format:
274 self._write_color("theme", 1)
275
276 if not is_dxf_format:
277 self._xml_empty_tag("name", [("val", xf_format.font_name)])
278
279 if xf_format.font_family:
280 self._xml_empty_tag("family", [("val", xf_format.font_family)])
281
282 if xf_format.font_charset:
283 self._xml_empty_tag("charset", [("val", xf_format.font_charset)])
284
285 if xf_format.font_name == "Calibri" and not xf_format.hyperlink:
286 self._xml_empty_tag("scheme", [("val", xf_format.font_scheme)])
287
288 if xf_format.hyperlink:
289 self.has_hyperlink = True
290 if self.hyperlink_font_id == 0:
291 self.hyperlink_font_id = xf_format.font_index
292
293 self._xml_end_tag("font")
294
295 def _write_comment_font(self):
296 # Write the <font> element for comments.
297 self._xml_start_tag("font")
298
299 self._xml_empty_tag("sz", [("val", 8)])
300 self._write_color("indexed", 81)
301 self._xml_empty_tag("name", [("val", "Tahoma")])
302 self._xml_empty_tag("family", [("val", 2)])
303
304 self._xml_end_tag("font")
305
306 def _write_underline(self, underline):
307 # Write the underline font element.
308
309 if underline == 2:
310 attributes = [("val", "double")]
311 elif underline == 33:
312 attributes = [("val", "singleAccounting")]
313 elif underline == 34:
314 attributes = [("val", "doubleAccounting")]
315 else:
316 # Default to single underline.
317 attributes = []
318
319 self._xml_empty_tag("u", attributes)
320
321 def _write_vert_align(self, val):
322 # Write the <vertAlign> font sub-element.
323 attributes = [("val", val)]
324
325 self._xml_empty_tag("vertAlign", attributes)
326
327 def _write_color(self, name, value):
328 # Write the <color> element.
329 attributes = [(name, value)]
330
331 self._xml_empty_tag("color", attributes)
332
333 def _write_fills(self):
334 # Write the <fills> element.
335 attributes = [("count", self.fill_count)]
336
337 self._xml_start_tag("fills", attributes)
338
339 # Write the default fill element.
340 self._write_default_fill("none")
341 self._write_default_fill("gray125")
342
343 # Write the fill elements for xf_format objects that have them.
344 for xf_format in self.xf_formats:
345 if xf_format.has_fill:
346 self._write_fill(xf_format)
347
348 self._xml_end_tag("fills")
349
350 def _write_default_fill(self, pattern_type):
351 # Write the <fill> element for the default fills.
352 self._xml_start_tag("fill")
353 self._xml_empty_tag("patternFill", [("patternType", pattern_type)])
354 self._xml_end_tag("fill")
355
356 def _write_fill(self, xf_format, is_dxf_format=False):
357 # Write the <fill> element.
358 pattern = xf_format.pattern
359 bg_color = xf_format.bg_color
360 fg_color = xf_format.fg_color
361
362 # Colors for dxf formats are handled differently from normal formats
363 # since the normal xf_format reverses the meaning of BG and FG for
364 # solid fills.
365 if is_dxf_format:
366 bg_color = xf_format.dxf_bg_color
367 fg_color = xf_format.dxf_fg_color
368
369 patterns = (
370 "none",
371 "solid",
372 "mediumGray",
373 "darkGray",
374 "lightGray",
375 "darkHorizontal",
376 "darkVertical",
377 "darkDown",
378 "darkUp",
379 "darkGrid",
380 "darkTrellis",
381 "lightHorizontal",
382 "lightVertical",
383 "lightDown",
384 "lightUp",
385 "lightGrid",
386 "lightTrellis",
387 "gray125",
388 "gray0625",
389 )
390
391 # Special handling for pattern only case.
392 if not fg_color and not bg_color and patterns[pattern]:
393 self._write_default_fill(patterns[pattern])
394 return
395
396 self._xml_start_tag("fill")
397
398 # The "none" pattern is handled differently for dxf formats.
399 if is_dxf_format and pattern <= 1:
400 self._xml_start_tag("patternFill")
401 else:
402 self._xml_start_tag("patternFill", [("patternType", patterns[pattern])])
403
404 if fg_color:
405 fg_color = self._get_palette_color(fg_color)
406 if fg_color != "Automatic":
407 self._xml_empty_tag("fgColor", [("rgb", fg_color)])
408
409 if bg_color:
410 bg_color = self._get_palette_color(bg_color)
411 if bg_color != "Automatic":
412 self._xml_empty_tag("bgColor", [("rgb", bg_color)])
413 else:
414 if not is_dxf_format and pattern <= 1:
415 self._xml_empty_tag("bgColor", [("indexed", 64)])
416
417 self._xml_end_tag("patternFill")
418 self._xml_end_tag("fill")
419
420 def _write_borders(self):
421 # Write the <borders> element.
422 attributes = [("count", self.border_count)]
423
424 self._xml_start_tag("borders", attributes)
425
426 # Write the border elements for xf_format objects that have them.
427 for xf_format in self.xf_formats:
428 if xf_format.has_border:
429 self._write_border(xf_format)
430
431 self._xml_end_tag("borders")
432
433 def _write_border(self, xf_format, is_dxf_format=False):
434 # Write the <border> element.
435 attributes = []
436
437 # Diagonal borders add attributes to the <border> element.
438 if xf_format.diag_type == 1:
439 attributes.append(("diagonalUp", 1))
440 elif xf_format.diag_type == 2:
441 attributes.append(("diagonalDown", 1))
442 elif xf_format.diag_type == 3:
443 attributes.append(("diagonalUp", 1))
444 attributes.append(("diagonalDown", 1))
445
446 # Ensure that a default diag border is set if the diag type is set.
447 if xf_format.diag_type and not xf_format.diag_border:
448 xf_format.diag_border = 1
449
450 # Write the start border tag.
451 self._xml_start_tag("border", attributes)
452
453 # Write the <border> sub elements.
454 self._write_sub_border("left", xf_format.left, xf_format.left_color)
455
456 self._write_sub_border("right", xf_format.right, xf_format.right_color)
457
458 self._write_sub_border("top", xf_format.top, xf_format.top_color)
459
460 self._write_sub_border("bottom", xf_format.bottom, xf_format.bottom_color)
461
462 # Condition DXF formats don't allow diagonal borders.
463 if not is_dxf_format:
464 self._write_sub_border(
465 "diagonal", xf_format.diag_border, xf_format.diag_color
466 )
467
468 if is_dxf_format:
469 self._write_sub_border("vertical", None, None)
470 self._write_sub_border("horizontal", None, None)
471
472 self._xml_end_tag("border")
473
474 def _write_sub_border(self, border_type, style, color):
475 # Write the <border> sub elements such as <right>, <top>, etc.
476 attributes = []
477
478 if not style:
479 self._xml_empty_tag(border_type)
480 return
481
482 border_styles = (
483 "none",
484 "thin",
485 "medium",
486 "dashed",
487 "dotted",
488 "thick",
489 "double",
490 "hair",
491 "mediumDashed",
492 "dashDot",
493 "mediumDashDot",
494 "dashDotDot",
495 "mediumDashDotDot",
496 "slantDashDot",
497 )
498
499 attributes.append(("style", border_styles[style]))
500
501 self._xml_start_tag(border_type, attributes)
502
503 if color and color != "Automatic":
504 color = self._get_palette_color(color)
505 self._xml_empty_tag("color", [("rgb", color)])
506 else:
507 self._xml_empty_tag("color", [("auto", 1)])
508
509 self._xml_end_tag(border_type)
510
511 def _write_cell_style_xfs(self):
512 # Write the <cellStyleXfs> element.
513 count = 1
514
515 if self.has_hyperlink:
516 count = 2
517
518 attributes = [("count", count)]
519
520 self._xml_start_tag("cellStyleXfs", attributes)
521 self._write_style_xf()
522
523 if self.has_hyperlink:
524 self._write_style_xf(True, self.hyperlink_font_id)
525
526 self._xml_end_tag("cellStyleXfs")
527
528 def _write_cell_xfs(self):
529 # Write the <cellXfs> element.
530 formats = self.xf_formats
531
532 # Workaround for when the last xf_format is used for the comment font
533 # and shouldn't be used for cellXfs.
534 last_format = formats[-1]
535 if last_format.font_only:
536 formats.pop()
537
538 attributes = [("count", len(formats))]
539 self._xml_start_tag("cellXfs", attributes)
540
541 # Write the xf elements.
542 for xf_format in formats:
543 self._write_xf(xf_format)
544
545 self._xml_end_tag("cellXfs")
546
547 def _write_style_xf(self, has_hyperlink=False, font_id=0):
548 # Write the style <xf> element.
549 num_fmt_id = 0
550 fill_id = 0
551 border_id = 0
552
553 attributes = [
554 ("numFmtId", num_fmt_id),
555 ("fontId", font_id),
556 ("fillId", fill_id),
557 ("borderId", border_id),
558 ]
559
560 if has_hyperlink:
561 attributes.append(("applyNumberFormat", 0))
562 attributes.append(("applyFill", 0))
563 attributes.append(("applyBorder", 0))
564 attributes.append(("applyAlignment", 0))
565 attributes.append(("applyProtection", 0))
566
567 self._xml_start_tag("xf", attributes)
568 self._xml_empty_tag("alignment", [("vertical", "top")])
569 self._xml_empty_tag("protection", [("locked", 0)])
570 self._xml_end_tag("xf")
571
572 else:
573 self._xml_empty_tag("xf", attributes)
574
575 def _write_xf(self, xf_format):
576 # Write the <xf> element.
577 num_fmt_id = xf_format.num_format_index
578 font_id = xf_format.font_index
579 fill_id = xf_format.fill_index
580 border_id = xf_format.border_index
581 xf_id = xf_format.xf_id
582 has_align = 0
583 has_protect = 0
584
585 attributes = [
586 ("numFmtId", num_fmt_id),
587 ("fontId", font_id),
588 ("fillId", fill_id),
589 ("borderId", border_id),
590 ("xfId", xf_id),
591 ]
592
593 if xf_format.quote_prefix:
594 attributes.append(("quotePrefix", 1))
595
596 if xf_format.num_format_index > 0:
597 attributes.append(("applyNumberFormat", 1))
598
599 # Add applyFont attribute if XF format uses a font element.
600 if xf_format.font_index > 0 and not xf_format.hyperlink:
601 attributes.append(("applyFont", 1))
602
603 # Add applyFill attribute if XF format uses a fill element.
604 if xf_format.fill_index > 0:
605 attributes.append(("applyFill", 1))
606
607 # Add applyBorder attribute if XF format uses a border element.
608 if xf_format.border_index > 0:
609 attributes.append(("applyBorder", 1))
610
611 # Check if XF format has alignment properties set.
612 (apply_align, align) = xf_format._get_align_properties()
613
614 # Check if an alignment sub-element should be written.
615 if apply_align and align:
616 has_align = 1
617
618 # We can also have applyAlignment without a sub-element.
619 if apply_align or xf_format.hyperlink:
620 attributes.append(("applyAlignment", 1))
621
622 # Check for cell protection properties.
623 protection = xf_format._get_protection_properties()
624
625 if protection or xf_format.hyperlink:
626 attributes.append(("applyProtection", 1))
627
628 if not xf_format.hyperlink:
629 has_protect = 1
630
631 # Write XF with sub-elements if required.
632 if has_align or has_protect:
633 self._xml_start_tag("xf", attributes)
634 if has_align:
635 self._xml_empty_tag("alignment", align)
636 if has_protect:
637 self._xml_empty_tag("protection", protection)
638 self._xml_end_tag("xf")
639 else:
640 self._xml_empty_tag("xf", attributes)
641
642 def _write_cell_styles(self):
643 # Write the <cellStyles> element.
644 count = 1
645
646 if self.has_hyperlink:
647 count = 2
648
649 attributes = [("count", count)]
650
651 self._xml_start_tag("cellStyles", attributes)
652
653 if self.has_hyperlink:
654 self._write_cell_style("Hyperlink", 1, 8)
655
656 self._write_cell_style()
657
658 self._xml_end_tag("cellStyles")
659
660 def _write_cell_style(self, name="Normal", xf_id=0, builtin_id=0):
661 # Write the <cellStyle> element.
662 attributes = [
663 ("name", name),
664 ("xfId", xf_id),
665 ("builtinId", builtin_id),
666 ]
667
668 self._xml_empty_tag("cellStyle", attributes)
669
670 def _write_dxfs(self):
671 # Write the <dxfs> element.
672 formats = self.dxf_formats
673 count = len(formats)
674
675 attributes = [("count", len(formats))]
676
677 if count:
678 self._xml_start_tag("dxfs", attributes)
679
680 # Write the font elements for xf_format objects that have them.
681 for xf_format in self.dxf_formats:
682 self._xml_start_tag("dxf")
683 if xf_format.has_dxf_font:
684 self._write_font(xf_format, True)
685
686 if xf_format.num_format_index:
687 self._write_num_fmt(
688 xf_format.num_format_index, xf_format.num_format
689 )
690
691 if xf_format.has_dxf_fill:
692 self._write_fill(xf_format, True)
693 if xf_format.has_dxf_border:
694 self._write_border(xf_format, True)
695 self._xml_end_tag("dxf")
696
697 self._xml_end_tag("dxfs")
698 else:
699 self._xml_empty_tag("dxfs", attributes)
700
701 def _write_table_styles(self):
702 # Write the <tableStyles> element.
703 count = 0
704 default_table_style = "TableStyleMedium9"
705 default_pivot_style = "PivotStyleLight16"
706
707 attributes = [
708 ("count", count),
709 ("defaultTableStyle", default_table_style),
710 ("defaultPivotStyle", default_pivot_style),
711 ]
712
713 self._xml_empty_tag("tableStyles", attributes)
714
715 def _write_colors(self):
716 # Write the <colors> element.
717 custom_colors = self.custom_colors
718
719 if not custom_colors:
720 return
721
722 self._xml_start_tag("colors")
723 self._write_mru_colors(custom_colors)
724 self._xml_end_tag("colors")
725
726 def _write_mru_colors(self, custom_colors):
727 # Write the <mruColors> element for the most recently used colors.
728
729 # Write the custom custom_colors in reverse order.
730 custom_colors.reverse()
731
732 # Limit the mruColors to the last 10.
733 if len(custom_colors) > 10:
734 custom_colors = custom_colors[0:10]
735
736 self._xml_start_tag("mruColors")
737
738 # Write the custom custom_colors in reverse order.
739 for color in custom_colors:
740 self._write_color("rgb", color)
741
742 self._xml_end_tag("mruColors")
743
744 def _write_condense(self):
745 # Write the <condense> element.
746 attributes = [("val", 0)]
747
748 self._xml_empty_tag("condense", attributes)
749
750 def _write_extend(self):
751 # Write the <extend> element.
752 attributes = [("val", 0)]
753
754 self._xml_empty_tag("extend", attributes)