1###############################################################################
2#
3# Worksheet - A class for writing Excel Worksheets.
4#
5# SPDX-License-Identifier: BSD-2-Clause
6# Copyright 2013-2024, John McNamara, jmcnamara@cpan.org
7#
8import datetime
9import hashlib
10import os
11import re
12from struct import unpack
13from warnings import warn
14from .exceptions import UndefinedImageSize
15from .exceptions import UnsupportedImageFormat
16
17COL_NAMES = {}
18
19CHAR_WIDTHS = {
20 " ": 3,
21 "!": 5,
22 '"': 6,
23 "#": 7,
24 "$": 7,
25 "%": 11,
26 "&": 10,
27 "'": 3,
28 "(": 5,
29 ")": 5,
30 "*": 7,
31 "+": 7,
32 ",": 4,
33 "-": 5,
34 ".": 4,
35 "/": 6,
36 "0": 7,
37 "1": 7,
38 "2": 7,
39 "3": 7,
40 "4": 7,
41 "5": 7,
42 "6": 7,
43 "7": 7,
44 "8": 7,
45 "9": 7,
46 ":": 4,
47 ";": 4,
48 "<": 7,
49 "=": 7,
50 ">": 7,
51 "?": 7,
52 "@": 13,
53 "A": 9,
54 "B": 8,
55 "C": 8,
56 "D": 9,
57 "E": 7,
58 "F": 7,
59 "G": 9,
60 "H": 9,
61 "I": 4,
62 "J": 5,
63 "K": 8,
64 "L": 6,
65 "M": 12,
66 "N": 10,
67 "O": 10,
68 "P": 8,
69 "Q": 10,
70 "R": 8,
71 "S": 7,
72 "T": 7,
73 "U": 9,
74 "V": 9,
75 "W": 13,
76 "X": 8,
77 "Y": 7,
78 "Z": 7,
79 "[": 5,
80 "\\": 6,
81 "]": 5,
82 "^": 7,
83 "_": 7,
84 "`": 4,
85 "a": 7,
86 "b": 8,
87 "c": 6,
88 "d": 8,
89 "e": 8,
90 "f": 5,
91 "g": 7,
92 "h": 8,
93 "i": 4,
94 "j": 4,
95 "k": 7,
96 "l": 4,
97 "m": 12,
98 "n": 8,
99 "o": 8,
100 "p": 8,
101 "q": 8,
102 "r": 5,
103 "s": 6,
104 "t": 5,
105 "u": 8,
106 "v": 7,
107 "w": 11,
108 "x": 7,
109 "y": 7,
110 "z": 6,
111 "{": 5,
112 "|": 7,
113 "}": 5,
114 "~": 7,
115}
116
117# The following is a list of Emojis used to decide if worksheet names require
118# quoting since there is (currently) no native support for matching them in
119# Python regular expressions. It is probably unnecessary to exclude them since
120# the default quoting is safe in Excel even when unnecessary (the reverse isn't
121# true). The Emoji list was generated from:
122#
123# https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEmoji%3DYes%3A%5D&abb=on&esc=on&g=&i=
124#
125emojis = "\u00A9\u00AE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299\U0001F004\U0001F0CF\U0001F170\U0001F171\U0001F17E\U0001F17F\U0001F18E\U0001F191-\U0001F19A\U0001F1E6-\U0001F1FF\U0001F201\U0001F202\U0001F21A\U0001F22F\U0001F232-\U0001F23A\U0001F250\U0001F251\U0001F300-\U0001F321\U0001F324-\U0001F393\U0001F396\U0001F397\U0001F399-\U0001F39B\U0001F39E-\U0001F3F0\U0001F3F3-\U0001F3F5\U0001F3F7-\U0001F4FD\U0001F4FF-\U0001F53D\U0001F549-\U0001F54E\U0001F550-\U0001F567\U0001F56F\U0001F570\U0001F573-\U0001F57A\U0001F587\U0001F58A-\U0001F58D\U0001F590\U0001F595\U0001F596\U0001F5A4\U0001F5A5\U0001F5A8\U0001F5B1\U0001F5B2\U0001F5BC\U0001F5C2-\U0001F5C4\U0001F5D1-\U0001F5D3\U0001F5DC-\U0001F5DE\U0001F5E1\U0001F5E3\U0001F5E8\U0001F5EF\U0001F5F3\U0001F5FA-\U0001F64F\U0001F680-\U0001F6C5\U0001F6CB-\U0001F6D2\U0001F6D5-\U0001F6D7\U0001F6DC-\U0001F6E5\U0001F6E9\U0001F6EB\U0001F6EC\U0001F6F0\U0001F6F3-\U0001F6FC\U0001F7E0-\U0001F7EB\U0001F7F0\U0001F90C-\U0001F93A\U0001F93C-\U0001F945\U0001F947-\U0001F9FF\U0001FA70-\U0001FA7C\U0001FA80-\U0001FA88\U0001FA90-\U0001FABD\U0001FABF-\U0001FAC5\U0001FACE-\U0001FADB\U0001FAE0-\U0001FAE8\U0001FAF0-\U0001FAF8" # noqa
126
127# Compile performance critical regular expressions.
128re_leading = re.compile(r"^\s")
129re_trailing = re.compile(r"\s$")
130re_range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
131re_quote_rule1 = re.compile(rf"[^\w\.{emojis}]")
132re_quote_rule2 = re.compile(rf"^[\d\.{emojis}]")
133re_quote_rule3 = re.compile(r"^([A-Z]{1,3}\d+)$")
134re_quote_rule4_row = re.compile(r"^R(\d+)")
135re_quote_rule4_column = re.compile(r"^R?C(\d+)")
136
137
138def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
139 """
140 Convert a zero indexed row and column cell reference to a A1 style string.
141
142 Args:
143 row: The cell row. Int.
144 col: The cell column. Int.
145 row_abs: Optional flag to make the row absolute. Bool.
146 col_abs: Optional flag to make the column absolute. Bool.
147
148 Returns:
149 A1 style string.
150
151 """
152 if row < 0:
153 warn("Row number %d must be >= 0" % row)
154 return None
155
156 if col < 0:
157 warn("Col number %d must be >= 0" % col)
158 return None
159
160 row += 1 # Change to 1-index.
161 row_abs = "$" if row_abs else ""
162
163 col_str = xl_col_to_name(col, col_abs)
164
165 return col_str + row_abs + str(row)
166
167
168def xl_rowcol_to_cell_fast(row, col):
169 """
170 Optimized version of the xl_rowcol_to_cell function. Only used internally.
171
172 Args:
173 row: The cell row. Int.
174 col: The cell column. Int.
175
176 Returns:
177 A1 style string.
178
179 """
180 if col in COL_NAMES:
181 col_str = COL_NAMES[col]
182 else:
183 col_str = xl_col_to_name(col)
184 COL_NAMES[col] = col_str
185
186 return col_str + str(row + 1)
187
188
189def xl_col_to_name(col, col_abs=False):
190 """
191 Convert a zero indexed column cell reference to a string.
192
193 Args:
194 col: The cell column. Int.
195 col_abs: Optional flag to make the column absolute. Bool.
196
197 Returns:
198 Column style string.
199
200 """
201 col_num = col
202 if col_num < 0:
203 warn("Col number %d must be >= 0" % col_num)
204 return None
205
206 col_num += 1 # Change to 1-index.
207 col_str = ""
208 col_abs = "$" if col_abs else ""
209
210 while col_num:
211 # Set remainder from 1 .. 26
212 remainder = col_num % 26
213
214 if remainder == 0:
215 remainder = 26
216
217 # Convert the remainder to a character.
218 col_letter = chr(ord("A") + remainder - 1)
219
220 # Accumulate the column letters, right to left.
221 col_str = col_letter + col_str
222
223 # Get the next order of magnitude.
224 col_num = int((col_num - 1) / 26)
225
226 return col_abs + col_str
227
228
229def xl_cell_to_rowcol(cell_str):
230 """
231 Convert a cell reference in A1 notation to a zero indexed row and column.
232
233 Args:
234 cell_str: A1 style string.
235
236 Returns:
237 row, col: Zero indexed cell row and column indices.
238
239 """
240 if not cell_str:
241 return 0, 0
242
243 match = re_range_parts.match(cell_str)
244 col_str = match.group(2)
245 row_str = match.group(4)
246
247 # Convert base26 column string to number.
248 expn = 0
249 col = 0
250 for char in reversed(col_str):
251 col += (ord(char) - ord("A") + 1) * (26**expn)
252 expn += 1
253
254 # Convert 1-index to zero-index
255 row = int(row_str) - 1
256 col -= 1
257
258 return row, col
259
260
261def xl_cell_to_rowcol_abs(cell_str):
262 """
263 Convert an absolute cell reference in A1 notation to a zero indexed
264 row and column, with True/False values for absolute rows or columns.
265
266 Args:
267 cell_str: A1 style string.
268
269 Returns:
270 row, col, row_abs, col_abs: Zero indexed cell row and column indices.
271
272 """
273 if not cell_str:
274 return 0, 0, False, False
275
276 match = re_range_parts.match(cell_str)
277
278 col_abs = match.group(1)
279 col_str = match.group(2)
280 row_abs = match.group(3)
281 row_str = match.group(4)
282
283 if col_abs:
284 col_abs = True
285 else:
286 col_abs = False
287
288 if row_abs:
289 row_abs = True
290 else:
291 row_abs = False
292
293 # Convert base26 column string to number.
294 expn = 0
295 col = 0
296 for char in reversed(col_str):
297 col += (ord(char) - ord("A") + 1) * (26**expn)
298 expn += 1
299
300 # Convert 1-index to zero-index
301 row = int(row_str) - 1
302 col -= 1
303
304 return row, col, row_abs, col_abs
305
306
307def xl_range(first_row, first_col, last_row, last_col):
308 """
309 Convert zero indexed row and col cell references to a A1:B1 range string.
310
311 Args:
312 first_row: The first cell row. Int.
313 first_col: The first cell column. Int.
314 last_row: The last cell row. Int.
315 last_col: The last cell column. Int.
316
317 Returns:
318 A1:B1 style range string.
319
320 """
321 range1 = xl_rowcol_to_cell(first_row, first_col)
322 range2 = xl_rowcol_to_cell(last_row, last_col)
323
324 if range1 is None or range2 is None:
325 warn("Row and column numbers must be >= 0")
326 return None
327
328 if range1 == range2:
329 return range1
330 else:
331 return range1 + ":" + range2
332
333
334def xl_range_abs(first_row, first_col, last_row, last_col):
335 """
336 Convert zero indexed row and col cell references to a $A$1:$B$1 absolute
337 range string.
338
339 Args:
340 first_row: The first cell row. Int.
341 first_col: The first cell column. Int.
342 last_row: The last cell row. Int.
343 last_col: The last cell column. Int.
344
345 Returns:
346 $A$1:$B$1 style range string.
347
348 """
349 range1 = xl_rowcol_to_cell(first_row, first_col, True, True)
350 range2 = xl_rowcol_to_cell(last_row, last_col, True, True)
351
352 if range1 is None or range2 is None:
353 warn("Row and column numbers must be >= 0")
354 return None
355
356 if range1 == range2:
357 return range1
358 else:
359 return range1 + ":" + range2
360
361
362def xl_range_formula(sheetname, first_row, first_col, last_row, last_col):
363 """
364 Convert worksheet name and zero indexed row and col cell references to
365 a Sheet1!A1:B1 range formula string.
366
367 Args:
368 sheetname: The worksheet name. String.
369 first_row: The first cell row. Int.
370 first_col: The first cell column. Int.
371 last_row: The last cell row. Int.
372 last_col: The last cell column. Int.
373
374 Returns:
375 A1:B1 style range string.
376
377 """
378 cell_range = xl_range_abs(first_row, first_col, last_row, last_col)
379 sheetname = quote_sheetname(sheetname)
380
381 return sheetname + "!" + cell_range
382
383
384def quote_sheetname(sheetname):
385 """
386 Sheetnames used in references should be quoted if they contain any spaces,
387 special characters or if they look like a A1 or RC cell reference. The rules
388 are shown inline below.
389
390 Args:
391 sheetname: The worksheet name. String.
392
393 Returns:
394 A quoted worksheet string.
395
396 """
397 uppercase_sheetname = sheetname.upper()
398 requires_quoting = False
399 col_max = 163_84
400 row_max = 1048576
401
402 # Don't quote sheetname if it is already quoted by the user.
403 if not sheetname.startswith("'"):
404
405 # --------------------------------------------------------------------
406 # Rule 1. Sheet names that contain anything other than \w and "."
407 # characters must be quoted.
408 # --------------------------------------------------------------------
409 if re_quote_rule1.search(sheetname):
410 requires_quoting = True
411
412 # --------------------------------------------------------------------
413 # Rule 2. Sheet names that start with a digit or "." must be quoted.
414 # --------------------------------------------------------------------
415 elif re_quote_rule2.search(sheetname):
416 requires_quoting = True
417
418 # --------------------------------------------------------------------
419 # Rule 3. Sheet names must not be a valid A1 style cell reference.
420 # Valid means that the row and column range values must also be within
421 # Excel row and column limits.
422 # --------------------------------------------------------------------
423 elif re_quote_rule3.match(uppercase_sheetname):
424 match = re_quote_rule3.match(uppercase_sheetname)
425 cell = match.group(1)
426 (row, col) = xl_cell_to_rowcol(cell)
427
428 if row >= 0 and row < row_max and col >= 0 and col < col_max:
429 requires_quoting = True
430
431 # --------------------------------------------------------------------
432 # Rule 4. Sheet names must not *start* with a valid RC style cell
433 # reference. Other characters after the valid RC reference are ignored
434 # by Excel. Valid means that the row and column range values must also
435 # be within Excel row and column limits.
436 #
437 # Note: references without trailing characters like R12345 or C12345
438 # are caught by Rule 3. Negative references like R-12345 are caught by
439 # Rule 1 due to dash.
440 # --------------------------------------------------------------------
441
442 # Rule 4a. Check for sheet names that start with R1 style references.
443 elif re_quote_rule4_row.match(uppercase_sheetname):
444 match = re_quote_rule4_row.match(uppercase_sheetname)
445 row = int(match.group(1))
446
447 if row > 0 and row <= row_max:
448 requires_quoting = True
449
450 # Rule 4b. Check for sheet names that start with C1 or RC1 style
451 elif re_quote_rule4_column.match(uppercase_sheetname):
452 match = re_quote_rule4_column.match(uppercase_sheetname)
453 col = int(match.group(1))
454
455 if col > 0 and col <= col_max:
456 requires_quoting = True
457
458 # Rule 4c. Check for some single R/C references.
459 elif uppercase_sheetname in ("R", "C", "RC"):
460 requires_quoting = True
461
462 if requires_quoting:
463 # Double quote any single quotes.
464 sheetname = sheetname.replace("'", "''")
465
466 # Single quote the sheet name.
467 sheetname = "'%s'" % sheetname
468
469 return sheetname
470
471
472def xl_pixel_width(string):
473 """
474 Get the pixel width of a string based on individual character widths taken
475 from Excel. UTF8 characters, and other unhandled characters, are given a
476 default width of 8.
477
478 Args:
479 string: The string to calculate the width for. String.
480
481 Returns:
482 The string width in pixels. Note, Excel adds an additional 7 pixels of
483 padding in the cell.
484
485 """
486 length = 0
487 for char in string:
488 length += CHAR_WIDTHS.get(char, 8)
489
490 return length
491
492
493def xl_color(color):
494 # Used in conjunction with the XlsxWriter *color() methods to convert
495 # a color name into an RGB formatted string. These colors are for
496 # backward compatibility with older versions of Excel.
497 named_colors = {
498 "black": "#000000",
499 "blue": "#0000FF",
500 "brown": "#800000",
501 "cyan": "#00FFFF",
502 "gray": "#808080",
503 "green": "#008000",
504 "lime": "#00FF00",
505 "magenta": "#FF00FF",
506 "navy": "#000080",
507 "orange": "#FF6600",
508 "pink": "#FF00FF",
509 "purple": "#800080",
510 "red": "#FF0000",
511 "silver": "#C0C0C0",
512 "white": "#FFFFFF",
513 "yellow": "#FFFF00",
514 }
515
516 if color in named_colors:
517 color = named_colors[color]
518
519 if not re.match("#[0-9a-fA-F]{6}", color):
520 warn("Color '%s' isn't a valid Excel color" % color)
521
522 # Convert the RGB color to the Excel ARGB format.
523 return "FF" + color.lstrip("#").upper()
524
525
526def get_rgb_color(color):
527 # Convert the user specified color to an RGB color.
528 rgb_color = xl_color(color)
529
530 # Remove leading FF from RGB color for charts.
531 rgb_color = re.sub(r"^FF", "", rgb_color)
532
533 return rgb_color
534
535
536def get_sparkline_style(style_id):
537 styles = [
538 {
539 "series": {"theme": "4", "tint": "-0.499984740745262"},
540 "negative": {"theme": "5"},
541 "markers": {"theme": "4", "tint": "-0.499984740745262"},
542 "first": {"theme": "4", "tint": "0.39997558519241921"},
543 "last": {"theme": "4", "tint": "0.39997558519241921"},
544 "high": {"theme": "4"},
545 "low": {"theme": "4"},
546 }, # 0
547 {
548 "series": {"theme": "4", "tint": "-0.499984740745262"},
549 "negative": {"theme": "5"},
550 "markers": {"theme": "4", "tint": "-0.499984740745262"},
551 "first": {"theme": "4", "tint": "0.39997558519241921"},
552 "last": {"theme": "4", "tint": "0.39997558519241921"},
553 "high": {"theme": "4"},
554 "low": {"theme": "4"},
555 }, # 1
556 {
557 "series": {"theme": "5", "tint": "-0.499984740745262"},
558 "negative": {"theme": "6"},
559 "markers": {"theme": "5", "tint": "-0.499984740745262"},
560 "first": {"theme": "5", "tint": "0.39997558519241921"},
561 "last": {"theme": "5", "tint": "0.39997558519241921"},
562 "high": {"theme": "5"},
563 "low": {"theme": "5"},
564 }, # 2
565 {
566 "series": {"theme": "6", "tint": "-0.499984740745262"},
567 "negative": {"theme": "7"},
568 "markers": {"theme": "6", "tint": "-0.499984740745262"},
569 "first": {"theme": "6", "tint": "0.39997558519241921"},
570 "last": {"theme": "6", "tint": "0.39997558519241921"},
571 "high": {"theme": "6"},
572 "low": {"theme": "6"},
573 }, # 3
574 {
575 "series": {"theme": "7", "tint": "-0.499984740745262"},
576 "negative": {"theme": "8"},
577 "markers": {"theme": "7", "tint": "-0.499984740745262"},
578 "first": {"theme": "7", "tint": "0.39997558519241921"},
579 "last": {"theme": "7", "tint": "0.39997558519241921"},
580 "high": {"theme": "7"},
581 "low": {"theme": "7"},
582 }, # 4
583 {
584 "series": {"theme": "8", "tint": "-0.499984740745262"},
585 "negative": {"theme": "9"},
586 "markers": {"theme": "8", "tint": "-0.499984740745262"},
587 "first": {"theme": "8", "tint": "0.39997558519241921"},
588 "last": {"theme": "8", "tint": "0.39997558519241921"},
589 "high": {"theme": "8"},
590 "low": {"theme": "8"},
591 }, # 5
592 {
593 "series": {"theme": "9", "tint": "-0.499984740745262"},
594 "negative": {"theme": "4"},
595 "markers": {"theme": "9", "tint": "-0.499984740745262"},
596 "first": {"theme": "9", "tint": "0.39997558519241921"},
597 "last": {"theme": "9", "tint": "0.39997558519241921"},
598 "high": {"theme": "9"},
599 "low": {"theme": "9"},
600 }, # 6
601 {
602 "series": {"theme": "4", "tint": "-0.249977111117893"},
603 "negative": {"theme": "5"},
604 "markers": {"theme": "5", "tint": "-0.249977111117893"},
605 "first": {"theme": "5", "tint": "-0.249977111117893"},
606 "last": {"theme": "5", "tint": "-0.249977111117893"},
607 "high": {"theme": "5", "tint": "-0.249977111117893"},
608 "low": {"theme": "5", "tint": "-0.249977111117893"},
609 }, # 7
610 {
611 "series": {"theme": "5", "tint": "-0.249977111117893"},
612 "negative": {"theme": "6"},
613 "markers": {"theme": "6", "tint": "-0.249977111117893"},
614 "first": {"theme": "6", "tint": "-0.249977111117893"},
615 "last": {"theme": "6", "tint": "-0.249977111117893"},
616 "high": {"theme": "6", "tint": "-0.249977111117893"},
617 "low": {"theme": "6", "tint": "-0.249977111117893"},
618 }, # 8
619 {
620 "series": {"theme": "6", "tint": "-0.249977111117893"},
621 "negative": {"theme": "7"},
622 "markers": {"theme": "7", "tint": "-0.249977111117893"},
623 "first": {"theme": "7", "tint": "-0.249977111117893"},
624 "last": {"theme": "7", "tint": "-0.249977111117893"},
625 "high": {"theme": "7", "tint": "-0.249977111117893"},
626 "low": {"theme": "7", "tint": "-0.249977111117893"},
627 }, # 9
628 {
629 "series": {"theme": "7", "tint": "-0.249977111117893"},
630 "negative": {"theme": "8"},
631 "markers": {"theme": "8", "tint": "-0.249977111117893"},
632 "first": {"theme": "8", "tint": "-0.249977111117893"},
633 "last": {"theme": "8", "tint": "-0.249977111117893"},
634 "high": {"theme": "8", "tint": "-0.249977111117893"},
635 "low": {"theme": "8", "tint": "-0.249977111117893"},
636 }, # 10
637 {
638 "series": {"theme": "8", "tint": "-0.249977111117893"},
639 "negative": {"theme": "9"},
640 "markers": {"theme": "9", "tint": "-0.249977111117893"},
641 "first": {"theme": "9", "tint": "-0.249977111117893"},
642 "last": {"theme": "9", "tint": "-0.249977111117893"},
643 "high": {"theme": "9", "tint": "-0.249977111117893"},
644 "low": {"theme": "9", "tint": "-0.249977111117893"},
645 }, # 11
646 {
647 "series": {"theme": "9", "tint": "-0.249977111117893"},
648 "negative": {"theme": "4"},
649 "markers": {"theme": "4", "tint": "-0.249977111117893"},
650 "first": {"theme": "4", "tint": "-0.249977111117893"},
651 "last": {"theme": "4", "tint": "-0.249977111117893"},
652 "high": {"theme": "4", "tint": "-0.249977111117893"},
653 "low": {"theme": "4", "tint": "-0.249977111117893"},
654 }, # 12
655 {
656 "series": {"theme": "4"},
657 "negative": {"theme": "5"},
658 "markers": {"theme": "4", "tint": "-0.249977111117893"},
659 "first": {"theme": "4", "tint": "-0.249977111117893"},
660 "last": {"theme": "4", "tint": "-0.249977111117893"},
661 "high": {"theme": "4", "tint": "-0.249977111117893"},
662 "low": {"theme": "4", "tint": "-0.249977111117893"},
663 }, # 13
664 {
665 "series": {"theme": "5"},
666 "negative": {"theme": "6"},
667 "markers": {"theme": "5", "tint": "-0.249977111117893"},
668 "first": {"theme": "5", "tint": "-0.249977111117893"},
669 "last": {"theme": "5", "tint": "-0.249977111117893"},
670 "high": {"theme": "5", "tint": "-0.249977111117893"},
671 "low": {"theme": "5", "tint": "-0.249977111117893"},
672 }, # 14
673 {
674 "series": {"theme": "6"},
675 "negative": {"theme": "7"},
676 "markers": {"theme": "6", "tint": "-0.249977111117893"},
677 "first": {"theme": "6", "tint": "-0.249977111117893"},
678 "last": {"theme": "6", "tint": "-0.249977111117893"},
679 "high": {"theme": "6", "tint": "-0.249977111117893"},
680 "low": {"theme": "6", "tint": "-0.249977111117893"},
681 }, # 15
682 {
683 "series": {"theme": "7"},
684 "negative": {"theme": "8"},
685 "markers": {"theme": "7", "tint": "-0.249977111117893"},
686 "first": {"theme": "7", "tint": "-0.249977111117893"},
687 "last": {"theme": "7", "tint": "-0.249977111117893"},
688 "high": {"theme": "7", "tint": "-0.249977111117893"},
689 "low": {"theme": "7", "tint": "-0.249977111117893"},
690 }, # 16
691 {
692 "series": {"theme": "8"},
693 "negative": {"theme": "9"},
694 "markers": {"theme": "8", "tint": "-0.249977111117893"},
695 "first": {"theme": "8", "tint": "-0.249977111117893"},
696 "last": {"theme": "8", "tint": "-0.249977111117893"},
697 "high": {"theme": "8", "tint": "-0.249977111117893"},
698 "low": {"theme": "8", "tint": "-0.249977111117893"},
699 }, # 17
700 {
701 "series": {"theme": "9"},
702 "negative": {"theme": "4"},
703 "markers": {"theme": "9", "tint": "-0.249977111117893"},
704 "first": {"theme": "9", "tint": "-0.249977111117893"},
705 "last": {"theme": "9", "tint": "-0.249977111117893"},
706 "high": {"theme": "9", "tint": "-0.249977111117893"},
707 "low": {"theme": "9", "tint": "-0.249977111117893"},
708 }, # 18
709 {
710 "series": {"theme": "4", "tint": "0.39997558519241921"},
711 "negative": {"theme": "0", "tint": "-0.499984740745262"},
712 "markers": {"theme": "4", "tint": "0.79998168889431442"},
713 "first": {"theme": "4", "tint": "-0.249977111117893"},
714 "last": {"theme": "4", "tint": "-0.249977111117893"},
715 "high": {"theme": "4", "tint": "-0.499984740745262"},
716 "low": {"theme": "4", "tint": "-0.499984740745262"},
717 }, # 19
718 {
719 "series": {"theme": "5", "tint": "0.39997558519241921"},
720 "negative": {"theme": "0", "tint": "-0.499984740745262"},
721 "markers": {"theme": "5", "tint": "0.79998168889431442"},
722 "first": {"theme": "5", "tint": "-0.249977111117893"},
723 "last": {"theme": "5", "tint": "-0.249977111117893"},
724 "high": {"theme": "5", "tint": "-0.499984740745262"},
725 "low": {"theme": "5", "tint": "-0.499984740745262"},
726 }, # 20
727 {
728 "series": {"theme": "6", "tint": "0.39997558519241921"},
729 "negative": {"theme": "0", "tint": "-0.499984740745262"},
730 "markers": {"theme": "6", "tint": "0.79998168889431442"},
731 "first": {"theme": "6", "tint": "-0.249977111117893"},
732 "last": {"theme": "6", "tint": "-0.249977111117893"},
733 "high": {"theme": "6", "tint": "-0.499984740745262"},
734 "low": {"theme": "6", "tint": "-0.499984740745262"},
735 }, # 21
736 {
737 "series": {"theme": "7", "tint": "0.39997558519241921"},
738 "negative": {"theme": "0", "tint": "-0.499984740745262"},
739 "markers": {"theme": "7", "tint": "0.79998168889431442"},
740 "first": {"theme": "7", "tint": "-0.249977111117893"},
741 "last": {"theme": "7", "tint": "-0.249977111117893"},
742 "high": {"theme": "7", "tint": "-0.499984740745262"},
743 "low": {"theme": "7", "tint": "-0.499984740745262"},
744 }, # 22
745 {
746 "series": {"theme": "8", "tint": "0.39997558519241921"},
747 "negative": {"theme": "0", "tint": "-0.499984740745262"},
748 "markers": {"theme": "8", "tint": "0.79998168889431442"},
749 "first": {"theme": "8", "tint": "-0.249977111117893"},
750 "last": {"theme": "8", "tint": "-0.249977111117893"},
751 "high": {"theme": "8", "tint": "-0.499984740745262"},
752 "low": {"theme": "8", "tint": "-0.499984740745262"},
753 }, # 23
754 {
755 "series": {"theme": "9", "tint": "0.39997558519241921"},
756 "negative": {"theme": "0", "tint": "-0.499984740745262"},
757 "markers": {"theme": "9", "tint": "0.79998168889431442"},
758 "first": {"theme": "9", "tint": "-0.249977111117893"},
759 "last": {"theme": "9", "tint": "-0.249977111117893"},
760 "high": {"theme": "9", "tint": "-0.499984740745262"},
761 "low": {"theme": "9", "tint": "-0.499984740745262"},
762 }, # 24
763 {
764 "series": {"theme": "1", "tint": "0.499984740745262"},
765 "negative": {"theme": "1", "tint": "0.249977111117893"},
766 "markers": {"theme": "1", "tint": "0.249977111117893"},
767 "first": {"theme": "1", "tint": "0.249977111117893"},
768 "last": {"theme": "1", "tint": "0.249977111117893"},
769 "high": {"theme": "1", "tint": "0.249977111117893"},
770 "low": {"theme": "1", "tint": "0.249977111117893"},
771 }, # 25
772 {
773 "series": {"theme": "1", "tint": "0.34998626667073579"},
774 "negative": {"theme": "0", "tint": "-0.249977111117893"},
775 "markers": {"theme": "0", "tint": "-0.249977111117893"},
776 "first": {"theme": "0", "tint": "-0.249977111117893"},
777 "last": {"theme": "0", "tint": "-0.249977111117893"},
778 "high": {"theme": "0", "tint": "-0.249977111117893"},
779 "low": {"theme": "0", "tint": "-0.249977111117893"},
780 }, # 26
781 {
782 "series": {"rgb": "FF323232"},
783 "negative": {"rgb": "FFD00000"},
784 "markers": {"rgb": "FFD00000"},
785 "first": {"rgb": "FFD00000"},
786 "last": {"rgb": "FFD00000"},
787 "high": {"rgb": "FFD00000"},
788 "low": {"rgb": "FFD00000"},
789 }, # 27
790 {
791 "series": {"rgb": "FF000000"},
792 "negative": {"rgb": "FF0070C0"},
793 "markers": {"rgb": "FF0070C0"},
794 "first": {"rgb": "FF0070C0"},
795 "last": {"rgb": "FF0070C0"},
796 "high": {"rgb": "FF0070C0"},
797 "low": {"rgb": "FF0070C0"},
798 }, # 28
799 {
800 "series": {"rgb": "FF376092"},
801 "negative": {"rgb": "FFD00000"},
802 "markers": {"rgb": "FFD00000"},
803 "first": {"rgb": "FFD00000"},
804 "last": {"rgb": "FFD00000"},
805 "high": {"rgb": "FFD00000"},
806 "low": {"rgb": "FFD00000"},
807 }, # 29
808 {
809 "series": {"rgb": "FF0070C0"},
810 "negative": {"rgb": "FF000000"},
811 "markers": {"rgb": "FF000000"},
812 "first": {"rgb": "FF000000"},
813 "last": {"rgb": "FF000000"},
814 "high": {"rgb": "FF000000"},
815 "low": {"rgb": "FF000000"},
816 }, # 30
817 {
818 "series": {"rgb": "FF5F5F5F"},
819 "negative": {"rgb": "FFFFB620"},
820 "markers": {"rgb": "FFD70077"},
821 "first": {"rgb": "FF5687C2"},
822 "last": {"rgb": "FF359CEB"},
823 "high": {"rgb": "FF56BE79"},
824 "low": {"rgb": "FFFF5055"},
825 }, # 31
826 {
827 "series": {"rgb": "FF5687C2"},
828 "negative": {"rgb": "FFFFB620"},
829 "markers": {"rgb": "FFD70077"},
830 "first": {"rgb": "FF777777"},
831 "last": {"rgb": "FF359CEB"},
832 "high": {"rgb": "FF56BE79"},
833 "low": {"rgb": "FFFF5055"},
834 }, # 32
835 {
836 "series": {"rgb": "FFC6EFCE"},
837 "negative": {"rgb": "FFFFC7CE"},
838 "markers": {"rgb": "FF8CADD6"},
839 "first": {"rgb": "FFFFDC47"},
840 "last": {"rgb": "FFFFEB9C"},
841 "high": {"rgb": "FF60D276"},
842 "low": {"rgb": "FFFF5367"},
843 }, # 33
844 {
845 "series": {"rgb": "FF00B050"},
846 "negative": {"rgb": "FFFF0000"},
847 "markers": {"rgb": "FF0070C0"},
848 "first": {"rgb": "FFFFC000"},
849 "last": {"rgb": "FFFFC000"},
850 "high": {"rgb": "FF00B050"},
851 "low": {"rgb": "FFFF0000"},
852 }, # 34
853 {
854 "series": {"theme": "3"},
855 "negative": {"theme": "9"},
856 "markers": {"theme": "8"},
857 "first": {"theme": "4"},
858 "last": {"theme": "5"},
859 "high": {"theme": "6"},
860 "low": {"theme": "7"},
861 }, # 35
862 {
863 "series": {"theme": "1"},
864 "negative": {"theme": "9"},
865 "markers": {"theme": "8"},
866 "first": {"theme": "4"},
867 "last": {"theme": "5"},
868 "high": {"theme": "6"},
869 "low": {"theme": "7"},
870 }, # 36
871 ]
872
873 return styles[style_id]
874
875
876def supported_datetime(dt):
877 # Determine is an argument is a supported datetime object.
878 return isinstance(
879 dt, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
880 )
881
882
883def remove_datetime_timezone(dt_obj, remove_timezone):
884 # Excel doesn't support timezones in datetimes/times so we remove the
885 # tzinfo from the object if the user has specified that option in the
886 # constructor.
887 if remove_timezone:
888 dt_obj = dt_obj.replace(tzinfo=None)
889 else:
890 if dt_obj.tzinfo:
891 raise TypeError(
892 "Excel doesn't support timezones in datetimes. "
893 "Set the tzinfo in the datetime/time object to None or "
894 "use the 'remove_timezone' Workbook() option"
895 )
896
897 return dt_obj
898
899
900def datetime_to_excel_datetime(dt_obj, date_1904, remove_timezone):
901 # Convert a datetime object to an Excel serial date and time. The integer
902 # part of the number stores the number of days since the epoch and the
903 # fractional part stores the percentage of the day.
904 date_type = dt_obj
905 is_timedelta = False
906
907 if date_1904:
908 # Excel for Mac date epoch.
909 epoch = datetime.datetime(1904, 1, 1)
910 else:
911 # Default Excel epoch.
912 epoch = datetime.datetime(1899, 12, 31)
913
914 # We handle datetime .datetime, .date and .time objects but convert
915 # them to datetime.datetime objects and process them in the same way.
916 if isinstance(dt_obj, datetime.datetime):
917 dt_obj = remove_datetime_timezone(dt_obj, remove_timezone)
918 delta = dt_obj - epoch
919 elif isinstance(dt_obj, datetime.date):
920 dt_obj = datetime.datetime.fromordinal(dt_obj.toordinal())
921 delta = dt_obj - epoch
922 elif isinstance(dt_obj, datetime.time):
923 dt_obj = datetime.datetime.combine(epoch, dt_obj)
924 dt_obj = remove_datetime_timezone(dt_obj, remove_timezone)
925 delta = dt_obj - epoch
926 elif isinstance(dt_obj, datetime.timedelta):
927 is_timedelta = True
928 delta = dt_obj
929 else:
930 raise TypeError("Unknown or unsupported datetime type")
931
932 # Convert a Python datetime.datetime value to an Excel date number.
933 excel_time = delta.days + (
934 float(delta.seconds) + float(delta.microseconds) / 1e6
935 ) / (60 * 60 * 24)
936
937 # The following is a workaround for the fact that in Excel a time only
938 # value is represented as 1899-12-31+time whereas in datetime.datetime()
939 # it is 1900-1-1+time so we need to subtract the 1 day difference.
940 if isinstance(date_type, datetime.datetime) and dt_obj.isocalendar() == (
941 1900,
942 1,
943 1,
944 ):
945 excel_time -= 1
946
947 # Account for Excel erroneously treating 1900 as a leap year.
948 if not date_1904 and not is_timedelta and excel_time > 59:
949 excel_time += 1
950
951 return excel_time
952
953
954def preserve_whitespace(string):
955 # Check if a string has leading or trailing whitespace that requires a
956 # "preserve" attribute.
957 if re_leading.search(string) or re_trailing.search(string):
958 return True
959 else:
960 return False
961
962
963def get_image_properties(filename, image_data):
964 # Extract dimension information from the image file.
965 height = 0
966 width = 0
967 x_dpi = 96
968 y_dpi = 96
969
970 if not image_data:
971 # Open the image file and read in the data.
972 fh = open(filename, "rb")
973 data = fh.read()
974 else:
975 # Read the image data from the user supplied byte stream.
976 data = image_data.getvalue()
977
978 digest = hashlib.sha256(data).hexdigest()
979
980 # Get the image filename without the path.
981 image_name = os.path.basename(filename)
982
983 # Look for some common image file markers.
984 marker1 = unpack("3s", data[1:4])[0]
985 marker2 = unpack(">H", data[:2])[0]
986 marker3 = unpack("2s", data[:2])[0]
987 marker4 = unpack("<L", data[:4])[0]
988 marker5 = (unpack("4s", data[40:44]))[0]
989 marker6 = unpack("4s", data[:4])[0]
990
991 png_marker = b"PNG"
992 bmp_marker = b"BM"
993 emf_marker = b" EMF"
994 gif_marker = b"GIF8"
995
996 if marker1 == png_marker:
997 (image_type, width, height, x_dpi, y_dpi) = _process_png(data)
998
999 elif marker2 == 0xFFD8:
1000 (image_type, width, height, x_dpi, y_dpi) = _process_jpg(data)
1001
1002 elif marker3 == bmp_marker:
1003 (image_type, width, height) = _process_bmp(data)
1004
1005 elif marker4 == 0x9AC6CDD7:
1006 (image_type, width, height, x_dpi, y_dpi) = _process_wmf(data)
1007
1008 elif marker4 == 1 and marker5 == emf_marker:
1009 (image_type, width, height, x_dpi, y_dpi) = _process_emf(data)
1010
1011 elif marker6 == gif_marker:
1012 (image_type, width, height, x_dpi, y_dpi) = _process_gif(data)
1013
1014 else:
1015 raise UnsupportedImageFormat(
1016 "%s: Unknown or unsupported image file format." % filename
1017 )
1018
1019 # Check that we found the required data.
1020 if not height or not width:
1021 raise UndefinedImageSize("%s: no size data found in image file." % filename)
1022
1023 if not image_data:
1024 fh.close()
1025
1026 # Set a default dpi for images with 0 dpi.
1027 if x_dpi == 0:
1028 x_dpi = 96
1029 if y_dpi == 0:
1030 y_dpi = 96
1031
1032 return image_type, width, height, image_name, x_dpi, y_dpi, digest
1033
1034
1035def _process_png(data):
1036 # Extract width and height information from a PNG file.
1037 offset = 8
1038 data_length = len(data)
1039 end_marker = False
1040 width = 0
1041 height = 0
1042 x_dpi = 96
1043 y_dpi = 96
1044
1045 # Search through the image data to read the height and width in the
1046 # IHDR element. Also read the DPI in the pHYs element.
1047 while not end_marker and offset < data_length:
1048 length = unpack(">I", data[offset + 0 : offset + 4])[0]
1049 marker = unpack("4s", data[offset + 4 : offset + 8])[0]
1050
1051 # Read the image dimensions.
1052 if marker == b"IHDR":
1053 width = unpack(">I", data[offset + 8 : offset + 12])[0]
1054 height = unpack(">I", data[offset + 12 : offset + 16])[0]
1055
1056 # Read the image DPI.
1057 if marker == b"pHYs":
1058 x_density = unpack(">I", data[offset + 8 : offset + 12])[0]
1059 y_density = unpack(">I", data[offset + 12 : offset + 16])[0]
1060 units = unpack("b", data[offset + 16 : offset + 17])[0]
1061
1062 if units == 1:
1063 x_dpi = x_density * 0.0254
1064 y_dpi = y_density * 0.0254
1065
1066 if marker == b"IEND":
1067 end_marker = True
1068 continue
1069
1070 offset = offset + length + 12
1071
1072 return "png", width, height, x_dpi, y_dpi
1073
1074
1075def _process_jpg(data):
1076 # Extract width and height information from a JPEG file.
1077 offset = 2
1078 data_length = len(data)
1079 end_marker = False
1080 width = 0
1081 height = 0
1082 x_dpi = 96
1083 y_dpi = 96
1084
1085 # Search through the image data to read the JPEG markers.
1086 while not end_marker and offset < data_length:
1087 marker = unpack(">H", data[offset + 0 : offset + 2])[0]
1088 length = unpack(">H", data[offset + 2 : offset + 4])[0]
1089
1090 # Read the height and width in the 0xFFCn elements (except C4, C8
1091 # and CC which aren't SOF markers).
1092 if (
1093 (marker & 0xFFF0) == 0xFFC0
1094 and marker != 0xFFC4
1095 and marker != 0xFFC8
1096 and marker != 0xFFCC
1097 ):
1098 height = unpack(">H", data[offset + 5 : offset + 7])[0]
1099 width = unpack(">H", data[offset + 7 : offset + 9])[0]
1100
1101 # Read the DPI in the 0xFFE0 element.
1102 if marker == 0xFFE0:
1103 units = unpack("b", data[offset + 11 : offset + 12])[0]
1104 x_density = unpack(">H", data[offset + 12 : offset + 14])[0]
1105 y_density = unpack(">H", data[offset + 14 : offset + 16])[0]
1106
1107 if units == 1:
1108 x_dpi = x_density
1109 y_dpi = y_density
1110
1111 if units == 2:
1112 x_dpi = x_density * 2.54
1113 y_dpi = y_density * 2.54
1114
1115 # Workaround for incorrect dpi.
1116 if x_dpi == 1:
1117 x_dpi = 96
1118 if y_dpi == 1:
1119 y_dpi = 96
1120
1121 if marker == 0xFFDA:
1122 end_marker = True
1123 continue
1124
1125 offset = offset + length + 2
1126
1127 return "jpeg", width, height, x_dpi, y_dpi
1128
1129
1130def _process_gif(data):
1131 # Extract width and height information from a GIF file.
1132 x_dpi = 96
1133 y_dpi = 96
1134
1135 width = unpack("<h", data[6:8])[0]
1136 height = unpack("<h", data[8:10])[0]
1137
1138 return "gif", width, height, x_dpi, y_dpi
1139
1140
1141def _process_bmp(data):
1142 # Extract width and height information from a BMP file.
1143 width = unpack("<L", data[18:22])[0]
1144 height = unpack("<L", data[22:26])[0]
1145 return "bmp", width, height
1146
1147
1148def _process_wmf(data):
1149 # Extract width and height information from a WMF file.
1150 x_dpi = 96
1151 y_dpi = 96
1152
1153 # Read the bounding box, measured in logical units.
1154 x1 = unpack("<h", data[6:8])[0]
1155 y1 = unpack("<h", data[8:10])[0]
1156 x2 = unpack("<h", data[10:12])[0]
1157 y2 = unpack("<h", data[12:14])[0]
1158
1159 # Read the number of logical units per inch. Used to scale the image.
1160 inch = unpack("<H", data[14:16])[0]
1161
1162 # Convert to rendered height and width.
1163 width = float((x2 - x1) * x_dpi) / inch
1164 height = float((y2 - y1) * y_dpi) / inch
1165
1166 return "wmf", width, height, x_dpi, y_dpi
1167
1168
1169def _process_emf(data):
1170 # Extract width and height information from a EMF file.
1171
1172 # Read the bounding box, measured in logical units.
1173 bound_x1 = unpack("<l", data[8:12])[0]
1174 bound_y1 = unpack("<l", data[12:16])[0]
1175 bound_x2 = unpack("<l", data[16:20])[0]
1176 bound_y2 = unpack("<l", data[20:24])[0]
1177
1178 # Convert the bounds to width and height.
1179 width = bound_x2 - bound_x1
1180 height = bound_y2 - bound_y1
1181
1182 # Read the rectangular frame in units of 0.01mm.
1183 frame_x1 = unpack("<l", data[24:28])[0]
1184 frame_y1 = unpack("<l", data[28:32])[0]
1185 frame_x2 = unpack("<l", data[32:36])[0]
1186 frame_y2 = unpack("<l", data[36:40])[0]
1187
1188 # Convert the frame bounds to mm width and height.
1189 width_mm = 0.01 * (frame_x2 - frame_x1)
1190 height_mm = 0.01 * (frame_y2 - frame_y1)
1191
1192 # Get the dpi based on the logical size.
1193 x_dpi = width * 25.4 / width_mm
1194 y_dpi = height * 25.4 / height_mm
1195
1196 # This is to match Excel's calculation. It is probably to account for
1197 # the fact that the bounding box is inclusive-inclusive. Or a bug.
1198 width += 1
1199 height += 1
1200
1201 return "emf", width, height, x_dpi, y_dpi