diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/xlsxwriter/utility.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlsxwriter/utility.py | 1207 |
1 files changed, 1207 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/utility.py b/.venv/lib/python3.12/site-packages/xlsxwriter/utility.py new file mode 100644 index 00000000..95a5452c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/utility.py @@ -0,0 +1,1207 @@ +############################################################################### +# +# Worksheet - A class for writing Excel Worksheets. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# +import datetime +import hashlib +import os +import re +from struct import unpack +from warnings import warn + +from .exceptions import UndefinedImageSize, UnsupportedImageFormat + +COL_NAMES = {} + +CHAR_WIDTHS = { + " ": 3, + "!": 5, + '"': 6, + "#": 7, + "$": 7, + "%": 11, + "&": 10, + "'": 3, + "(": 5, + ")": 5, + "*": 7, + "+": 7, + ",": 4, + "-": 5, + ".": 4, + "/": 6, + "0": 7, + "1": 7, + "2": 7, + "3": 7, + "4": 7, + "5": 7, + "6": 7, + "7": 7, + "8": 7, + "9": 7, + ":": 4, + ";": 4, + "<": 7, + "=": 7, + ">": 7, + "?": 7, + "@": 13, + "A": 9, + "B": 8, + "C": 8, + "D": 9, + "E": 7, + "F": 7, + "G": 9, + "H": 9, + "I": 4, + "J": 5, + "K": 8, + "L": 6, + "M": 12, + "N": 10, + "O": 10, + "P": 8, + "Q": 10, + "R": 8, + "S": 7, + "T": 7, + "U": 9, + "V": 9, + "W": 13, + "X": 8, + "Y": 7, + "Z": 7, + "[": 5, + "\\": 6, + "]": 5, + "^": 7, + "_": 7, + "`": 4, + "a": 7, + "b": 8, + "c": 6, + "d": 8, + "e": 8, + "f": 5, + "g": 7, + "h": 8, + "i": 4, + "j": 4, + "k": 7, + "l": 4, + "m": 12, + "n": 8, + "o": 8, + "p": 8, + "q": 8, + "r": 5, + "s": 6, + "t": 5, + "u": 8, + "v": 7, + "w": 11, + "x": 7, + "y": 7, + "z": 6, + "{": 5, + "|": 7, + "}": 5, + "~": 7, +} + +# The following is a list of Emojis used to decide if worksheet names require +# quoting since there is (currently) no native support for matching them in +# Python regular expressions. It is probably unnecessary to exclude them since +# the default quoting is safe in Excel even when unnecessary (the reverse isn't +# true). The Emoji list was generated from: +# +# https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEmoji%3DYes%3A%5D&abb=on&esc=on&g=&i= +# +# pylint: disable-next=line-too-long +EMOJIS = "\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 + +# Compile performance critical regular expressions. +RE_LEADING_WHITESPACE = re.compile(r"^\s") +RE_TRAILING_WHITESPACE = re.compile(r"\s$") +RE_RANGE_PARTS = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)") +RE_QUOTE_RULE1 = re.compile(rf"[^\w\.{EMOJIS}]") +RE_QUOTE_RULE2 = re.compile(rf"^[\d\.{EMOJIS}]") +RE_QUOTE_RULE3 = re.compile(r"^([A-Z]{1,3}\d+)$") +RE_QUOTE_RULE4_ROW = re.compile(r"^R(\d+)") +RE_QUOTE_RULE4_COLUMN = re.compile(r"^R?C(\d+)") + + +def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False): + """ + Convert a zero indexed row and column cell reference to a A1 style string. + + Args: + row: The cell row. Int. + col: The cell column. Int. + row_abs: Optional flag to make the row absolute. Bool. + col_abs: Optional flag to make the column absolute. Bool. + + Returns: + A1 style string. + + """ + if row < 0: + warn(f"Row number '{row}' must be >= 0") + return None + + if col < 0: + warn(f"Col number '{col}' must be >= 0") + return None + + row += 1 # Change to 1-index. + row_abs = "$" if row_abs else "" + + col_str = xl_col_to_name(col, col_abs) + + return col_str + row_abs + str(row) + + +def xl_rowcol_to_cell_fast(row, col): + """ + Optimized version of the xl_rowcol_to_cell function. Only used internally. + + Args: + row: The cell row. Int. + col: The cell column. Int. + + Returns: + A1 style string. + + """ + if col in COL_NAMES: + col_str = COL_NAMES[col] + else: + col_str = xl_col_to_name(col) + COL_NAMES[col] = col_str + + return col_str + str(row + 1) + + +def xl_col_to_name(col, col_abs=False): + """ + Convert a zero indexed column cell reference to a string. + + Args: + col: The cell column. Int. + col_abs: Optional flag to make the column absolute. Bool. + + Returns: + Column style string. + + """ + col_num = col + if col_num < 0: + warn(f"Col number '{col_num}' must be >= 0") + return None + + col_num += 1 # Change to 1-index. + col_str = "" + col_abs = "$" if col_abs else "" + + while col_num: + # Set remainder from 1 .. 26 + remainder = col_num % 26 + + if remainder == 0: + remainder = 26 + + # Convert the remainder to a character. + col_letter = chr(ord("A") + remainder - 1) + + # Accumulate the column letters, right to left. + col_str = col_letter + col_str + + # Get the next order of magnitude. + col_num = int((col_num - 1) / 26) + + return col_abs + col_str + + +def xl_cell_to_rowcol(cell_str): + """ + Convert a cell reference in A1 notation to a zero indexed row and column. + + Args: + cell_str: A1 style string. + + Returns: + row, col: Zero indexed cell row and column indices. + + """ + if not cell_str: + return 0, 0 + + match = RE_RANGE_PARTS.match(cell_str) + col_str = match.group(2) + row_str = match.group(4) + + # Convert base26 column string to number. + expn = 0 + col = 0 + for char in reversed(col_str): + col += (ord(char) - ord("A") + 1) * (26**expn) + expn += 1 + + # Convert 1-index to zero-index + row = int(row_str) - 1 + col -= 1 + + return row, col + + +def xl_cell_to_rowcol_abs(cell_str): + """ + Convert an absolute cell reference in A1 notation to a zero indexed + row and column, with True/False values for absolute rows or columns. + + Args: + cell_str: A1 style string. + + Returns: + row, col, row_abs, col_abs: Zero indexed cell row and column indices. + + """ + if not cell_str: + return 0, 0, False, False + + match = RE_RANGE_PARTS.match(cell_str) + + col_abs = bool(match.group(1)) + col_str = match.group(2) + row_abs = bool(match.group(3)) + row_str = match.group(4) + + # Convert base26 column string to number. + expn = 0 + col = 0 + for char in reversed(col_str): + col += (ord(char) - ord("A") + 1) * (26**expn) + expn += 1 + + # Convert 1-index to zero-index + row = int(row_str) - 1 + col -= 1 + + return row, col, row_abs, col_abs + + +def xl_range(first_row, first_col, last_row, last_col): + """ + Convert zero indexed row and col cell references to a A1:B1 range string. + + Args: + first_row: The first cell row. Int. + first_col: The first cell column. Int. + last_row: The last cell row. Int. + last_col: The last cell column. Int. + + Returns: + A1:B1 style range string. + + """ + range1 = xl_rowcol_to_cell(first_row, first_col) + range2 = xl_rowcol_to_cell(last_row, last_col) + + if range1 is None or range2 is None: + warn("Row and column numbers must be >= 0") + return None + + if range1 == range2: + return range1 + + return range1 + ":" + range2 + + +def xl_range_abs(first_row, first_col, last_row, last_col): + """ + Convert zero indexed row and col cell references to a $A$1:$B$1 absolute + range string. + + Args: + first_row: The first cell row. Int. + first_col: The first cell column. Int. + last_row: The last cell row. Int. + last_col: The last cell column. Int. + + Returns: + $A$1:$B$1 style range string. + + """ + range1 = xl_rowcol_to_cell(first_row, first_col, True, True) + range2 = xl_rowcol_to_cell(last_row, last_col, True, True) + + if range1 is None or range2 is None: + warn("Row and column numbers must be >= 0") + return None + + if range1 == range2: + return range1 + + return range1 + ":" + range2 + + +def xl_range_formula(sheetname, first_row, first_col, last_row, last_col): + """ + Convert worksheet name and zero indexed row and col cell references to + a Sheet1!A1:B1 range formula string. + + Args: + sheetname: The worksheet name. String. + first_row: The first cell row. Int. + first_col: The first cell column. Int. + last_row: The last cell row. Int. + last_col: The last cell column. Int. + + Returns: + A1:B1 style range string. + + """ + cell_range = xl_range_abs(first_row, first_col, last_row, last_col) + sheetname = quote_sheetname(sheetname) + + return sheetname + "!" + cell_range + + +def quote_sheetname(sheetname): + """ + Sheetnames used in references should be quoted if they contain any spaces, + special characters or if they look like a A1 or RC cell reference. The rules + are shown inline below. + + Args: + sheetname: The worksheet name. String. + + Returns: + A quoted worksheet string. + + """ + uppercase_sheetname = sheetname.upper() + requires_quoting = False + col_max = 163_84 + row_max = 1048576 + + # Don't quote sheetname if it is already quoted by the user. + if not sheetname.startswith("'"): + + # -------------------------------------------------------------------- + # Rule 1. Sheet names that contain anything other than \w and "." + # characters must be quoted. + # -------------------------------------------------------------------- + if RE_QUOTE_RULE1.search(sheetname): + requires_quoting = True + + # -------------------------------------------------------------------- + # Rule 2. Sheet names that start with a digit or "." must be quoted. + # -------------------------------------------------------------------- + elif RE_QUOTE_RULE2.search(sheetname): + requires_quoting = True + + # -------------------------------------------------------------------- + # Rule 3. Sheet names must not be a valid A1 style cell reference. + # Valid means that the row and column range values must also be within + # Excel row and column limits. + # -------------------------------------------------------------------- + elif RE_QUOTE_RULE3.match(uppercase_sheetname): + match = RE_QUOTE_RULE3.match(uppercase_sheetname) + cell = match.group(1) + (row, col) = xl_cell_to_rowcol(cell) + + if 0 <= row < row_max and 0 <= col < col_max: + requires_quoting = True + + # -------------------------------------------------------------------- + # Rule 4. Sheet names must not *start* with a valid RC style cell + # reference. Other characters after the valid RC reference are ignored + # by Excel. Valid means that the row and column range values must also + # be within Excel row and column limits. + # + # Note: references without trailing characters like R12345 or C12345 + # are caught by Rule 3. Negative references like R-12345 are caught by + # Rule 1 due to the dash. + # -------------------------------------------------------------------- + + # Rule 4a. Check for sheet names that start with R1 style references. + elif RE_QUOTE_RULE4_ROW.match(uppercase_sheetname): + match = RE_QUOTE_RULE4_ROW.match(uppercase_sheetname) + row = int(match.group(1)) + + if 0 < row <= row_max: + requires_quoting = True + + # Rule 4b. Check for sheet names that start with C1 or RC1 style + elif RE_QUOTE_RULE4_COLUMN.match(uppercase_sheetname): + match = RE_QUOTE_RULE4_COLUMN.match(uppercase_sheetname) + col = int(match.group(1)) + + if 0 < col <= col_max: + requires_quoting = True + + # Rule 4c. Check for some single R/C references. + elif uppercase_sheetname in ("R", "C", "RC"): + requires_quoting = True + + if requires_quoting: + # Double quote any single quotes. + sheetname = sheetname.replace("'", "''") + + # Single quote the sheet name. + sheetname = f"'{sheetname}'" + + return sheetname + + +def cell_autofit_width(string): + """ + Calculate the width required to auto-fit a string in a cell. + + Args: + string: The string to calculate the cell width for. String. + + Returns: + The string autofit width in pixels. Returns 0 if the string is empty. + + """ + if not string or len(string) == 0: + return 0 + + # Excel adds an additional 7 pixels of padding to the cell boundary. + return xl_pixel_width(string) + 7 + + +def xl_pixel_width(string): + """ + Get the pixel width of a string based on individual character widths taken + from Excel. UTF8 characters, and other unhandled characters, are given a + default width of 8. + + Args: + string: The string to calculate the width for. String. + + Returns: + The string width in pixels. Note, Excel adds an additional 7 pixels of + padding in the cell. + + """ + length = 0 + for char in string: + length += CHAR_WIDTHS.get(char, 8) + + return length + + +def _xl_color(color): + # Used in conjunction with the XlsxWriter *color() methods to convert + # a color name into an RGB formatted string. These colors are for + # backward compatibility with older versions of Excel. + named_colors = { + "black": "#000000", + "blue": "#0000FF", + "brown": "#800000", + "cyan": "#00FFFF", + "gray": "#808080", + "green": "#008000", + "lime": "#00FF00", + "magenta": "#FF00FF", + "navy": "#000080", + "orange": "#FF6600", + "pink": "#FF00FF", + "purple": "#800080", + "red": "#FF0000", + "silver": "#C0C0C0", + "white": "#FFFFFF", + "yellow": "#FFFF00", + } + + color = named_colors.get(color, color) + + if not re.match("#[0-9a-fA-F]{6}", color): + warn(f"Color '{color}' isn't a valid Excel color") + + # Convert the RGB color to the Excel ARGB format. + return "FF" + color.lstrip("#").upper() + + +def _get_rgb_color(color): + # Convert the user specified color to an RGB color. + rgb_color = _xl_color(color) + + # Remove leading FF from RGB color for charts. + rgb_color = re.sub(r"^FF", "", rgb_color) + + return rgb_color + + +def _get_sparkline_style(style_id): + styles = [ + { + "series": {"theme": "4", "tint": "-0.499984740745262"}, + "negative": {"theme": "5"}, + "markers": {"theme": "4", "tint": "-0.499984740745262"}, + "first": {"theme": "4", "tint": "0.39997558519241921"}, + "last": {"theme": "4", "tint": "0.39997558519241921"}, + "high": {"theme": "4"}, + "low": {"theme": "4"}, + }, # 0 + { + "series": {"theme": "4", "tint": "-0.499984740745262"}, + "negative": {"theme": "5"}, + "markers": {"theme": "4", "tint": "-0.499984740745262"}, + "first": {"theme": "4", "tint": "0.39997558519241921"}, + "last": {"theme": "4", "tint": "0.39997558519241921"}, + "high": {"theme": "4"}, + "low": {"theme": "4"}, + }, # 1 + { + "series": {"theme": "5", "tint": "-0.499984740745262"}, + "negative": {"theme": "6"}, + "markers": {"theme": "5", "tint": "-0.499984740745262"}, + "first": {"theme": "5", "tint": "0.39997558519241921"}, + "last": {"theme": "5", "tint": "0.39997558519241921"}, + "high": {"theme": "5"}, + "low": {"theme": "5"}, + }, # 2 + { + "series": {"theme": "6", "tint": "-0.499984740745262"}, + "negative": {"theme": "7"}, + "markers": {"theme": "6", "tint": "-0.499984740745262"}, + "first": {"theme": "6", "tint": "0.39997558519241921"}, + "last": {"theme": "6", "tint": "0.39997558519241921"}, + "high": {"theme": "6"}, + "low": {"theme": "6"}, + }, # 3 + { + "series": {"theme": "7", "tint": "-0.499984740745262"}, + "negative": {"theme": "8"}, + "markers": {"theme": "7", "tint": "-0.499984740745262"}, + "first": {"theme": "7", "tint": "0.39997558519241921"}, + "last": {"theme": "7", "tint": "0.39997558519241921"}, + "high": {"theme": "7"}, + "low": {"theme": "7"}, + }, # 4 + { + "series": {"theme": "8", "tint": "-0.499984740745262"}, + "negative": {"theme": "9"}, + "markers": {"theme": "8", "tint": "-0.499984740745262"}, + "first": {"theme": "8", "tint": "0.39997558519241921"}, + "last": {"theme": "8", "tint": "0.39997558519241921"}, + "high": {"theme": "8"}, + "low": {"theme": "8"}, + }, # 5 + { + "series": {"theme": "9", "tint": "-0.499984740745262"}, + "negative": {"theme": "4"}, + "markers": {"theme": "9", "tint": "-0.499984740745262"}, + "first": {"theme": "9", "tint": "0.39997558519241921"}, + "last": {"theme": "9", "tint": "0.39997558519241921"}, + "high": {"theme": "9"}, + "low": {"theme": "9"}, + }, # 6 + { + "series": {"theme": "4", "tint": "-0.249977111117893"}, + "negative": {"theme": "5"}, + "markers": {"theme": "5", "tint": "-0.249977111117893"}, + "first": {"theme": "5", "tint": "-0.249977111117893"}, + "last": {"theme": "5", "tint": "-0.249977111117893"}, + "high": {"theme": "5", "tint": "-0.249977111117893"}, + "low": {"theme": "5", "tint": "-0.249977111117893"}, + }, # 7 + { + "series": {"theme": "5", "tint": "-0.249977111117893"}, + "negative": {"theme": "6"}, + "markers": {"theme": "6", "tint": "-0.249977111117893"}, + "first": {"theme": "6", "tint": "-0.249977111117893"}, + "last": {"theme": "6", "tint": "-0.249977111117893"}, + "high": {"theme": "6", "tint": "-0.249977111117893"}, + "low": {"theme": "6", "tint": "-0.249977111117893"}, + }, # 8 + { + "series": {"theme": "6", "tint": "-0.249977111117893"}, + "negative": {"theme": "7"}, + "markers": {"theme": "7", "tint": "-0.249977111117893"}, + "first": {"theme": "7", "tint": "-0.249977111117893"}, + "last": {"theme": "7", "tint": "-0.249977111117893"}, + "high": {"theme": "7", "tint": "-0.249977111117893"}, + "low": {"theme": "7", "tint": "-0.249977111117893"}, + }, # 9 + { + "series": {"theme": "7", "tint": "-0.249977111117893"}, + "negative": {"theme": "8"}, + "markers": {"theme": "8", "tint": "-0.249977111117893"}, + "first": {"theme": "8", "tint": "-0.249977111117893"}, + "last": {"theme": "8", "tint": "-0.249977111117893"}, + "high": {"theme": "8", "tint": "-0.249977111117893"}, + "low": {"theme": "8", "tint": "-0.249977111117893"}, + }, # 10 + { + "series": {"theme": "8", "tint": "-0.249977111117893"}, + "negative": {"theme": "9"}, + "markers": {"theme": "9", "tint": "-0.249977111117893"}, + "first": {"theme": "9", "tint": "-0.249977111117893"}, + "last": {"theme": "9", "tint": "-0.249977111117893"}, + "high": {"theme": "9", "tint": "-0.249977111117893"}, + "low": {"theme": "9", "tint": "-0.249977111117893"}, + }, # 11 + { + "series": {"theme": "9", "tint": "-0.249977111117893"}, + "negative": {"theme": "4"}, + "markers": {"theme": "4", "tint": "-0.249977111117893"}, + "first": {"theme": "4", "tint": "-0.249977111117893"}, + "last": {"theme": "4", "tint": "-0.249977111117893"}, + "high": {"theme": "4", "tint": "-0.249977111117893"}, + "low": {"theme": "4", "tint": "-0.249977111117893"}, + }, # 12 + { + "series": {"theme": "4"}, + "negative": {"theme": "5"}, + "markers": {"theme": "4", "tint": "-0.249977111117893"}, + "first": {"theme": "4", "tint": "-0.249977111117893"}, + "last": {"theme": "4", "tint": "-0.249977111117893"}, + "high": {"theme": "4", "tint": "-0.249977111117893"}, + "low": {"theme": "4", "tint": "-0.249977111117893"}, + }, # 13 + { + "series": {"theme": "5"}, + "negative": {"theme": "6"}, + "markers": {"theme": "5", "tint": "-0.249977111117893"}, + "first": {"theme": "5", "tint": "-0.249977111117893"}, + "last": {"theme": "5", "tint": "-0.249977111117893"}, + "high": {"theme": "5", "tint": "-0.249977111117893"}, + "low": {"theme": "5", "tint": "-0.249977111117893"}, + }, # 14 + { + "series": {"theme": "6"}, + "negative": {"theme": "7"}, + "markers": {"theme": "6", "tint": "-0.249977111117893"}, + "first": {"theme": "6", "tint": "-0.249977111117893"}, + "last": {"theme": "6", "tint": "-0.249977111117893"}, + "high": {"theme": "6", "tint": "-0.249977111117893"}, + "low": {"theme": "6", "tint": "-0.249977111117893"}, + }, # 15 + { + "series": {"theme": "7"}, + "negative": {"theme": "8"}, + "markers": {"theme": "7", "tint": "-0.249977111117893"}, + "first": {"theme": "7", "tint": "-0.249977111117893"}, + "last": {"theme": "7", "tint": "-0.249977111117893"}, + "high": {"theme": "7", "tint": "-0.249977111117893"}, + "low": {"theme": "7", "tint": "-0.249977111117893"}, + }, # 16 + { + "series": {"theme": "8"}, + "negative": {"theme": "9"}, + "markers": {"theme": "8", "tint": "-0.249977111117893"}, + "first": {"theme": "8", "tint": "-0.249977111117893"}, + "last": {"theme": "8", "tint": "-0.249977111117893"}, + "high": {"theme": "8", "tint": "-0.249977111117893"}, + "low": {"theme": "8", "tint": "-0.249977111117893"}, + }, # 17 + { + "series": {"theme": "9"}, + "negative": {"theme": "4"}, + "markers": {"theme": "9", "tint": "-0.249977111117893"}, + "first": {"theme": "9", "tint": "-0.249977111117893"}, + "last": {"theme": "9", "tint": "-0.249977111117893"}, + "high": {"theme": "9", "tint": "-0.249977111117893"}, + "low": {"theme": "9", "tint": "-0.249977111117893"}, + }, # 18 + { + "series": {"theme": "4", "tint": "0.39997558519241921"}, + "negative": {"theme": "0", "tint": "-0.499984740745262"}, + "markers": {"theme": "4", "tint": "0.79998168889431442"}, + "first": {"theme": "4", "tint": "-0.249977111117893"}, + "last": {"theme": "4", "tint": "-0.249977111117893"}, + "high": {"theme": "4", "tint": "-0.499984740745262"}, + "low": {"theme": "4", "tint": "-0.499984740745262"}, + }, # 19 + { + "series": {"theme": "5", "tint": "0.39997558519241921"}, + "negative": {"theme": "0", "tint": "-0.499984740745262"}, + "markers": {"theme": "5", "tint": "0.79998168889431442"}, + "first": {"theme": "5", "tint": "-0.249977111117893"}, + "last": {"theme": "5", "tint": "-0.249977111117893"}, + "high": {"theme": "5", "tint": "-0.499984740745262"}, + "low": {"theme": "5", "tint": "-0.499984740745262"}, + }, # 20 + { + "series": {"theme": "6", "tint": "0.39997558519241921"}, + "negative": {"theme": "0", "tint": "-0.499984740745262"}, + "markers": {"theme": "6", "tint": "0.79998168889431442"}, + "first": {"theme": "6", "tint": "-0.249977111117893"}, + "last": {"theme": "6", "tint": "-0.249977111117893"}, + "high": {"theme": "6", "tint": "-0.499984740745262"}, + "low": {"theme": "6", "tint": "-0.499984740745262"}, + }, # 21 + { + "series": {"theme": "7", "tint": "0.39997558519241921"}, + "negative": {"theme": "0", "tint": "-0.499984740745262"}, + "markers": {"theme": "7", "tint": "0.79998168889431442"}, + "first": {"theme": "7", "tint": "-0.249977111117893"}, + "last": {"theme": "7", "tint": "-0.249977111117893"}, + "high": {"theme": "7", "tint": "-0.499984740745262"}, + "low": {"theme": "7", "tint": "-0.499984740745262"}, + }, # 22 + { + "series": {"theme": "8", "tint": "0.39997558519241921"}, + "negative": {"theme": "0", "tint": "-0.499984740745262"}, + "markers": {"theme": "8", "tint": "0.79998168889431442"}, + "first": {"theme": "8", "tint": "-0.249977111117893"}, + "last": {"theme": "8", "tint": "-0.249977111117893"}, + "high": {"theme": "8", "tint": "-0.499984740745262"}, + "low": {"theme": "8", "tint": "-0.499984740745262"}, + }, # 23 + { + "series": {"theme": "9", "tint": "0.39997558519241921"}, + "negative": {"theme": "0", "tint": "-0.499984740745262"}, + "markers": {"theme": "9", "tint": "0.79998168889431442"}, + "first": {"theme": "9", "tint": "-0.249977111117893"}, + "last": {"theme": "9", "tint": "-0.249977111117893"}, + "high": {"theme": "9", "tint": "-0.499984740745262"}, + "low": {"theme": "9", "tint": "-0.499984740745262"}, + }, # 24 + { + "series": {"theme": "1", "tint": "0.499984740745262"}, + "negative": {"theme": "1", "tint": "0.249977111117893"}, + "markers": {"theme": "1", "tint": "0.249977111117893"}, + "first": {"theme": "1", "tint": "0.249977111117893"}, + "last": {"theme": "1", "tint": "0.249977111117893"}, + "high": {"theme": "1", "tint": "0.249977111117893"}, + "low": {"theme": "1", "tint": "0.249977111117893"}, + }, # 25 + { + "series": {"theme": "1", "tint": "0.34998626667073579"}, + "negative": {"theme": "0", "tint": "-0.249977111117893"}, + "markers": {"theme": "0", "tint": "-0.249977111117893"}, + "first": {"theme": "0", "tint": "-0.249977111117893"}, + "last": {"theme": "0", "tint": "-0.249977111117893"}, + "high": {"theme": "0", "tint": "-0.249977111117893"}, + "low": {"theme": "0", "tint": "-0.249977111117893"}, + }, # 26 + { + "series": {"rgb": "FF323232"}, + "negative": {"rgb": "FFD00000"}, + "markers": {"rgb": "FFD00000"}, + "first": {"rgb": "FFD00000"}, + "last": {"rgb": "FFD00000"}, + "high": {"rgb": "FFD00000"}, + "low": {"rgb": "FFD00000"}, + }, # 27 + { + "series": {"rgb": "FF000000"}, + "negative": {"rgb": "FF0070C0"}, + "markers": {"rgb": "FF0070C0"}, + "first": {"rgb": "FF0070C0"}, + "last": {"rgb": "FF0070C0"}, + "high": {"rgb": "FF0070C0"}, + "low": {"rgb": "FF0070C0"}, + }, # 28 + { + "series": {"rgb": "FF376092"}, + "negative": {"rgb": "FFD00000"}, + "markers": {"rgb": "FFD00000"}, + "first": {"rgb": "FFD00000"}, + "last": {"rgb": "FFD00000"}, + "high": {"rgb": "FFD00000"}, + "low": {"rgb": "FFD00000"}, + }, # 29 + { + "series": {"rgb": "FF0070C0"}, + "negative": {"rgb": "FF000000"}, + "markers": {"rgb": "FF000000"}, + "first": {"rgb": "FF000000"}, + "last": {"rgb": "FF000000"}, + "high": {"rgb": "FF000000"}, + "low": {"rgb": "FF000000"}, + }, # 30 + { + "series": {"rgb": "FF5F5F5F"}, + "negative": {"rgb": "FFFFB620"}, + "markers": {"rgb": "FFD70077"}, + "first": {"rgb": "FF5687C2"}, + "last": {"rgb": "FF359CEB"}, + "high": {"rgb": "FF56BE79"}, + "low": {"rgb": "FFFF5055"}, + }, # 31 + { + "series": {"rgb": "FF5687C2"}, + "negative": {"rgb": "FFFFB620"}, + "markers": {"rgb": "FFD70077"}, + "first": {"rgb": "FF777777"}, + "last": {"rgb": "FF359CEB"}, + "high": {"rgb": "FF56BE79"}, + "low": {"rgb": "FFFF5055"}, + }, # 32 + { + "series": {"rgb": "FFC6EFCE"}, + "negative": {"rgb": "FFFFC7CE"}, + "markers": {"rgb": "FF8CADD6"}, + "first": {"rgb": "FFFFDC47"}, + "last": {"rgb": "FFFFEB9C"}, + "high": {"rgb": "FF60D276"}, + "low": {"rgb": "FFFF5367"}, + }, # 33 + { + "series": {"rgb": "FF00B050"}, + "negative": {"rgb": "FFFF0000"}, + "markers": {"rgb": "FF0070C0"}, + "first": {"rgb": "FFFFC000"}, + "last": {"rgb": "FFFFC000"}, + "high": {"rgb": "FF00B050"}, + "low": {"rgb": "FFFF0000"}, + }, # 34 + { + "series": {"theme": "3"}, + "negative": {"theme": "9"}, + "markers": {"theme": "8"}, + "first": {"theme": "4"}, + "last": {"theme": "5"}, + "high": {"theme": "6"}, + "low": {"theme": "7"}, + }, # 35 + { + "series": {"theme": "1"}, + "negative": {"theme": "9"}, + "markers": {"theme": "8"}, + "first": {"theme": "4"}, + "last": {"theme": "5"}, + "high": {"theme": "6"}, + "low": {"theme": "7"}, + }, # 36 + ] + + return styles[style_id] + + +def _supported_datetime(dt): + # Determine is an argument is a supported datetime object. + return isinstance( + dt, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) + ) + + +def _remove_datetime_timezone(dt_obj, remove_timezone): + # Excel doesn't support timezones in datetimes/times so we remove the + # tzinfo from the object if the user has specified that option in the + # constructor. + if remove_timezone: + dt_obj = dt_obj.replace(tzinfo=None) + else: + if dt_obj.tzinfo: + raise TypeError( + "Excel doesn't support timezones in datetimes. " + "Set the tzinfo in the datetime/time object to None or " + "use the 'remove_timezone' Workbook() option" + ) + + return dt_obj + + +def _datetime_to_excel_datetime(dt_obj, date_1904, remove_timezone): + # Convert a datetime object to an Excel serial date and time. The integer + # part of the number stores the number of days since the epoch and the + # fractional part stores the percentage of the day. + date_type = dt_obj + is_timedelta = False + + if date_1904: + # Excel for Mac date epoch. + epoch = datetime.datetime(1904, 1, 1) + else: + # Default Excel epoch. + epoch = datetime.datetime(1899, 12, 31) + + # We handle datetime .datetime, .date and .time objects but convert + # them to datetime.datetime objects and process them in the same way. + if isinstance(dt_obj, datetime.datetime): + dt_obj = _remove_datetime_timezone(dt_obj, remove_timezone) + delta = dt_obj - epoch + elif isinstance(dt_obj, datetime.date): + dt_obj = datetime.datetime.fromordinal(dt_obj.toordinal()) + delta = dt_obj - epoch + elif isinstance(dt_obj, datetime.time): + dt_obj = datetime.datetime.combine(epoch, dt_obj) + dt_obj = _remove_datetime_timezone(dt_obj, remove_timezone) + delta = dt_obj - epoch + elif isinstance(dt_obj, datetime.timedelta): + is_timedelta = True + delta = dt_obj + else: + raise TypeError("Unknown or unsupported datetime type") + + # Convert a Python datetime.datetime value to an Excel date number. + excel_time = delta.days + ( + float(delta.seconds) + float(delta.microseconds) / 1e6 + ) / (60 * 60 * 24) + + # The following is a workaround for the fact that in Excel a time only + # value is represented as 1899-12-31+time whereas in datetime.datetime() + # it is 1900-1-1+time so we need to subtract the 1 day difference. + if isinstance(date_type, datetime.datetime) and dt_obj.isocalendar() == ( + 1900, + 1, + 1, + ): + excel_time -= 1 + + # Account for Excel erroneously treating 1900 as a leap year. + if not date_1904 and not is_timedelta and excel_time > 59: + excel_time += 1 + + return excel_time + + +def _preserve_whitespace(string): + # Check if a string has leading or trailing whitespace that requires a + # "preserve" attribute. + return RE_LEADING_WHITESPACE.search(string) or RE_TRAILING_WHITESPACE.search(string) + + +def _get_image_properties(filename, image_data): + # Extract dimension information from the image file. + height = 0 + width = 0 + x_dpi = 96 + y_dpi = 96 + + if not image_data: + # Open the image file and read in the data. + with open(filename, "rb") as fh: + data = fh.read() + else: + # Read the image data from the user supplied byte stream. + data = image_data.getvalue() + + digest = hashlib.sha256(data).hexdigest() + + # Get the image filename without the path. + image_name = os.path.basename(filename) + + # Look for some common image file markers. + marker1 = unpack("3s", data[1:4])[0] + marker2 = unpack(">H", data[:2])[0] + marker3 = unpack("2s", data[:2])[0] + marker4 = unpack("<L", data[:4])[0] + marker5 = (unpack("4s", data[40:44]))[0] + marker6 = unpack("4s", data[:4])[0] + + png_marker = b"PNG" + bmp_marker = b"BM" + emf_marker = b" EMF" + gif_marker = b"GIF8" + + if marker1 == png_marker: + (image_type, width, height, x_dpi, y_dpi) = _process_png(data) + + elif marker2 == 0xFFD8: + (image_type, width, height, x_dpi, y_dpi) = _process_jpg(data) + + elif marker3 == bmp_marker: + (image_type, width, height) = _process_bmp(data) + + elif marker4 == 0x9AC6CDD7: + (image_type, width, height, x_dpi, y_dpi) = _process_wmf(data) + + elif marker4 == 1 and marker5 == emf_marker: + (image_type, width, height, x_dpi, y_dpi) = _process_emf(data) + + elif marker6 == gif_marker: + (image_type, width, height, x_dpi, y_dpi) = _process_gif(data) + + else: + raise UnsupportedImageFormat( + f"{filename}: Unknown or unsupported image file format." + ) + + # Check that we found the required data. + if not height or not width: + raise UndefinedImageSize(f"{filename}: no size data found in image file.") + + if not image_data: + fh.close() + + # Set a default dpi for images with 0 dpi. + if x_dpi == 0: + x_dpi = 96 + if y_dpi == 0: + y_dpi = 96 + + return image_type, width, height, image_name, x_dpi, y_dpi, digest + + +def _process_png(data): + # Extract width and height information from a PNG file. + offset = 8 + data_length = len(data) + end_marker = False + width = 0 + height = 0 + x_dpi = 96 + y_dpi = 96 + + # Search through the image data to read the height and width in the + # IHDR element. Also read the DPI in the pHYs element. + while not end_marker and offset < data_length: + length = unpack(">I", data[offset + 0 : offset + 4])[0] + marker = unpack("4s", data[offset + 4 : offset + 8])[0] + + # Read the image dimensions. + if marker == b"IHDR": + width = unpack(">I", data[offset + 8 : offset + 12])[0] + height = unpack(">I", data[offset + 12 : offset + 16])[0] + + # Read the image DPI. + if marker == b"pHYs": + x_density = unpack(">I", data[offset + 8 : offset + 12])[0] + y_density = unpack(">I", data[offset + 12 : offset + 16])[0] + units = unpack("b", data[offset + 16 : offset + 17])[0] + + if units == 1 and x_density > 0 and y_density > 0: + x_dpi = x_density * 0.0254 + y_dpi = y_density * 0.0254 + + if marker == b"IEND": + end_marker = True + continue + + offset = offset + length + 12 + + return "png", width, height, x_dpi, y_dpi + + +def _process_jpg(data): + # Extract width and height information from a JPEG file. + offset = 2 + data_length = len(data) + end_marker = False + width = 0 + height = 0 + x_dpi = 96 + y_dpi = 96 + + # Search through the image data to read the JPEG markers. + while not end_marker and offset < data_length: + marker = unpack(">H", data[offset + 0 : offset + 2])[0] + length = unpack(">H", data[offset + 2 : offset + 4])[0] + + # Read the height and width in the 0xFFCn elements (except C4, C8 + # and CC which aren't SOF markers). + if ( + (marker & 0xFFF0) == 0xFFC0 + and marker != 0xFFC4 + and marker != 0xFFC8 + and marker != 0xFFCC + ): + height = unpack(">H", data[offset + 5 : offset + 7])[0] + width = unpack(">H", data[offset + 7 : offset + 9])[0] + + # Read the DPI in the 0xFFE0 element. + if marker == 0xFFE0: + units = unpack("b", data[offset + 11 : offset + 12])[0] + x_density = unpack(">H", data[offset + 12 : offset + 14])[0] + y_density = unpack(">H", data[offset + 14 : offset + 16])[0] + + if units == 1: + x_dpi = x_density + y_dpi = y_density + + if units == 2: + x_dpi = x_density * 2.54 + y_dpi = y_density * 2.54 + + # Workaround for incorrect dpi. + if x_dpi == 1: + x_dpi = 96 + if y_dpi == 1: + y_dpi = 96 + + if marker == 0xFFDA: + end_marker = True + continue + + offset = offset + length + 2 + + return "jpeg", width, height, x_dpi, y_dpi + + +def _process_gif(data): + # Extract width and height information from a GIF file. + x_dpi = 96 + y_dpi = 96 + + width = unpack("<h", data[6:8])[0] + height = unpack("<h", data[8:10])[0] + + return "gif", width, height, x_dpi, y_dpi + + +def _process_bmp(data): + # Extract width and height information from a BMP file. + width = unpack("<L", data[18:22])[0] + height = unpack("<L", data[22:26])[0] + return "bmp", width, height + + +def _process_wmf(data): + # Extract width and height information from a WMF file. + x_dpi = 96 + y_dpi = 96 + + # Read the bounding box, measured in logical units. + x1 = unpack("<h", data[6:8])[0] + y1 = unpack("<h", data[8:10])[0] + x2 = unpack("<h", data[10:12])[0] + y2 = unpack("<h", data[12:14])[0] + + # Read the number of logical units per inch. Used to scale the image. + inch = unpack("<H", data[14:16])[0] + + # Convert to rendered height and width. + width = float((x2 - x1) * x_dpi) / inch + height = float((y2 - y1) * y_dpi) / inch + + return "wmf", width, height, x_dpi, y_dpi + + +def _process_emf(data): + # Extract width and height information from a EMF file. + + # Read the bounding box, measured in logical units. + bound_x1 = unpack("<l", data[8:12])[0] + bound_y1 = unpack("<l", data[12:16])[0] + bound_x2 = unpack("<l", data[16:20])[0] + bound_y2 = unpack("<l", data[20:24])[0] + + # Convert the bounds to width and height. + width = bound_x2 - bound_x1 + height = bound_y2 - bound_y1 + + # Read the rectangular frame in units of 0.01mm. + frame_x1 = unpack("<l", data[24:28])[0] + frame_y1 = unpack("<l", data[28:32])[0] + frame_x2 = unpack("<l", data[32:36])[0] + frame_y2 = unpack("<l", data[36:40])[0] + + # Convert the frame bounds to mm width and height. + width_mm = 0.01 * (frame_x2 - frame_x1) + height_mm = 0.01 * (frame_y2 - frame_y1) + + # Get the dpi based on the logical size. + x_dpi = width * 25.4 / width_mm + y_dpi = height * 25.4 / height_mm + + # This is to match Excel's calculation. It is probably to account for + # the fact that the bounding box is inclusive-inclusive. Or a bug. + width += 1 + height += 1 + + return "emf", width, height, x_dpi, y_dpi |