1###############################################################################
2#
3# Format - 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
11from warnings import warn
12
13
14class Format(xmlwriter.XMLwriter):
15 """
16 A class for writing the Excel XLSX Format file.
17
18
19 """
20
21 ###########################################################################
22 #
23 # Public API.
24 #
25 ###########################################################################
26
27 def __init__(self, properties=None, xf_indices=None, dxf_indices=None):
28 """
29 Constructor.
30
31 """
32 if properties is None:
33 properties = {}
34
35 super(Format, self).__init__()
36
37 self.xf_format_indices = xf_indices
38 self.dxf_format_indices = dxf_indices
39 self.xf_index = None
40 self.dxf_index = None
41
42 self.num_format = "General"
43 self.num_format_index = 0
44 self.font_index = 0
45 self.has_font = 0
46 self.has_dxf_font = 0
47
48 self.bold = 0
49 self.underline = 0
50 self.italic = 0
51 self.font_name = "Calibri"
52 self.font_size = 11
53 self.font_color = 0x0
54 self.font_strikeout = 0
55 self.font_outline = 0
56 self.font_shadow = 0
57 self.font_script = 0
58 self.font_family = 2
59 self.font_charset = 0
60 self.font_scheme = "minor"
61 self.font_condense = 0
62 self.font_extend = 0
63 self.theme = 0
64 self.hyperlink = False
65 self.xf_id = 0
66
67 self.hidden = 0
68 self.locked = 1
69
70 self.text_h_align = 0
71 self.text_wrap = 0
72 self.text_v_align = 0
73 self.text_justlast = 0
74 self.rotation = 0
75
76 self.fg_color = 0
77 self.bg_color = 0
78 self.pattern = 0
79 self.has_fill = 0
80 self.has_dxf_fill = 0
81 self.fill_index = 0
82 self.fill_count = 0
83
84 self.border_index = 0
85 self.has_border = 0
86 self.has_dxf_border = 0
87 self.border_count = 0
88
89 self.bottom = 0
90 self.bottom_color = 0
91 self.diag_border = 0
92 self.diag_color = 0
93 self.diag_type = 0
94 self.left = 0
95 self.left_color = 0
96 self.right = 0
97 self.right_color = 0
98 self.top = 0
99 self.top_color = 0
100
101 self.indent = 0
102 self.shrink = 0
103 self.merge_range = 0
104 self.reading_order = 0
105 self.just_distrib = 0
106 self.color_indexed = 0
107 self.font_only = 0
108
109 self.quote_prefix = False
110
111 # Convert properties in the constructor to method calls.
112 for key, value in properties.items():
113 getattr(self, "set_" + key)(value)
114
115 self._format_key = None
116
117 ###########################################################################
118 #
119 # Format properties.
120 #
121 ###########################################################################
122
123 def set_font_name(self, font_name):
124 """
125 Set the Format font_name property such as 'Time New Roman'. The
126 default Excel font is 'Calibri'.
127
128 Args:
129 font_name: String with the font name. No default.
130
131 Returns:
132 Nothing.
133
134 """
135 self.font_name = font_name
136
137 def set_font_size(self, font_size=11):
138 """
139 Set the Format font_size property. The default Excel font size is 11.
140
141 Args:
142 font_size: Int with font size. No default.
143
144 Returns:
145 Nothing.
146
147 """
148 self.font_size = font_size
149
150 def set_font_color(self, font_color):
151 """
152 Set the Format font_color property. The Excel default is black.
153
154 Args:
155 font_color: String with the font color. No default.
156
157 Returns:
158 Nothing.
159
160 """
161 self.font_color = self._get_color(font_color)
162
163 def set_bold(self, bold=True):
164 """
165 Set the Format bold property.
166
167 Args:
168 bold: Default is True, turns property on.
169
170 Returns:
171 Nothing.
172
173 """
174 self.bold = bold
175
176 def set_italic(self, italic=True):
177 """
178 Set the Format italic property.
179
180 Args:
181 italic: Default is True, turns property on.
182
183 Returns:
184 Nothing.
185
186 """
187 self.italic = italic
188
189 def set_underline(self, underline=1):
190 """
191 Set the Format underline property.
192
193 Args:
194 underline: Default is 1, single underline.
195
196 Returns:
197 Nothing.
198
199 """
200 self.underline = underline
201
202 def set_font_strikeout(self, font_strikeout=True):
203 """
204 Set the Format font_strikeout property.
205
206 Args:
207 font_strikeout: Default is True, turns property on.
208
209 Returns:
210 Nothing.
211
212 """
213 self.font_strikeout = font_strikeout
214
215 def set_font_script(self, font_script=1):
216 """
217 Set the Format font_script property.
218
219 Args:
220 font_script: Default is 1, superscript.
221
222 Returns:
223 Nothing.
224
225 """
226 self.font_script = font_script
227
228 def set_font_outline(self, font_outline=True):
229 """
230 Set the Format font_outline property.
231
232 Args:
233 font_outline: Default is True, turns property on.
234
235 Returns:
236 Nothing.
237
238 """
239 self.font_outline = font_outline
240
241 def set_font_shadow(self, font_shadow=True):
242 """
243 Set the Format font_shadow property.
244
245 Args:
246 font_shadow: Default is True, turns property on.
247
248 Returns:
249 Nothing.
250
251 """
252 self.font_shadow = font_shadow
253
254 def set_num_format(self, num_format):
255 """
256 Set the Format num_format property such as '#,##0'.
257
258 Args:
259 num_format: String representing the number format. No default.
260
261 Returns:
262 Nothing.
263
264 """
265 self.num_format = num_format
266
267 def set_locked(self, locked=True):
268 """
269 Set the Format locked property.
270
271 Args:
272 locked: Default is True, turns property on.
273
274 Returns:
275 Nothing.
276
277 """
278 self.locked = locked
279
280 def set_hidden(self, hidden=True):
281 """
282 Set the Format hidden property.
283
284 Args:
285 hidden: Default is True, turns property on.
286
287 Returns:
288 Nothing.
289
290 """
291 self.hidden = hidden
292
293 def set_align(self, alignment):
294 """
295 Set the Format cell alignment.
296
297 Args:
298 alignment: String representing alignment. No default.
299
300 Returns:
301 Nothing.
302 """
303 alignment = alignment.lower()
304
305 # Set horizontal alignment properties.
306 if alignment == "left":
307 self.set_text_h_align(1)
308 if alignment == "centre":
309 self.set_text_h_align(2)
310 if alignment == "center":
311 self.set_text_h_align(2)
312 if alignment == "right":
313 self.set_text_h_align(3)
314 if alignment == "fill":
315 self.set_text_h_align(4)
316 if alignment == "justify":
317 self.set_text_h_align(5)
318 if alignment == "center_across":
319 self.set_text_h_align(6)
320 if alignment == "centre_across":
321 self.set_text_h_align(6)
322 if alignment == "distributed":
323 self.set_text_h_align(7)
324 if alignment == "justify_distributed":
325 self.set_text_h_align(7)
326
327 if alignment == "justify_distributed":
328 self.just_distrib = 1
329
330 # Set vertical alignment properties.
331 if alignment == "top":
332 self.set_text_v_align(1)
333 if alignment == "vcentre":
334 self.set_text_v_align(2)
335 if alignment == "vcenter":
336 self.set_text_v_align(2)
337 if alignment == "bottom":
338 self.set_text_v_align(3)
339 if alignment == "vjustify":
340 self.set_text_v_align(4)
341 if alignment == "vdistributed":
342 self.set_text_v_align(5)
343
344 def set_center_across(self, align_type=None):
345 """
346 Set the Format center_across property.
347
348 Returns:
349 Nothing.
350
351 """
352 self.set_text_h_align(6)
353
354 def set_text_wrap(self, text_wrap=True):
355 """
356 Set the Format text_wrap property.
357
358 Args:
359 text_wrap: Default is True, turns property on.
360
361 Returns:
362 Nothing.
363
364 """
365 self.text_wrap = text_wrap
366
367 def set_rotation(self, rotation):
368 """
369 Set the Format rotation property.
370
371 Args:
372 rotation: Rotation angle. No default.
373
374 Returns:
375 Nothing.
376
377 """
378 rotation = int(rotation)
379
380 # Map user angle to Excel angle.
381 if rotation == 270:
382 rotation = 255
383 elif -90 <= rotation <= 90:
384 if rotation < 0:
385 rotation = -rotation + 90
386 else:
387 warn("Rotation rotation outside range: -90 <= angle <= 90")
388 return
389
390 self.rotation = rotation
391
392 def set_indent(self, indent=1):
393 """
394 Set the Format indent property.
395
396 Args:
397 indent: Default is 1, first indentation level.
398
399 Returns:
400 Nothing.
401
402 """
403 self.indent = indent
404
405 def set_shrink(self, shrink=True):
406 """
407 Set the Format shrink property.
408
409 Args:
410 shrink: Default is True, turns property on.
411
412 Returns:
413 Nothing.
414
415 """
416 self.shrink = shrink
417
418 def set_text_justlast(self, text_justlast=True):
419 """
420 Set the Format text_justlast property.
421
422 Args:
423 text_justlast: Default is True, turns property on.
424
425 Returns:
426 Nothing.
427
428 """
429 self.text_justlast = text_justlast
430
431 def set_pattern(self, pattern=1):
432 """
433 Set the Format pattern property.
434
435 Args:
436 pattern: Default is 1, solid fill.
437
438 Returns:
439 Nothing.
440
441 """
442 self.pattern = pattern
443
444 def set_bg_color(self, bg_color):
445 """
446 Set the Format bg_color property.
447
448 Args:
449 bg_color: Background color. No default.
450
451 Returns:
452 Nothing.
453
454 """
455 self.bg_color = self._get_color(bg_color)
456
457 def set_fg_color(self, fg_color):
458 """
459 Set the Format fg_color property.
460
461 Args:
462 fg_color: Foreground color. No default.
463
464 Returns:
465 Nothing.
466
467 """
468 self.fg_color = self._get_color(fg_color)
469
470 # set_border(style) Set cells borders to the same style
471 def set_border(self, style=1):
472 """
473 Set the Format bottom property.
474
475 Args:
476 bottom: Default is 1, border type 1.
477
478 Returns:
479 Nothing.
480
481 """
482 self.set_bottom(style)
483 self.set_top(style)
484 self.set_left(style)
485 self.set_right(style)
486
487 # set_border_color(color) Set cells border to the same color
488 def set_border_color(self, color):
489 """
490 Set the Format bottom property.
491
492 Args:
493 color: Color string. No default.
494
495 Returns:
496 Nothing.
497
498 """
499 self.set_bottom_color(color)
500 self.set_top_color(color)
501 self.set_left_color(color)
502 self.set_right_color(color)
503
504 def set_bottom(self, bottom=1):
505 """
506 Set the Format bottom property.
507
508 Args:
509 bottom: Default is 1, border type 1.
510
511 Returns:
512 Nothing.
513
514 """
515 self.bottom = bottom
516
517 def set_bottom_color(self, bottom_color):
518 """
519 Set the Format bottom_color property.
520
521 Args:
522 bottom_color: Color string. No default.
523
524 Returns:
525 Nothing.
526
527 """
528 self.bottom_color = self._get_color(bottom_color)
529
530 def set_diag_type(self, diag_type=1):
531 """
532 Set the Format diag_type property.
533
534 Args:
535 diag_type: Default is 1, border type 1.
536
537 Returns:
538 Nothing.
539
540 """
541 self.diag_type = diag_type
542
543 def set_left(self, left=1):
544 """
545 Set the Format left property.
546
547 Args:
548 left: Default is 1, border type 1.
549
550 Returns:
551 Nothing.
552
553 """
554 self.left = left
555
556 def set_left_color(self, left_color):
557 """
558 Set the Format left_color property.
559
560 Args:
561 left_color: Color string. No default.
562
563 Returns:
564 Nothing.
565
566 """
567 self.left_color = self._get_color(left_color)
568
569 def set_right(self, right=1):
570 """
571 Set the Format right property.
572
573 Args:
574 right: Default is 1, border type 1.
575
576 Returns:
577 Nothing.
578
579 """
580 self.right = right
581
582 def set_right_color(self, right_color):
583 """
584 Set the Format right_color property.
585
586 Args:
587 right_color: Color string. No default.
588
589 Returns:
590 Nothing.
591
592 """
593 self.right_color = self._get_color(right_color)
594
595 def set_top(self, top=1):
596 """
597 Set the Format top property.
598
599 Args:
600 top: Default is 1, border type 1.
601
602 Returns:
603 Nothing.
604
605 """
606 self.top = top
607
608 def set_top_color(self, top_color):
609 """
610 Set the Format top_color property.
611
612 Args:
613 top_color: Color string. No default.
614
615 Returns:
616 Nothing.
617
618 """
619 self.top_color = self._get_color(top_color)
620
621 def set_diag_color(self, diag_color):
622 """
623 Set the Format diag_color property.
624
625 Args:
626 diag_color: Color string. No default.
627
628 Returns:
629 Nothing.
630
631 """
632 self.diag_color = self._get_color(diag_color)
633
634 def set_diag_border(self, diag_border=1):
635 """
636 Set the Format diag_border property.
637
638 Args:
639 diag_border: Default is 1, border type 1.
640
641 Returns:
642 Nothing.
643
644 """
645 self.diag_border = diag_border
646
647 def set_quote_prefix(self, quote_prefix=True):
648 """
649 Set the Format quote prefix property.
650
651 Args:
652 quote_prefix: Default is True, turns property on.
653
654 Returns:
655 Nothing.
656
657 """
658 self.quote_prefix = quote_prefix
659
660 ###########################################################################
661 #
662 # Internal Format properties. These aren't documented since they are
663 # either only used internally or else are unlikely to be set by the user.
664 #
665 ###########################################################################
666
667 def set_has_font(self, has_font=True):
668 # Set the has_font property.
669 self.has_font = has_font
670
671 def set_has_fill(self, has_fill=True):
672 # Set the has_fill property.
673 self.has_fill = has_fill
674
675 def set_font_index(self, font_index):
676 # Set the font_index property.
677 self.font_index = font_index
678
679 def set_xf_index(self, xf_index):
680 # Set the xf_index property.
681 self.xf_index = xf_index
682
683 def set_dxf_index(self, dxf_index):
684 # Set the xf_index property.
685 self.dxf_index = dxf_index
686
687 def set_num_format_index(self, num_format_index):
688 # Set the num_format_index property.
689 self.num_format_index = num_format_index
690
691 def set_text_h_align(self, text_h_align):
692 # Set the text_h_align property.
693 self.text_h_align = text_h_align
694
695 def set_text_v_align(self, text_v_align):
696 # Set the text_v_align property.
697 self.text_v_align = text_v_align
698
699 def set_reading_order(self, direction=0):
700 # Set the reading_order property.
701 self.reading_order = direction
702
703 def set_valign(self, align):
704 # Set vertical cell alignment. This is required by the constructor
705 # properties dict to differentiate between the vertical and horizontal
706 # properties.
707 self.set_align(align)
708
709 def set_font_family(self, font_family):
710 # Set the Format font_family property.
711 self.font_family = font_family
712
713 def set_font_charset(self, font_charset):
714 # Set the Format font_charset property.
715 self.font_charset = font_charset
716
717 def set_font_scheme(self, font_scheme):
718 # Set the Format font_scheme property.
719 self.font_scheme = font_scheme
720
721 def set_font_condense(self, font_condense):
722 # Set the Format font_condense property.
723 self.font_condense = font_condense
724
725 def set_font_extend(self, font_extend):
726 # Set the Format font_extend property.
727 self.font_extend = font_extend
728
729 def set_theme(self, theme):
730 # Set the Format theme property.
731 self.theme = theme
732
733 def set_hyperlink(self, hyperlink=True):
734 # Set the properties for the hyperlink style. This isn't
735 # currently public. To be fixed when styles are supported.
736 self.xf_id = 1
737 self.set_underline(1)
738 self.set_theme(10)
739 self.hyperlink = hyperlink
740
741 def set_color_indexed(self, color_index):
742 # Used in the cell comment format.
743 self.color_indexed = color_index
744
745 def set_font_only(self, font_only=True):
746 # Used in the cell comment format.
747 self.font_only = font_only
748
749 # Compatibility methods.
750 def set_font(self, font_name):
751 # For compatibility with Excel::Writer::XLSX.
752 self.font_name = font_name
753
754 def set_size(self, font_size):
755 # For compatibility with Excel::Writer::XLSX.
756 self.font_size = font_size
757
758 def set_color(self, font_color):
759 # For compatibility with Excel::Writer::XLSX.
760 self.font_color = self._get_color(font_color)
761
762 ###########################################################################
763 #
764 # Private API.
765 #
766 ###########################################################################
767
768 def _get_align_properties(self):
769 # Return properties for an Style xf <alignment> sub-element.
770 changed = 0
771 align = []
772
773 # Check if any alignment options in the format have been changed.
774 if (
775 self.text_h_align
776 or self.text_v_align
777 or self.indent
778 or self.rotation
779 or self.text_wrap
780 or self.shrink
781 or self.reading_order
782 ):
783 changed = 1
784 else:
785 return changed, align
786
787 # Indent is only allowed for some alignment properties. If it is
788 # defined for any other alignment or no alignment has been set then
789 # default to left alignment.
790 if (
791 self.indent
792 and self.text_h_align != 1
793 and self.text_h_align != 3
794 and self.text_h_align != 7
795 and self.text_v_align != 1
796 and self.text_v_align != 3
797 and self.text_v_align != 5
798 ):
799 self.text_h_align = 1
800
801 # Check for properties that are mutually exclusive.
802 if self.text_wrap:
803 self.shrink = 0
804 if self.text_h_align == 4:
805 self.shrink = 0
806 if self.text_h_align == 5:
807 self.shrink = 0
808 if self.text_h_align == 7:
809 self.shrink = 0
810 if self.text_h_align != 7:
811 self.just_distrib = 0
812 if self.indent:
813 self.just_distrib = 0
814
815 continuous = "centerContinuous"
816
817 if self.text_h_align == 1:
818 align.append(("horizontal", "left"))
819 if self.text_h_align == 2:
820 align.append(("horizontal", "center"))
821 if self.text_h_align == 3:
822 align.append(("horizontal", "right"))
823 if self.text_h_align == 4:
824 align.append(("horizontal", "fill"))
825 if self.text_h_align == 5:
826 align.append(("horizontal", "justify"))
827 if self.text_h_align == 6:
828 align.append(("horizontal", continuous))
829 if self.text_h_align == 7:
830 align.append(("horizontal", "distributed"))
831
832 if self.just_distrib:
833 align.append(("justifyLastLine", 1))
834
835 # Property 'vertical' => 'bottom' is a default. It sets applyAlignment
836 # without an alignment sub-element.
837 if self.text_v_align == 1:
838 align.append(("vertical", "top"))
839 if self.text_v_align == 2:
840 align.append(("vertical", "center"))
841 if self.text_v_align == 4:
842 align.append(("vertical", "justify"))
843 if self.text_v_align == 5:
844 align.append(("vertical", "distributed"))
845
846 if self.rotation:
847 align.append(("textRotation", self.rotation))
848 if self.indent:
849 align.append(("indent", self.indent))
850
851 if self.text_wrap:
852 align.append(("wrapText", 1))
853 if self.shrink:
854 align.append(("shrinkToFit", 1))
855
856 if self.reading_order == 1:
857 align.append(("readingOrder", 1))
858 if self.reading_order == 2:
859 align.append(("readingOrder", 2))
860
861 return changed, align
862
863 def _get_protection_properties(self):
864 # Return properties for an Excel XML <Protection> element.
865 attribs = []
866
867 if not self.locked:
868 attribs.append(("locked", 0))
869 if self.hidden:
870 attribs.append(("hidden", 1))
871
872 return attribs
873
874 def _get_format_key(self):
875 # Returns a unique hash key for a format. Used by Workbook.
876 if self._format_key is None:
877 self._format_key = ":".join(
878 str(x)
879 for x in (
880 self._get_font_key(),
881 self._get_border_key(),
882 self._get_fill_key(),
883 self._get_alignment_key(),
884 self.num_format,
885 self.locked,
886 self.quote_prefix,
887 self.hidden,
888 )
889 )
890
891 return self._format_key
892
893 def _get_font_key(self):
894 # Returns a unique hash key for a font. Used by Workbook.
895 key = ":".join(
896 str(x)
897 for x in (
898 self.bold,
899 self.font_color,
900 self.font_charset,
901 self.font_family,
902 self.font_outline,
903 self.font_script,
904 self.font_shadow,
905 self.font_strikeout,
906 self.font_name,
907 self.italic,
908 self.font_size,
909 self.underline,
910 self.theme,
911 )
912 )
913
914 return key
915
916 def _get_border_key(self):
917 # Returns a unique hash key for a border style. Used by Workbook.
918 key = ":".join(
919 str(x)
920 for x in (
921 self.bottom,
922 self.bottom_color,
923 self.diag_border,
924 self.diag_color,
925 self.diag_type,
926 self.left,
927 self.left_color,
928 self.right,
929 self.right_color,
930 self.top,
931 self.top_color,
932 )
933 )
934
935 return key
936
937 def _get_fill_key(self):
938 # Returns a unique hash key for a fill style. Used by Workbook.
939 key = ":".join(str(x) for x in (self.pattern, self.bg_color, self.fg_color))
940
941 return key
942
943 def _get_alignment_key(self):
944 # Returns a unique hash key for alignment formats.
945
946 key = ":".join(
947 str(x)
948 for x in (
949 self.text_h_align,
950 self.text_v_align,
951 self.indent,
952 self.rotation,
953 self.text_wrap,
954 self.shrink,
955 self.reading_order,
956 )
957 )
958
959 return key
960
961 def _get_xf_index(self):
962 # Returns the XF index number used by Excel to identify a format.
963 if self.xf_index is not None:
964 # Format already has an index number so return it.
965 return self.xf_index
966 else:
967 # Format doesn't have an index number so assign one.
968 key = self._get_format_key()
969
970 if key in self.xf_format_indices:
971 # Format matches existing format with an index.
972 return self.xf_format_indices[key]
973 else:
974 # New format requiring an index. Note. +1 since Excel
975 # has an implicit "General" format at index 0.
976 index = 1 + len(self.xf_format_indices)
977 self.xf_format_indices[key] = index
978 self.xf_index = index
979 return index
980
981 def _get_dxf_index(self):
982 # Returns the DXF index number used by Excel to identify a format.
983 if self.dxf_index is not None:
984 # Format already has an index number so return it.
985 return self.dxf_index
986 else:
987 # Format doesn't have an index number so assign one.
988 key = self._get_format_key()
989
990 if key in self.dxf_format_indices:
991 # Format matches existing format with an index.
992 return self.dxf_format_indices[key]
993 else:
994 # New format requiring an index.
995 index = len(self.dxf_format_indices)
996 self.dxf_format_indices[key] = index
997 self.dxf_index = index
998 return index
999
1000 def _get_color(self, color):
1001 # Used in conjunction with the set_xxx_color methods to convert a
1002 # color name into an RGB formatted string. These colors are for
1003 # backward compatibility with older versions of Excel.
1004 named_colors = {
1005 "black": "#000000",
1006 "blue": "#0000FF",
1007 "brown": "#800000",
1008 "cyan": "#00FFFF",
1009 "gray": "#808080",
1010 "green": "#008000",
1011 "lime": "#00FF00",
1012 "magenta": "#FF00FF",
1013 "navy": "#000080",
1014 "orange": "#FF6600",
1015 "pink": "#FF00FF",
1016 "purple": "#800080",
1017 "red": "#FF0000",
1018 "silver": "#C0C0C0",
1019 "white": "#FFFFFF",
1020 "yellow": "#FFFF00",
1021 "automatic": "Automatic",
1022 }
1023
1024 if color in named_colors:
1025 color = named_colors[color]
1026
1027 return color
1028
1029 ###########################################################################
1030 #
1031 # XML methods.
1032 #
1033 ###########################################################################